@bensandee/tooling 0.41.1 → 0.43.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 CHANGED
@@ -78,6 +78,15 @@ Custom blocks are anchored to the nearest preceding non-blank line. When `repo:s
78
78
 
79
79
  To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first line — `repo:sync` will leave the file untouched.
80
80
 
81
+ ### Repository setup
82
+
83
+ | Command | Description |
84
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
85
+ | `tooling setup:secrets` | Configure CI secrets (Forgejo or GitHub). Auto-detects platform from `package.json` repository URL. Flags: `--dry-run`, `--no-docker`. |
86
+ | `tooling forgejo:secrets` | Manage Forgejo Actions secrets directly. Subcommands: `set`, `list`, `delete`. |
87
+
88
+ `setup:secrets` detects the hosting platform from the `repository` field in `package.json`. For Forgejo repositories, it prompts for username and password, creates an access token with appropriate scopes, then sets `RELEASE_TOKEN` (and Docker secrets if Docker packages are detected). For GitHub repositories, it prompts for an existing Personal Access Token and uses `gh secret set` to configure secrets.
89
+
81
90
  ### Release management
82
91
 
83
92
  | Command | Description |
package/dist/bin.mjs CHANGED
@@ -625,7 +625,9 @@ const ToolingConfigSchema = z.strictObject({
625
625
  })).optional().meta({ description: "Docker package overrides (package name → config)" }),
626
626
  dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
627
627
  registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
628
- registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" })
628
+ registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" }),
629
+ postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: curl POST_RELEASE_HOOK_URL secret if set)" }),
630
+ postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" })
629
631
  });
630
632
  /** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
631
633
  function loadToolingConfig(targetDir) {
@@ -1205,6 +1207,26 @@ function publishSteps(nodeVersionYaml, registryDerivable) {
1205
1207
  ];
1206
1208
  }
1207
1209
  const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
1210
+ const PostReleaseHookSchema$1 = z.object({
1211
+ postReleaseHook: z.string().optional(),
1212
+ postReleaseHookSecrets: z.array(z.string()).optional()
1213
+ });
1214
+ /** Read the postReleaseHook command override from .tooling.json, if configured. */
1215
+ function getPostReleaseHookCommand(ctx) {
1216
+ const raw = ctx.read(".tooling.json");
1217
+ if (!raw) return void 0;
1218
+ const result = PostReleaseHookSchema$1.safeParse(JSON.parse(raw));
1219
+ if (!result.success) return void 0;
1220
+ return result.data.postReleaseHook;
1221
+ }
1222
+ /** Read the postReleaseHookSecrets array from .tooling.json, if configured. */
1223
+ function getPostReleaseHookSecrets(ctx) {
1224
+ const raw = ctx.read(".tooling.json");
1225
+ if (!raw) return void 0;
1226
+ const result = PostReleaseHookSchema$1.safeParse(JSON.parse(raw));
1227
+ if (!result.success) return void 0;
1228
+ return result.data.postReleaseHookSecrets;
1229
+ }
1208
1230
  /** Get names of packages that have Docker builds (by convention or .tooling.json config). */
1209
1231
  function getDockerPackageNames(ctx) {
1210
1232
  const names = [];
@@ -1332,7 +1354,7 @@ const PER_PACKAGE_DEV_DEPS = {
1332
1354
  "release-it": "19.2.4",
1333
1355
  "tsdown": "0.21.6",
1334
1356
  "typescript": "6.0.2",
1335
- "vitest": "^4.1.1",
1357
+ "vitest": "4.1.2",
1336
1358
  "pnpm": "10.33.0"
1337
1359
  }["@types/node"],
1338
1360
  tsdown: {
@@ -1348,7 +1370,7 @@ const PER_PACKAGE_DEV_DEPS = {
1348
1370
  "release-it": "19.2.4",
1349
1371
  "tsdown": "0.21.6",
1350
1372
  "typescript": "6.0.2",
1351
- "vitest": "^4.1.1",
1373
+ "vitest": "4.1.2",
1352
1374
  "pnpm": "10.33.0"
1353
1375
  }["tsdown"],
1354
1376
  typescript: {
@@ -1364,7 +1386,7 @@ const PER_PACKAGE_DEV_DEPS = {
1364
1386
  "release-it": "19.2.4",
1365
1387
  "tsdown": "0.21.6",
1366
1388
  "typescript": "6.0.2",
1367
- "vitest": "^4.1.1",
1389
+ "vitest": "4.1.2",
1368
1390
  "pnpm": "10.33.0"
1369
1391
  }["typescript"],
1370
1392
  vitest: {
@@ -1380,7 +1402,7 @@ const PER_PACKAGE_DEV_DEPS = {
1380
1402
  "release-it": "19.2.4",
1381
1403
  "tsdown": "0.21.6",
1382
1404
  "typescript": "6.0.2",
1383
- "vitest": "^4.1.1",
1405
+ "vitest": "4.1.2",
1384
1406
  "pnpm": "10.33.0"
1385
1407
  }["vitest"]
1386
1408
  };
@@ -1399,7 +1421,7 @@ const ROOT_DEV_DEPS = {
1399
1421
  "release-it": "19.2.4",
1400
1422
  "tsdown": "0.21.6",
1401
1423
  "typescript": "6.0.2",
1402
- "vitest": "^4.1.1",
1424
+ "vitest": "4.1.2",
1403
1425
  "pnpm": "10.33.0"
1404
1426
  }["knip"],
1405
1427
  lefthook: {
@@ -1415,7 +1437,7 @@ const ROOT_DEV_DEPS = {
1415
1437
  "release-it": "19.2.4",
1416
1438
  "tsdown": "0.21.6",
1417
1439
  "typescript": "6.0.2",
1418
- "vitest": "^4.1.1",
1440
+ "vitest": "4.1.2",
1419
1441
  "pnpm": "10.33.0"
1420
1442
  }["lefthook"],
1421
1443
  oxlint: {
@@ -1431,7 +1453,7 @@ const ROOT_DEV_DEPS = {
1431
1453
  "release-it": "19.2.4",
1432
1454
  "tsdown": "0.21.6",
1433
1455
  "typescript": "6.0.2",
1434
- "vitest": "^4.1.1",
1456
+ "vitest": "4.1.2",
1435
1457
  "pnpm": "10.33.0"
1436
1458
  }["oxlint"]
1437
1459
  };
@@ -1472,7 +1494,7 @@ function addReleaseDeps(deps, config) {
1472
1494
  "release-it": "19.2.4",
1473
1495
  "tsdown": "0.21.6",
1474
1496
  "typescript": "6.0.2",
1475
- "vitest": "^4.1.1",
1497
+ "vitest": "4.1.2",
1476
1498
  "pnpm": "10.33.0"
1477
1499
  }["release-it"];
1478
1500
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
@@ -1488,7 +1510,7 @@ function addReleaseDeps(deps, config) {
1488
1510
  "release-it": "19.2.4",
1489
1511
  "tsdown": "0.21.6",
1490
1512
  "typescript": "6.0.2",
1491
- "vitest": "^4.1.1",
1513
+ "vitest": "4.1.2",
1492
1514
  "pnpm": "10.33.0"
1493
1515
  }["@release-it/bumper"];
1494
1516
  break;
@@ -1506,7 +1528,7 @@ function addReleaseDeps(deps, config) {
1506
1528
  "release-it": "19.2.4",
1507
1529
  "tsdown": "0.21.6",
1508
1530
  "typescript": "6.0.2",
1509
- "vitest": "^4.1.1",
1531
+ "vitest": "4.1.2",
1510
1532
  "pnpm": "10.33.0"
1511
1533
  }["commit-and-tag-version"];
1512
1534
  break;
@@ -1524,7 +1546,7 @@ function addReleaseDeps(deps, config) {
1524
1546
  "release-it": "19.2.4",
1525
1547
  "tsdown": "0.21.6",
1526
1548
  "typescript": "6.0.2",
1527
- "vitest": "^4.1.1",
1549
+ "vitest": "4.1.2",
1528
1550
  "pnpm": "10.33.0"
1529
1551
  }["@changesets/cli"];
1530
1552
  break;
@@ -1549,7 +1571,7 @@ async function generatePackageJson(ctx) {
1549
1571
  const devDeps = { ...ROOT_DEV_DEPS };
1550
1572
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1551
1573
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
1552
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.41.1";
1574
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.43.0";
1553
1575
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
1554
1576
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1555
1577
  "@changesets/cli": "2.30.0",
@@ -1564,7 +1586,7 @@ async function generatePackageJson(ctx) {
1564
1586
  "release-it": "19.2.4",
1565
1587
  "tsdown": "0.21.6",
1566
1588
  "typescript": "6.0.2",
1567
- "vitest": "^4.1.1",
1589
+ "vitest": "4.1.2",
1568
1590
  "pnpm": "10.33.0"
1569
1591
  }["oxfmt"];
1570
1592
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
@@ -1580,7 +1602,7 @@ async function generatePackageJson(ctx) {
1580
1602
  "release-it": "19.2.4",
1581
1603
  "tsdown": "0.21.6",
1582
1604
  "typescript": "6.0.2",
1583
- "vitest": "^4.1.1",
1605
+ "vitest": "4.1.2",
1584
1606
  "pnpm": "10.33.0"
1585
1607
  }["prettier"];
1586
1608
  addReleaseDeps(devDeps, ctx.config);
@@ -1664,7 +1686,7 @@ async function generatePackageJson(ctx) {
1664
1686
  "release-it": "19.2.4",
1665
1687
  "tsdown": "0.21.6",
1666
1688
  "typescript": "6.0.2",
1667
- "vitest": "^4.1.1",
1689
+ "vitest": "4.1.2",
1668
1690
  "pnpm": "10.33.0"
1669
1691
  }["pnpm"]}`
1670
1692
  };
@@ -2156,9 +2178,209 @@ async function generateGitignore(ctx) {
2156
2178
  };
2157
2179
  }
2158
2180
  //#endregion
