@bensandee/tooling 0.38.0 → 0.39.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +200 -93
  2. package/package.json +3 -2
package/dist/bin.mjs CHANGED
@@ -1563,9 +1563,9 @@ async function generatePackageJson(ctx) {
1563
1563
  }
1564
1564
  const devDeps = { ...ROOT_DEV_DEPS };
1565
1565
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1566
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.2";
1567
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.38.0";
1568
- if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.10.0";
1566
+ devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.3";
1567
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.39.0";
1568
+ if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.11.0";
1569
1569
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1570
1570
  "@changesets/cli": "2.30.0",
1571
1571
  "@release-it/bumper": "7.0.5",
@@ -1932,6 +1932,7 @@ import { defineConfig } from "oxlint";
1932
1932
 
1933
1933
  export default defineConfig({
1934
1934
  extends: [recommended],
1935
+ ignorePatterns: [".claude/hooks/bst-hook.mjs"],
1935
1936
  });
1936
1937
  `;
1937
1938
  const CONFIG_PRESET_ONLY = `import { presetRules } from "@bensandee/config/oxlint";
@@ -1939,6 +1940,7 @@ import { defineConfig } from "oxlint";
1939
1940
 
1940
1941
  export default defineConfig({
1941
1942
  rules: presetRules,
1943
+ ignorePatterns: [".claude/hooks/bst-hook.mjs"],
1942
1944
  });
1943
1945
  `;
1944
1946
  async function generateOxlint(ctx) {
@@ -1966,7 +1968,10 @@ async function generateOxlint(ctx) {
1966
1968
  }
1967
1969
  //#endregion
1968
1970
  //#region src/generators/formatter.ts
1969
- const OXFMT_CONFIG = `{}\n`;
1971
+ const OXFMT_CONFIG = `{
1972
+ "ignorePatterns": [".claude"]
1973
+ }
1974
+ `;
1970
1975
  const PRETTIER_CONFIG = `{
1971
1976
  "semi": true,
1972
1977
  "singleQuote": false,
@@ -2055,7 +2060,11 @@ const REQUIRED_ENTRIES = [
2055
2060
  "!.env.example"
2056
2061
  ];
2057
2062
  /** Tooling-specific entries added during init/update but not required for repo:sync --check. */
2058
- const OPTIONAL_ENTRIES = [".tooling-migrate.md", ".tooling-archived/"];
2063
+ const OPTIONAL_ENTRIES = [
2064
+ ".tooling-migrate.md",
2065
+ ".tooling-archived/",
2066
+ ".claude/hooks/bst-hook.audit.local.jsonl"
2067
+ ];
2059
2068
  const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
2060
2069
  /** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
2061
2070
  function normalizeEntry(entry) {
@@ -2214,12 +2223,13 @@ const KNIP_CONFIG_SINGLE = `import type { KnipConfig } from "knip";
2214
2223
  export default {
2215
2224
  entry: ["src/index.ts"],
2216
2225
  project: ["src/**/*.ts"],
2217
- ignore: ["dist/**"],
2226
+ ignore: ["dist/**", ".claude/hooks/**"],
2218
2227
  } satisfies KnipConfig;
2219
2228
  `;
2220
2229
  const KNIP_CONFIG_MONOREPO = `import type { KnipConfig } from "knip";
2221
2230
 
