@bensandee/tooling 0.45.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -4,16 +4,16 @@ import { defineCommand, runMain } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  import { isCancel, select } from "@clack/prompts";
6
6
  import path from "node:path";
7
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
7
+ import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { execFileSync, execSync } from "node:child_process";
9
9
  import JSON5 from "json5";
10
10
  import { parse } from "jsonc-parser";
11
11
  import { z } from "zod";
12
12
  import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
13
13
  import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
14
- import { createHmac } from "node:crypto";
15
14
  import picomatch from "picomatch";
16
15
  import { tmpdir } from "node:os";
16
+ import { createHmac } from "node:crypto";
17
17
  //#region src/types.ts
18
18
  const LEGACY_TOOLS = [
19
19
  "eslint",
@@ -538,7 +538,7 @@ function createContext(config, confirmOverwrite) {
538
538
  remove: (rel) => {
539
539
  assertContained(config.targetDir, rel);
540
540
  const fullPath = path.join(config.targetDir, rel);
541
- if (existsSync(fullPath)) rmSync(fullPath);
541
+ if (existsSync(fullPath)) rmSync(fullPath, { recursive: true });
542
542
  },
543
543
  confirmOverwrite
544
544
  };
@@ -1129,13 +1129,14 @@ function normalizeWorkflow(content) {
1129
1129
  const DEV_BINARY_PATTERN = /node\s+packages\/tooling-cli\/dist\/bin\.mjs/;
1130
1130
  /**
1131
1131
  * If the existing file uses the dev binary path (`node packages/tooling-cli/dist/bin.mjs`),
1132
- * substitute it back into the generated content so we don't overwrite it with `pnpm exec bst`.
1132
+ * substitute it back into the generated content so we don't overwrite it with `pnpm exec bst`
1133
+ * or `pnpm dlx @bensandee/tooling`.
1133
1134
  */
1134
1135
  function preserveDevBinaryPath(generated, existing) {
1135
1136
  if (!existing) return generated;
1136
1137
  const match = DEV_BINARY_PATTERN.exec(existing);
1137
1138
  if (!match) return generated;
1138
- return generated.replaceAll("pnpm exec bst", match[0]);
1139
+ return generated.replaceAll("pnpm exec bst", match[0]).replaceAll("pnpm dlx @bensandee/tooling", match[0]);
1139
1140
  }
1140
1141
  /** Build a complete workflow YAML string from structured options. Single source of truth for workflow files. */
1141
1142
  function buildWorkflowYaml(options) {
@@ -1167,6 +1168,18 @@ function hasEnginesNode(ctx) {
1167
1168
  function computeNodeVersionYaml(ctx) {
1168
1169
  return hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
1169
1170
  }
1171
+ /** Build the docker publish env block, omitting registry vars when derivable at runtime. */
1172
+ function dockerPublishEnv(registryDerivable) {
1173
+ return {
1174
+ ...registryDerivable ? {} : {
1175
+ DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
1176
+ DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
1177
+ },
1178
+ DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
1179
+ DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
1180
+ RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
1181
+ };
1182
+ }
1170
1183
  //#endregion
1171
1184
  //#region src/generators/publish-ci.ts
1172
1185
  /**
@@ -1179,18 +1192,6 @@ function hasRepositoryUrl(ctx) {
1179
1192
  if (!raw) return false;
1180
1193
  return parsePackageJson(raw)?.repository !== void 0;
1181
1194
  }
1182
- /** Build the docker publish env block, omitting registry vars when derivable at runtime. */
1183
- function dockerPublishEnv$1(registryDerivable) {
1184
- return {
1185
- ...registryDerivable ? {} : {
1186
- DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
1187
- DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
1188
- },
1189
- DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
1190
- DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
1191
- RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
1192
- };
1193
- }
1194
1195
  function publishSteps(nodeVersionYaml, registryDerivable) {
1195
1196
  return [
1196
1197
  { step: { uses: "actions/checkout@v6" } },
@@ -1202,13 +1203,13 @@ function publishSteps(nodeVersionYaml, registryDerivable) {
1202
1203
  { step: { run: "pnpm install --frozen-lockfile" } },
1203
1204
  { step: {
1204
1205
  name: "Publish Docker images",
1205
- env: dockerPublishEnv$1(registryDerivable),
1206
+ env: dockerPublishEnv(registryDerivable),
1206
1207
  run: "pnpm exec bst docker:publish"
1207
1208
  } }
1208
1209
  ];
1209
1210
  }
1210
1211
  const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
1211
- const PostReleaseHookSchema$1 = z.object({
1212
+ const PostReleaseHookSchema = z.object({
1212
1213
  postReleaseHook: z.string().optional(),
1213
1214
  postReleaseHookSecrets: z.array(z.string()).optional()
1214
1215
  });
@@ -1217,7 +1218,7 @@ function getPostReleaseHookCommand(ctx) {
1217
1218
  const raw = ctx.read(".tooling.json");
1218
1219
  if (!raw) return void 0;
1219
1220
  try {
1220
- const result = PostReleaseHookSchema$1.safeParse(JSON.parse(raw));
1221
+ const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
1221
1222
  if (!result.success) return void 0;
1222
1223
  return result.data.postReleaseHook;
1223
1224
  } catch (error) {
@@ -1230,7 +1231,7 @@ function getPostReleaseHookSecrets(ctx) {
1230
1231
  const raw = ctx.read(".tooling.json");
1231
1232
  if (!raw) return void 0;
1232
1233
  try {
1233
- const result = PostReleaseHookSchema$1.safeParse(JSON.parse(raw));
1234
+ const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
1234
1235
  if (!result.success) return void 0;
1235
1236
  return result.data.postReleaseHookSecrets;
1236
1237
  } catch (error) {
@@ -1584,7 +1585,7 @@ async function generatePackageJson(ctx) {
1584
1585
  const devDeps = { ...ROOT_DEV_DEPS };
1585
1586
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1586
1587
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
1587
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.45.1";
1588
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.47.0";
1588
1589
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
1589
1590
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1590
1591
  "@changesets/cli": "2.30.0",
@@ -1653,6 +1654,16 @@ async function generatePackageJson(ctx) {
1653
1654
  changes.push(`updated devDependency: ${key} to ${value}`);
1654
1655
  }
1655
1656
  pkg.devDependencies = existingDevDeps;
1657
+ const existingDeps = pkg.dependencies ?? {};
1658
+ const commonKey = "@bensandee/common";
1659
+ if (commonKey in existingDeps && existingDeps[commonKey] !== "workspace:*") {
1660
+ const commonVersion = isWorkspacePackage(ctx, commonKey) ? "workspace:*" : "0.1.2";
1661
+ if (existingDeps[commonKey] !== commonVersion) {
1662
+ existingDeps[commonKey] = commonVersion;
1663
+ pkg.dependencies = existingDeps;
1664
+ changes.push(`updated dependency: ${commonKey} to ${commonVersion}`);
1665
+ }
1666
+ }
1656
1667
  if (!pkg.repository) {
1657
1668
  const repoUrl = detectGitRemoteUrl(ctx.targetDir);
1658
1669
  if (repoUrl) {
@@ -2194,7 +2205,7 @@ async function generateGitignore(ctx) {
2194
2205
  //#region src/generators/release-ci.ts
2195
2206
  const DEFAULT_POST_RELEASE_COMMAND = [
2196
2207
  "if [ -n \"$POST_RELEASE_HOOK_URL\" ]; then",
2197
- " pnpm dlx @bensandee/tooling webhook:send",
2208
+ " pnpm exec bst webhook:send",
2198
2209
  " \"$POST_RELEASE_HOOK_URL\"; fi"
2199
2210
  ].join("");
2200
2211
  /** Build env vars for hook secrets to inject into a workflow step. */
@@ -2208,9 +2219,10 @@ function hookSecretEnv(secrets) {
2208
2219
  }
2209
2220
  /** Step that runs after release, gated on the POST_RELEASE_HOOK_URL secret being set. */
2210
2221
  function postReleaseHookStep(opts) {
2222
+ const ifCondition = opts?.condition ?? (opts?.guardMain ? "github.ref == 'refs/heads/main'" : void 0);
2211
2223
  return { step: {
2212
2224
  name: "Post-release hook",
2213
- ...opts?.guardMain && { if: "github.ref == 'refs/heads/main'" },
2225
+ ...ifCondition && { if: ifCondition },
2214
2226
  env: hookSecretEnv(opts?.secrets),
2215
2227
  run: opts?.command ?? DEFAULT_POST_RELEASE_COMMAND
2216
2228
  } };
@@ -2282,35 +2294,25 @@ function simpleReleaseCommand() {
2282
2294
  "pnpm exec bst release:simple $FLAGS"
2283
2295
  ].join("\n");
2284
2296
  }
2285
- /** Build the docker publish env block, omitting registry vars when derivable at runtime. */
2286
- function dockerPublishEnv(registryDerivable) {
2287
- return {
2288
- ...registryDerivable ? {} : {
2289
- DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
2290
- DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
2291
- },
2292
- DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
2293
- DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
2294
- RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2297
+ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
2298
+ const releaseEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
2299
+ FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
2300
+ FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2301
+ RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
2295
2302
  };
2296
- }
2297
- function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
2303
+ const pushedIf = "steps.release.outputs.pushed == 'true'";
2298
2304
  const releaseStep = { step: {
2305
+ id: "release",
2299
2306
  name: "Release",
2300
2307
  env: {
2301
- ...ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
2302
- FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
2303
- FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2304
- RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
2305
- },
2306
- ...hookSecretEnv(hookSecrets),
2308
+ ...releaseEnv,
2307
2309
  RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2308
2310
  },
2309
2311
  run: simpleReleaseCommand()
2310
2312
  } };
2311
2313
  const dockerStep = { step: {
2312
2314
  name: "Publish Docker images",
2313
- if: "success()",
2315
+ if: pushedIf,
2314
2316
  env: dockerPublishEnv(registryDerivable),
2315
2317
  run: "pnpm exec bst docker:publish"
2316
2318
  } };
@@ -2320,10 +2322,10 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, regist
2320
2322
  ...hasDocker ? [dockerStep] : []
2321
2323
  ];
2322
2324
  }
2323
- function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
2325
+ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
2324
2326
  switch (strategy) {
2325
2327
  case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
2326
- case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets);
2328
+ case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
2327
2329
  default: return null;
2328
2330
  }
2329
2331
  }
@@ -2346,7 +2348,7 @@ async function generateReleaseCi(ctx) {
2346
2348
  const hasDocker = hasDockerPackages(ctx);
2347
2349
  const registryDerivable = hasRepositoryUrl(ctx);
2348
2350
  const hookSecrets = getPostReleaseHookSecrets(ctx);
2349
- const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets);
2351
+ const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
2350
2352
  if (!steps) return {
2351
2353
  filePath,
2352
2354
  action: "skipped",
@@ -2356,6 +2358,11 @@ async function generateReleaseCi(ctx) {
2356
2358
  command: getPostReleaseHookCommand(ctx),
2357
2359
  secrets: hookSecrets
2358
2360
  }));
2361
+ else if (ctx.config.releaseStrategy === "simple") steps.push(postReleaseHookStep({
2362
+ command: getPostReleaseHookCommand(ctx),
2363
+ secrets: hookSecrets,
2364
+ condition: "steps.release.outputs.pushed == 'true'"
2365
+ }));
2359
2366
  const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
2360
2367
  const content = buildWorkflowYaml({
2361
2368
  ci: ctx.config.ci,
@@ -2391,8 +2398,9 @@ async function generateReleaseCi(ctx) {
2391
2398
  //#endregion
2392
2399
  //#region src/generators/ci.ts
2393
2400
  /** Build the release step for the check job (changesets strategy). */
2394
- function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
2401
+ function changesetsReleaseStep(ci, publishesNpm) {
2395
2402
  if (ci === "github") return { step: {
2403
+ id: "release",
2396
2404
  uses: "changesets/action@v1",
2397
2405
  if: "github.ref == 'refs/heads/main'",
2398
2406
  with: {
@@ -2405,6 +2413,7 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
2405
2413
  }
2406
2414
  } };
2407
2415
  return { step: {
2416
+ id: "release",
2408
2417
  name: "Release",
2409
2418
  if: "github.ref == 'refs/heads/main'",
2410
2419
  env: {
@@ -2412,7 +2421,6 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
2412
2421
  FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2413
2422
  RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
2414
2423
  ...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") },
2415
- ...hookSecretEnv(hookSecrets),
2416
2424
  RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
2417
2425
  },
2418
2426
  run: "pnpm exec bst release:changesets"
@@ -2462,14 +2470,22 @@ async function generateCi(ctx) {
2462
2470
  const publishesNpm = ctx.config.publishNpm === true;
2463
2471
  const hookCommand = getPostReleaseHookCommand(ctx);
2464
2472
  const hookSecrets = getPostReleaseHookSecrets(ctx);
2465
- const needsHookStep = isChangesets && isGitHub;
2473
+ const hasDocker = isChangesets && hasDockerPackages(ctx);
2474
+ const registryDerivable = hasDocker && hasRepositoryUrl(ctx);
2475
+ const publishedIf = "steps.release.outputs.published == 'true'";
2466
2476
  const steps = [
2467
2477
  ...checkSteps(nodeVersionYaml, isChangesets && publishesNpm),
2468
- ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, hookSecrets)] : [],
2469
- ...needsHookStep ? [postReleaseHookStep({
2478
+ ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : [],
2479
+ ...hasDocker ? [{ step: {
2480
+ name: "Publish Docker images",
2481
+ if: publishedIf,
2482
+ env: dockerPublishEnv(registryDerivable),
2483
+ run: "pnpm exec bst docker:publish"
2484
+ } }] : [],
2485
+ ...isChangesets ? [postReleaseHookStep({
2470
2486
  command: hookCommand,
2471
2487
  secrets: hookSecrets,
2472
- guardMain: true
2488
+ condition: publishedIf
2473
2489
  })] : []
2474
2490
  ];
2475
2491
  const content = buildWorkflowYaml({
@@ -2718,7 +2734,7 @@ function writeIfChanged(ctx, filePath, content, label) {
2718
2734
  };
2719
2735
  }
2720
2736
  async function generateClaudeHooks(ctx) {
2721
- return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.3\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\";import{execFileSync as u}from\"node:child_process\";Object.freeze({status:`aborted`});function d(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 f=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const p={};function m(e){return e&&Object.assign(p,e),p}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 h(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 ie=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ae(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 oe(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 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 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})},ce=d(`$ZodError`,S),C=d(`$ZodError`,S,{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 f;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,m())));throw ie(t,i?.callee),t}return o.value})(C),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=>x(e,a,m())));throw ie(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 f;return a.issues.length?{success:!1,error:new(e??ce)(a.issues.map(e=>x(e,i,m())))}:{success:!0,data:a.value}})(C),T=(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,m())))}:{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},E=d(`$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 f;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 f;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 f;return o.then(e=>t(e,r,a))}return t(o,r,a)}}h(e,`~standard`,()=>({validate:t=>{try{let n=w(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return T(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=d(`$ZodString`,(e,t)=>{E.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=d(`$ZodUnknown`,(e,t)=>{E.init(e,t),e._zod.parse=e=>e});function ge(e,t,n){e.issues.length&&t.issues.push(...y(n,e.issues)),t.value[n]=e.value}const _e=d(`$ZodArray`,(e,t)=>{E.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!Array.isArray(i))return n.issues.push({expected:`array`,code:`invalid_type`,input:i,inst:e}),n;n.value=Array(i.length);let a=[];for(let e=0;e<i.length;e++){let o=i[e],s=t.element._zod.run({value:o,issues:[]},r);s instanceof Promise?a.push(s.then(t=>ge(t,n,e))):ge(s,n,e)}return a.length?Promise.all(a).then(()=>n):n}});function D(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 ve(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 ye(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=>D(e,n,i,t,u))):D(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 be=d(`$ZodObject`,(e,t)=>{if(E.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(()=>ve(t));h(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=>D(n,t,e,s,r))):D(i,t,e,s,r)}return i?ye(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),xe=d(`$ZodRecord`,(e,t)=>{E.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(...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,m())),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 Se(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const Ce=d(`$ZodOptional`,(e,t)=>{E.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,h(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),h(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=>Se(t,e.value)):Se(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var we,Te=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 Ee(){return new Te}(we=globalThis).__zod_globalRegistry??(we.__zod_globalRegistry=Ee()),globalThis.__zod_globalRegistry;function De(e,t){return new e({type:`string`,..._(t)})}function Oe(e){return new e({type:`unknown`})}const O=d(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);E.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)=>w(e,t,n),e.parseAsync=async(t,n)=>ue(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>T(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)}),ke=d(`ZodMiniString`,(e,t)=>{me.init(e,t),O.init(e,t)});function k(e){return De(ke,e)}const Ae=d(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),O.init(e,t)});function A(){return Oe(Ae)}const je=d(`ZodMiniArray`,(e,t)=>{_e.init(e,t),O.init(e,t)});function Me(e,t){return new je({type:`array`,element:e,..._(t)})}const Ne=d(`ZodMiniObject`,(e,t)=>{be.init(e,t),O.init(e,t),h(e,`shape`,()=>t.shape)});function j(e,t){return new Ne({type:`object`,shape:e??{},..._(t)})}const Pe=d(`ZodMiniRecord`,(e,t)=>{xe.init(e,t),O.init(e,t)});function M(e,t,n){return new Pe({type:`record`,keyType:e,valueType:t,..._(n)})}const Fe=d(`ZodMiniOptional`,(e,t)=>{Ce.init(e,t),O.init(e,t)});function N(e){return new Fe({type:`optional`,innerType:e})}const Ie=j({session_id:k(),transcript_path:k(),cwd:k(),permission_mode:k(),hook_event_name:k(),tool_name:k(),tool_input:M(k(),A())});async function Le(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Ie.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Re=j({command:k(),description:N(k())}),ze=j({file_path:k()}),Be=j({pattern:k(),path:N(k())});function Ve(e){let t=Re.safeParse(e);return t.success?t.data:null}function He(e){let t=ze.safeParse(e);return t.success?t.data:null}function Ue(e){let t=Be.safeParse(e);return t.success?t.data:null}function P(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function We(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ge(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let Ke=!1;function qe(t){try{let n=We();Ke||=(Ge(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 F(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 Je(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]===`&`){I(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){I(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){I(t,n,`||`),n=``,r+=2;continue}if(o===`|`){I(t,n,`|`),n=``,r++;continue}if(o===`;`){I(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function I(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const L=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ye(e,t,n=!1){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(`>(`)&&!L.has(t))return{safe:!1,reason:`process substitution`}}if(!n)for(let t=0;t<e.length;t++){let n=e[t];if(!L.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 Xe=[`.sample`,`.example`,`.template`],Ze=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Qe(e){return Xe.some(t=>e.endsWith(t))}function R(e){return Qe(a.basename(e))?!1:Ze.test(e)}function $e(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&R(e))}const et=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`]),tt=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),nt=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),rt=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function z(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(rt.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function it(e){for(let t of e.slice(1))if(nt.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=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return et.has(t)}function at(e){let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return tt.has(t)}const ot=new Set([`push`,`pop`,`apply`,`show`]);function B(e){let t=z(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:ot.has(t)}return!1}const V=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(`.`)),st=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function ct(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!V.has(e))){n=e;break}return n?st.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const lt=new Set([`view`,`info`,`show`]);function ut(e){let t=e[1];return t!==void 0&&lt.has(t)}function dt(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 H(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 ft=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function pt(e){return!e.some(e=>ft.has(e))}function mt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ht(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 gt(e){return e.some(e=>e===`777`||e===`0777`)}function _t(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 vt(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 yt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ht(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=>_t(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>vt(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>at(e)&&!B(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=>gt(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=>$e(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.\"}],U=[{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=>ut(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=>pt(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>mt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>it(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>B(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>ct(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>H(e,t)}],W=`.claude/hooks/bst-hook.settings.json`,bt=j({allow:Me(k())});function xt(e,t){try{return u(`git`,[`ls-files`,`--error-unmatch`,t],{cwd:e,stdio:`ignore`}),u(`git`,[`diff`,`--quiet`,`HEAD`,`--`,t],{cwd:e,stdio:`ignore`}),!0}catch{return!1}}function St(e){let r=Dt(e)??e,i=s(r,W);if(!t(i)||!xt(r,W))return[];try{let e=n(i,`utf-8`),t=JSON.parse(e),r=bt.safeParse(t);return r.success?r.data.allow.filter(e=>typeof e==`string`&&e.length>0):[]}catch{return[]}}function Ct(e,t,n){let r=n.split(/\\s+/).filter(Boolean);if(r.length===0||r[0]!==e)return!1;let i=r.slice(1),a=t.slice(1);for(let e=0;e<i.length;e++)if(e>=a.length||a[e]!==i[e])return!1;return!0}const wt=j({scripts:M(k(),A())});function G(e,t){let n=Nt(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 Tt(e,t,n){let r=kt(t,n);if(!r)return null;let i=G(e,r);return i?{...i,pkgDir:r}:null}function Et(e,t){let n=Ot(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 Dt(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 Ot(e){let t=At(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 kt(e,t){let n=Ot(t);if(!n)return null;for(let t of n){let n=Mt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function At(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 jt=j({name:k()});function Mt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=jt.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Nt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=wt.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}function Pt(e){return xt(e,`package.json`)}const Ft=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`]),It=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Lt(e){return Ft.has(e)?!0:It.some(t=>e.startsWith(t))}function Rt(e,t){let n=F(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 K(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=Je(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=Rt(e.command,o);t&&(o={...o,cwd:t})}let t=zt(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 q(e,t){let n=e.pkgDir??t.cwd,r=t.insideResolvedScript??Pt(n)?{...t,insideResolvedScript:!0}:t;if(e.pre){let t=K(e.pre,r);if(t.action!==`allow`)return t}let i=K(e.main,r);if(i.action!==`allow`)return i;if(e.post){let t=K(e.post,r);if(t.action!==`allow`)return t}return`allow`}function zt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=F(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(Lt(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=Ye(i,e,t.insideResolvedScript);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};if(!t.insideResolvedScript)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 K(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(Lt(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:K(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return Bt(i,t);if(l===`npm`)return Ht(i);if(l===`bash`||l===`sh`||l===`zsh`)return Gt(i,t);if(l===`xargs`)return qt(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 yt)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 U)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};if(t.repoAllowPatterns){for(let e of t.repoAllowPatterns)if(Ct(l,i,e))return{action:`allow`,rule:`repo:${l}`,reason:null}}return t.insideResolvedScript?{action:`allow`,rule:`resolved-script`,reason:null}:{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function Bt(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 Ut(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(`-`)&&!V.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`}:K(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(!V.has(r))l=r;else{for(let n of U)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 Vt(l,t);if(s){let e=Dt(t.cwd);return e?J(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return J(l,{...t,cwd:e})}return J(l,t)}function J(e,t){let n=G(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=q(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function Vt(e,t){let n=Et(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=q(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 Ht(e){for(let t of U)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:dt(e)}}function Ut(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(`-`)&&!V.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`}:K(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(V.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Tt(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=q(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 Wt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Gt(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(`-`)||Wt.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?K(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 Y=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`]),Kt=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function qt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(Y.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(Y.has(e)){n++;continue}}if(t.length>2&&Y.has(t.slice(0,2))){n++;continue}if(Kt.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`}:K(e.slice(n).join(` `),{...t,depth:t.depth+1})}const Jt=new Set([`Read`,`Glob`,`Grep`]),X=new Set([`Write`,`Edit`]),Yt=[{name:`env-file-read`,tools:Jt,action:`deny`,when:e=>R(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=>R(e),reason:`Don't write to .env files. These contain secrets.`},{name:`hooks-folder-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e),r=a.join(t.cwd,`.claude`,`hooks`);return n.startsWith(r+a.sep)||n===r},reason:`Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.`},{name:`claude-settings-write`,tools:X,action:`deny`,when:(e,t)=>a.resolve(t.cwd,e)===a.join(t.cwd,`.claude`,`settings.json`),reason:`Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.`},{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.`}],Xt=[{name:`cwd-read`,tools:Jt,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)}}],Zt=new Set([`Read`,`Write`,`Edit`]),Qt=new Set([`Glob`,`Grep`]);function $t(e,t,n){return Zt.has(e)?He(t)?.file_path??null:Qt.has(e)?Ue(t)?.path??n:null}function en(e,t,n){let r=$t(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Yt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of Xt)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 Le();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);const tn=St(Z.cwd);let Q,$;if(Z.tool_name===`Bash`){let e=Ve(Z.tool_input);e||process.exit(0),Q=K(e.command,{cwd:Z.cwd,depth:0,repoAllowPatterns:tn}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=en(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}qe({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?P(`allow`):Q.action===`deny`&&P(`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- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **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")];
2737
+ return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.4\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\";import{execFileSync as u}from\"node:child_process\";Object.freeze({status:`aborted`});function d(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 f=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const ee={};function p(e){return e&&Object.assign(ee,e),ee}function te(e,t){return typeof t==`bigint`?t.toString():t}function ne(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function re(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const ie=Symbol(`evaluating`);function m(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==ie)return r===void 0&&(r=ie,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const ae=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function oe(e){if(h(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(h(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function se(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function g(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 ce(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function _(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 v(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function y(e){return typeof e==`string`?e:e?.message}function b(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=y(e.inst?._zod.def?.error?.(e))??y(t?.error?.(e))??y(n.customError?.(e))??y(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const le=(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,te,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},ue=d(`$ZodError`,le),x=d(`$ZodError`,le,{Parent:Error}),de=(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 f;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>b(e,a,p())));throw ae(t,i?.callee),t}return o.value})(x),fe=(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=>b(e,a,p())));throw ae(t,i?.callee),t}return o.value})(x),S=(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 f;return a.issues.length?{success:!1,error:new(e??ue)(a.issues.map(e=>b(e,i,p())))}:{success:!0,data:a.value}})(x),C=(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=>b(e,i,p())))}:{success:!0,data:a.value}})(x),pe=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},me=/^-?\\d+(?:\\.\\d+)?$/,he={major:4,minor:3,patch:6},w=d(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=he;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=_(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 f;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=_(e,t))});else{if(e.issues.length===t)continue;r||=_(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(_(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new f;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 f;return o.then(e=>t(e,r,a))}return t(o,r,a)}}m(e,`~standard`,()=>({validate:t=>{try{let n=S(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return C(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),ge=d(`$ZodString`,(e,t)=>{w.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??pe(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}}),_e=d(`$ZodUnknown`,(e,t)=>{w.init(e,t),e._zod.parse=e=>e});function ve(e,t,n){e.issues.length&&t.issues.push(...v(n,e.issues)),t.value[n]=e.value}const ye=d(`$ZodArray`,(e,t)=>{w.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!Array.isArray(i))return n.issues.push({expected:`array`,code:`invalid_type`,input:i,inst:e}),n;n.value=Array(i.length);let a=[];for(let e=0;e<i.length;e++){let o=i[e],s=t.element._zod.run({value:o,issues:[]},r);s instanceof Promise?a.push(s.then(t=>ve(t,n,e))):ve(s,n,e)}return a.length?Promise.all(a).then(()=>n):n}});function T(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...v(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function be(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=ce(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function xe(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=>T(e,n,i,t,u))):T(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 Se=d(`$ZodObject`,(e,t)=>{if(w.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=ne(()=>be(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=h,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=>T(n,t,e,s,r))):T(i,t,e,s,r)}return i?xe(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),Ce=d(`$ZodRecord`,(e,t)=>{w.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!oe(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(...v(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...v(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`&&me.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=>b(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(...v(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...v(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function E(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const we=d(`$ZodOptional`,(e,t)=>{w.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(`^(${re(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=>E(t,e.value)):E(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var D,Te=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 Ee(){return new Te}(D=globalThis).__zod_globalRegistry??(D.__zod_globalRegistry=Ee()),globalThis.__zod_globalRegistry;function De(e,t){return new e({type:`string`,...g(t)})}function Oe(e){return new e({type:`unknown`})}const O=d(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);w.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>de(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>S(e,t,n),e.parseAsync=async(t,n)=>fe(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>C(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)=>se(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),ke=d(`ZodMiniString`,(e,t)=>{ge.init(e,t),O.init(e,t)});function k(e){return De(ke,e)}const Ae=d(`ZodMiniUnknown`,(e,t)=>{_e.init(e,t),O.init(e,t)});function je(){return Oe(Ae)}const Me=d(`ZodMiniArray`,(e,t)=>{ye.init(e,t),O.init(e,t)});function Ne(e,t){return new Me({type:`array`,element:e,...g(t)})}const Pe=d(`ZodMiniObject`,(e,t)=>{Se.init(e,t),O.init(e,t),m(e,`shape`,()=>t.shape)});function A(e,t){return new Pe({type:`object`,shape:e??{},...g(t)})}const Fe=d(`ZodMiniRecord`,(e,t)=>{Ce.init(e,t),O.init(e,t)});function j(e,t,n){return new Fe({type:`record`,keyType:e,valueType:t,...g(n)})}const Ie=d(`ZodMiniOptional`,(e,t)=>{we.init(e,t),O.init(e,t)});function M(e){return new Ie({type:`optional`,innerType:e})}const Le=A({session_id:k(),transcript_path:k(),cwd:k(),permission_mode:k(),hook_event_name:k(),tool_name:k(),tool_input:j(k(),je())});async function Re(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t;try{t=JSON.parse(e.join(``))}catch{return{ok:!1,raw:e.join(``).slice(0,200),error:`invalid JSON`}}let n=Le.safeParse(t);return n.success?{ok:!0,data:n.data}:{ok:!1,raw:t,error:n.error.message}}const ze=A({command:k(),description:M(k())}),Be=A({file_path:k()}),Ve=A({pattern:k(),path:M(k())});function He(e){let t=ze.safeParse(e);return t.success?t.data:null}function Ue(e){let t=Be.safeParse(e);return t.success?t.data:null}function We(e){let t=Ve.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 Ge(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ke(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let qe=!1;function P(t){try{let n=Ge();qe||=(Ke(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 F(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 Je(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]===`&`){I(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){I(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){I(t,n,`||`),n=``,r+=2;continue}if(o===`|`){I(t,n,`|`),n=``,r++;continue}if(o===`;`){I(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function I(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const L=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ye(e,t,n=!1){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(`>(`)&&!L.has(t))return{safe:!1,reason:`process substitution`}}if(!n)for(let t=0;t<e.length;t++){let n=e[t];if(!L.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 Xe=[`.sample`,`.example`,`.template`],Ze=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Qe(e){return Xe.some(t=>e.endsWith(t))}function R(e){return Qe(a.basename(e))?!1:Ze.test(e)}function $e(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&R(e))}const et=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`]),tt=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),nt=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),rt=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function z(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(rt.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function it(e){for(let t of e.slice(1))if(nt.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=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return et.has(t)}function at(e){let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return tt.has(t)}const ot=new Set([`push`,`pop`,`apply`,`show`]);function B(e){let t=z(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:ot.has(t)}return!1}const V=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(`.`)),st=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function ct(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!V.has(e))){n=e;break}return n?st.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const lt=new Set([`view`,`info`,`show`]);function ut(e){let t=e[1];return t!==void 0&&lt.has(t)}function dt(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 H(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 ft=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function pt(e){return!e.some(e=>ft.has(e))}function mt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ht(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 gt(e){return e.some(e=>e===`777`||e===`0777`)}function _t(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 vt(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 yt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ht(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=>_t(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>vt(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>at(e)&&!B(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=>gt(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=>$e(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.\"}],U=[{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=>ut(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=>pt(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>mt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>it(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>B(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>ct(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>H(e,t)}],bt=`.claude/hooks/bst-hook.settings.json`,xt=A({allow:Ne(k())});function St(e,t){try{return u(`git`,[`ls-files`,`--error-unmatch`,t],{cwd:e,stdio:`ignore`}),u(`git`,[`diff`,`--quiet`,`HEAD`,`--`,t],{cwd:e,stdio:`ignore`}),!0}catch{return!1}}function Ct(e){let r=Ot(e)??e,i=s(r,bt);if(!t(i)||!St(r,bt))return[];try{let e=n(i,`utf-8`),t=JSON.parse(e),r=xt.safeParse(t);return r.success?r.data.allow.filter(e=>typeof e==`string`&&e.length>0):[]}catch{return[]}}function wt(e,t,n){let r=n.split(/\\s+/).filter(Boolean);if(r.length===0||r[0]!==e)return!1;let i=r.slice(1),a=t.slice(1);for(let e=0;e<i.length;e++)if(e>=a.length||a[e]!==i[e])return!1;return!0}const Tt=A({scripts:j(k(),je())});function W(e,t){let n=Pt(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 Et(e,t,n){let r=At(t,n);if(!r)return null;let i=W(e,r);return i?{...i,pkgDir:r}:null}function Dt(e,t){let n=kt(t);if(!n)return null;let r=[];for(let t of n){let n=W(e,t);n&&r.push({...n,pkgDir:t})}return r}function Ot(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 kt(e){let t=jt(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 At(e,t){let n=kt(t);if(!n)return null;for(let t of n){let n=Nt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function jt(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 Mt=A({name:k()});function Nt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Mt.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Pt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Tt.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}function Ft(e){return St(e,`package.json`)}const It=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`]),Lt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Rt(e){return It.has(e)?!0:Lt.some(t=>e.startsWith(t))}function zt(e,t){let n=F(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=Je(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=zt(e.command,o);t&&(o={...o,cwd:t})}let t=Bt(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){let n=e.pkgDir??t.cwd,r=t.insideResolvedScript??Ft(n)?{...t,insideResolvedScript:!0}:t;if(e.pre){let t=G(e.pre,r);if(t.action!==`allow`)return t}let i=G(e.main,r);if(i.action!==`allow`)return i;if(e.post){let t=G(e.post,r);if(t.action!==`allow`)return t}return`allow`}function Bt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=F(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(Rt(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=Ye(i,e,t.insideResolvedScript);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};if(!t.insideResolvedScript)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(Rt(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 Vt(i,t);if(l===`npm`)return Ut(i);if(l===`bash`||l===`sh`||l===`zsh`)return Kt(i,t);if(l===`xargs`)return Jt(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 yt)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 U)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};if(t.repoAllowPatterns){for(let e of t.repoAllowPatterns)if(wt(l,i,e))return{action:`allow`,rule:`repo:${l}`,reason:null}}return t.insideResolvedScript?{action:`allow`,rule:`resolved-script`,reason:null}:{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function Vt(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 Wt(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(`-`)&&!V.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(!V.has(r))l=r;else{for(let n of U)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 Ht(l,t);if(s){let e=Ot(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=W(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 Ht(e,t){let n=Dt(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 Ut(e){for(let t of U)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:dt(e)}}function Wt(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(`-`)&&!V.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(V.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Et(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 Gt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Kt(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(`-`)||Gt.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`]),qt=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function Jt(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(t.length>2&&J.has(t.slice(0,2))){n++;continue}if(qt.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 Yt=new Set([`Read`,`Glob`,`Grep`]),Y=new Set([`Write`,`Edit`]),Xt=[{name:`env-file-read`,tools:Yt,action:`deny`,when:e=>R(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:Y,action:`deny`,when:e=>R(e),reason:`Don't write to .env files. These contain secrets.`},{name:`hooks-folder-write`,tools:Y,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e),r=a.join(t.cwd,`.claude`,`hooks`);return n.startsWith(r+a.sep)||n===r},reason:`Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.`},{name:`claude-settings-write`,tools:Y,action:`deny`,when:(e,t)=>a.resolve(t.cwd,e)===a.join(t.cwd,`.claude`,`settings.json`),reason:`Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.`},{name:`outside-cwd-write`,tools:Y,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:Yt,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:Y,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Qt=new Set([`Read`,`Write`,`Edit`]),$t=new Set([`Glob`,`Grep`]);function en(e,t,n){return Qt.has(e)?Ue(t)?.file_path??null:$t.has(e)?We(t)?.path??n:null}function tn(e,t,n){let r=en(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Xt)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 X=await Re();X.ok||(process.stderr.write(`[bst-hook] invalid hook input: ${X.error}\\n`),P({sessionId:`unknown`,cwd:`unknown`,command:`<invalid input: ${X.error}>`,decision:`passthrough`,rule:null,reason:`validation failed: ${X.error}`}),process.exit(0));const Z=X.data;Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);const nn=Ct(Z.cwd);let Q,$;if(Z.tool_name===`Bash`){let e=He(Z.tool_input);e||(P({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:`<invalid Bash input: ${JSON.stringify(Z.tool_input).slice(0,200)}>`,decision:`passthrough`,rule:null,reason:`Bash tool_input missing or invalid 'command' field`}),process.exit(0)),Q=G(e.command,{cwd:Z.cwd,depth:0,repoAllowPatterns:nn}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=tn(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}P({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- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **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")];
2722
2738
  }
2723
2739
  //#endregion
2724
2740
  //#region src/generators/claude-settings.ts
@@ -2761,9 +2777,9 @@ function buildSettings(ctx) {
2761
2777
  const allow = [];
2762
2778
  const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
2763
2779
  const extraKnownMarketplaces = {};
2764
- if (ctx.config.structure !== "monorepo") {
2765
- if (ctx.packageJson ? packageHasWebUIDeps(ctx.packageJson) : false) enabledPlugins["frontend-design@claude-plugins-official"] = true;
2766
- }
2780
+ if (ctx.config.structure === "monorepo") {
2781
+ if (getMonorepoPackages(ctx.targetDir).some((pkg) => hasWebUIDeps(pkg.dir))) enabledPlugins["frontend-design@claude-plugins-official"] = true;
2782
+ } else if (ctx.packageJson ? packageHasWebUIDeps(ctx.packageJson) : false) enabledPlugins["frontend-design@claude-plugins-official"] = true;
2767
2783
  return {
2768
2784
  permissions: {
2769
2785
  allow,
@@ -2883,19 +2899,16 @@ async function generateClaudeSettings(ctx) {
2883
2899
  const results = [];
2884
2900
  results.push(writeOrMergeSettings(ctx, ".claude/settings.json", buildSettings(ctx)));
2885
2901
  if (ctx.config.structure === "monorepo") for (const pkg of getMonorepoPackages(ctx.targetDir)) {
2886
- if (!hasWebUIDeps(pkg.dir)) continue;
2887
2902
  const pkgRelDir = path.relative(ctx.targetDir, pkg.dir);
2888
- const pkgSettingsPath = path.join(pkgRelDir, ".claude/settings.json");
2889
- results.push(writeOrMergeSettings(ctx, pkgSettingsPath, {
2890
- permissions: {
2891
- allow: [],
2892
- deny: []
2893
- },
2894
- hooks: {},
2895
- instructions: [],
2896
- enabledPlugins: { "frontend-design@claude-plugins-official": true },
2897
- extraKnownMarketplaces: {}
2898
- }));
2903
+ const claudeDir = path.join(pkgRelDir, ".claude");
2904
+ if (ctx.exists(claudeDir)) {
2905
+ ctx.remove(claudeDir);
2906
+ results.push({
2907
+ filePath: claudeDir,
2908
+ action: "removed",
2909
+ description: `Removed subpackage ${claudeDir} (plugins consolidated at root)`
2910
+ });
2911
+ }
2899
2912
  }
2900
2913
  return results;
2901
2914
  }
@@ -3344,7 +3357,7 @@ async function runGenerators(ctx) {
3344
3357
  function generateMigratePrompt(results, config, detected) {
3345
3358
  const sections = [];
3346
3359
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3347
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.45.1 repo:sync\` on ${timestamp}_\n`);
3360
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.47.0 repo:sync\` on ${timestamp}_\n`);
3348
3361
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3349
3362
  if (changed.length > 0) {
3350
3363
  const items = changed.map((r) => {
@@ -3511,10 +3524,13 @@ async function runInit(config, options = {}) {
3511
3524
  });
3512
3525
  const created = results.filter((r) => r.action === "created");
3513
3526
  const updated = results.filter((r) => r.action === "updated");
3514
- const hasChanges = created.length > 0 || updated.length > 0 || archivedFiles.length > 0;
3515
- const prompt = generateMigratePrompt(results, config, detected);
3527
+ const removed = results.filter((r) => r.action === "removed");
3528
+ const hasChanges = created.length > 0 || updated.length > 0 || removed.length > 0 || archivedFiles.length > 0;
3516
3529
  const promptPath = ".tooling-migrate.md";
3517
- ctx.write(promptPath, prompt);
3530
+ if (!options.noPrompt) {
3531
+ const prompt = generateMigratePrompt(results, config, detected);
3532
+ ctx.write(promptPath, prompt);
3533
+ }
3518
3534
  if (!hasChanges && options.noPrompt) {
3519
3535
  log.success("Repository is up to date.");
3520
3536
  return results;
@@ -3529,6 +3545,7 @@ async function runInit(config, options = {}) {
3529
3545
  const summaryLines = [];
3530
3546
  if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
3531
3547
  if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
3548
+ if (removed.length > 0) summaryLines.push(`Removed: ${removed.map((r) => r.filePath).join(", ")}`);
3532
3549
  note(summaryLines.join("\n"), "Summary");
3533
3550
  if (!options.noPrompt) log.info(`Migration prompt written to ${promptPath}`);
3534
3551
  const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
@@ -3606,8 +3623,8 @@ const syncCommand = defineCommand({
3606
3623
  let config;
3607
3624
  if (args.yes || !isFirstRun) {
3608
3625
  const detected = buildDefaultConfig(targetDir, {
3609
- eslintPlugin: args["eslint-plugin"] === true ? true : void 0,
3610
- noCi: args["no-ci"] === true ? true : void 0
3626
+ eslintPlugin: typeof args["eslint-plugin"] === "boolean" ? args["eslint-plugin"] : void 0,
3627
+ noCi: typeof args["no-ci"] === "boolean" ? args["no-ci"] : void 0
3611
3628
  });
3612
3629
  config = saved ? mergeWithSavedConfig(detected, saved) : detected;
3613
3630
  } else config = await runInitPrompts(targetDir, saved);
@@ -3624,9 +3641,10 @@ async function runCheck(targetDir) {
3624
3641
  const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
3625
3642
  logDetectionSummary(ctx);
3626
3643
  const actionable = (await runGenerators(ctx)).filter((r) => {
3644
+ if (r.action === "removed") return true;
3627
3645
  if (r.action !== "created" && r.action !== "updated") return false;
3628
3646
  const newContent = pendingWrites.get(r.filePath);
3629
- if (newContent && r.action === "updated") {
3647
+ if (newContent !== void 0 && r.action === "updated") {
3630
3648
  const existingPath = path.join(targetDir, r.filePath);
3631
3649
  const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
3632
3650
  if (existing && contentEqual(r.filePath, existing, newContent)) return false;
@@ -3641,7 +3659,7 @@ async function runCheck(targetDir) {
3641
3659
  for (const r of actionable) {
3642
3660
  log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
3643
3661
  const newContent = pendingWrites.get(r.filePath);
3644
- if (!newContent) continue;
3662
+ if (newContent === void 0) continue;
3645
3663
  const existingPath = path.join(targetDir, r.filePath);
3646
3664
  const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
3647
3665
  if (isBundledFile(r.filePath)) log.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} → ${formatBytes(newContent.length)})`);
@@ -4251,6 +4269,111 @@ async function runVersionMode(executor, config) {
4251
4269
  };
4252
4270
  }
4253
4271
  //#endregion
4272
+ //#region src/release/publish.ts
4273
+ /** Mode 2: publish to npm, push tags, and create Forgejo releases. */
4274
+ async function runPublishMode(executor, config) {
4275
+ log.info("No changesets — publishing packages");
4276
+ const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
4277
+ debugExec(config, "pnpm changeset publish", publishResult);
4278
+ if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
4279
+ const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
4280
+ debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
4281
+ const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
4282
+ debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
4283
+ const remoteTags = parseRemoteTags(executor.exec("git", [
4284
+ "ls-remote",
4285
+ "--tags",
4286
+ "origin"
4287
+ ], { cwd: config.cwd }).stdout);
4288
+ debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
4289
+ const remoteSet = new Set(remoteTags);
4290
+ const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
4291
+ debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
4292
+ if (config.dryRun) {
4293
+ if (tagsToPush.length === 0) {
4294
+ log.info("No packages were published");
4295
+ return { mode: "none" };
4296
+ }
4297
+ log.info(`Tags to process: ${tagsToPush.join(", ")}`);
4298
+ log.info("[dry-run] Would push tags and create releases");
4299
+ return {
4300
+ mode: "publish",
4301
+ tags: tagsToPush
4302
+ };
4303
+ }
4304
+ const conn = {
4305
+ serverUrl: config.serverUrl,
4306
+ repository: config.repository,
4307
+ token: config.token
4308
+ };
4309
+ const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
4310
+ const tagsWithMissingReleases = [];
4311
+ for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
4312
+ const allTags = [...tagsToPush, ...tagsWithMissingReleases];
4313
+ if (allTags.length === 0) {
4314
+ log.info("No packages were published");
4315
+ return { mode: "none" };
4316
+ }
4317
+ log.info(`Tags to process: ${allTags.join(", ")}`);
4318
+ const errors = [];
4319
+ for (const tag of allTags) try {
4320
+ if (!remoteSet.has(tag)) {
4321
+ if (executor.exec("git", [
4322
+ "tag",
4323
+ "-l",
4324
+ tag
4325
+ ], { cwd: config.cwd }).stdout.trim() === "") {
4326
+ const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
4327
+ if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
4328
+ }
4329
+ const pushTagResult = executor.exec("git", [
4330
+ "push",
4331
+ "origin",
4332
+ `refs/tags/${tag}`
4333
+ ], { cwd: config.cwd });
4334
+ if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
4335
+ }
4336
+ if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
4337
+ else log.info(`Created release for ${tag}`);
4338
+ } catch (error) {
4339
+ errors.push({
4340
+ tag,
4341
+ error
4342
+ });
4343
+ log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
4344
+ }
4345
+ if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
4346
+ const slidingTags = allTags.flatMap((t) => {
4347
+ const atIndex = t.lastIndexOf("@");
4348
+ if (atIndex <= 0) return [];
4349
+ const name = t.slice(0, atIndex);
4350
+ return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
4351
+ });
4352
+ if (slidingTags.length > 0) {
4353
+ for (const tag of slidingTags) {
4354
+ const tagResult = executor.exec("git", [
4355
+ "tag",
4356
+ "-f",
4357
+ tag
4358
+ ], { cwd: config.cwd });
4359
+ if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
4360
+ }
4361
+ const forcePushResult = executor.exec("git", [
4362
+ "push",
4363
+ "origin",
4364
+ ...slidingTags.map((t) => `refs/tags/${t}`),
4365
+ "--force"
4366
+ ], { cwd: config.cwd });
4367
+ debugExec(config, "force-push sliding tags", forcePushResult);
4368
+ if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4369
+ else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
4370
+ }
4371
+ return {
4372
+ mode: "publish",
4373
+ tags: allTags
4374
+ };
4375
+ }
4376
+ //#endregion
4254
4377
  //#region src/release/connection.ts
4255
4378
  const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
4256
4379
  /**
@@ -4364,345 +4487,56 @@ function configureGitIdentity(executor, platform, cwd) {
4364
4487
  ], { cwd });
4365
4488
  }
4366
4489
  //#endregion
4367
- //#region src/release/webhook.ts
4368
- /** Build a webhook payload from changesets publish context. */
4369
- function buildChangesetsPayload(options) {
4370
- const tagName = options.tags.join(", ");
4371
- const actor = options.actor ?? ciActorName(options.platform ?? "forgejo");
4372
- return {
4373
- action: "published",
4374
- release: {
4375
- tag_name: tagName,
4376
- name: tagName,
4377
- draft: false,
4378
- prerelease: false,
4379
- author: { login: actor }
4380
- },
4381
- repository: {
4382
- full_name: options.repository,
4383
- html_url: `${options.serverUrl}/${options.repository}`
4490
+ //#region src/commands/release-changesets.ts
4491
+ const releaseForgejoCommand = defineCommand({
4492
+ meta: {
4493
+ name: "release:changesets",
4494
+ description: "Changesets version/publish for Forgejo CI"
4495
+ },
4496
+ args: {
4497
+ "dry-run": {
4498
+ type: "boolean",
4499
+ description: "Skip push, API calls, and publishing side effects"
4384
4500
  },
4385
- sender: { login: actor }
4386
- };
4387
- }
4388
- /** Build a webhook payload from simple release context. */
4389
- function buildSimplePayload(options) {
4390
- const repository = options.repository ?? "";
4391
- const serverUrl = options.serverUrl ?? "https://github.com";
4392
- const actor = options.actor ?? ciActorName(options.platform ?? "github");
4501
+ verbose: {
4502
+ type: "boolean",
4503
+ description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
4504
+ }
4505
+ },
4506
+ async run({ args }) {
4507
+ const result = await runRelease(buildReleaseConfig({
4508
+ dryRun: args["dry-run"] === true,
4509
+ verbose: args.verbose === true || isEnvVerbose()
4510
+ }), createRealExecutor());
4511
+ if (result.mode === "publish") {
4512
+ const outputFile = process.env["GITHUB_OUTPUT"];
4513
+ if (outputFile) try {
4514
+ appendFileSync(outputFile, "published=true\n");
4515
+ } catch (_error) {}
4516
+ }
4517
+ if (result.mode === "none") process.exitCode = 0;
4518
+ }
4519
+ });
4520
+ /** Build release config from environment / package.json and CLI flags. */
4521
+ function buildReleaseConfig(flags) {
4522
+ const resolved = resolveConnection(process.cwd());
4523
+ if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
4393
4524
  return {
4394
- action: "published",
4395
- release: {
4396
- tag_name: options.tag,
4397
- name: options.tag,
4398
- draft: false,
4399
- prerelease: options.prerelease ?? false,
4400
- author: { login: actor }
4401
- },
4402
- repository: {
4403
- full_name: repository,
4404
- html_url: repository ? `${serverUrl}/${repository}` : ""
4405
- },
4406
- sender: { login: actor }
4525
+ ...resolved.conn,
4526
+ cwd: process.cwd(),
4527
+ dryRun: flags.dryRun ?? false,
4528
+ verbose: flags.verbose ?? false
4407
4529
  };
4408
4530
  }
4409
- /** Build a webhook payload from GitHub Actions environment variables. */
4410
- function buildPayloadFromEnv() {
4411
- const repository = process.env["GITHUB_REPOSITORY"];
4531
+ /** Resolve the current branch from CI env vars or git. */
4532
+ function getCurrentBranch(executor, cwd) {
4412
4533
  const ref = process.env["GITHUB_REF"];
4413
- const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
4414
- const actor = process.env["GITHUB_ACTOR"] ?? "github-actions[bot]";
4415
- if (!repository || !ref) return void 0;
4416
- const tagName = ref.startsWith("refs/tags/") ? ref.slice(10) : ref;
4417
- return {
4418
- action: "published",
4419
- release: {
4420
- tag_name: tagName,
4421
- name: tagName,
4422
- draft: false,
4423
- prerelease: false,
4424
- author: { login: actor }
4425
- },
4426
- repository: {
4427
- full_name: repository,
4428
- html_url: `${serverUrl}/${repository}`
4429
- },
4430
- sender: { login: actor }
4431
- };
4432
- }
4433
- /**
4434
- * Compute the HMAC-SHA256 hex digest of data using the given secret.
4435
- * Returns the signature in GitHub-style format: `sha256=<hex>`.
4436
- */
4437
- function computeSignature(secret, data) {
4438
- const hmac = createHmac("sha256", secret);
4439
- hmac.update(data);
4440
- return `sha256=${hmac.digest("hex")}`;
4441
- }
4442
- /**
4443
- * POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
4444
- *
4445
- * Sends a JSON-serialized release payload (or empty string if no payload).
4446
- * If a secret is provided, computes the HMAC-SHA256 signature of the body
4447
- * and includes it in the `X-Hub-Signature-256` header.
4448
- *
4449
- * Never throws — returns a result object indicating success or failure.
4450
- */
4451
- async function sendWebhook(options) {
4452
- const { url, secret, payload, fetcher = globalThis.fetch } = options;
4453
- const body = payload ? JSON.stringify(payload) : "";
4454
- const headers = { "Content-Type": "application/json" };
4455
- if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
4456
- try {
4457
- const response = await fetcher(url, {
4458
- method: "POST",
4459
- headers,
4460
- body
4461
- });
4462
- if (!response.ok) return {
4463
- ok: false,
4464
- status: response.status,
4465
- error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
4466
- };
4467
- return {
4468
- ok: true,
4469
- status: response.status
4470
- };
4471
- } catch (error) {
4472
- return {
4473
- ok: false,
4474
- error: error instanceof Error ? error.message : String(error)
4475
- };
4476
- }
4477
- }
4478
- /**
4479
- * Send a webhook and log the result. Intended for CLI usage.
4480
- * Never throws.
4481
- */
4482
- async function sendWebhookWithLogging(url, secret, payload) {
4483
- const result = await sendWebhook({
4484
- url,
4485
- secret,
4486
- payload
4487
- });
4488
- if (result.ok) log.info("Post-release hook completed successfully");
4489
- else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
4490
- }
4491
- //#endregion
4492
- //#region src/release/hook.ts
4493
- const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
4494
- /** Read the postReleaseHook command override from .tooling.json at runtime. */
4495
- function loadHookCommand(executor, cwd) {
4496
- const configPath = path.join(cwd, ".tooling.json");
4497
- const raw = executor.readFile(configPath);
4498
- if (!raw) return void 0;
4499
- try {
4500
- const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
4501
- if (!result.success) return void 0;
4502
- return result.data.postReleaseHook;
4503
- } catch (_error) {
4504
- return;
4505
- }
4506
- }
4507
- /**
4508
- * Execute the post-release hook after a successful publish.
4509
- *
4510
- * Uses the `postReleaseHook` command from `.tooling.json` if configured,
4511
- * otherwise POSTs to `POST_RELEASE_HOOK_URL` with HMAC-SHA256 authentication
4512
- * using `WEBHOOK_SECRET`. Both env vars must be set by the CI workflow.
4513
- *
4514
- * Failures are logged as warnings — they never fail the release.
4515
- */
4516
- async function runPostReleaseHook(executor, config, payload) {
4517
- if (config.dryRun) {
4518
- debug(config, "Skipping post-release hook (dry-run)");
4519
- return;
4520
- }
4521
- const customCommand = loadHookCommand(executor, config.cwd);
4522
- if (customCommand) {
4523
- debug(config, `Running custom post-release hook: ${customCommand}`);
4524
- executeHookCommand(executor, config, customCommand);
4525
- return;
4526
- }
4527
- const hookUrl = process.env["POST_RELEASE_HOOK_URL"];
4528
- if (!hookUrl) {
4529
- debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
4530
- return;
4531
- }
4532
- const hookSecret = process.env["WEBHOOK_SECRET"];
4533
- debug(config, `Running post-release hook: POST ${hookUrl}`);
4534
- const result = await sendWebhook({
4535
- url: hookUrl,
4536
- secret: hookSecret || void 0,
4537
- payload,
4538
- fetcher: executor.fetch.bind(executor)
4539
- });
4540
- if (result.ok) log.info("Post-release hook completed successfully");
4541
- else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
4542
- }
4543
- function executeHookCommand(executor, config, command) {
4544
- try {
4545
- const result = executor.exec("sh", ["-c", command], { cwd: config.cwd });
4546
- if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
4547
- else log.info("Post-release hook completed successfully");
4548
- } catch (error) {
4549
- log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
4550
- }
4551
- }
4552
- //#endregion
4553
- //#region src/release/publish.ts
4554
- /** Mode 2: publish to npm, push tags, and create Forgejo releases. */
4555
- async function runPublishMode(executor, config) {
4556
- log.info("No changesets — publishing packages");
4557
- const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
4558
- debugExec(config, "pnpm changeset publish", publishResult);
4559
- if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
4560
- const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
4561
- debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
4562
- const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
4563
- debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
4564
- const remoteTags = parseRemoteTags(executor.exec("git", [
4565
- "ls-remote",
4566
- "--tags",
4567
- "origin"
4568
- ], { cwd: config.cwd }).stdout);
4569
- debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
4570
- const remoteSet = new Set(remoteTags);
4571
- const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
4572
- debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
4573
- if (config.dryRun) {
4574
- if (tagsToPush.length === 0) {
4575
- log.info("No packages were published");
4576
- return { mode: "none" };
4577
- }
4578
- log.info(`Tags to process: ${tagsToPush.join(", ")}`);
4579
- log.info("[dry-run] Would push tags and create releases");
4580
- return {
4581
- mode: "publish",
4582
- tags: tagsToPush
4583
- };
4584
- }
4585
- const conn = {
4586
- serverUrl: config.serverUrl,
4587
- repository: config.repository,
4588
- token: config.token
4589
- };
4590
- const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
4591
- const tagsWithMissingReleases = [];
4592
- for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
4593
- const allTags = [...tagsToPush, ...tagsWithMissingReleases];
4594
- if (allTags.length === 0) {
4595
- log.info("No packages were published");
4596
- return { mode: "none" };
4597
- }
4598
- log.info(`Tags to process: ${allTags.join(", ")}`);
4599
- const errors = [];
4600
- for (const tag of allTags) try {
4601
- if (!remoteSet.has(tag)) {
4602
- if (executor.exec("git", [
4603
- "tag",
4604
- "-l",
4605
- tag
4606
- ], { cwd: config.cwd }).stdout.trim() === "") {
4607
- const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
4608
- if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
4609
- }
4610
- const pushTagResult = executor.exec("git", [
4611
- "push",
4612
- "origin",
4613
- `refs/tags/${tag}`
4614
- ], { cwd: config.cwd });
4615
- if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
4616
- }
4617
- if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
4618
- else log.info(`Created release for ${tag}`);
4619
- } catch (error) {
4620
- errors.push({
4621
- tag,
4622
- error
4623
- });
4624
- log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
4625
- }
4626
- if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
4627
- const slidingTags = allTags.flatMap((t) => {
4628
- const atIndex = t.lastIndexOf("@");
4629
- if (atIndex <= 0) return [];
4630
- const name = t.slice(0, atIndex);
4631
- return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
4632
- });
4633
- if (slidingTags.length > 0) {
4634
- for (const tag of slidingTags) {
4635
- const tagResult = executor.exec("git", [
4636
- "tag",
4637
- "-f",
4638
- tag
4639
- ], { cwd: config.cwd });
4640
- if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
4641
- }
4642
- const forcePushResult = executor.exec("git", [
4643
- "push",
4644
- "origin",
4645
- ...slidingTags.map((t) => `refs/tags/${t}`),
4646
- "--force"
4647
- ], { cwd: config.cwd });
4648
- debugExec(config, "force-push sliding tags", forcePushResult);
4649
- if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4650
- else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
4651
- }
4652
- await runPostReleaseHook(executor, config, buildChangesetsPayload({
4653
- tags: allTags,
4654
- repository: config.repository,
4655
- serverUrl: config.serverUrl
4656
- }));
4657
- return {
4658
- mode: "publish",
4659
- tags: allTags
4660
- };
4661
- }
4662
- //#endregion
4663
- //#region src/commands/release-changesets.ts
4664
- const releaseForgejoCommand = defineCommand({
4665
- meta: {
4666
- name: "release:changesets",
4667
- description: "Changesets version/publish for Forgejo CI"
4668
- },
4669
- args: {
4670
- "dry-run": {
4671
- type: "boolean",
4672
- description: "Skip push, API calls, and publishing side effects"
4673
- },
4674
- verbose: {
4675
- type: "boolean",
4676
- description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
4677
- }
4678
- },
4679
- async run({ args }) {
4680
- if ((await runRelease(buildReleaseConfig({
4681
- dryRun: args["dry-run"] === true,
4682
- verbose: args.verbose === true || isEnvVerbose()
4683
- }), createRealExecutor())).mode === "none") process.exitCode = 0;
4684
- }
4685
- });
4686
- /** Build release config from environment / package.json and CLI flags. */
4687
- function buildReleaseConfig(flags) {
4688
- const resolved = resolveConnection(process.cwd());
4689
- if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
4690
- return {
4691
- ...resolved.conn,
4692
- cwd: process.cwd(),
4693
- dryRun: flags.dryRun ?? false,
4694
- verbose: flags.verbose ?? false
4695
- };
4696
- }
4697
- /** Resolve the current branch from CI env vars or git. */
4698
- function getCurrentBranch(executor, cwd) {
4699
- const ref = process.env["GITHUB_REF"];
4700
- if (ref?.startsWith("refs/heads/")) return ref.slice(11);
4701
- return executor.exec("git", [
4702
- "rev-parse",
4703
- "--abbrev-ref",
4704
- "HEAD"
4705
- ], { cwd }).stdout.trim();
4534
+ if (ref?.startsWith("refs/heads/")) return ref.slice(11);
4535
+ return executor.exec("git", [
4536
+ "rev-parse",
4537
+ "--abbrev-ref",
4538
+ "HEAD"
4539
+ ], { cwd }).stdout.trim();
4706
4540
  }
4707
4541
  /** Core release logic — testable with a mock executor. */
4708
4542
  async function runRelease(config, executor) {
@@ -4970,16 +4804,6 @@ async function runSimpleRelease(executor, config) {
4970
4804
  }
4971
4805
  let releaseCreated = false;
4972
4806
  if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
4973
- if (pushed) {
4974
- const conn = config.platform?.type === "forgejo" ? config.platform.conn : void 0;
4975
- await runPostReleaseHook(executor, config, buildSimplePayload({
4976
- tag,
4977
- repository: conn?.repository,
4978
- serverUrl: conn?.serverUrl,
4979
- prerelease: Boolean(config.prerelease),
4980
- platform: config.platform?.type
4981
- }));
4982
- }
4983
4807
  return {
4984
4808
  version,
4985
4809
  tag,
@@ -5078,7 +4902,12 @@ const releaseSimpleCommand = defineCommand({
5078
4902
  prerelease: args.prerelease,
5079
4903
  platform
5080
4904
  };
5081
- await runSimpleRelease(createRealExecutor(), config);
4905
+ if ((await runSimpleRelease(createRealExecutor(), config)).pushed) {
4906
+ const outputFile = process.env["GITHUB_OUTPUT"];
4907
+ if (outputFile) try {
4908
+ appendFileSync(outputFile, "pushed=true\n");
4909
+ } catch (_error) {}
4910
+ }
5082
4911
  }
5083
4912
  });
5084
4913
  //#endregion
@@ -5088,13 +4917,13 @@ const CHECKS = [
5088
4917
  { name: "typecheck" },
5089
4918
  { name: "lint" },
5090
4919
  { name: "test" },
5091
- { name: "docker:build" },
5092
4920
  {
5093
4921
  name: "format",
5094
4922
  args: "--check"
5095
4923
  },
5096
4924
  { name: "knip" },
5097
4925
  { name: "tooling:check" },
4926
+ { name: "docker:build" },
5098
4927
  { name: "docker:check" }
5099
4928
  ];
5100
4929
  /** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
@@ -6105,6 +5934,111 @@ const forgejoSecretsCommand = defineCommand({
6105
5934
  }
6106
5935
  });
6107
5936
  //#endregion
5937
+ //#region src/release/webhook.ts
5938
+ /** Build a webhook payload from simple release context. */
5939
+ function buildSimplePayload(options) {
5940
+ const repository = options.repository ?? "";
5941
+ const serverUrl = options.serverUrl ?? "https://github.com";
5942
+ const actor = options.actor ?? ciActorName(options.platform ?? "github");
5943
+ return {
5944
+ action: "published",
5945
+ release: {
5946
+ tag_name: options.tag,
5947
+ name: options.tag,
5948
+ draft: false,
5949
+ prerelease: options.prerelease ?? false,
5950
+ author: { login: actor }
5951
+ },
5952
+ repository: {
5953
+ full_name: repository,
5954
+ html_url: repository ? `${serverUrl}/${repository}` : ""
5955
+ },
5956
+ sender: { login: actor }
5957
+ };
5958
+ }
5959
+ /** Build a webhook payload from GitHub Actions environment variables. */
5960
+ function buildPayloadFromEnv() {
5961
+ const repository = process.env["GITHUB_REPOSITORY"];
5962
+ const ref = process.env["GITHUB_REF"];
5963
+ const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
5964
+ const actor = process.env["GITHUB_ACTOR"] ?? "github-actions[bot]";
5965
+ if (!repository || !ref) return void 0;
5966
+ const tagName = ref.startsWith("refs/tags/") ? ref.slice(10) : ref;
5967
+ return {
5968
+ action: "published",
5969
+ release: {
5970
+ tag_name: tagName,
5971
+ name: tagName,
5972
+ draft: false,
5973
+ prerelease: false,
5974
+ author: { login: actor }
5975
+ },
5976
+ repository: {
5977
+ full_name: repository,
5978
+ html_url: `${serverUrl}/${repository}`
5979
+ },
5980
+ sender: { login: actor }
5981
+ };
5982
+ }
5983
+ /**
5984
+ * Compute the HMAC-SHA256 hex digest of data using the given secret.
5985
+ * Returns the signature in GitHub-style format: `sha256=<hex>`.
5986
+ */
5987
+ function computeSignature(secret, data) {
5988
+ const hmac = createHmac("sha256", secret);
5989
+ hmac.update(data);
5990
+ return `sha256=${hmac.digest("hex")}`;
5991
+ }
5992
+ /**
5993
+ * POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
5994
+ *
5995
+ * Sends a JSON-serialized release payload (or empty string if no payload).
5996
+ * If a secret is provided, computes the HMAC-SHA256 signature of the body
5997
+ * and includes it in the `X-Hub-Signature-256` header.
5998
+ *
5999
+ * Never throws — returns a result object indicating success or failure.
6000
+ */
6001
+ async function sendWebhook(options) {
6002
+ const { url, secret, payload, fetcher = globalThis.fetch } = options;
6003
+ const body = payload ? JSON.stringify(payload) : "";
6004
+ const headers = { "Content-Type": "application/json" };
6005
+ if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
6006
+ try {
6007
+ const response = await fetcher(url, {
6008
+ method: "POST",
6009
+ headers,
6010
+ body
6011
+ });
6012
+ if (!response.ok) return {
6013
+ ok: false,
6014
+ status: response.status,
6015
+ error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
6016
+ };
6017
+ return {
6018
+ ok: true,
6019
+ status: response.status
6020
+ };
6021
+ } catch (error) {
6022
+ return {
6023
+ ok: false,
6024
+ error: error instanceof Error ? error.message : String(error)
6025
+ };
6026
+ }
6027
+ }
6028
+ /**
6029
+ * Send a webhook and log the result. Intended for CLI usage.
6030
+ * Never throws.
6031
+ */
6032
+ async function sendWebhookWithLogging(url, secret, payload) {
6033
+ const result = await sendWebhook({
6034
+ url,
6035
+ secret,
6036
+ payload
6037
+ });
6038
+ if (result.ok) log.info("Post-release hook completed successfully");
6039
+ else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
6040
+ }
6041
+ //#endregion
6108
6042
  //#region src/commands/webhook-send.ts
6109
6043
  const webhookSendCommand = defineCommand({
6110
6044
  meta: {
@@ -6155,7 +6089,7 @@ const webhookSendCommand = defineCommand({
6155
6089
  const main = defineCommand({
6156
6090
  meta: {
6157
6091
  name: "bst",
6158
- version: "0.45.1",
6092
+ version: "0.47.0",
6159
6093
  description: "Bootstrap and maintain standardized TypeScript project tooling"
6160
6094
  },
6161
6095
  subCommands: {
@@ -6175,7 +6109,7 @@ const main = defineCommand({
6175
6109
  "webhook:send": webhookSendCommand
6176
6110
  }
6177
6111
  });
6178
- console.log(`@bensandee/tooling v0.45.1`);
6112
+ console.log(`@bensandee/tooling v0.47.0`);
6179
6113
  async function run() {
6180
6114
  await runMain(main);
6181
6115
  process.exit(process.exitCode ?? 0);
package/dist/index.d.mts CHANGED
@@ -57,7 +57,7 @@ interface ProjectConfig {
57
57
  /** Result from a single generator: what file was written and how. */
58
58
  interface GeneratorResult {
59
59
  filePath: string;
60
- action: "created" | "updated" | "skipped" | "archived";
60
+ action: "created" | "updated" | "skipped" | "archived" | "removed";
61
61
  /** Human-readable description of what changed */
62
62
  description: string;
63
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.45.1",
3
+ "version": "0.47.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,8 +54,8 @@
54
54
  "prettier": "3.8.1",
55
55
  "release-it": "19.2.4",
56
56
  "tsdown": "0.21.6",
57
- "@bensandee/config": "0.10.1",
58
- "@bensandee/claude-hooks": "0.6.3"
57
+ "@bensandee/claude-hooks": "0.6.4",
58
+ "@bensandee/config": "0.10.1"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",