2181
+ //#region src/generators/release-ci.ts
2182
+ const DEFAULT_POST_RELEASE_COMMAND = [
2183
+ "if [ -n \"$POST_RELEASE_HOOK_URL\" ]; then",
2184
+ " curl -fsS -X POST",
2185
+ " $",
2186
+ "{POST_RELEASE_HOOK_SECRET:+-H \"Authorization: Bearer $POST_RELEASE_HOOK_SECRET\"}",
2187
+ " \"$POST_RELEASE_HOOK_URL\"; fi"
2188
+ ].join("");
2189
+ /** Build env vars for hook secrets to inject into a Release step (for runtime hook execution). */
2190
+ function hookSecretEnv(secrets) {
2191
+ const env = {
2192
+ POST_RELEASE_HOOK_URL: actionsExpr("secrets.POST_RELEASE_HOOK_URL"),
2193
+ POST_RELEASE_HOOK_SECRET: actionsExpr("secrets.POST_RELEASE_HOOK_SECRET")
2194
+ };
2195
+ for (const name of secrets ?? []) env[name] = actionsExpr(`secrets.${name}`);
2196
+ return env;
2197
+ }
2198
+ /** Step that runs after release, gated on the POST_RELEASE_HOOK_URL secret being set. */
2199
+ function postReleaseHookStep(opts) {
2200
+ return { step: {
2201
+ name: "Post-release hook",
2202
+ ...opts?.guardMain && { if: "github.ref == 'refs/heads/main'" },
2203
+ env: hookSecretEnv(opts?.secrets),
2204
+ run: opts?.command ?? DEFAULT_POST_RELEASE_COMMAND
2205
+ } };
2206
+ }
2207
+ /** Common setup steps shared by release-it and simple strategies. */
2208
+ function commonSteps(nodeVersionYaml, publishesNpm) {
2209
+ const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
2210
+ return [
2211
+ { step: {
2212
+ uses: "actions/checkout@v6",
2213
+ with: { "fetch-depth": 0 }
2214
+ } },
2215
+ { step: { uses: "pnpm/action-setup@v5" } },
2216
+ { step: {
2217
+ uses: "actions/setup-node@v6",
2218
+ with: {
2219
+ ...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
2220
+ cache: "pnpm",
2221
+ ...publishesNpm && { "registry-url": actionsExpr("vars.NPM_REGISTRY_URL || 'https://registry.npmjs.org'") }
2222
+ }
2223
+ } },
2224
+ { step: { run: "pnpm install --frozen-lockfile" } }
2225
+ ];
2226
+ }
2227
+ function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
2228
+ const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
2229
+ const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
2230
+ return [...commonSteps(nodeVersionYaml, publishesNpm), { step: {
2231
+ run: "pnpm release-it --ci",
2232
+ env: {
2233
+ ...tokenEnv,
2234
+ ...npmEnv,
2235
+ RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2236
+ }
2237
+ } }];
2238
+ }
2239
+ /** Build the workflow_dispatch trigger with optional inputs for the simple strategy. */
2240
+ function simpleWorkflowDispatchTrigger() {
2241
+ return { workflow_dispatch: { inputs: {
2242
+ bump: {
2243
+ description: "Version bump type (default: conventional-commits auto-detect)",
2244
+ required: false,
2245
+ type: "choice",
2246
+ default: "auto",
2247
+ options: [
2248
+ "auto",
2249
+ "major",
2250
+ "minor",
2251
+ "patch",
2252
+ "first-release"
2253
+ ]
2254
+ },
2255
+ prerelease: {
2256
+ description: "Create a prerelease with the given tag (e.g., beta, alpha)",
2257
+ required: false,
2258
+ type: "string"
2259
+ }
2260
+ } } };
2261
+ }
2262
+ /** Build the release:simple run command with conditional flags from workflow inputs. */
2263
+ function simpleReleaseCommand() {
2264
+ return [
2265
+ "FLAGS=",
2266
+ `case "${actionsExpr("inputs.bump")}" in`,
2267
+ " major|minor|patch) FLAGS=\"$FLAGS --release-as " + actionsExpr("inputs.bump") + "\" ;;",
2268
+ " first-release) FLAGS=\"$FLAGS --first-release\" ;;",
2269
+ "esac",
2270
+ `if [ -n "${actionsExpr("inputs.prerelease")}" ]; then FLAGS="$FLAGS --prerelease ${actionsExpr("inputs.prerelease")}"; fi`,
2271
+ "pnpm exec bst release:simple $FLAGS"
2272
+ ].join("\n");
2273
+ }
2274
+ /** Build the docker publish env block, omitting registry vars when derivable at runtime. */
2275
+ function dockerPublishEnv(registryDerivable) {
2276
+ return {
2277
+ ...registryDerivable ? {} : {
2278
+ DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
2279
+ DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
2280
+ },
2281
+ DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
2282
+ DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
2283
+ RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2284
+ };
2285
+ }
2286
+ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
2287
+ const releaseStep = { step: {
2288
+ name: "Release",
2289
+ env: {
2290
+ ...ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
2291
+ FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
2292
+ FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2293
+ RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
2294
+ },
2295
+ ...hookSecretEnv(hookSecrets),
2296
+ RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2297
+ },
2298
+ run: simpleReleaseCommand()
2299
+ } };
2300
+ const dockerStep = { step: {
2301
+ name: "Publish Docker images",
2302
+ if: "success()",
2303
+ env: dockerPublishEnv(registryDerivable),
2304
+ run: "pnpm exec bst docker:publish"
2305
+ } };
2306
+ return [
2307
+ ...commonSteps(nodeVersionYaml, publishesNpm),
2308
+ releaseStep,
2309
+ ...hasDocker ? [dockerStep] : []
2310
+ ];
2311
+ }
2312
+ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
2313
+ switch (strategy) {
2314
+ case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
2315
+ case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets);
2316
+ default: return null;
2317
+ }
2318
+ }
2319
+ async function generateReleaseCi(ctx) {
2320
+ const filePath = "release-ci";
2321
+ if (ctx.config.releaseStrategy === "none" || ctx.config.ci === "none") return {
2322
+ filePath,
2323
+ action: "skipped",
2324
+ description: "Release CI workflow not applicable"
2325
+ };
2326
+ const publishesNpm = ctx.config.publishNpm === true;
2327
+ if (ctx.config.releaseStrategy === "changesets") return {
2328
+ filePath,
2329
+ action: "skipped",
2330
+ description: "Release step included in CI workflow"
2331
+ };
2332
+ const isGitHub = ctx.config.ci === "github";
2333
+ const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
2334
+ const nodeVersionYaml = computeNodeVersionYaml(ctx);
2335
+ const hasDocker = hasDockerPackages(ctx);
2336
+ const registryDerivable = hasRepositoryUrl(ctx);
2337
+ const hookSecrets = getPostReleaseHookSecrets(ctx);
2338
+ const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets);
2339
+ if (!steps) return {
2340
+ filePath,
2341
+ action: "skipped",
2342
+ description: "Release CI workflow not applicable"
2343
+ };
2344
+ if (ctx.config.releaseStrategy === "release-it") steps.push(postReleaseHookStep({
2345
+ command: getPostReleaseHookCommand(ctx),
2346
+ secrets: hookSecrets
2347
+ }));
2348
+ const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
2349
+ const content = buildWorkflowYaml({
2350
+ ci: ctx.config.ci,
2351
+ name: "Release",
2352
+ on,
2353
+ ...isGitHub && { permissions: { contents: "write" } },
2354
+ jobName: "release",
2355
+ steps
2356
+ });
2357
+ const alreadyExists = ctx.exists(workflowPath);
2358
+ const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
2359
+ if (existing) {
2360
+ if (isToolingIgnored(existing)) return {
2361
+ filePath: workflowPath,
2362
+ action: "skipped",
2363
+ description: "Release workflow has ignore comment"
2364
+ };
2365
+ if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
2366
+ filePath: workflowPath,
2367
+ action: "skipped",
2368
+ description: "Release workflow already up to date"
2369
+ };
2370
+ }
2371
+ const withDevPath = preserveDevBinaryPath(content, existing);
2372
+ const final = existing ? insertCustomBlocks(withDevPath, extractCustomBlocks(existing)) : withDevPath;
2373
+ ctx.write(workflowPath, final);
2374
+ return {
2375
+ filePath: workflowPath,
2376
+ action: alreadyExists ? "updated" : "created",
2377
+ description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions release workflow`
2378
+ };
2379
+ }
2380
+ //#endregion
2159
2381
  //#region src/generators/ci.ts
2160
2382
  /** Build the release step for the check job (changesets strategy). */
2161
- function changesetsReleaseStep(ci, publishesNpm) {
2383
+ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
2162
2384
  if (ci === "github") return { step: {
2163
2385
  uses: "changesets/action@v1",
2164
2386
  if: "github.ref == 'refs/heads/main'",
@@ -2179,6 +2401,7 @@ function changesetsReleaseStep(ci, publishesNpm) {
2179
2401
  FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2180
2402
  RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
2181
2403
  ...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") },
2404
+ ...hookSecretEnv(hookSecrets),
2182
2405
  RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2183
2406
  },
2184
2407
  run: "pnpm exec bst release:changesets"
@@ -2226,7 +2449,18 @@ async function generateCi(ctx) {
2226
2449
  const isChangesets = ctx.config.releaseStrategy === "changesets";
2227
2450
  const nodeVersionYaml = computeNodeVersionYaml(ctx);
2228
2451
  const publishesNpm = ctx.config.publishNpm === true;
2229
- const steps = [...checkSteps(nodeVersionYaml, isChangesets && publishesNpm), ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : []];
2452
+ const hookCommand = getPostReleaseHookCommand(ctx);
2453
+ const hookSecrets = getPostReleaseHookSecrets(ctx);
2454
+ const needsHookStep = isChangesets && isGitHub;
2455
+ const steps = [
2456
+ ...checkSteps(nodeVersionYaml, isChangesets && publishesNpm),
2457
+ ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, hookSecrets)] : [],
2458
+ ...needsHookStep ? [postReleaseHookStep({
2459
+ command: hookCommand,
2460
+ secrets: hookSecrets,
2461
+ guardMain: true
2462
+ })] : []
2463
+ ];
2230
2464
  const content = buildWorkflowYaml({
2231
2465
  ci: ctx.config.ci,
2232
2466
  name: "CI",
@@ -2473,7 +2707,7 @@ function writeIfChanged(ctx, filePath, content, label) {
2473
2707
  };
2474
2708
  }
2475
2709
  async function generateClaudeHooks(ctx) {
2476
- return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.5.0\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync — do not edit manually.\n */\nimport{a as e,i as t,n,o as r,r as i,t as a}from\"./file-rules-cP7-5vh6.mjs\";import*as o from\"node:fs\";import{existsSync as s,readFileSync as c,readdirSync as l,statSync as u}from\"node:fs\";import*as d from\"node:path\";import{dirname as ee,join as f,resolve as p}from\"node:path\";import{fileURLToPath as te}from\"node:url\";Object.freeze({status:`aborted`});function m(e,t,n){function r(n,r){if(n._zod||Object.defineProperty(n,`_zod`,{value:{def:r,constr:o,traits:new Set},enumerable:!1}),n._zod.traits.has(e))return;n._zod.traits.add(e),t(n,r);let i=o.prototype,a=Object.keys(i);for(let e=0;e<a.length;e++){let t=a[e];t in n||(n[t]=i[t].bind(n))}}let i=n?.Parent??Object;class a extends i{}Object.defineProperty(a,`name`,{value:e});function o(e){var t;let i=n?.Parent?new a:this;r(i,e),(t=i._zod).deferred??(t.deferred=[]);for(let e of i._zod.deferred)e();return i}return Object.defineProperty(o,`init`,{value:r}),Object.defineProperty(o,Symbol.hasInstance,{value:t=>n?.Parent&&t instanceof n.Parent?!0:t?._zod?.traits?.has(e)}),Object.defineProperty(o,`name`,{value:e}),o}var h=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const g={};function _(e){return e&&Object.assign(g,e),g}function ne(e,t){return typeof t==`bigint`?t.toString():t}function re(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function ie(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const v=Symbol(`evaluating`);function y(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==v)return r===void 0&&(r=v,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const b=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function x(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ae(e){if(x(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(x(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function oe(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function S(e){let t=e;if(!t)return{};if(typeof t==`string`)return{error:()=>t};if(t?.message!==void 0){if(t?.error!==void 0)throw Error(\"Cannot specify both `message` and `error` params\");t.error=t.message}return delete t.message,typeof t.error==`string`?{...t,error:()=>t.error}:t}function se(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function C(e,t=0){if(e.aborted===!0)return!0;for(let n=t;n<e.issues.length;n++)if(e.issues[n]?.continue!==!0)return!0;return!1}function w(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function T(e){return typeof e==`string`?e:e?.message}function E(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=T(e.inst?._zod.def?.error?.(e))??T(t?.error?.(e))??T(n.customError?.(e))??T(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const D=(e,t)=>{e.name=`$ZodError`,Object.defineProperty(e,`_zod`,{value:e._zod,enumerable:!1}),Object.defineProperty(e,`issues`,{value:t,enumerable:!1}),e.message=JSON.stringify(t,ne,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},ce=m(`$ZodError`,D),O=m(`$ZodError`,D,{Parent:Error}),le=(e=>(t,n,r,i)=>{let a=r?Object.assign(r,{async:!1}):{async:!1},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise)throw new h;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>E(e,a,_())));throw b(t,i?.callee),t}return o.value})(O),ue=(e=>async(t,n,r,i)=>{let a=r?Object.assign(r,{async:!0}):{async:!0},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise&&(o=await o),o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>E(e,a,_())));throw b(t,i?.callee),t}return o.value})(O),k=(e=>(t,n,r)=>{let i=r?{...r,async:!1}:{async:!1},a=t._zod.run({value:n,issues:[]},i);if(a instanceof Promise)throw new h;return a.issues.length?{success:!1,error:new(e??ce)(a.issues.map(e=>E(e,i,_())))}:{success:!0,data:a.value}})(O),A=(e=>async(t,n,r)=>{let i=r?Object.assign(r,{async:!0}):{async:!0},a=t._zod.run({value:n,issues:[]},i);return a instanceof Promise&&(a=await a),a.issues.length?{success:!1,error:new e(a.issues.map(e=>E(e,i,_())))}:{success:!0,data:a.value}})(O),de=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},fe=/^-?\\d+(?:\\.\\d+)?$/,pe={major:4,minor:3,patch:6},j=m(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=pe;let r=[...e._zod.def.checks??[]];e._zod.traits.has(`$ZodCheck`)&&r.unshift(e);for(let t of r)for(let n of t._zod.onattach)n(e);if(r.length===0)(n=e._zod).deferred??(n.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{let t=(e,t,n)=>{let r=C(e),i;for(let a of t){if(a._zod.def.when){if(!a._zod.def.when(e))continue}else if(r)continue;let t=e.issues.length,o=a._zod.check(e);if(o instanceof Promise&&n?.async===!1)throw new h;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=C(e,t))});else{if(e.issues.length===t)continue;r||=C(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(C(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new h;return o.then(t=>e._zod.parse(t,a))}return e._zod.parse(o,a)};e._zod.run=(i,a)=>{if(a.skipChecks)return e._zod.parse(i,a);if(a.direction===`backward`){let t=e._zod.parse({value:i.value,issues:[]},{...a,skipChecks:!0});return t instanceof Promise?t.then(e=>n(e,i,a)):n(t,i,a)}let o=e._zod.parse(i,a);if(o instanceof Promise){if(a.async===!1)throw new h;return o.then(e=>t(e,r,a))}return t(o,r,a)}}y(e,`~standard`,()=>({validate:t=>{try{let n=k(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return A(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=m(`$ZodString`,(e,t)=>{j.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??de(e._zod.bag),e._zod.parse=(n,r)=>{if(t.coerce)try{n.value=String(n.value)}catch{}return typeof n.value==`string`||n.issues.push({expected:`string`,code:`invalid_type`,input:n.value,inst:e}),n}}),he=m(`$ZodUnknown`,(e,t)=>{j.init(e,t),e._zod.parse=e=>e});function M(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...w(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function ge(e){let t=Object.keys(e.shape);for(let n of t)if(!e.shape?.[n]?._zod?.traits?.has(`$ZodType`))throw Error(`Invalid element at key \"${n}\": expected a Zod schema`);let n=se(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function _e(e,t,n,r,i,a){let o=[],s=i.keySet,c=i.catchall._zod,l=c.def.type,u=c.optout===`optional`;for(let i in t){if(s.has(i))continue;if(l===`never`){o.push(i);continue}let a=c.run({value:t[i],issues:[]},r);a instanceof Promise?e.push(a.then(e=>M(e,n,i,t,u))):M(a,n,i,t,u)}return o.length&&n.issues.push({code:`unrecognized_keys`,keys:o,input:t,inst:a}),e.length?Promise.all(e).then(()=>n):n}const ve=m(`$ZodObject`,(e,t)=>{if(j.init(e,t),!Object.getOwnPropertyDescriptor(t,`shape`)?.get){let e=t.shape;Object.defineProperty(t,`shape`,{get:()=>{let n={...e};return Object.defineProperty(t,`shape`,{value:n}),n}})}let n=re(()=>ge(t));y(e._zod,`propValues`,()=>{let e=t.shape,n={};for(let t in e){let r=e[t]._zod;if(r.values){n[t]??(n[t]=new Set);for(let e of r.values)n[t].add(e)}}return n});let r=x,i=t.catchall,a;e._zod.parse=(t,o)=>{a??=n.value;let s=t.value;if(!r(s))return t.issues.push({expected:`object`,code:`invalid_type`,input:s,inst:e}),t;t.value={};let c=[],l=a.shape;for(let e of a.keys){let n=l[e],r=n._zod.optout===`optional`,i=n._zod.run({value:s[e],issues:[]},o);i instanceof Promise?c.push(i.then(n=>M(n,t,e,s,r))):M(i,t,e,s,r)}return i?_e(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),ye=m(`$ZodRecord`,(e,t)=>{j.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!ae(i))return n.issues.push({expected:`record`,code:`invalid_type`,input:i,inst:e}),n;let a=[],o=t.keyType._zod.values;if(o){n.value={};let s=new Set;for(let e of o)if(typeof e==`string`||typeof e==`number`||typeof e==`symbol`){s.add(typeof e==`number`?e.toString():e);let o=t.valueType._zod.run({value:i[e],issues:[]},r);o instanceof Promise?a.push(o.then(t=>{t.issues.length&&n.issues.push(...w(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...w(e,o.issues)),n.value[e]=o.value)}let c;for(let e in i)s.has(e)||(c??=[],c.push(e));c&&c.length>0&&n.issues.push({code:`unrecognized_keys`,input:i,inst:e,keys:c})}else{n.value={};for(let o of Reflect.ownKeys(i)){if(o===`__proto__`)continue;let s=t.keyType._zod.run({value:o,issues:[]},r);if(s instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);if(typeof o==`string`&&fe.test(o)&&s.issues.length){let e=t.keyType._zod.run({value:Number(o),issues:[]},r);if(e instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);e.issues.length===0&&(s=e)}if(s.issues.length){t.mode===`loose`?n.value[o]=i[o]:n.issues.push({code:`invalid_key`,origin:`record`,issues:s.issues.map(e=>E(e,r,_())),input:o,path:[o],inst:e});continue}let c=t.valueType._zod.run({value:i[o],issues:[]},r);c instanceof Promise?a.push(c.then(e=>{e.issues.length&&n.issues.push(...w(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...w(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function N(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const be=m(`$ZodOptional`,(e,t)=>{j.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,y(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),y(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${ie(e.source)})?$`):void 0}),e._zod.parse=(e,n)=>{if(t.innerType._zod.optin===`optional`){let r=t.innerType._zod.run(e,n);return r instanceof Promise?r.then(t=>N(t,e.value)):N(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var P,xe=class{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){let n=t[0];return this._map.set(e,n),n&&typeof n==`object`&&`id`in n&&this._idmap.set(n.id,e),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){let t=this._map.get(e);return t&&typeof t==`object`&&`id`in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){let t=e._zod.parent;if(t){let n={...this.get(t)??{}};delete n.id;let r={...n,...this._map.get(e)};return Object.keys(r).length?r:void 0}return this._map.get(e)}has(e){return this._map.has(e)}};function Se(){return new xe}(P=globalThis).__zod_globalRegistry??(P.__zod_globalRegistry=Se()),globalThis.__zod_globalRegistry;function Ce(e,t){return new e({type:`string`,...S(t)})}function we(e){return new e({type:`unknown`})}const F=m(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);j.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>le(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>k(e,t,n),e.parseAsync=async(t,n)=>ue(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>A(e,t,n),e.check=(...n)=>e.clone({...t,checks:[...t.checks??[],...n.map(e=>typeof e==`function`?{_zod:{check:e,def:{check:`custom`},onattach:[]}}:e)]},{parent:!0}),e.with=e.check,e.clone=(t,n)=>oe(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),Te=m(`ZodMiniString`,(e,t)=>{me.init(e,t),F.init(e,t)});function I(e){return Ce(Te,e)}const Ee=m(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),F.init(e,t)});function L(){return we(Ee)}const De=m(`ZodMiniObject`,(e,t)=>{ve.init(e,t),F.init(e,t),y(e,`shape`,()=>t.shape)});function R(e,t){return new De({type:`object`,shape:e??{},...S(t)})}const Oe=m(`ZodMiniRecord`,(e,t)=>{ye.init(e,t),F.init(e,t)});function z(e,t,n){return new Oe({type:`record`,keyType:e,valueType:t,...S(n)})}const ke=m(`ZodMiniOptional`,(e,t)=>{be.init(e,t),F.init(e,t)});function B(e){return new ke({type:`optional`,innerType:e})}const Ae=R({session_id:I(),transcript_path:I(),cwd:I(),permission_mode:I(),hook_event_name:I(),tool_name:I(),tool_input:z(I(),L())});async function je(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Ae.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Me=R({command:I(),description:B(I())}),Ne=R({file_path:I()}),Pe=R({pattern:I(),path:B(I())});function Fe(e){let t=Me.safeParse(e);return t.success?t.data:null}function Ie(e){let t=Ne.safeParse(e);return t.success?t.data:null}function Le(e){let t=Pe.safeParse(e);return t.success?t.data:null}function V(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Re(){let e=d.dirname(te(import.meta.url));return d.join(e,`bst-hook.audit.local.jsonl`)}function ze(e){try{if(o.statSync(e).size>1048576){let t=`${e}.1`;o.renameSync(e,t)}}catch{}}let Be=!1;function Ve(e){try{let t=Re();Be||=(ze(t),!0);let n={timestamp:new Date().toISOString(),...e};o.appendFileSync(t,JSON.stringify(n)+`\n`)}catch(e){process.stderr.write(`[bst-hook] audit write failed: ${e instanceof Error?e.message:String(e)}\\n`)}}function H(e){let t=[],n=``,r=0,i=!1;for(;r<e.length;){let a=e[r];if(a===`'`){for(i=!0,r++;r<e.length&&e[r]!==`'`;)n+=e[r],r++;if(r>=e.length)return{ok:!1,error:`unterminated single quote`};r++}else if(a===`\"`){for(i=!0,r++;r<e.length&&e[r]!==`\"`;){if(e[r]===`\\\\`&&r+1<e.length){let t=e[r+1];if(t===`\"`||t===`\\\\`||t===`$`||t===\"`\"){n+=t,r+=2;continue}}n+=e[r],r++}if(r>=e.length)return{ok:!1,error:`unterminated double quote`};r++}else a===`\\\\`&&r+1<e.length?(i=!0,n+=e[r+1],r+=2):a===` `||a===` `?(i&&=(t.push(n),n=``,!1),r++):(i=!0,n+=a,r++)}return i&&t.push(n),{ok:!0,tokens:t}}function He(e){if(!e||!e.trim())return[];let t=[],n=``,r=0,i=!1,a=!1;for(;r<e.length;){let o=e[r];if(o===`'`&&!a){i=!i,n+=o,r++;continue}if(o===`\"`&&!i){a=!a,n+=o,r++;continue}if(o===`\\\\`&&!i&&r+1<e.length){n+=o+e[r+1],r+=2;continue}if(!i&&!a){if(o===`&`&&r+1<e.length&&e[r+1]===`&`){U(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){U(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){U(t,n,`||`),n=``,r+=2;continue}if(o===`|`){U(t,n,`|`),n=``,r++;continue}if(o===`;`){U(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function U(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const W=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ue(e,t){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!W.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!W.has(n)){if(n.includes(`<`)&&!n.includes(`<(`))return{safe:!1,reason:`input redirect`};if(n.includes(`>`)){if(n.endsWith(`>/dev/null`)||n.match(/^(&>>|&>|>>|>|1>|2>)$/)&&e[t+1]===`/dev/null`)continue;return{safe:!1,reason:`output redirect to file`}}}}return{safe:!0}}const We=R({scripts:z(I(),L())});function G(e,t){let n=Qe(t);if(!n)return null;let r=n[e];if(typeof r!=`string`)return null;let i=n[`pre${e}`],a=n[`post${e}`];return{pre:typeof i==`string`?i:null,main:r,post:typeof a==`string`?a:null}}function Ge(e,t,n){let r=Je(t,n);if(!r)return null;let i=G(e,r);return i?{...i,pkgDir:r}:null}function Ke(e,t){let n=K(t);if(!n)return null;let r=[];for(let t of n){let n=G(e,t);n&&r.push({...n,pkgDir:t})}return r}function qe(e){let t=p(e);for(;;){if(s(f(t,`pnpm-workspace.yaml`)))return t;let e=ee(t);if(e===t)return null;t=e}}function K(e){let t=Ye(e);if(!t)return null;let n=[];for(let r of t){if(!r.endsWith(`/*`))continue;let t=p(e,r.slice(0,-2)),i;try{i=l(t)}catch{continue}for(let e of i){let r=f(t,e);try{if(!u(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function Je(e,t){let n=K(t);if(!n)return null;for(let t of n){let n=Ze(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function Ye(e){try{let t=c(f(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),n=t.findIndex(e=>/^packages\\s*:/.test(e));if(n===-1)return null;let r=[];for(let e=n+1;e<t.length;e++){let n=t[e],i=/^\\s+-\\s+(.+)$/.exec(n);if(!i)break;r.push(i[1].trim().replace(/['\"`]/g,``))}return r.length>0?r:null}catch{return null}}const Xe=R({name:I()});function Ze(e){try{let t=c(f(e,`package.json`),`utf-8`),n=Xe.safeParse(JSON.parse(t));return n.success?n.data.name:null}catch{return null}}function Qe(e){try{let t=c(f(e,`package.json`),`utf-8`),n=We.safeParse(JSON.parse(t));return n.success?n.data.scripts:null}catch{return null}}const $e=new Set([`LD_PRELOAD`,`LD_LIBRARY_PATH`,`DYLD_INSERT_LIBRARIES`,`NODE_OPTIONS`,`PYTHONPATH`,`PYTHONSTARTUP`,`PERL5OPT`,`PERL5LIB`,`RUBYOPT`,`RUBYLIB`,`BASH_ENV`,`ENV`,`npm_config_prefix`,`npm_config_registry`,`EDITOR`,`VISUAL`]),et=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function q(e){return $e.has(e)?!0:et.some(t=>e.startsWith(t))}function tt(e,t){let n=H(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return d.isAbsolute(i)?i:d.resolve(t.cwd,i)}function J(e,t){if(t.depth>=5)return{action:`passthrough`,rule:null,reason:`max evaluation depth exceeded`};let n=e.trim();if(!n)return{action:`passthrough`,rule:null,reason:null};if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=He(n);if(r.length===0)return{action:`passthrough`,rule:null,reason:null};let i=null,a=null,o=t;for(let e of r){if(e.operator===`&&`){let t=tt(e.command,o);t&&(o={...o,cwd:t})}let t=nt(e.command,o);if(t.action===`deny`)return t;t.action===`passthrough`&&(i||=t),t.action===`allow`&&(a=t)}return i||(r.length===1&&a?a:{action:`allow`,rule:`all-segments`,reason:null})}function Y(e,t){if(e.pre){let n=J(e.pre,t);if(n.action!==`allow`)return n}let n=J(e.main,t);if(n.action!==`allow`)return n;if(e.post){let n=J(e.post,t);if(n.action!==`allow`)return n}return`allow`}function nt(n,r){let i=n.trim();if(i.startsWith(`(`)&&i.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let a=H(n);if(!a.ok)return{action:`passthrough`,rule:null,reason:a.error};let o=a.tokens;if(o.length===0)return{action:`passthrough`,rule:null,reason:null};let s=0;for(;s<o.length&&/^\\w+=/.test(o[s]);){let e=o[s].split(`=`)[0];if(q(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};s++}if(s>=o.length)return{action:`allow`,rule:`env-only`,reason:null};o=o.slice(s);let c=Ue(o,n);if(!c.safe)return{action:`passthrough`,rule:null,reason:c.reason};for(let e=0;e<o.length;e++){let t=o[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(o[e+1]===`/dev/null`)continue;return{action:`passthrough`,rule:null,reason:`output redirect to file`}}if(t===`<`)return{action:`passthrough`,rule:null,reason:`input redirect`}}}let l=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);o=o.filter((e,t)=>!(l.has(e)||(e===`>`||e===`>>`)&&o[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(o[t-1]===`>`||o[t-1]===`>>`)));let u=d.basename(o[0]);if(u===`command`&&o.length>1)return J(o.slice(1).join(` `),{...r,depth:r.depth+1});if(u===`env`){let e=1;for(;e<o.length&&/^\\w+=/.test(o[e]);){let t=o[e].split(`=`)[0];if(q(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=o.length?{action:`allow`,rule:`env-only`,reason:null}:J(o.slice(e).join(` `),{...r,depth:r.depth+1})}if(u===`pnpm`)return rt(o,r);if(u===`npm`)return at(o);if(u===`bash`||u===`sh`||u===`zsh`)return ct(o,r);if(u===`node`&&(o.includes(`-e`)||o.includes(`-p`)||o.includes(`--eval`)||o.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(u===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let t of e)if(t.match.test(u)&&(!t.when||t.when(o,r)))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let e of t)if(e.match.test(u)&&(!e.when||e.when(o,r)))return{action:`allow`,rule:e.name,reason:null};return{action:`passthrough`,rule:null,reason:`no rule matched: ${u}`}}function rt(e,n){let r=e.slice(1),a,o=-1,s=!1,c=!1,l;for(let t=0;t<r.length;t++){let u=r[t];if(u===`--filter`||u.startsWith(`--filter=`))return ot(e,r,t,n);if(u===`-r`||u===`--recursive`){s=!0;continue}if(u===`-w`||u===`--workspace-root`){c=!0;continue}if(u===`-C`||u===`--dir`){l=r[++t];continue}if(u.startsWith(`-C=`)||u.startsWith(`--dir=`)){l=u.split(`=`,2)[1];continue}if(!(u.startsWith(`-`)&&!i.has(u))){a=u,o=t;break}}if(!a)return{action:`passthrough`,rule:null,reason:null};if(a===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(a===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(a===`exec`){let e=r.slice(o+1);return e.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:J(e.join(` `),{...n,depth:n.depth+1})}let u;if(a===`run`){let e=r[o+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};u=e}else if(!i.has(a))u=a;else{for(let r of t)if(r.match.test(`pnpm`)&&(!r.when||r.when(e,n)))return{action:`allow`,rule:r.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}if(s)return it(u,n);if(c){let e=qe(n.cwd);return e?X(u,{...n,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(l){let e=d.resolve(n.cwd,l);return X(u,{...n,cwd:e})}return X(u,n)}function X(e,t){let n=G(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=Y(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function it(e,t){let n=Ke(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=Y(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function at(e){for(let n of t)if(n.match.test(`npm`)&&(!n.when||n.when(e,{cwd:``,depth:0})))return{action:`allow`,rule:n.name,reason:null};return{action:`deny`,rule:`npm-redirect`,reason:r(e)}}function ot(e,t,n,r){let a=t[n],o,s;if(a.startsWith(`--filter=`))o=a.slice(9),s=n+1;else{if(o=t[n+1],!o||o.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};s=n+2}if(!o)return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};let c;for(let e=s;e<t.length;e++){let n=t[e];if(!(n.startsWith(`-`)&&!i.has(n))){c=n;break}}if(!c)return{action:`passthrough`,rule:null,reason:`pnpm --filter without subcommand`};if(c===`install`||c===`i`)return t.slice(t.indexOf(c)+1).every(e=>e.startsWith(`-`))?{action:`allow`,rule:`pnpm`,reason:null}:{action:`passthrough`,rule:null,reason:`pnpm --filter install with package arg`};if(c===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(c===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`,`licenses`,`audit`]).has(c))return{action:`allow`,rule:`pnpm`,reason:null};if(c===`exec`){let e=t.indexOf(`exec`,s),n=t.slice(e+1);return n.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:J(n.join(` `),{...r,depth:r.depth+1})}let l=c===`run`?t[t.indexOf(`run`,s)+1]:c;if(!l||l.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter run without script name`};if(i.has(l)&&l!==`run`)return{action:`passthrough`,rule:null,reason:null};let u=Ge(l,o,r.cwd);if(!u)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let d=Y(u,{...r,depth:r.depth+1,cwd:u.pkgDir??r.cwd});return d===`allow`?{action:`allow`,rule:`pnpm-filter:${o}:${l}`,reason:null}:d}const st=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function ct(e,t){let n=e.indexOf(`-c`);if(n===-1)return{action:`passthrough`,rule:null,reason:`shell without -c`};for(let t=1;t<e.length;t++){if(t===n||t===n+1)continue;let r=e[t];if(r.startsWith(`-`)||st.has(r))return{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}}let r=e[n+1];return r?e.length>n+2?{action:`passthrough`,rule:null,reason:`shell -c with positional args`}:n===1?J(r,{...t,depth:t.depth+1}):{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}:{action:`passthrough`,rule:null,reason:`shell -c without command`}}const lt=new Set([`Read`,`Write`,`Edit`]),ut=new Set([`Glob`,`Grep`]);function dt(e,t,n){return lt.has(e)?Ie(t)?.file_path??null:ut.has(e)?Le(t)?.path??n:null}function ft(e,t,r){let i=dt(e,t,r.cwd);if(i===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of n)if(t.tools.has(e)&&t.when(i,r))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of a)if(t.tools.has(e)&&t.when(i,r))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const Z=await je();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);let Q,$;if(Z.tool_name===`Bash`){let e=Fe(Z.tool_input);e||process.exit(0),Q=J(e.command,{cwd:Z.cwd,depth:0}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=ft(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}Ve({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?V(`allow`):Q.action===`deny`&&V(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **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- **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- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\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)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\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")];
2710
+ return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.1\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync — do not edit manually.\n */\nimport*as e from\"node:fs\";import{existsSync as t,readFileSync as n,readdirSync as r,statSync as i}from\"node:fs\";import*as a from\"node:path\";import{dirname as o,join as s,resolve as c}from\"node:path\";import{fileURLToPath as l}from\"node:url\";Object.freeze({status:`aborted`});function u(e,t,n){function r(n,r){if(n._zod||Object.defineProperty(n,`_zod`,{value:{def:r,constr:o,traits:new Set},enumerable:!1}),n._zod.traits.has(e))return;n._zod.traits.add(e),t(n,r);let i=o.prototype,a=Object.keys(i);for(let e=0;e<a.length;e++){let t=a[e];t in n||(n[t]=i[t].bind(n))}}let i=n?.Parent??Object;class a extends i{}Object.defineProperty(a,`name`,{value:e});function o(e){var t;let i=n?.Parent?new a:this;r(i,e),(t=i._zod).deferred??(t.deferred=[]);for(let e of i._zod.deferred)e();return i}return Object.defineProperty(o,`init`,{value:r}),Object.defineProperty(o,Symbol.hasInstance,{value:t=>n?.Parent&&t instanceof n.Parent?!0:t?._zod?.traits?.has(e)}),Object.defineProperty(o,`name`,{value:e}),o}var d=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const f={};function p(e){return e&&Object.assign(f,e),f}function ee(e,t){return typeof t==`bigint`?t.toString():t}function te(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function ne(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const re=Symbol(`evaluating`);function m(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==re)return r===void 0&&(r=re,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const h=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ie(e){if(g(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(g(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function ae(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function _(e){let t=e;if(!t)return{};if(typeof t==`string`)return{error:()=>t};if(t?.message!==void 0){if(t?.error!==void 0)throw Error(\"Cannot specify both `message` and `error` params\");t.error=t.message}return delete t.message,typeof t.error==`string`?{...t,error:()=>t.error}:t}function oe(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function v(e,t=0){if(e.aborted===!0)return!0;for(let n=t;n<e.issues.length;n++)if(e.issues[n]?.continue!==!0)return!0;return!1}function y(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function b(e){return typeof e==`string`?e:e?.message}function x(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=b(e.inst?._zod.def?.error?.(e))??b(t?.error?.(e))??b(n.customError?.(e))??b(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const S=(e,t)=>{e.name=`$ZodError`,Object.defineProperty(e,`_zod`,{value:e._zod,enumerable:!1}),Object.defineProperty(e,`issues`,{value:t,enumerable:!1}),e.message=JSON.stringify(t,ee,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},se=u(`$ZodError`,S),C=u(`$ZodError`,S,{Parent:Error}),ce=(e=>(t,n,r,i)=>{let a=r?Object.assign(r,{async:!1}):{async:!1},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise)throw new d;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,p())));throw h(t,i?.callee),t}return o.value})(C),le=(e=>async(t,n,r,i)=>{let a=r?Object.assign(r,{async:!0}):{async:!0},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise&&(o=await o),o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,p())));throw h(t,i?.callee),t}return o.value})(C),w=(e=>(t,n,r)=>{let i=r?{...r,async:!1}:{async:!1},a=t._zod.run({value:n,issues:[]},i);if(a instanceof Promise)throw new d;return a.issues.length?{success:!1,error:new(e??se)(a.issues.map(e=>x(e,i,p())))}:{success:!0,data:a.value}})(C),ue=(e=>async(t,n,r)=>{let i=r?Object.assign(r,{async:!0}):{async:!0},a=t._zod.run({value:n,issues:[]},i);return a instanceof Promise&&(a=await a),a.issues.length?{success:!1,error:new e(a.issues.map(e=>x(e,i,p())))}:{success:!0,data:a.value}})(C),de=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},fe=/^-?\\d+(?:\\.\\d+)?$/,pe={major:4,minor:3,patch:6},T=u(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=pe;let r=[...e._zod.def.checks??[]];e._zod.traits.has(`$ZodCheck`)&&r.unshift(e);for(let t of r)for(let n of t._zod.onattach)n(e);if(r.length===0)(n=e._zod).deferred??(n.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{let t=(e,t,n)=>{let r=v(e),i;for(let a of t){if(a._zod.def.when){if(!a._zod.def.when(e))continue}else if(r)continue;let t=e.issues.length,o=a._zod.check(e);if(o instanceof Promise&&n?.async===!1)throw new d;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=v(e,t))});else{if(e.issues.length===t)continue;r||=v(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(v(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new d;return o.then(t=>e._zod.parse(t,a))}return e._zod.parse(o,a)};e._zod.run=(i,a)=>{if(a.skipChecks)return e._zod.parse(i,a);if(a.direction===`backward`){let t=e._zod.parse({value:i.value,issues:[]},{...a,skipChecks:!0});return t instanceof Promise?t.then(e=>n(e,i,a)):n(t,i,a)}let o=e._zod.parse(i,a);if(o instanceof Promise){if(a.async===!1)throw new d;return o.then(e=>t(e,r,a))}return t(o,r,a)}}m(e,`~standard`,()=>({validate:t=>{try{let n=w(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return ue(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=u(`$ZodString`,(e,t)=>{T.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??de(e._zod.bag),e._zod.parse=(n,r)=>{if(t.coerce)try{n.value=String(n.value)}catch{}return typeof n.value==`string`||n.issues.push({expected:`string`,code:`invalid_type`,input:n.value,inst:e}),n}}),he=u(`$ZodUnknown`,(e,t)=>{T.init(e,t),e._zod.parse=e=>e});function E(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...y(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function ge(e){let t=Object.keys(e.shape);for(let n of t)if(!e.shape?.[n]?._zod?.traits?.has(`$ZodType`))throw Error(`Invalid element at key \"${n}\": expected a Zod schema`);let n=oe(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function _e(e,t,n,r,i,a){let o=[],s=i.keySet,c=i.catchall._zod,l=c.def.type,u=c.optout===`optional`;for(let i in t){if(s.has(i))continue;if(l===`never`){o.push(i);continue}let a=c.run({value:t[i],issues:[]},r);a instanceof Promise?e.push(a.then(e=>E(e,n,i,t,u))):E(a,n,i,t,u)}return o.length&&n.issues.push({code:`unrecognized_keys`,keys:o,input:t,inst:a}),e.length?Promise.all(e).then(()=>n):n}const ve=u(`$ZodObject`,(e,t)=>{if(T.init(e,t),!Object.getOwnPropertyDescriptor(t,`shape`)?.get){let e=t.shape;Object.defineProperty(t,`shape`,{get:()=>{let n={...e};return Object.defineProperty(t,`shape`,{value:n}),n}})}let n=te(()=>ge(t));m(e._zod,`propValues`,()=>{let e=t.shape,n={};for(let t in e){let r=e[t]._zod;if(r.values){n[t]??(n[t]=new Set);for(let e of r.values)n[t].add(e)}}return n});let r=g,i=t.catchall,a;e._zod.parse=(t,o)=>{a??=n.value;let s=t.value;if(!r(s))return t.issues.push({expected:`object`,code:`invalid_type`,input:s,inst:e}),t;t.value={};let c=[],l=a.shape;for(let e of a.keys){let n=l[e],r=n._zod.optout===`optional`,i=n._zod.run({value:s[e],issues:[]},o);i instanceof Promise?c.push(i.then(n=>E(n,t,e,s,r))):E(i,t,e,s,r)}return i?_e(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),ye=u(`$ZodRecord`,(e,t)=>{T.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!ie(i))return n.issues.push({expected:`record`,code:`invalid_type`,input:i,inst:e}),n;let a=[],o=t.keyType._zod.values;if(o){n.value={};let s=new Set;for(let e of o)if(typeof e==`string`||typeof e==`number`||typeof e==`symbol`){s.add(typeof e==`number`?e.toString():e);let o=t.valueType._zod.run({value:i[e],issues:[]},r);o instanceof Promise?a.push(o.then(t=>{t.issues.length&&n.issues.push(...y(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...y(e,o.issues)),n.value[e]=o.value)}let c;for(let e in i)s.has(e)||(c??=[],c.push(e));c&&c.length>0&&n.issues.push({code:`unrecognized_keys`,input:i,inst:e,keys:c})}else{n.value={};for(let o of Reflect.ownKeys(i)){if(o===`__proto__`)continue;let s=t.keyType._zod.run({value:o,issues:[]},r);if(s instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);if(typeof o==`string`&&fe.test(o)&&s.issues.length){let e=t.keyType._zod.run({value:Number(o),issues:[]},r);if(e instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);e.issues.length===0&&(s=e)}if(s.issues.length){t.mode===`loose`?n.value[o]=i[o]:n.issues.push({code:`invalid_key`,origin:`record`,issues:s.issues.map(e=>x(e,r,p())),input:o,path:[o],inst:e});continue}let c=t.valueType._zod.run({value:i[o],issues:[]},r);c instanceof Promise?a.push(c.then(e=>{e.issues.length&&n.issues.push(...y(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...y(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function be(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const xe=u(`$ZodOptional`,(e,t)=>{T.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,m(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),m(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${ne(e.source)})?$`):void 0}),e._zod.parse=(e,n)=>{if(t.innerType._zod.optin===`optional`){let r=t.innerType._zod.run(e,n);return r instanceof Promise?r.then(t=>be(t,e.value)):be(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var Se,Ce=class{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){let n=t[0];return this._map.set(e,n),n&&typeof n==`object`&&`id`in n&&this._idmap.set(n.id,e),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){let t=this._map.get(e);return t&&typeof t==`object`&&`id`in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){let t=e._zod.parent;if(t){let n={...this.get(t)??{}};delete n.id;let r={...n,...this._map.get(e)};return Object.keys(r).length?r:void 0}return this._map.get(e)}has(e){return this._map.has(e)}};function we(){return new Ce}(Se=globalThis).__zod_globalRegistry??(Se.__zod_globalRegistry=we()),globalThis.__zod_globalRegistry;function Te(e,t){return new e({type:`string`,..._(t)})}function Ee(e){return new e({type:`unknown`})}const D=u(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);T.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>ce(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>w(e,t,n),e.parseAsync=async(t,n)=>le(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>ue(e,t,n),e.check=(...n)=>e.clone({...t,checks:[...t.checks??[],...n.map(e=>typeof e==`function`?{_zod:{check:e,def:{check:`custom`},onattach:[]}}:e)]},{parent:!0}),e.with=e.check,e.clone=(t,n)=>ae(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),De=u(`ZodMiniString`,(e,t)=>{me.init(e,t),D.init(e,t)});function O(e){return Te(De,e)}const Oe=u(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),D.init(e,t)});function k(){return Ee(Oe)}const ke=u(`ZodMiniObject`,(e,t)=>{ve.init(e,t),D.init(e,t),m(e,`shape`,()=>t.shape)});function A(e,t){return new ke({type:`object`,shape:e??{},..._(t)})}const Ae=u(`ZodMiniRecord`,(e,t)=>{ye.init(e,t),D.init(e,t)});function j(e,t,n){return new Ae({type:`record`,keyType:e,valueType:t,..._(n)})}const je=u(`ZodMiniOptional`,(e,t)=>{xe.init(e,t),D.init(e,t)});function M(e){return new je({type:`optional`,innerType:e})}const Me=A({session_id:O(),transcript_path:O(),cwd:O(),permission_mode:O(),hook_event_name:O(),tool_name:O(),tool_input:j(O(),k())});async function Ne(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Me.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Pe=A({command:O(),description:M(O())}),Fe=A({file_path:O()}),Ie=A({pattern:O(),path:M(O())});function Le(e){let t=Pe.safeParse(e);return t.success?t.data:null}function Re(e){let t=Fe.safeParse(e);return t.success?t.data:null}function ze(e){let t=Ie.safeParse(e);return t.success?t.data:null}function N(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Be(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ve(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let He=!1;function Ue(t){try{let n=Be();He||=(Ve(n),!0);let r={timestamp:new Date().toISOString(),...t};e.appendFileSync(n,JSON.stringify(r)+`\n`)}catch(e){process.stderr.write(`[bst-hook] audit write failed: ${e instanceof Error?e.message:String(e)}\\n`)}}function P(e){let t=[],n=``,r=0,i=!1;for(;r<e.length;){let a=e[r];if(a===`'`){for(i=!0,r++;r<e.length&&e[r]!==`'`;)n+=e[r],r++;if(r>=e.length)return{ok:!1,error:`unterminated single quote`};r++}else if(a===`\"`){for(i=!0,r++;r<e.length&&e[r]!==`\"`;){if(e[r]===`\\\\`&&r+1<e.length){let t=e[r+1];if(t===`\"`||t===`\\\\`||t===`$`||t===\"`\"){n+=t,r+=2;continue}}n+=e[r],r++}if(r>=e.length)return{ok:!1,error:`unterminated double quote`};r++}else a===`\\\\`&&r+1<e.length?(i=!0,n+=e[r+1],r+=2):a===` `||a===` `?(i&&=(t.push(n),n=``,!1),r++):(i=!0,n+=a,r++)}return i&&t.push(n),{ok:!0,tokens:t}}function We(e){if(!e||!e.trim())return[];let t=[],n=``,r=0,i=!1,a=!1;for(;r<e.length;){let o=e[r];if(o===`'`&&!a){i=!i,n+=o,r++;continue}if(o===`\"`&&!i){a=!a,n+=o,r++;continue}if(o===`\\\\`&&!i&&r+1<e.length){n+=o+e[r+1],r+=2;continue}if(!i&&!a){if(o===`&`&&r+1<e.length&&e[r+1]===`&`){F(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){F(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){F(t,n,`||`),n=``,r+=2;continue}if(o===`|`){F(t,n,`|`),n=``,r++;continue}if(o===`;`){F(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function F(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const Ge=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ke(e,t){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!Ge.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!Ge.has(n)){if(n.includes(`<`)&&!n.includes(`<(`))return{safe:!1,reason:`input redirect`};if(n.includes(`>`)){if(n.endsWith(`>/dev/null`)||n.match(/^(&>>|&>|>>|>|1>|2>)$/)&&e[t+1]===`/dev/null`)continue;return{safe:!1,reason:`output redirect to file`}}}}return{safe:!0}}const qe=[`.sample`,`.example`,`.template`],Je=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Ye(e){return qe.some(t=>e.endsWith(t))}function I(e){return Ye(a.basename(e))?!1:Je.test(e)}function Xe(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&I(e))}const Ze=new Set([`status`,`log`,`diff`,`show`,`blame`,`grep`,`shortlog`,`range-diff`,`describe`,`branch`,`remote`,`fetch`,`ls-files`,`ls-tree`,`ls-remote`,`show-ref`,`for-each-ref`,`rev-parse`,`rev-list`,`cat-file`,`diff-tree`,`name-rev`,`merge-base`,`check-ignore`]),Qe=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),$e=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),et=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function L(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(et.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function tt(e){for(let t of e.slice(1))if($e.has(t)||t===`-o`||t.startsWith(`--output`)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`)||t.startsWith(`-o=`)||t.startsWith(`--output=`))return!1;let t=L(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Ze.has(t)}function nt(e){let t=L(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return Qe.has(t)}const rt=new Set([`push`,`pop`,`apply`,`show`]);function R(e){let t=L(e);if(!t)return!1;if(t===`add`)return!0;if(t===`checkout`)return e.slice(e.indexOf(`checkout`)+1).some(e=>e===`-b`||e===`-B`);if(t===`switch`)return!0;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===void 0?!0:rt.has(t)}return!1}const z=new Set(`install.i.add.remove.rm.uninstall.un.update.up.upgrade.list.ls.ll.la.why.view.info.show.outdated.audit.licenses.init.create.link.unlink.pack.publish.config.set.get.store.cache.rebuild.rb.prune.dedupe.dlx.exec.run.patch.patch-commit.recursive.-r`.split(`.`)),it=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function at(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!z.has(e))){n=e;break}return n?it.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const ot=new Set([`view`,`info`,`show`]);function st(e){let t=e[1];return t!==void 0&&ot.has(t)}function ct(e){let t=e[1],n=`This project uses pnpm.`;return t?t===`install`||t===`i`||t===`ci`?e.slice(2).some(e=>!e.startsWith(`-`))?`${n} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`:`${n} Use \\`pnpm install\\` instead of npm.`:t===`add`?`${n} Use \\`pnpm add <pkg>\\` instead of npm.`:t===`uninstall`||t===`remove`||t===`rm`||t===`un`||t===`r`?`${n} Use \\`pnpm remove <pkg>\\` instead of npm.`:t===`run`||t===`run-script`?`${n} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`:t===`test`||t===`t`||t===`tst`?`${n} Use \\`pnpm test\\` instead of npm.`:t===`start`?`${n} Use \\`pnpm start\\` instead of npm.`:t===`stop`?`${n} Use \\`pnpm stop\\` instead of npm.`:t===`restart`?`${n} Use \\`pnpm restart\\` instead of npm.`:t===`exec`?`${n} Use \\`pnpm exec <cmd>\\` instead of npm.`:t===`list`||t===`ls`||t===`ll`||t===`la`?`${n} Use \\`pnpm list\\` instead of npm.`:t===`why`?`${n} Use \\`pnpm why\\` instead of npm.`:t===`outdated`?`${n} Use \\`pnpm outdated\\` instead of npm.`:t===`audit`?`${n} Use \\`pnpm audit\\` instead of npm.`:t===`update`||t===`up`||t===`upgrade`?`${n} Use \\`pnpm update\\` instead of npm.`:t===`init`?`${n} Use \\`pnpm init\\` instead of npm.`:t===`publish`?`${n} Use \\`pnpm publish\\` instead of npm.`:t===`link`?`${n} Use \\`pnpm link\\` instead of npm.`:t===`unlink`?`${n} Use \\`pnpm unlink\\` instead of npm.`:t===`pack`?`${n} Use \\`pnpm pack\\` instead of npm.`:t===`cache`?`${n} Use \\`pnpm store\\` instead of \\`npm cache\\`.`:t===`config`||t===`set`||t===`get`?`${n} Use \\`pnpm config\\` instead of npm.`:`${n} Use the equivalent pnpm command instead of npm.`:`${n} Use \\`pnpm\\` instead of npm.`}function B(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`)){if(n===`-t`||n.startsWith(`--target-directory`))return!1;continue}if(a.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=a.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+a.sep))return!1}return!0}const lt=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function ut(e){return!e.some(e=>lt.has(e))}function dt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ft(e){let t=e.findIndex(e=>a.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),i=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=i.includes(`--recursive`);return o||s}function pt(e){return e.some(e=>e===`777`||e===`0777`)}function mt(e){let t=e.indexOf(`push`);if(t===-1)return!1;let n=e.slice(t+1);return n.some(e=>e===`--force`||e===`-f`&&!n.includes(`--force-with-lease`))}function ht(e){if(!(e.includes(`reset`)&&e.includes(`--hard`)))return!1;if(e.some(e=>e===`main`||e===`master`))return!0;let t=e.filter(e=>e!==`git`&&e!==`reset`&&!e.startsWith(`-`));return t.length===0||t.includes(`HEAD`)}const gt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ft(e),reason:\"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"},{name:`sudo`,match:/^sudo$/,action:`deny`,reason:`Don't use sudo. If you need elevated permissions, ask me to do it manually.`},{name:`git-force-push`,match:/^git$/,action:`deny`,when:e=>mt(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>ht(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>nt(e)&&!R(e),reason:`Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>pt(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-read`,match:/^(cat|head|tail|less|more|grep)$/,action:`deny`,when:e=>Xe(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`npx-redirect`,match:/^npx$/,action:`deny`,reason:\"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"}],V=[{name:`vitest`,match:/^vitest$/,action:`allow`},{name:`jest`,match:/^jest$/,action:`allow`},{name:`tsc`,match:/^tsc$/,action:`allow`},{name:`tsdown`,match:/^tsdown$/,action:`allow`},{name:`oxlint`,match:/^oxlint$/,action:`allow`},{name:`oxfmt`,match:/^oxfmt$/,action:`allow`},{name:`eslint`,match:/^eslint$/,action:`allow`},{name:`prettier`,match:/^prettier$/,action:`allow`},{name:`biome`,match:/^biome$/,action:`allow`},{name:`knip`,match:/^knip$/,action:`allow`},{name:`cat`,match:/^cat$/,action:`allow`},{name:`head`,match:/^head$/,action:`allow`},{name:`tail`,match:/^tail$/,action:`allow`},{name:`grep`,match:/^grep$/,action:`allow`},{name:`ls`,match:/^ls$/,action:`allow`},{name:`wc`,match:/^wc$/,action:`allow`},{name:`uniq`,match:/^uniq$/,action:`allow`},{name:`diff`,match:/^diff$/,action:`allow`},{name:`echo`,match:/^echo$/,action:`allow`},{name:`pwd`,match:/^pwd$/,action:`allow`},{name:`which`,match:/^which$/,action:`allow`},{name:`dirname`,match:/^dirname$/,action:`allow`},{name:`basename`,match:/^basename$/,action:`allow`},{name:`realpath`,match:/^realpath$/,action:`allow`},{name:`npm`,match:/^npm$/,action:`allow`,when:e=>st(e)},{name:`test`,match:/^test$/,action:`allow`},{name:`bracket-test`,match:/^\\[$/,action:`allow`},{name:`cd`,match:/^cd$/,action:`allow`},{name:`true`,match:/^true$/,action:`allow`},{name:`colon`,match:/^:$/,action:`allow`},{name:`find`,match:/^find$/,action:`allow`,when:e=>ut(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>dt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>tt(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>R(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>at(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>B(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>B(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>B(e,t)}],_t=A({scripts:j(O(),k())});function H(e,t){let n=Tt(t);if(!n)return null;let r=n[e];if(typeof r!=`string`)return null;let i=n[`pre${e}`],a=n[`post${e}`];return{pre:typeof i==`string`?i:null,main:r,post:typeof a==`string`?a:null}}function vt(e,t,n){let r=xt(t,n);if(!r)return null;let i=H(e,r);return i?{...i,pkgDir:r}:null}function yt(e,t){let n=U(t);if(!n)return null;let r=[];for(let t of n){let n=H(e,t);n&&r.push({...n,pkgDir:t})}return r}function bt(e){let n=c(e);for(;;){if(t(s(n,`pnpm-workspace.yaml`)))return n;let e=o(n);if(e===n)return null;n=e}}function U(e){let t=St(e);if(!t)return null;let n=[];for(let a of t){if(!a.endsWith(`/*`))continue;let t=c(e,a.slice(0,-2)),o;try{o=r(t)}catch{continue}for(let e of o){let r=s(t,e);try{if(!i(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function xt(e,t){let n=U(t);if(!n)return null;for(let t of n){let n=wt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function St(e){try{let t=n(s(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=t.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<t.length;e++){let n=t[e],r=/^\\s+-\\s+(.+)$/.exec(n);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const Ct=A({name:O()});function wt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Ct.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Tt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=_t.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}const Et=new Set([`LD_PRELOAD`,`LD_LIBRARY_PATH`,`DYLD_INSERT_LIBRARIES`,`NODE_OPTIONS`,`PYTHONPATH`,`PYTHONSTARTUP`,`PERL5OPT`,`PERL5LIB`,`RUBYOPT`,`RUBYLIB`,`BASH_ENV`,`ENV`,`npm_config_prefix`,`npm_config_registry`,`EDITOR`,`VISUAL`]),Dt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function W(e){return Et.has(e)?!0:Dt.some(t=>e.startsWith(t))}function Ot(e,t){let n=P(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return a.isAbsolute(i)?i:a.resolve(t.cwd,i)}function G(e,t){if(t.depth>=5)return{action:`passthrough`,rule:null,reason:`max evaluation depth exceeded`};let n=e.trim();if(!n)return{action:`passthrough`,rule:null,reason:null};if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=We(n);if(r.length===0)return{action:`passthrough`,rule:null,reason:null};let i=null,a=null,o=t;for(let e of r){if(e.operator===`&&`){let t=Ot(e.command,o);t&&(o={...o,cwd:t})}let t=kt(e.command,o);if(t.action===`deny`)return t;t.action===`passthrough`&&(i||=t),t.action===`allow`&&(a=t)}return i||(r.length===1&&a?a:{action:`allow`,rule:`all-segments`,reason:null})}function K(e,t){if(e.pre){let n=G(e.pre,t);if(n.action!==`allow`)return n}let n=G(e.main,t);if(n.action!==`allow`)return n;if(e.post){let n=G(e.post,t);if(n.action!==`allow`)return n}return`allow`}function kt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=P(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let i=r.tokens;if(i.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<i.length&&/^\\w+=/.test(i[o]);){let e=i[o].split(`=`)[0];if(W(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=i.length)return{action:`allow`,rule:`env-only`,reason:null};i=i.slice(o);let s=Ke(i,e);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};for(let e=0;e<i.length;e++){let t=i[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(i[e+1]===`/dev/null`)continue;return{action:`passthrough`,rule:null,reason:`output redirect to file`}}if(t===`<`)return{action:`passthrough`,rule:null,reason:`input redirect`}}}let c=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);i=i.filter((e,t)=>!(c.has(e)||(e===`>`||e===`>>`)&&i[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(i[t-1]===`>`||i[t-1]===`>>`)));let l=a.basename(i[0]);if(l===`command`&&i.length>1)return G(i.slice(1).join(` `),{...t,depth:t.depth+1});if(l===`env`){let e=1;for(;e<i.length&&/^\\w+=/.test(i[e]);){let t=i[e].split(`=`)[0];if(W(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:G(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return At(i,t);if(l===`npm`)return Mt(i);if(l===`bash`||l===`sh`||l===`zsh`)return Ft(i,t);if(l===`xargs`)return Lt(i,t);if(l===`node`&&(i.includes(`-e`)||i.includes(`-p`)||i.includes(`--eval`)||i.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(l===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of gt)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of V)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};return{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function At(e,t){let n=e.slice(1),r,i=-1,o=!1,s=!1,c;for(let a=0;a<n.length;a++){let l=n[a];if(l===`--filter`||l.startsWith(`--filter=`))return Nt(e,n,a,t);if(l===`-r`||l===`--recursive`){o=!0;continue}if(l===`-w`||l===`--workspace-root`){s=!0;continue}if(l===`-C`||l===`--dir`){c=n[++a];continue}if(l.startsWith(`-C=`)||l.startsWith(`--dir=`)){c=l.split(`=`,2)[1];continue}if(!(l.startsWith(`-`)&&!z.has(l))){r=l,i=a;break}}if(!r)return{action:`passthrough`,rule:null,reason:null};if(r===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(r===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(r===`exec`){let e=n.slice(i+1);return e.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:G(e.join(` `),{...t,depth:t.depth+1})}let l;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};l=e}else if(!z.has(r))l=r;else{for(let n of V)if(n.match.test(`pnpm`)&&(!n.when||n.when(e,t)))return{action:`allow`,rule:n.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}if(o)return jt(l,t);if(s){let e=bt(t.cwd);return e?q(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return q(l,{...t,cwd:e})}return q(l,t)}function q(e,t){let n=H(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=K(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function jt(e,t){let n=yt(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=K(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function Mt(e){for(let t of V)if(t.match.test(`npm`)&&(!t.when||t.when(e,{cwd:``,depth:0})))return{action:`allow`,rule:t.name,reason:null};return{action:`deny`,rule:`npm-redirect`,reason:ct(e)}}function Nt(e,t,n,r){let i=t[n],a,o;if(i.startsWith(`--filter=`))a=i.slice(9),o=n+1;else{if(a=t[n+1],!a||a.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};o=n+2}if(!a)return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};let s;for(let e=o;e<t.length;e++){let n=t[e];if(!(n.startsWith(`-`)&&!z.has(n))){s=n;break}}if(!s)return{action:`passthrough`,rule:null,reason:`pnpm --filter without subcommand`};if(s===`install`||s===`i`)return t.slice(t.indexOf(s)+1).every(e=>e.startsWith(`-`))?{action:`allow`,rule:`pnpm`,reason:null}:{action:`passthrough`,rule:null,reason:`pnpm --filter install with package arg`};if(s===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(s===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`,`licenses`,`audit`]).has(s))return{action:`allow`,rule:`pnpm`,reason:null};if(s===`exec`){let e=t.indexOf(`exec`,o),n=t.slice(e+1);return n.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:G(n.join(` `),{...r,depth:r.depth+1})}let c=s===`run`?t[t.indexOf(`run`,o)+1]:s;if(!c||c.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter run without script name`};if(z.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=vt(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=K(l,{...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd});return u===`allow`?{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}:u}const Pt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Ft(e,t){let n=e.indexOf(`-c`);if(n===-1)return{action:`passthrough`,rule:null,reason:`shell without -c`};for(let t=1;t<e.length;t++){if(t===n||t===n+1)continue;let r=e[t];if(r.startsWith(`-`)||Pt.has(r))return{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}}let r=e[n+1];return r?e.length>n+2?{action:`passthrough`,rule:null,reason:`shell -c with positional args`}:n===1?G(r,{...t,depth:t.depth+1}):{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}:{action:`passthrough`,rule:null,reason:`shell -c without command`}}const J=new Set([`-I`,`-d`,`-n`,`-P`,`-s`,`-L`,`-E`,`-a`,`--max-args`,`--max-procs`,`--delimiter`,`--replace`,`--max-lines`,`--max-chars`,`--arg-file`,`--process-slot-var`]),It=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function Lt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(J.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(J.has(e)){n++;continue}}if(It.has(t)){n++;continue}return{action:`passthrough`,rule:null,reason:`xargs with unrecognised flags`}}return n>=e.length?{action:`passthrough`,rule:null,reason:`xargs without command`}:G(e.slice(n).join(` `),{...t,depth:t.depth+1})}const Y=new Set([`Read`,`Glob`,`Grep`]),X=new Set([`Write`,`Edit`]),Rt=[{name:`env-file-read`,tools:Y,action:`deny`,when:e=>I(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:X,action:`deny`,when:e=>I(e),reason:`Don't write to .env files. These contain secrets.`},{name:`outside-cwd-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n!==t.cwd&&!n.startsWith(t.cwd+a.sep)},reason:`Don't write files outside the project directory.`}],zt=[{name:`cwd-read`,tools:Y,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}},{name:`cwd-write`,tools:X,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Bt=new Set([`Read`,`Write`,`Edit`]),Vt=new Set([`Glob`,`Grep`]);function Ht(e,t,n){return Bt.has(e)?Re(t)?.file_path??null:Vt.has(e)?ze(t)?.path??n:null}function Ut(e,t,n){let r=Ht(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Rt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of zt)if(t.tools.has(e)&&t.when(r,n))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const Z=await Ne();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);let Q,$;if(Z.tool_name===`Bash`){let e=Le(Z.tool_input);e||process.exit(0),Q=G(e.command,{cwd:Z.cwd,depth:0}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=Ut(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}Ue({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?N(`allow`):Q.action===`deny`&&N(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **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- **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- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\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)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\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")];
2477
2711
  }
2478
2712
  //#endregion
2479
2713
  //#region src/generators/claude-settings.ts
@@ -2742,175 +2976,6 @@ async function generateChangesets(ctx) {
2742
2976
  };
2743
2977
  }
2744
2978
  //#endregion
2745
- //#region src/generators/release-ci.ts
2746
- /** Common setup steps shared by release-it and simple strategies. */
2747
- function commonSteps(nodeVersionYaml, publishesNpm) {
2748
- const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
2749
- return [
2750
- { step: {
2751
- uses: "actions/checkout@v6",
2752
- with: { "fetch-depth": 0 }
2753
- } },
2754
- { step: { uses: "pnpm/action-setup@v5" } },
2755
- { step: {
2756
- uses: "actions/setup-node@v6",
2757
- with: {
2758
- ...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
2759
- cache: "pnpm",
2760
- ...publishesNpm && { "registry-url": actionsExpr("vars.NPM_REGISTRY_URL || 'https://registry.npmjs.org'") }
2761
- }
2762
- } },
2763
- { step: { run: "pnpm install --frozen-lockfile" } }
2764
- ];
2765
- }
2766
- function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
2767
- const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
2768
- const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
2769
- return [...commonSteps(nodeVersionYaml, publishesNpm), { step: {
2770
- run: "pnpm release-it --ci",
2771
- env: {
2772
- ...tokenEnv,
2773
- ...npmEnv,
2774
- RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2775
- }
2776
- } }];
2777
- }
2778
- /** Build the workflow_dispatch trigger with optional inputs for the simple strategy. */
2779
- function simpleWorkflowDispatchTrigger() {
2780
- return { workflow_dispatch: { inputs: {
2781
- bump: {
2782
- description: "Version bump type (default: conventional-commits auto-detect)",
2783
- required: false,
2784
- type: "choice",
2785
- default: "auto",
2786
- options: [
2787
- "auto",
2788
- "major",
2789
- "minor",
2790
- "patch",
2791
- "first-release"
2792
- ]
2793
- },
2794
- prerelease: {
2795
- description: "Create a prerelease with the given tag (e.g., beta, alpha)",
2796
- required: false,
2797
- type: "string"
2798
- }
2799
- } } };
2800
- }
2801
- /** Build the release:simple run command with conditional flags from workflow inputs. */
2802
- function simpleReleaseCommand() {
2803
- return [
2804
- "FLAGS=",
2805
- `case "${actionsExpr("inputs.bump")}" in`,
2806
- " major|minor|patch) FLAGS=\"$FLAGS --release-as " + actionsExpr("inputs.bump") + "\" ;;",
2807
- " first-release) FLAGS=\"$FLAGS --first-release\" ;;",
2808
- "esac",
2809
- `if [ -n "${actionsExpr("inputs.prerelease")}" ]; then FLAGS="$FLAGS --prerelease ${actionsExpr("inputs.prerelease")}"; fi`,
2810
- "pnpm exec bst release:simple $FLAGS"
2811
- ].join("\n");
2812
- }
2813
- /** Build the docker publish env block, omitting registry vars when derivable at runtime. */
2814
- function dockerPublishEnv(registryDerivable) {
2815
- return {
2816
- ...registryDerivable ? {} : {
2817
- DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
2818
- DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
2819
- },
2820
- DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
2821
- DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
2822
- RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2823
- };
2824
- }
2825
- function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
2826
- const releaseStep = { step: {
2827
- name: "Release",
2828
- env: {
2829
- ...ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
2830
- FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
2831
- FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2832
- RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
2833
- },
2834
- RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2835
- },
2836
- run: simpleReleaseCommand()
2837
- } };
2838
- const dockerStep = { step: {
2839
- name: "Publish Docker images",
2840
- if: "success()",
2841
- env: dockerPublishEnv(registryDerivable),
2842
- run: "pnpm exec bst docker:publish"
2843
- } };
2844
- return [
2845
- ...commonSteps(nodeVersionYaml, publishesNpm),
2846
- releaseStep,
2847
- ...hasDocker ? [dockerStep] : []
2848
- ];
2849
- }
2850
- function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
2851
- switch (strategy) {
2852
- case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
2853
- case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
2854
- default: return null;
2855
- }
2856
- }
2857
- async function generateReleaseCi(ctx) {
2858
- const filePath = "release-ci";
2859
- if (ctx.config.releaseStrategy === "none" || ctx.config.ci === "none") return {
2860
- filePath,
2861
- action: "skipped",
2862
- description: "Release CI workflow not applicable"
2863
- };
2864
- const publishesNpm = ctx.config.publishNpm === true;
2865
- if (ctx.config.releaseStrategy === "changesets") return {
2866
- filePath,
2867
- action: "skipped",
2868
- description: "Release step included in CI workflow"
2869
- };
2870
- const isGitHub = ctx.config.ci === "github";
2871
- const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
2872
- const nodeVersionYaml = computeNodeVersionYaml(ctx);
2873
- const hasDocker = hasDockerPackages(ctx);
2874
- const registryDerivable = hasRepositoryUrl(ctx);
2875
- const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
2876
- if (!steps) return {
2877
- filePath,
2878
- action: "skipped",
2879
- description: "Release CI workflow not applicable"
2880
- };
2881
- const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
2882
- const content = buildWorkflowYaml({
2883
- ci: ctx.config.ci,
2884
- name: "Release",
2885
- on,
2886
- ...isGitHub && { permissions: { contents: "write" } },
2887
- jobName: "release",
2888
- steps
2889
- });
2890
- const alreadyExists = ctx.exists(workflowPath);
2891
- const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
2892
- if (existing) {
2893
- if (isToolingIgnored(existing)) return {
2894
- filePath: workflowPath,
2895
- action: "skipped",
2896
- description: "Release workflow has ignore comment"
2897
- };
2898
- if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
2899
- filePath: workflowPath,
2900
- action: "skipped",
2901
- description: "Release workflow already up to date"
2902
- };
2903
- }
2904
- const withDevPath = preserveDevBinaryPath(content, existing);
2905
- const final = existing ? insertCustomBlocks(withDevPath, extractCustomBlocks(existing)) : withDevPath;
2906
- ctx.write(workflowPath, final);
2907
- return {
2908
- filePath: workflowPath,
2909
- action: alreadyExists ? "updated" : "created",
2910
- description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions release workflow`
2911
- };
2912
- }
2913
- //#endregion
2914
2979
  //#region src/generators/lefthook.ts
2915
2980
  function requiredCommands(formatter) {
2916
2981
  return {
@@ -3268,7 +3333,7 @@ async function runGenerators(ctx) {
3268
3333
  function generateMigratePrompt(results, config, detected) {
3269
3334
  const sections = [];
3270
3335
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3271
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.41.1 repo:sync\` on ${timestamp}_\n`);
3336
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.43.0 repo:sync\` on ${timestamp}_\n`);
3272
3337
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3273
3338
  if (changed.length > 0) {
3274
3339
  const items = changed.map((r) => {
@@ -4175,6 +4240,73 @@ async function runVersionMode(executor, config) {
4175
4240
  };
4176
4241
  }
4177
4242
  //#endregion
4243
+ //#region src/release/hook.ts
4244
+ const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
4245
+ /** Read the postReleaseHook command override from .tooling.json at runtime. */
4246
+ function loadHookCommand(executor, cwd) {
4247
+ const configPath = path.join(cwd, ".tooling.json");
4248
+ const raw = executor.readFile(configPath);
4249
+ if (!raw) return void 0;
4250
+ try {
4251
+ const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
4252
+ if (!result.success) return void 0;
4253
+ return result.data.postReleaseHook;
4254
+ } catch (_error) {
4255
+ return;
4256
+ }
4257
+ }
4258
+ /**
4259
+ * Execute the post-release hook after a successful publish.
4260
+ *
4261
+ * Uses the `postReleaseHook` command from `.tooling.json` if configured,
4262
+ * otherwise curls `POST_RELEASE_HOOK_URL` with optional Bearer auth from
4263
+ * `POST_RELEASE_HOOK_SECRET`. Both env vars must be set by the CI workflow.
4264
+ *
4265
+ * Failures are logged as warnings — they never fail the release.
4266
+ */
4267
+ function runPostReleaseHook(executor, config) {
4268
+ if (config.dryRun) {
4269
+ debug(config, "Skipping post-release hook (dry-run)");
4270
+ return;
4271
+ }
4272
+ const customCommand = loadHookCommand(executor, config.cwd);
4273
+ if (customCommand) {
4274
+ debug(config, `Running custom post-release hook: ${customCommand}`);
4275
+ executeHookCommand(executor, config, customCommand);
4276
+ return;
4277
+ }
4278
+ const hookUrl = process.env["POST_RELEASE_HOOK_URL"];
4279
+ if (!hookUrl) {
4280
+ debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
4281
+ return;
4282
+ }
4283
+ const hookSecret = process.env["POST_RELEASE_HOOK_SECRET"];
4284
+ const args = [
4285
+ "-fsS",
4286
+ "-X",
4287
+ "POST"
4288
+ ];
4289
+ if (hookSecret) args.push("-H", `Authorization: Bearer ${hookSecret}`);
4290
+ args.push(hookUrl);
4291
+ debug(config, `Running post-release hook: curl ${args.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}`);
4292
+ try {
4293
+ const result = executor.exec("curl", args, { cwd: config.cwd });
4294
+ if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
4295
+ else log.info("Post-release hook completed successfully");
4296
+ } catch (error) {
4297
+ log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
4298
+ }
4299
+ }
4300
+ function executeHookCommand(executor, config, command) {
4301
+ try {
4302
+ const result = executor.exec("sh", ["-c", command], { cwd: config.cwd });
4303
+ if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
4304
+ else log.info("Post-release hook completed successfully");
4305
+ } catch (error) {
4306
+ log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
4307
+ }
4308
+ }
4309
+ //#endregion
4178
4310
  //#region src/release/publish.ts
4179
4311
  /** Mode 2: publish to npm, push tags, and create Forgejo releases. */
4180
4312
  async function runPublishMode(executor, config) {
@@ -4274,6 +4406,7 @@ async function runPublishMode(executor, config) {
4274
4406
  if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4275
4407
  else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
4276
4408
  }
4409
+ runPostReleaseHook(executor, config);
4277
4410
  return {
4278
4411
  mode: "publish",
4279
4412
  tags: allTags
@@ -4697,6 +4830,7 @@ async function runSimpleRelease(executor, config) {
4697
4830
  }
4698
4831
  let releaseCreated = false;
4699
4832
  if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
4833
+ if (pushed) runPostReleaseHook(executor, config);
4700
4834
  return {
4701
4835
  version,
4702
4836
  tag,
@@ -5506,37 +5640,36 @@ const TOKEN_SCOPES = [
5506
5640
  "write:package",
5507
5641
  "write:repository"
5508
5642
  ];
5643
+ function createForgejoSecretsProvider(executor, conn) {
5644
+ return { async setSecret(_repo, name, value) {
5645
+ await setActionSecret(executor, conn, name, value);
5646
+ } };
5647
+ }
5648
+ function createGitHubSecretsProvider(executor) {
5649
+ return { async setSecret(repo, name, value) {
5650
+ const result = executor.exec("gh", [
5651
+ "secret",
5652
+ "set",
5653
+ name,
5654
+ "--body",
5655
+ value,
5656
+ "-R",
5657
+ repo
5658
+ ]);
5659
+ if (result.exitCode !== 0) throw new FatalError(`Failed to set GitHub secret ${name}: ${result.stderr}`);
5660
+ } };
5661
+ }
5509
5662
  /**
5510
5663
  * Core logic for setup:secrets, separated from the command handler for testability.
5664
+ * Platform-agnostic — uses the injected SecretsProvider to set secrets.
5511
5665
  */
5512
- async function runSetupSecrets(executor, config) {
5666
+ async function runSetupSecrets(executor, provider, config) {
5513
5667
  const repoName = config.repository.split("/")[1] ?? config.repository;
5514
- const tokenName = `bst-release-${repoName}`;
5515
5668
  const secretsSet = [];
5516
- let token;
5517
- if (config.dryRun) {
5518
- log.info(`[dry-run] Would create access token "${tokenName}" for ${config.username}`);
5519
- log.info(`[dry-run] Scopes: ${TOKEN_SCOPES.join(", ")}`);
5520
- token = "dry-run-token";
5521
- } else {
5522
- log.info(`Creating access token "${tokenName}"...`);
5523
- try {
5524
- token = await createAccessToken(executor, config.serverUrl, config.username, config.password, tokenName, TOKEN_SCOPES);
5525
- log.success(`Created access token "${tokenName}"`);
5526
- } catch (error) {
5527
- if (error instanceof Error && error.message.includes("422")) throw new FatalError(`Access token "${tokenName}" already exists. Delete it in Forgejo settings and re-run, or set secrets manually.`);
5528
- throw error;
5529
- }
5530
- }
5531
- const conn = {
5532
- serverUrl: config.serverUrl,
5533
- repository: config.repository,
5534
- token
5535
- };
5536
5669
  if (config.dryRun) log.info(`[dry-run] Would set secret RELEASE_TOKEN on ${config.repository}`);
5537
5670
  else {
5538
5671
  log.info("Setting RELEASE_TOKEN secret...");
5539
- await setActionSecret(executor, conn, "RELEASE_TOKEN", token);
5672
+ await provider.setSecret(config.repository, "RELEASE_TOKEN", config.releaseToken);
5540
5673
  log.success("Set RELEASE_TOKEN");
5541
5674
  }
5542
5675
  secretsSet.push("RELEASE_TOKEN");
@@ -5546,27 +5679,105 @@ async function runSetupSecrets(executor, config) {
5546
5679
  const dockerPackages = detectDockerPackages(executor, config.cwd, rootName);
5547
5680
  if (dockerPackages.length > 0) {
5548
5681
  log.info(`Detected ${String(dockerPackages.length)} Docker package(s) — setting Docker secrets`);
5682
+ const dockerUser = config.dockerCredentials?.username ?? repoName;
5683
+ const dockerPass = config.dockerCredentials?.password ?? config.releaseToken;
5684
+ if (!config.dockerCredentials) log.info(`Using defaults for Docker secrets: username="${dockerUser}", password=<release token>`);
5549
5685
  if (config.dryRun) {
5550
5686
  log.info(`[dry-run] Would set secret DOCKER_USERNAME on ${config.repository}`);
5551
5687
  log.info(`[dry-run] Would set secret DOCKER_PASSWORD on ${config.repository}`);
5552
5688
  } else {
5553
- await setActionSecret(executor, conn, "DOCKER_USERNAME", config.username);
5689
+ await provider.setSecret(config.repository, "DOCKER_USERNAME", dockerUser);
5554
5690
  log.success("Set DOCKER_USERNAME");
5555
- await setActionSecret(executor, conn, "DOCKER_PASSWORD", token);
5691
+ await provider.setSecret(config.repository, "DOCKER_PASSWORD", dockerPass);
5556
5692
  log.success("Set DOCKER_PASSWORD");
5557
5693
  }
5558
5694
  secretsSet.push("DOCKER_USERNAME", "DOCKER_PASSWORD");
5559
5695
  }
5560
5696
  }
5697
+ return { secretsSet };
5698
+ }
5699
+ /**
5700
+ * Create a Forgejo access token with user-friendly error wrapping.
5701
+ * Exported for testability — called by the command handler's Forgejo path.
5702
+ */
5703
+ async function createForgejoReleaseToken(executor, config) {
5704
+ const tokenName = `bst-release-${config.repository.split("/")[1] ?? config.repository}`;
5705
+ if (config.dryRun) {
5706
+ log.info(`[dry-run] Would create access token "${tokenName}" for ${config.username}`);
5707
+ log.info(`[dry-run] Scopes: ${TOKEN_SCOPES.join(", ")}`);
5708
+ return {
5709
+ token: "dry-run-token",
5710
+ tokenName
5711
+ };
5712
+ }
5713
+ log.info(`Creating access token "${tokenName}"...`);
5714
+ try {
5715
+ const token = await createAccessToken(executor, config.serverUrl, config.username, config.password, tokenName, TOKEN_SCOPES);
5716
+ log.success(`Created access token "${tokenName}"`);
5717
+ return {
5718
+ token,
5719
+ tokenName
5720
+ };
5721
+ } catch (error) {
5722
+ if (error instanceof Error && error.message.includes("422")) throw new FatalError(`Access token "${tokenName}" already exists. Delete it in Forgejo settings and re-run, or set secrets manually.`);
5723
+ throw error;
5724
+ }
5725
+ }
5726
+ async function setupForgejo(executor, serverUrl, repository, dryRun) {
5727
+ const username = await p.text({
5728
+ message: "Forgejo username",
5729
+ validate: (v) => !v || v.length === 0 ? "Username is required" : void 0
5730
+ });
5731
+ if (isCancelled(username)) {
5732
+ p.cancel("Cancelled.");
5733
+ process.exit(0);
5734
+ }
5735
+ const password = await p.password({
5736
+ message: "Forgejo password",
5737
+ validate: (v) => !v || v.length === 0 ? "Password is required" : void 0
5738
+ });
5739
+ if (isCancelled(password)) {
5740
+ p.cancel("Cancelled.");
5741
+ process.exit(0);
5742
+ }
5743
+ const { token } = await createForgejoReleaseToken(executor, {
5744
+ serverUrl,
5745
+ repository,
5746
+ username,
5747
+ password,
5748
+ dryRun
5749
+ });
5561
5750
  return {
5562
- tokenCreated: !config.dryRun,
5563
- secretsSet
5751
+ provider: createForgejoSecretsProvider(executor, {
5752
+ serverUrl,
5753
+ repository,
5754
+ token
5755
+ }),
5756
+ releaseToken: token,
5757
+ dockerCredentials: {
5758
+ username,
5759
+ password: token
5760
+ }
5761
+ };
5762
+ }
5763
+ async function setupGitHub(executor) {
5764
+ const token = await p.password({
5765
+ message: "GitHub Personal Access Token (PAT)",
5766
+ validate: (v) => !v || v.length === 0 ? "Token is required" : void 0
5767
+ });
5768
+ if (isCancelled(token)) {
5769
+ p.cancel("Cancelled.");
5770
+ process.exit(0);
5771
+ }
5772
+ return {
5773
+ provider: createGitHubSecretsProvider(executor),
5774
+ releaseToken: token
5564
5775
  };
5565
5776
  }
5566
5777
  const setupSecretsCommand = defineCommand({
5567
5778
  meta: {
5568
5779
  name: "setup:secrets",
5569
- description: "Create a Forgejo access token and configure CI secrets for this repository"
5780
+ description: "Configure CI secrets for this repository (Forgejo or GitHub)"
5570
5781
  },
5571
5782
  args: {
5572
5783
  "dry-run": {
@@ -5576,49 +5787,41 @@ const setupSecretsCommand = defineCommand({
5576
5787
  "no-docker": {
5577
5788
  type: "boolean",
5578
5789
  description: "Skip Docker secrets setup even if Docker packages are detected"
5579
- },
5580
- verbose: {
5581
- type: "boolean",
5582
- description: "Enable verbose output"
5583
5790
  }
5584
5791
  },
5585
5792
  async run({ args }) {
5586
5793
  const cwd = process.cwd();
5587
- const verbose = args.verbose === true || isEnvVerbose();
5588
5794
  const dryRun = args["dry-run"] === true;
5589
5795
  const noDocker = args["no-docker"] === true;
5590
5796
  p.intro("setup:secrets");
5591
5797
  const parsed = parseRepositoryUrl(cwd);
5592
- if (!parsed) throw new FatalError("No repository URL found in package.json. Add a `repository` field pointing to your Forgejo instance.");
5593
- if (parsed.hostname === "github.com") throw new FatalError("setup:secrets is for Forgejo repositories. GitHub uses different auth.");
5594
- const serverUrl = `${parsed.protocol}//${parsed.hostname}`;
5798
+ if (!parsed) throw new FatalError("No repository URL found in package.json. Add a `repository` field.");
5595
5799
  const repository = parsed.repository;
5596
- log.info(`Forgejo: ${serverUrl} / ${repository}`);
5597
- const username = await p.text({
5598
- message: "Forgejo username",
5599
- validate: (v) => !v || v.length === 0 ? "Username is required" : void 0
5600
- });
5601
- if (isCancelled(username)) {
5602
- p.cancel("Cancelled.");
5603
- process.exit(0);
5604
- }
5605
- const password = await p.password({
5606
- message: "Forgejo password",
5607
- validate: (v) => !v || v.length === 0 ? "Password is required" : void 0
5608
- });
5609
- if (isCancelled(password)) {
5610
- p.cancel("Cancelled.");
5611
- process.exit(0);
5800
+ const isGitHub = parsed.hostname === "github.com";
5801
+ const executor = createRealExecutor();
5802
+ let provider;
5803
+ let releaseToken;
5804
+ let dockerCredentials;
5805
+ if (isGitHub) {
5806
+ log.info(`GitHub: ${repository}`);
5807
+ const setup = await setupGitHub(executor);
5808
+ provider = setup.provider;
5809
+ releaseToken = setup.releaseToken;
5810
+ } else {
5811
+ const serverUrl = `${parsed.protocol}//${parsed.hostname}`;
5812
+ log.info(`Forgejo: ${serverUrl} / ${repository}`);
5813
+ const setup = await setupForgejo(executor, serverUrl, repository, dryRun);
5814
+ provider = setup.provider;
5815
+ releaseToken = setup.releaseToken;
5816
+ dockerCredentials = setup.dockerCredentials;
5612
5817
  }
5613
- const summary = (await runSetupSecrets(createRealExecutor(), {
5614
- serverUrl,
5818
+ const summary = (await runSetupSecrets(executor, provider, {
5615
5819
  repository,
5616
- username,
5617
- password,
5820
+ releaseToken,
5618
5821
  cwd,
5619
5822
  dryRun,
5620
5823
  noDocker,
5621
- verbose
5824
+ dockerCredentials
5622
5825
  })).secretsSet.map((s) => ` - ${s}`).join("\n");
5623
5826
  p.outro(`Done! Secrets ${dryRun ? "that would be" : ""} configured:\n${summary}`);
5624
5827
  }
@@ -5756,7 +5959,7 @@ const forgejoSecretsCommand = defineCommand({
5756
5959
  const main = defineCommand({
5757
5960
  meta: {
5758
5961
  name: "bst",
5759
- version: "0.41.1",
5962
+ version: "0.43.0",
5760
5963
  description: "Bootstrap and maintain standardized TypeScript project tooling"
5761
5964
  },
5762
5965
  subCommands: {
@@ -5775,7 +5978,7 @@ const main = defineCommand({
5775
5978
  "forgejo:secrets": forgejoSecretsCommand
5776
5979
  }
5777
5980
  });
5778
- console.log(`@bensandee/tooling v0.41.1`);
5981
+ console.log(`@bensandee/tooling v0.43.0`);
5779
5982
  async function run() {
5780
5983
  await runMain(main);
5781
5984
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.41.1",
3
+ "version": "0.43.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,7 +54,7 @@
54
54
  "prettier": "3.8.1",
55
55
  "release-it": "19.2.4",
56
56
  "tsdown": "0.21.6",
57
- "@bensandee/claude-hooks": "0.5.0",
57
+ "@bensandee/claude-hooks": "0.6.1",
58
58
  "@bensandee/config": "0.10.1"
59
59
  },
60
60
  "scripts": {
@@ -168,6 +168,17 @@
168
168
  "registryNamespace": {
169
169
  "description": "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)",
170
170
  "type": "string"
171
+ },
172
+ "postReleaseHook": {
173
+ "description": "Override the post-release hook command (default: curl POST_RELEASE_HOOK_URL secret if set)",
174
+ "type": "string"
175
+ },
176
+ "postReleaseHookSecrets": {
177
+ "description": "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])",
178
+ "type": "array",
179
+ "items": {
180
+ "type": "string"
181
+ }
171
182
  }
172
183
  },
173
184
  "additionalProperties": false