2222
2231
  export default {
2232
+ ignore: [".claude/hooks/**"],
2223
2233
  workspaces: {
2224
2234
  ".": {
2225
2235
  entry: [],
@@ -2350,7 +2360,33 @@ async function generatePnpmWorkspace(ctx) {
2350
2360
  };
2351
2361
  }
2352
2362
  //#endregion
2363
+ //#region src/generators/claude-hooks.ts
2364
+ const HOOK_PATH = ".claude/hooks/bst-hook.mjs";
2365
+ async function generateClaudeHooks(ctx) {
2366
+ const content = "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.1.0\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{readFileSync as t,readdirSync as n,statSync as r}from\"node:fs\";import*as i from\"node:path\";import{join as a,resolve as o}from\"node:path\";import{fileURLToPath as s}from\"node:url\";Object.freeze({status:`aborted`});function c(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 l=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const u={};function d(e){return e&&Object.assign(u,e),u}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 f=Symbol(`evaluating`);function p(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==f)return r===void 0&&(r=f,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const m=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function re(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 ie(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 _(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})},ae=c(`$ZodError`,S),C=c(`$ZodError`,S,{Parent:Error}),w=(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 l;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,d())));throw m(t,i?.callee),t}return o.value})(C),oe=(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,d())));throw m(t,i?.callee),t}return o.value})(C),T=(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 l;return a.issues.length?{success:!1,error:new(e??ae)(a.issues.map(e=>x(e,i,d())))}:{success:!0,data:a.value}})(C),E=(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,d())))}:{success:!0,data:a.value}})(C),se=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},ce=/^-?\\d+(?:\\.\\d+)?$/,le={major:4,minor:3,patch:6},D=c(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=le;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 l;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 l;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 l;return o.then(e=>t(e,r,a))}return t(o,r,a)}}p(e,`~standard`,()=>({validate:t=>{try{let n=T(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return E(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),ue=c(`$ZodString`,(e,t)=>{D.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??se(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}}),de=c(`$ZodUnknown`,(e,t)=>{D.init(e,t),e._zod.parse=e=>e});function O(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 fe(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=_(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function pe(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=>O(e,n,i,t,u))):O(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 me=c(`$ZodObject`,(e,t)=>{if(D.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(()=>fe(t));p(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=>O(n,t,e,s,r))):O(i,t,e,s,r)}return i?pe(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),he=c(`$ZodRecord`,(e,t)=>{D.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!re(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`&&ce.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,d())),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 k(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const ge=c(`$ZodOptional`,(e,t)=>{D.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,p(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),p(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=>k(t,e.value)):k(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var A,j=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 M(){return new j}(A=globalThis).__zod_globalRegistry??(A.__zod_globalRegistry=M()),globalThis.__zod_globalRegistry;function N(e,t){return new e({type:`string`,...g(t)})}function P(e){return new e({type:`unknown`})}const F=c(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);D.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>w(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>T(e,t,n),e.parseAsync=async(t,n)=>oe(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>E(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)=>ie(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),I=c(`ZodMiniString`,(e,t)=>{ue.init(e,t),F.init(e,t)});function L(e){return N(I,e)}const _e=c(`ZodMiniUnknown`,(e,t)=>{de.init(e,t),F.init(e,t)});function ve(){return P(_e)}const ye=c(`ZodMiniObject`,(e,t)=>{me.init(e,t),F.init(e,t),p(e,`shape`,()=>t.shape)});function R(e,t){return new ye({type:`object`,shape:e??{},...g(t)})}const be=c(`ZodMiniRecord`,(e,t)=>{he.init(e,t),F.init(e,t)});function xe(e,t,n){return new be({type:`record`,keyType:e,valueType:t,...g(n)})}const Se=c(`ZodMiniOptional`,(e,t)=>{ge.init(e,t),F.init(e,t)});function Ce(e){return new Se({type:`optional`,innerType:e})}const we=R({session_id:L(),transcript_path:L(),cwd:L(),permission_mode:L(),hook_event_name:L(),tool_name:L(),tool_input:R({command:L(),description:Ce(L())})});async function Te(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=we.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}function z(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Ee(){let e=i.dirname(s(import.meta.url));return i.join(e,`bst-hook.audit.local.jsonl`)}function De(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let Oe=!1;function ke(t){try{let n=Ee();Oe||=(De(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 B(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 Ae(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]===`&`){V(t,n,`&&`),n=``,r+=2;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){V(t,n,`||`),n=``,r+=2;continue}if(o===`|`){V(t,n,`|`),n=``,r++;continue}if(o===`;`){V(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function V(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const H=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function je(e,t){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!H.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!H.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 Me=new Set([`status`,`log`,`diff`,`show`,`branch`,`remote`,`fetch`,`rev-parse`]),Ne=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]);function Pe(e){for(let t of e.slice(1))if(Ne.has(t)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`))return!1;for(let t of e.slice(1))if(!t.startsWith(`-`)){if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Me.has(t)}return!1}const U=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(`.`)),Fe=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function Ie(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!U.has(e))){n=e;break}return n?Fe.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const Le=new Set([`view`,`info`,`show`]);function Re(e){let t=e[1];return t!==void 0&&Le.has(t)}function ze(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 W(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`))continue;if(i.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=i.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+i.sep))return!1}return!0}const Be=new Set([`-exec`,`-execdir`,`-ok`,`-delete`]);function Ve(e){return!e.some(e=>Be.has(e))}function He(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function Ue(e){let t=e.findIndex(e=>i.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),a=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=a.includes(`--recursive`);return o||s}function We(e){return e.some(e=>e===`777`||e===`0777`)}const Ge=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function G(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&Ge.test(e))}function Ke(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 qe(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 Je=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>Ue(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=>Ke(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>qe(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>We(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-cat`,match:/^cat$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-head`,match:/^head$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-tail`,match:/^tail$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-less`,match:/^less$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-more`,match:/^more$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-grep`,match:/^grep$/,action:`deny`,when:e=>G(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.\"}],K=[{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=>Re(e)},{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=>Ve(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>He(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>Pe(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>Ie(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>W(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>W(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>W(e,t)}],Ye=R({scripts:xe(L(),ve())});function q(e,t){let n=et(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 Xe(e,t,n){let r=Ze(t,n);if(!r)return null;let i=q(e,r);return i?{...i,pkgDir:r}:null}function Ze(e,t){let i=Qe(t);if(!i)return null;for(let s of i){if(!s.endsWith(`/*`))continue;let i=o(t,s.slice(0,-2)),c;try{c=n(i)}catch{continue}for(let t of c){let n=a(i,t);try{if(!r(n).isDirectory())continue}catch{continue}let o=J(n);if(o===e||o===`@${e}`||t===e||o&&(o.startsWith(`@`)?o.split(`/`)[1]:o)===e)return n}}return null}function Qe(e){try{let n=t(a(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=n.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<n.length;e++){let t=n[e],r=/^\\s+-\\s+(.+)$/.exec(t);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const $e=R({name:L()});function J(e){try{let n=t(a(e,`package.json`),`utf-8`),r=$e.safeParse(JSON.parse(n));return r.success?r.data.name:null}catch{return null}}function et(e){try{let n=t(a(e,`package.json`),`utf-8`),r=Ye.safeParse(JSON.parse(n));return r.success?r.data.scripts:null}catch{return null}}const tt=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`]),nt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Y(e){return tt.has(e)?!0:nt.some(t=>e.startsWith(t))}function rt(e,t){let n=B(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let a=r[1];return i.isAbsolute(a)?a:i.resolve(t.cwd,a)}function X(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=Ae(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=it(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 it(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=B(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let a=r.tokens;if(a.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<a.length&&/^\\w+=/.test(a[o]);){let e=a[o].split(`=`)[0];if(Y(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=a.length)return{action:`allow`,rule:`env-only`,reason:null};a=a.slice(o);let s=je(a,e);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};for(let e=0;e<a.length;e++){let t=a[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(a[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=i.basename(a[0]);if(c===`command`&&a.length>1)return X(a.slice(1).join(` `),{...t,depth:t.depth+1});if(c===`env`){let e=1;for(;e<a.length&&/^\\w+=/.test(a[e]);){let t=a[e].split(`=`)[0];if(Y(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=a.length?{action:`allow`,rule:`env-only`,reason:null}:X(a.slice(e).join(` `),{...t,depth:t.depth+1})}if(c===`pnpm`)return at(a,t);if(c===`npm`)return ot(a);if(c===`bash`||c===`sh`||c===`zsh`)return lt(a,t);if(c===`node`&&(a.includes(`-e`)||a.includes(`-p`)||a.includes(`--eval`)||a.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(c===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of Je)if(e.match.test(c)&&(!e.when||e.when(a,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of K)if(e.match.test(c)&&(!e.when||e.when(a,t)))return{action:`allow`,rule:e.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}function at(e,t){let n=e.slice(1),r,i=-1;for(let a=0;a<n.length;a++){let o=n[a];if(o===`--filter`||o.startsWith(`--filter=`))return st(e,n,a,t);if(o===`-r`||o===`--recursive`)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};if(!(o.startsWith(`-`)&&!U.has(o))){r=o,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`}:X(e.join(` `),{...t,depth:t.depth+1})}if(r===`run`){let e=n[i+1];return!e||e.startsWith(`-`)?{action:`passthrough`,rule:null,reason:`pnpm run without script name`}:Z(e,t)}if(U.has(r)){for(let n of K)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}}return Z(r,t)}function Z(e,t){let n=q(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r={...t,depth:t.depth+1};if(n.pre){let e=X(n.pre,r);if(e.action!==`allow`)return e}let i=X(n.main,r);if(i.action!==`allow`)return i;if(n.post){let e=X(n.post,r);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-script:${e}`,reason:null}}function ot(e){for(let t of K)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:ze(e)}}function st(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(`-`)&&!U.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`}:X(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(U.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Xe(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u={...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd};if(l.pre){let e=X(l.pre,u);if(e.action!==`allow`)return e}let d=X(l.main,u);if(d.action!==`allow`)return d;if(l.post){let e=X(l.post,u);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}}const ct=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function lt(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(`-`)||ct.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?X(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 Q=await Te();(Q.hook_event_name!==`PreToolUse`||Q.tool_name!==`Bash`)&&process.exit(0);const $=X(Q.tool_input.command,{cwd:Q.cwd,depth:0});ke({sessionId:Q.session_id,command:Q.tool_input.command,decision:$.action,rule:$.rule,reason:$.reason}),$.action===`allow`?z(`allow`):$.action===`deny`&&z(`deny`,$.reason);export{};";
2367
+ const existing = ctx.read(HOOK_PATH);
2368
+ if (existing === content) return {
2369
+ filePath: HOOK_PATH,
2370
+ action: "skipped",
2371
+ description: "Hook is up to date"
2372
+ };
2373
+ ctx.write(HOOK_PATH, content);
2374
+ return {
2375
+ filePath: HOOK_PATH,
2376
+ action: existing ? "updated" : "created",
2377
+ description: existing ? "Updated bst-hook.mjs" : "Generated bst-hook.mjs"
2378
+ };
2379
+ }
2380
+ //#endregion
2353
2381
  //#region src/generators/claude-settings.ts
2382
+ const HookEntrySchema = z.object({
2383
+ type: z.string(),
2384
+ command: z.string()
2385
+ });
2386
+ const HookMatcherSchema = z.object({
2387
+ matcher: z.string(),
2388
+ hooks: z.array(HookEntrySchema).default([])
2389
+ });
2354
2390
  const ClaudeSettingsSchema = z.object({
2355
2391
  permissions: z.object({
2356
2392
  allow: z.array(z.string()).default([]),
@@ -2359,6 +2395,7 @@ const ClaudeSettingsSchema = z.object({
2359
2395
  allow: [],
2360
2396
  deny: []
2361
2397
  }),
2398
+ hooks: z.record(z.string(), z.array(HookMatcherSchema)).default({}),
2362
2399
  instructions: z.array(z.string()).default([]),
2363
2400
  enabledPlugins: z.record(z.string(), z.boolean()).default({}),
2364
2401
  extraKnownMarketplaces: z.record(z.string(), z.record(z.string(), z.unknown())).default({})
@@ -2380,64 +2417,11 @@ function parseClaudeSettings(raw) {
2380
2417
  function buildSettings(ctx) {
2381
2418
  const pm = "pnpm";
2382
2419
  const allow = [
2383
- `Bash(${pm} install)`,
2384
- `Bash(${pm} install *)`,
2385
- `Bash(${pm} add *)`,
2386
- `Bash(${pm} update *)`,
2387
- `Bash(${pm} view *)`,
2388
- `Bash(${pm} why *)`,
2389
- `Bash(${pm} list)`,
2390
- `Bash(${pm} list *)`,
2391
- `Bash(${pm} ls)`,
2392
- `Bash(${pm} ls *)`,
2393
- "Bash(npm view *)",
2394
- "Bash(npm info *)",
2395
- "Bash(npm show *)",
2396
- `Bash(${pm} build)`,
2397
- `Bash(${pm} -r build)`,
2398
- `Bash(${pm} dev)`,
2399
- `Bash(${pm} test)`,
2400
- `Bash(${pm} test *)`,
2401
- `Bash(${pm} -r test)`,
2402
- `Bash(${pm} vitest)`,
2403
- `Bash(${pm} vitest *)`,
2404
- `Bash(${pm} exec vitest)`,
2405
- `Bash(${pm} exec vitest *)`,
2406
- `Bash(${pm} exec oxfmt)`,
2407
- `Bash(${pm} exec oxfmt *)`,
2408
- `Bash(${pm} typecheck)`,
2409
- `Bash(${pm} -r --parallel run typecheck)`,
2410
- `Bash(${pm} tsc *)`,
2411
- `Bash(${pm} exec tsc *)`,
2412
- `Bash(${pm} lint)`,
2413
- `Bash(${pm} lint *)`,
2414
- `Bash(${pm} format)`,
2415
- `Bash(${pm} format *)`,
2416
- `Bash(${pm} knip)`,
2417
- `Bash(${pm} knip *)`,
2418
- `Bash(${pm} check)`,
2419
- `Bash(${pm} ci:check)`,
2420
- `Bash(${pm} exec oxlint *)`,
2421
- `Bash(${pm} exec knip *)`,
2422
- "Bash(git status *)",
2423
- "Bash(git log *)",
2424
- "Bash(git diff *)",
2425
- "Bash(git branch *)",
2426
- "Bash(git show *)",
2427
- "Bash(git rev-parse *)",
2428
- "Bash(ls *)",
2429
- "Bash(cat *)",
2430
- "Bash(head *)",
2431
- "Bash(tail *)",
2432
- "Bash(wc *)",
2433
2420
  "Bash(test *)",
2434
2421
  "Bash([ *)",
2435
- "Bash(grep *)",
2436
- "Bash(which *)",
2437
- "Bash(node -e *)",
2438
- "Bash(node -p *)"
2422
+ "Bash(git -C * diff HEAD -- *)"
2439
2423
  ];
2440
- if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm} --filter *)`, `Bash(${pm} -r *)`);
2424
+ if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm} -r *)`);
2441
2425
  const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
2442
2426
  const extraKnownMarketplaces = {};
2443
2427
  if (ctx.config.structure !== "monorepo") {
@@ -2447,7 +2431,6 @@ function buildSettings(ctx) {
2447
2431
  permissions: {
2448
2432
  allow,
2449
2433
  deny: [
2450
- "Bash(npx *)",
2451
2434
  "Bash(git push *)",
2452
2435
  "Bash(git push)",
2453
2436
  "Bash(git add *)",
@@ -2466,35 +2449,19 @@ function buildSettings(ctx) {
2466
2449
  "Bash(git clean *)",
2467
2450
  "Bash(git rm *)",
2468
2451
  "Bash(git mv *)",
2469
- "Bash(rm -rf *)",
2470
- "Bash(cat *.env)",
2471
- "Bash(cat *.env.*)",
2472
- "Bash(cat .env)",
2473
- "Bash(cat .env.*)",
2474
- "Bash(head *.env)",
2475
- "Bash(head *.env.*)",
2476
- "Bash(head .env)",
2477
- "Bash(head .env.*)",
2478
- "Bash(tail *.env)",
2479
- "Bash(tail *.env.*)",
2480
- "Bash(tail .env)",
2481
- "Bash(tail .env.*)",
2482
- "Bash(less *.env)",
2483
- "Bash(less *.env.*)",
2484
- "Bash(less .env)",
2485
- "Bash(less .env.*)",
2486
- "Bash(more *.env)",
2487
- "Bash(more *.env.*)",
2488
- "Bash(more .env)",
2489
- "Bash(more .env.*)",
2490
- "Bash(grep * .env)",
2491
- "Bash(grep * .env.*)",
2492
2452
  "Read(.env)",
2493
2453
  "Read(.env.*)",
2494
2454
  "Read(*.env)",
2495
2455
  "Read(*.env.*)"
2496
2456
  ]
2497
2457
  },
2458
+ hooks: { PreToolUse: [{
2459
+ matcher: "Bash",
2460
+ hooks: [{
2461
+ type: "command",
2462
+ command: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/bst-hook.mjs"`
2463
+ }]
2464
+ }] },
2498
2465
  instructions: [
2499
2466
  "Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
2500
2467
  "No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
@@ -2505,10 +2472,16 @@ function buildSettings(ctx) {
2505
2472
  extraKnownMarketplaces
2506
2473
  };
2507
2474
  }
2508
- /** Remove enabledPlugins/extraKnownMarketplaces when empty to keep the file clean. */
2475
+ const BST_HOOK_MARKER = "bst-hook.mjs";
2476
+ /** Check if a hook entry list already contains the bst-hook command. */
2477
+ function hasBstHook(matchers) {
2478
+ return matchers.some((m) => m.matcher === "Bash" && m.hooks.some((h) => h.command.includes(BST_HOOK_MARKER)));
2479
+ }
2480
+ /** Remove enabledPlugins/extraKnownMarketplaces/hooks when empty to keep the file clean. */
2509
2481
  function serializeSettings$1(settings) {
2510
- const { enabledPlugins, extraKnownMarketplaces, ...rest } = settings;
2482
+ const { enabledPlugins, extraKnownMarketplaces, hooks, ...rest } = settings;
2511
2483
  const out = { ...rest };
2484
+ if (Object.keys(hooks).length > 0) out["hooks"] = hooks;
2512
2485
  if (Object.keys(enabledPlugins).length > 0) out["enabledPlugins"] = enabledPlugins;
2513
2486
  if (Object.keys(extraKnownMarketplaces).length > 0) out["extraKnownMarketplaces"] = extraKnownMarketplaces;
2514
2487
  return JSON.stringify(out, null, 2) + "\n";
@@ -2534,9 +2507,18 @@ function writeOrMergeSettings(ctx, filePath, generated) {
2534
2507
  else mergedInstructions.push(inst);
2535
2508
  instructionChanges++;
2536
2509
  }
2510
+ let hookChanges = 0;
2511
+ const mergedHooks = { ...settings.hooks };
2512
+ for (const [event, generatedMatchers] of Object.entries(generated.hooks)) {
2513
+ const existingMatchers = mergedHooks[event] ?? [];
2514
+ if (!hasBstHook(existingMatchers)) {
2515
+ mergedHooks[event] = [...existingMatchers, ...generatedMatchers];
2516
+ hookChanges++;
2517
+ }
2518
+ }
2537
2519
  const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in settings.enabledPlugins));
2538
2520
  const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in settings.extraKnownMarketplaces));
2539
- const changed = missingAllow.length + missingDeny.length + instructionChanges + missingPlugins.length + missingMarketplaces.length;
2521
+ const changed = missingAllow.length + missingDeny.length + hookChanges + instructionChanges + missingPlugins.length + missingMarketplaces.length;
2540
2522
  if (changed === 0) return {
2541
2523
  filePath,
2542
2524
  action: "skipped",
@@ -2546,6 +2528,7 @@ function writeOrMergeSettings(ctx, filePath, generated) {
2546
2528
  allow: [...settings.permissions.allow, ...missingAllow],
2547
2529
  deny: [...settings.permissions.deny, ...missingDeny]
2548
2530
  };
2531
+ if (Object.keys(mergedHooks).length > 0) rawJson["hooks"] = mergedHooks;
2549
2532
  rawJson["instructions"] = mergedInstructions;
2550
2533
  const updatedPlugins = { ...settings.enabledPlugins };
2551
2534
  for (const [key, value] of missingPlugins) updatedPlugins[key] = value;
@@ -2581,6 +2564,7 @@ async function generateClaudeSettings(ctx) {
2581
2564
  allow: [],
2582
2565
  deny: []
2583
2566
  },
2567
+ hooks: {},
2584
2568
  instructions: [],
2585
2569
  enabledPlugins: { "frontend-design@claude-plugins-official": true },
2586
2570
  extraKnownMarketplaces: {}
@@ -3181,6 +3165,7 @@ async function runGenerators(ctx) {
3181
3165
  results.push(await generateKnip(ctx));
3182
3166
  results.push(await generateRenovate(ctx));
3183
3167
  results.push(await generateCi(ctx));
3168
+ results.push(await generateClaudeHooks(ctx));
3184
3169
  results.push(...await generateClaudeSettings(ctx));
3185
3170
  results.push(await generateReleaseIt(ctx));
3186
3171
  results.push(await generateVersionrc(ctx));
@@ -3201,7 +3186,7 @@ async function runGenerators(ctx) {
3201
3186
  function generateMigratePrompt(results, config, detected) {
3202
3187
  const sections = [];
3203
3188
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3204
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.38.0 repo:sync\` on ${timestamp}_\n`);
3189
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.39.0 repo:sync\` on ${timestamp}_\n`);
3205
3190
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3206
3191
  if (changed.length > 0) {
3207
3192
  const items = changed.map((r) => {
@@ -3501,7 +3486,8 @@ async function runCheck(targetDir) {
3501
3486
  if (!newContent) continue;
3502
3487
  const existingPath = path.join(targetDir, r.filePath);
3503
3488
  const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
3504
- if (!existing) {
3489
+ if (isBundledFile(r.filePath)) log.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} → ${formatBytes(newContent.length)})`);
3490
+ else if (!existing) {
3505
3491
  const lineCount = newContent.split("\n").length - 1;
3506
3492
  log.info(` + ${lineCount} new lines`);
3507
3493
  } else {
@@ -3511,6 +3497,14 @@ async function runCheck(targetDir) {
3511
3497
  }
3512
3498
  return 1;
3513
3499
  }
3500
+ /** Files that are minified/bundled and produce unreadable diffs. */
3501
+ function isBundledFile(filePath) {
3502
+ return filePath.endsWith("bst-hook.mjs");
3503
+ }
3504
+ function formatBytes(bytes) {
3505
+ if (bytes < 1024) return `${String(bytes)} B`;
3506
+ return `${(bytes / 1024).toFixed(1)} kB`;
3507
+ }
3514
3508
  const normalize = (line) => line.trimEnd();
3515
3509
  function lineDiff(oldText, newText) {
3516
3510
  const oldLines = oldText.split("\n").map(normalize);
@@ -3818,6 +3812,30 @@ async function ensureRelease(executor, conn, tag) {
3818
3812
  //#endregion
3819
3813
  //#region src/release/version.ts
3820
3814
  const BRANCH = "changeset-release/main";
3815
+ /** Pattern matching a dateless version heading like `## 1.2.3` or `## 1.2.3-beta.0`. */
3816
+ const DATELESS_VERSION_RE = /^## (\d+\.\d+\.\d+(?:-[^\s)]+)?)$/gm;
3817
+ /** Format today's date as YYYY-MM-DD. */
3818
+ function todayIso() {
3819
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3820
+ }
3821
+ /** Add today's date to dateless version headings in a changelog string. */
3822
+ function stampDates(content, date) {
3823
+ return content.replace(DATELESS_VERSION_RE, `## $1 (${date})`);
3824
+ }
3825
+ /**
3826
+ * Stamp release dates on CHANGELOGs for packages that were just versioned.
3827
+ * Only modifies headings that don't already have a date (idempotent).
3828
+ */
3829
+ function stampChangelogDates(executor, cwd, changedDirs, isMonorepo) {
3830
+ const date = todayIso();
3831
+ const changelogPaths = isMonorepo ? changedDirs.map((dir) => path.join(cwd, "packages", dir, "CHANGELOG.md")) : [path.join(cwd, "CHANGELOG.md")];
3832
+ for (const changelogPath of changelogPaths) {
3833
+ const content = executor.readFile(changelogPath);
3834
+ if (!content) continue;
3835
+ const updated = stampDates(content, date);
3836
+ if (updated !== content) executor.writeFile(changelogPath, updated);
3837
+ }
3838
+ }
3821
3839
  /** Extract the latest changelog entry (content between first and second `## ` heading). */
3822
3840
  function extractLatestEntry(changelog) {
3823
3841
  const lines = changelog.split("\n");
@@ -3905,6 +3923,12 @@ async function runVersionMode(executor, config) {
3905
3923
  debugExec(config, "pnpm changeset version", versionResult);
3906
3924
  if (originalConfig) executor.writeFile(changesetConfigPath, originalConfig);
3907
3925
  if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
3926
+ const packagesAfterVersion = executor.listWorkspacePackages(config.cwd);
3927
+ if (packagesBefore.length > 0 || packagesAfterVersion.length > 0) {
3928
+ const beforeMap = new Map(packagesBefore.map((pkg) => [pkg.name, pkg.version]));
3929
+ const changedDirs = packagesAfterVersion.filter((pkg) => beforeMap.get(pkg.name) !== pkg.version).map((pkg) => pkg.dir);
3930
+ stampChangelogDates(executor, config.cwd, changedDirs, true);
3931
+ } else stampChangelogDates(executor, config.cwd, [], false);
3908
3932
  debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm", ["install", "--no-frozen-lockfile"], { cwd: config.cwd }));
3909
3933
  const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
3910
3934
  debug(config, `PR title: ${title}`);
@@ -5173,11 +5197,93 @@ const dockerCheckCommand = defineCommand({
5173
5197
  }
5174
5198
  });
5175
5199
  //#endregion
5200
+ //#region src/commands/hooks-audit.ts
5201
+ const AuditEntrySchema = z.object({
5202
+ timestamp: z.string(),
5203
+ sessionId: z.string(),
5204
+ command: z.string(),
5205
+ decision: z.enum([
5206
+ "allow",
5207
+ "deny",
5208
+ "passthrough"
5209
+ ]),
5210
+ rule: z.string().nullable(),
5211
+ reason: z.string().nullable()
5212
+ });
5213
+ function parseAuditLog(logPath) {
5214
+ if (!existsSync(logPath)) return [];
5215
+ const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
5216
+ const entries = [];
5217
+ for (const line of lines) try {
5218
+ const result = AuditEntrySchema.safeParse(JSON.parse(line));
5219
+ if (result.success) entries.push(result.data);
5220
+ } catch (_error) {}
5221
+ return entries;
5222
+ }
5223
+ const hooksAuditCommand = defineCommand({
5224
+ meta: {
5225
+ name: "hooks:audit",
5226
+ description: "Summarize the bst-hook audit log"
5227
+ },
5228
+ args: {
5229
+ dir: {
5230
+ type: "positional",
5231
+ description: "Project directory (default: current directory)",
5232
+ required: false
5233
+ },
5234
+ tail: {
5235
+ type: "string",
5236
+ description: "Show the last N entries (default: 20)"
5237
+ },
5238
+ decision: {
5239
+ type: "string",
5240
+ description: "Filter by decision: allow, deny, or passthrough"
5241
+ },
5242
+ json: {
5243
+ type: "boolean",
5244
+ description: "Output raw JSON lines instead of formatted summary"
5245
+ }
5246
+ },
5247
+ async run({ args }) {
5248
+ const targetDir = path.resolve(args.dir ?? ".");
5249
+ const logPath = path.join(targetDir, ".claude/hooks/bst-hook.audit.local.jsonl");
5250
+ let entries = parseAuditLog(logPath);
5251
+ if (entries.length === 0) {
5252
+ log.info("No audit log entries found.");
5253
+ return;
5254
+ }
5255
+ const decisionFilter = args.decision;
5256
+ if (decisionFilter) entries = entries.filter((e) => e.decision === decisionFilter);
5257
+ const tailCount = args.tail ? Number.parseInt(args.tail, 10) : 20;
5258
+ if (entries.length > tailCount) entries = entries.slice(-tailCount);
5259
+ if (args.json === true) {
5260
+ for (const entry of entries) console.log(JSON.stringify(entry));
5261
+ return;
5262
+ }
5263
+ const allEntries = parseAuditLog(logPath);
5264
+ const counts = {
5265
+ allow: 0,
5266
+ deny: 0,
5267
+ passthrough: 0
5268
+ };
5269
+ for (const e of allEntries) counts[e.decision]++;
5270
+ log.info(`Audit log: ${String(allEntries.length)} entries — ${String(counts.allow)} allow, ${String(counts.deny)} deny, ${String(counts.passthrough)} passthrough`);
5271
+ log.info("");
5272
+ for (const entry of entries) {
5273
+ const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
5274
+ const icon = entry.decision === "allow" ? "+" : entry.decision === "deny" ? "x" : "?";
5275
+ const rule = entry.rule ? ` [${entry.rule}]` : "";
5276
+ const reason = entry.reason ? ` — ${entry.reason}` : "";
5277
+ log.info(` ${icon} ${ts} ${entry.decision.padEnd(11)} ${entry.command}${rule}${reason}`);
5278
+ }
5279
+ }
5280
+ });
5281
+ //#endregion
5176
5282
  //#region src/bin.ts
5177
5283
  const main = defineCommand({
5178
5284
  meta: {
5179
5285
  name: "bst",
5180
- version: "0.38.0",
5286
+ version: "0.39.0",
5181
5287
  description: "Bootstrap and maintain standardized TypeScript project tooling"
5182
5288
  },
5183
5289
  subCommands: {
@@ -5190,10 +5296,11 @@ const main = defineCommand({
5190
5296
  "release:simple": releaseSimpleCommand,
5191
5297
  "docker:publish": publishDockerCommand,
5192
5298
  "docker:build": dockerBuildCommand,
5193
- "docker:check": dockerCheckCommand
5299
+ "docker:check": dockerCheckCommand,
5300
+ "hooks:audit": hooksAuditCommand
5194
5301
  }
5195
5302
  });
5196
- console.log(`@bensandee/tooling v0.38.0`);
5303
+ console.log(`@bensandee/tooling v0.39.0`);
5197
5304
  async function run() {
5198
5305
  await runMain(main);
5199
5306
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,7 +60,8 @@
60
60
  "tsdown": "0.21.4",
61
61
  "typescript": "5.9.3",
62
62
  "vitest": "4.1.0",
63
- "@bensandee/config": "0.9.2"
63
+ "@bensandee/claude-hooks": "0.1.0",
64
+ "@bensandee/config": "0.9.3"
64
65
  },
65
66
  "scripts": {
66
67
  "build": "tsdown",