@bensandee/tooling 0.38.0 → 0.40.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 +276 -121
- 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.
|
|
1567
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1568
|
-
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.
|
|
1566
|
+
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.0";
|
|
1567
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.40.0";
|
|
1568
|
+
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.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/**"],
|
|
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/**"],
|
|
1942
1944
|
});
|
|
1943
1945
|
`;
|
|
1944
1946
|
async function generateOxlint(ctx) {
|
|
@@ -1966,7 +1968,14 @@ async function generateOxlint(ctx) {
|
|
|
1966
1968
|
}
|
|
1967
1969
|
//#endregion
|
|
1968
1970
|
//#region src/generators/formatter.ts
|
|
1969
|
-
|
|
1971
|
+
/** Ignore patterns that must always be present in .oxfmtrc.json. */
|
|
1972
|
+
const BASE_IGNORE_PATTERNS = [".claude"];
|
|
1973
|
+
/** Extra ignore patterns when a release strategy generates CHANGELOGs. */
|
|
1974
|
+
const RELEASE_IGNORE_PATTERNS = ["**/CHANGELOG.md"];
|
|
1975
|
+
function requiredIgnorePatterns(releaseStrategy) {
|
|
1976
|
+
if (releaseStrategy === "none") return BASE_IGNORE_PATTERNS;
|
|
1977
|
+
return [...BASE_IGNORE_PATTERNS, ...RELEASE_IGNORE_PATTERNS];
|
|
1978
|
+
}
|
|
1970
1979
|
const PRETTIER_CONFIG = `{
|
|
1971
1980
|
"semi": true,
|
|
1972
1981
|
"singleQuote": false,
|
|
@@ -1981,16 +1990,55 @@ async function generateFormatter(ctx) {
|
|
|
1981
1990
|
}
|
|
1982
1991
|
async function generateOxfmt(ctx) {
|
|
1983
1992
|
const filePath = ".oxfmtrc.json";
|
|
1984
|
-
|
|
1993
|
+
const required = requiredIgnorePatterns(ctx.config.releaseStrategy);
|
|
1994
|
+
const existing = ctx.read(filePath);
|
|
1995
|
+
if (!existing) {
|
|
1996
|
+
const config = { ignorePatterns: required };
|
|
1997
|
+
ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
1998
|
+
return {
|
|
1999
|
+
filePath,
|
|
2000
|
+
action: "created",
|
|
2001
|
+
description: "Generated .oxfmtrc.json"
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
const OxfmtConfigSchema = z.record(z.string(), z.unknown());
|
|
2005
|
+
let jsonValue;
|
|
2006
|
+
try {
|
|
2007
|
+
jsonValue = JSON.parse(existing);
|
|
2008
|
+
} catch {
|
|
2009
|
+
const config = { ignorePatterns: required };
|
|
2010
|
+
ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
2011
|
+
return {
|
|
2012
|
+
filePath,
|
|
2013
|
+
action: "updated",
|
|
2014
|
+
description: "Replaced malformed .oxfmtrc.json"
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
const parseResult = OxfmtConfigSchema.safeParse(jsonValue);
|
|
2018
|
+
if (!parseResult.success) {
|
|
2019
|
+
const config = { ignorePatterns: required };
|
|
2020
|
+
ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
2021
|
+
return {
|
|
2022
|
+
filePath,
|
|
2023
|
+
action: "updated",
|
|
2024
|
+
description: "Replaced malformed .oxfmtrc.json"
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const parsed = parseResult.data;
|
|
2028
|
+
const ignorePatterns = Array.isArray(parsed["ignorePatterns"]) ? parsed["ignorePatterns"] : [];
|
|
2029
|
+
const existingSet = new Set(ignorePatterns.filter((p) => typeof p === "string"));
|
|
2030
|
+
const missing = required.filter((p) => !existingSet.has(p));
|
|
2031
|
+
if (missing.length === 0) return {
|
|
1985
2032
|
filePath,
|
|
1986
2033
|
action: "skipped",
|
|
1987
|
-
description: "Existing oxfmt config
|
|
2034
|
+
description: "Existing oxfmt config already has required ignorePatterns"
|
|
1988
2035
|
};
|
|
1989
|
-
|
|
2036
|
+
parsed["ignorePatterns"] = [...ignorePatterns, ...missing];
|
|
2037
|
+
ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
|
|
1990
2038
|
return {
|
|
1991
2039
|
filePath,
|
|
1992
|
-
action: "
|
|
1993
|
-
description:
|
|
2040
|
+
action: "updated",
|
|
2041
|
+
description: `Added ignorePatterns: ${missing.join(", ")}`
|
|
1994
2042
|
};
|
|
1995
2043
|
}
|
|
1996
2044
|
async function generatePrettier(ctx) {
|
|
@@ -2055,7 +2103,11 @@ const REQUIRED_ENTRIES = [
|
|
|
2055
2103
|
"!.env.example"
|
|
2056
2104
|
];
|
|
2057
2105
|
/** Tooling-specific entries added during init/update but not required for repo:sync --check. */
|
|
2058
|
-
const OPTIONAL_ENTRIES = [
|
|
2106
|
+
const OPTIONAL_ENTRIES = [
|
|
2107
|
+
".tooling-migrate.md",
|
|
2108
|
+
".tooling-archived/",
|
|
2109
|
+
".claude/hooks/bst-hook.audit.local.jsonl"
|
|
2110
|
+
];
|
|
2059
2111
|
const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
|
|
2060
2112
|
/** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
|
|
2061
2113
|
function normalizeEntry(entry) {
|
|
@@ -2214,12 +2266,13 @@ const KNIP_CONFIG_SINGLE = `import type { KnipConfig } from "knip";
|
|
|
2214
2266
|
export default {
|
|
2215
2267
|
entry: ["src/index.ts"],
|
|
2216
2268
|
project: ["src/**/*.ts"],
|
|
2217
|
-
ignore: ["dist/**"],
|
|
2269
|
+
ignore: ["dist/**", ".claude/hooks/**"],
|
|
2218
2270
|
} satisfies KnipConfig;
|
|
2219
2271
|
`;
|
|
2220
2272
|
const KNIP_CONFIG_MONOREPO = `import type { KnipConfig } from "knip";
|
|
2221
2273
|
|
|
2222
2274
|
export default {
|
|
2275
|
+
ignore: [".claude/hooks/**"],
|
|
2223
2276
|
workspaces: {
|
|
2224
2277
|
".": {
|
|
2225
2278
|
entry: [],
|
|
@@ -2350,7 +2403,33 @@ async function generatePnpmWorkspace(ctx) {
|
|
|
2350
2403
|
};
|
|
2351
2404
|
}
|
|
2352
2405
|
//#endregion
|
|
2406
|
+
//#region src/generators/claude-hooks.ts
|
|
2407
|
+
const HOOK_PATH = ".claude/hooks/bst-hook.mjs";
|
|
2408
|
+
async function generateClaudeHooks(ctx) {
|
|
2409
|
+
const content = "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.2.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,_e=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 j(){return new _e}(A=globalThis).__zod_globalRegistry??(A.__zod_globalRegistry=j()),globalThis.__zod_globalRegistry;function M(e,t){return new e({type:`string`,...g(t)})}function N(e){return new e({type:`unknown`})}const P=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)}),F=c(`ZodMiniString`,(e,t)=>{ue.init(e,t),P.init(e,t)});function I(e){return M(F,e)}const ve=c(`ZodMiniUnknown`,(e,t)=>{de.init(e,t),P.init(e,t)});function ye(){return N(ve)}const be=c(`ZodMiniObject`,(e,t)=>{me.init(e,t),P.init(e,t),p(e,`shape`,()=>t.shape)});function L(e,t){return new be({type:`object`,shape:e??{},...g(t)})}const xe=c(`ZodMiniRecord`,(e,t)=>{he.init(e,t),P.init(e,t)});function Se(e,t,n){return new xe({type:`record`,keyType:e,valueType:t,...g(n)})}const Ce=c(`ZodMiniOptional`,(e,t)=>{ge.init(e,t),P.init(e,t)});function we(e){return new Ce({type:`optional`,innerType:e})}const Te=L({session_id:I(),transcript_path:I(),cwd:I(),permission_mode:I(),hook_event_name:I(),tool_name:I(),tool_input:L({command:I(),description:we(I())})});async function Ee(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Te.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}function R(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function De(){let e=i.dirname(s(import.meta.url));return i.join(e,`bst-hook.audit.local.jsonl`)}function Oe(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let ke=!1;function Ae(t){try{let n=De();ke||=(Oe(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 z(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]===`&`){B(t,n,`&&`),n=``,r+=2;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){B(t,n,`||`),n=``,r+=2;continue}if(o===`|`){B(t,n,`|`),n=``,r++;continue}if(o===`;`){B(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function B(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const V=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Me(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(`>(`)&&!V.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!V.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 Ne=new Set([`status`,`log`,`diff`,`show`,`branch`,`remote`,`fetch`,`rev-parse`]),Pe=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),Fe=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),Ie=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function H(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(Ie.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function Le(e){for(let t of e.slice(1))if(Fe.has(t)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`))return!1;let t=H(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Ne.has(t)}function Re(e){let t=H(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return Pe.has(t)}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(`.`)),ze=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function Be(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!U.has(e))){n=e;break}return n?ze.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const Ve=new Set([`view`,`info`,`show`]);function He(e){let t=e[1];return t!==void 0&&Ve.has(t)}function Ue(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 We=new Set([`-exec`,`-execdir`,`-ok`,`-delete`]);function Ge(e){return!e.some(e=>We.has(e))}function Ke(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function qe(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 Je(e){return e.some(e=>e===`777`||e===`0777`)}const Ye=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function G(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&Ye.test(e))}function Xe(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 Ze(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 Qe=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>qe(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=>Xe(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>Ze(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>Re(e),reason:`Git write operations (push, add, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>Je(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=>He(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=>Ge(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>Ke(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>Le(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>Be(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)}],$e=L({scripts:Se(I(),ye())});function q(e,t){let n=Y(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=nt(t,n);if(!r)return null;let i=q(e,r);return i?{...i,pkgDir:r}:null}function tt(e,t){let n=J(t);if(!n)return null;let r=[];for(let t of n){let n=q(e,t);n&&r.push({...n,pkgDir:t})}return r}function J(e){let t=rt(e);if(!t)return null;let i=[];for(let s of t){if(!s.endsWith(`/*`))continue;let t=o(e,s.slice(0,-2)),c;try{c=n(t)}catch{continue}for(let e of c){let n=a(t,e);try{if(!r(n).isDirectory())continue}catch{continue}i.push(n)}}return i.length>0?i:null}function nt(e,t){let n=J(t);if(!n)return null;for(let t of n){let n=at(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function rt(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 it=L({name:I()});function at(e){try{let n=t(a(e,`package.json`),`utf-8`),r=it.safeParse(JSON.parse(n));return r.success?r.data.name:null}catch{return null}}function Y(e){try{let n=t(a(e,`package.json`),`utf-8`),r=$e.safeParse(JSON.parse(n));return r.success?r.data.scripts:null}catch{return null}}const ot=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`]),st=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function X(e){return ot.has(e)?!0:st.some(t=>e.startsWith(t))}function ct(e,t){let n=z(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 Z(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=ct(e.command,o);t&&(o={...o,cwd:t})}let t=lt(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 lt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=z(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(X(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=Me(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 Z(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(X(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=a.length?{action:`allow`,rule:`env-only`,reason:null}:Z(a.slice(e).join(` `),{...t,depth:t.depth+1})}if(c===`pnpm`)return ut(a,t);if(c===`npm`)return pt(a);if(c===`bash`||c===`sh`||c===`zsh`)return gt(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 Qe)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:`no rule matched: ${c}`}}function ut(e,t){let n=e.slice(1),r,i=-1,a=!1;for(let o=0;o<n.length;o++){let s=n[o];if(s===`--filter`||s.startsWith(`--filter=`))return mt(e,n,o,t);if(s===`-r`||s===`--recursive`){a=!0;continue}if(!(s.startsWith(`-`)&&!U.has(s))){r=s,i=o;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`}:Z(e.join(` `),{...t,depth:t.depth+1})}let o;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};o=e}else if(!U.has(r))o=r;else{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 a?ft(o,t):dt(o,t)}function dt(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=Z(n.pre,r);if(e.action!==`allow`)return e}let i=Z(n.main,r);if(i.action!==`allow`)return i;if(n.post){let e=Z(n.post,r);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-script:${e}`,reason:null}}function ft(e,t){let n=tt(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n={...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd};if(e.pre){let t=Z(e.pre,n);if(t.action!==`allow`)return t}let r=Z(e.main,n);if(r.action!==`allow`)return r;if(e.post){let t=Z(e.post,n);if(t.action!==`allow`)return t}}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function pt(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:Ue(e)}}function mt(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`}:Z(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=et(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=Z(l.pre,u);if(e.action!==`allow`)return e}let d=Z(l.main,u);if(d.action!==`allow`)return d;if(l.post){let e=Z(l.post,u);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}}const ht=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(`-`)||ht.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?Z(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 Ee();(Q.hook_event_name!==`PreToolUse`||Q.tool_name!==`Bash`)&&process.exit(0);const $=Z(Q.tool_input.command,{cwd:Q.cwd,depth:0});Ae({sessionId:Q.session_id,cwd:Q.cwd,command:Q.tool_input.command,decision:$.action,rule:$.rule,reason:$.reason}),$.action===`allow`?R(`allow`):$.action===`deny`&&R(`deny`,$.reason);export{};";
|
|
2410
|
+
const existing = ctx.read(HOOK_PATH);
|
|
2411
|
+
if (existing === content) return {
|
|
2412
|
+
filePath: HOOK_PATH,
|
|
2413
|
+
action: "skipped",
|
|
2414
|
+
description: "Hook is up to date"
|
|
2415
|
+
};
|
|
2416
|
+
ctx.write(HOOK_PATH, content);
|
|
2417
|
+
return {
|
|
2418
|
+
filePath: HOOK_PATH,
|
|
2419
|
+
action: existing ? "updated" : "created",
|
|
2420
|
+
description: existing ? "Updated bst-hook.mjs" : "Generated bst-hook.mjs"
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
//#endregion
|
|
2353
2424
|
//#region src/generators/claude-settings.ts
|
|
2425
|
+
const HookEntrySchema = z.object({
|
|
2426
|
+
type: z.string(),
|
|
2427
|
+
command: z.string()
|
|
2428
|
+
});
|
|
2429
|
+
const HookMatcherSchema = z.object({
|
|
2430
|
+
matcher: z.string(),
|
|
2431
|
+
hooks: z.array(HookEntrySchema).default([])
|
|
2432
|
+
});
|
|
2354
2433
|
const ClaudeSettingsSchema = z.object({
|
|
2355
2434
|
permissions: z.object({
|
|
2356
2435
|
allow: z.array(z.string()).default([]),
|
|
@@ -2359,6 +2438,7 @@ const ClaudeSettingsSchema = z.object({
|
|
|
2359
2438
|
allow: [],
|
|
2360
2439
|
deny: []
|
|
2361
2440
|
}),
|
|
2441
|
+
hooks: z.record(z.string(), z.array(HookMatcherSchema)).default({}),
|
|
2362
2442
|
instructions: z.array(z.string()).default([]),
|
|
2363
2443
|
enabledPlugins: z.record(z.string(), z.boolean()).default({}),
|
|
2364
2444
|
extraKnownMarketplaces: z.record(z.string(), z.record(z.string(), z.unknown())).default({})
|
|
@@ -2378,66 +2458,7 @@ function parseClaudeSettings(raw) {
|
|
|
2378
2458
|
}
|
|
2379
2459
|
}
|
|
2380
2460
|
function buildSettings(ctx) {
|
|
2381
|
-
const
|
|
2382
|
-
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
|
-
"Bash(test *)",
|
|
2434
|
-
"Bash([ *)",
|
|
2435
|
-
"Bash(grep *)",
|
|
2436
|
-
"Bash(which *)",
|
|
2437
|
-
"Bash(node -e *)",
|
|
2438
|
-
"Bash(node -p *)"
|
|
2439
|
-
];
|
|
2440
|
-
if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm} --filter *)`, `Bash(${pm} -r *)`);
|
|
2461
|
+
const allow = [];
|
|
2441
2462
|
const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
|
|
2442
2463
|
const extraKnownMarketplaces = {};
|
|
2443
2464
|
if (ctx.config.structure !== "monorepo") {
|
|
@@ -2447,54 +2468,19 @@ function buildSettings(ctx) {
|
|
|
2447
2468
|
permissions: {
|
|
2448
2469
|
allow,
|
|
2449
2470
|
deny: [
|
|
2450
|
-
"Bash(npx *)",
|
|
2451
|
-
"Bash(git push *)",
|
|
2452
|
-
"Bash(git push)",
|
|
2453
|
-
"Bash(git add *)",
|
|
2454
|
-
"Bash(git add)",
|
|
2455
|
-
"Bash(git commit *)",
|
|
2456
|
-
"Bash(git commit)",
|
|
2457
|
-
"Bash(git reset *)",
|
|
2458
|
-
"Bash(git merge *)",
|
|
2459
|
-
"Bash(git rebase *)",
|
|
2460
|
-
"Bash(git cherry-pick *)",
|
|
2461
|
-
"Bash(git checkout *)",
|
|
2462
|
-
"Bash(git switch *)",
|
|
2463
|
-
"Bash(git stash *)",
|
|
2464
|
-
"Bash(git tag *)",
|
|
2465
|
-
"Bash(git revert *)",
|
|
2466
|
-
"Bash(git clean *)",
|
|
2467
|
-
"Bash(git rm *)",
|
|
2468
|
-
"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
2471
|
"Read(.env)",
|
|
2493
2472
|
"Read(.env.*)",
|
|
2494
2473
|
"Read(*.env)",
|
|
2495
2474
|
"Read(*.env.*)"
|
|
2496
2475
|
]
|
|
2497
2476
|
},
|
|
2477
|
+
hooks: { PreToolUse: [{
|
|
2478
|
+
matcher: "Bash",
|
|
2479
|
+
hooks: [{
|
|
2480
|
+
type: "command",
|
|
2481
|
+
command: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/bst-hook.mjs"`
|
|
2482
|
+
}]
|
|
2483
|
+
}] },
|
|
2498
2484
|
instructions: [
|
|
2499
2485
|
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
2500
2486
|
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
@@ -2505,10 +2491,16 @@ function buildSettings(ctx) {
|
|
|
2505
2491
|
extraKnownMarketplaces
|
|
2506
2492
|
};
|
|
2507
2493
|
}
|
|
2508
|
-
|
|
2494
|
+
const BST_HOOK_MARKER = "bst-hook.mjs";
|
|
2495
|
+
/** Check if a hook entry list already contains the bst-hook command. */
|
|
2496
|
+
function hasBstHook(matchers) {
|
|
2497
|
+
return matchers.some((m) => m.matcher === "Bash" && m.hooks.some((h) => h.command.includes(BST_HOOK_MARKER)));
|
|
2498
|
+
}
|
|
2499
|
+
/** Remove enabledPlugins/extraKnownMarketplaces/hooks when empty to keep the file clean. */
|
|
2509
2500
|
function serializeSettings$1(settings) {
|
|
2510
|
-
const { enabledPlugins, extraKnownMarketplaces, ...rest } = settings;
|
|
2501
|
+
const { enabledPlugins, extraKnownMarketplaces, hooks, ...rest } = settings;
|
|
2511
2502
|
const out = { ...rest };
|
|
2503
|
+
if (Object.keys(hooks).length > 0) out["hooks"] = hooks;
|
|
2512
2504
|
if (Object.keys(enabledPlugins).length > 0) out["enabledPlugins"] = enabledPlugins;
|
|
2513
2505
|
if (Object.keys(extraKnownMarketplaces).length > 0) out["extraKnownMarketplaces"] = extraKnownMarketplaces;
|
|
2514
2506
|
return JSON.stringify(out, null, 2) + "\n";
|
|
@@ -2534,9 +2526,18 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
2534
2526
|
else mergedInstructions.push(inst);
|
|
2535
2527
|
instructionChanges++;
|
|
2536
2528
|
}
|
|
2529
|
+
let hookChanges = 0;
|
|
2530
|
+
const mergedHooks = { ...settings.hooks };
|
|
2531
|
+
for (const [event, generatedMatchers] of Object.entries(generated.hooks)) {
|
|
2532
|
+
const existingMatchers = mergedHooks[event] ?? [];
|
|
2533
|
+
if (!hasBstHook(existingMatchers)) {
|
|
2534
|
+
mergedHooks[event] = [...existingMatchers, ...generatedMatchers];
|
|
2535
|
+
hookChanges++;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2537
2538
|
const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in settings.enabledPlugins));
|
|
2538
2539
|
const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in settings.extraKnownMarketplaces));
|
|
2539
|
-
const changed = missingAllow.length + missingDeny.length + instructionChanges + missingPlugins.length + missingMarketplaces.length;
|
|
2540
|
+
const changed = missingAllow.length + missingDeny.length + hookChanges + instructionChanges + missingPlugins.length + missingMarketplaces.length;
|
|
2540
2541
|
if (changed === 0) return {
|
|
2541
2542
|
filePath,
|
|
2542
2543
|
action: "skipped",
|
|
@@ -2546,6 +2547,7 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
2546
2547
|
allow: [...settings.permissions.allow, ...missingAllow],
|
|
2547
2548
|
deny: [...settings.permissions.deny, ...missingDeny]
|
|
2548
2549
|
};
|
|
2550
|
+
if (Object.keys(mergedHooks).length > 0) rawJson["hooks"] = mergedHooks;
|
|
2549
2551
|
rawJson["instructions"] = mergedInstructions;
|
|
2550
2552
|
const updatedPlugins = { ...settings.enabledPlugins };
|
|
2551
2553
|
for (const [key, value] of missingPlugins) updatedPlugins[key] = value;
|
|
@@ -2581,6 +2583,7 @@ async function generateClaudeSettings(ctx) {
|
|
|
2581
2583
|
allow: [],
|
|
2582
2584
|
deny: []
|
|
2583
2585
|
},
|
|
2586
|
+
hooks: {},
|
|
2584
2587
|
instructions: [],
|
|
2585
2588
|
enabledPlugins: { "frontend-design@claude-plugins-official": true },
|
|
2586
2589
|
extraKnownMarketplaces: {}
|
|
@@ -3181,6 +3184,7 @@ async function runGenerators(ctx) {
|
|
|
3181
3184
|
results.push(await generateKnip(ctx));
|
|
3182
3185
|
results.push(await generateRenovate(ctx));
|
|
3183
3186
|
results.push(await generateCi(ctx));
|
|
3187
|
+
results.push(await generateClaudeHooks(ctx));
|
|
3184
3188
|
results.push(...await generateClaudeSettings(ctx));
|
|
3185
3189
|
results.push(await generateReleaseIt(ctx));
|
|
3186
3190
|
results.push(await generateVersionrc(ctx));
|
|
@@ -3201,7 +3205,7 @@ async function runGenerators(ctx) {
|
|
|
3201
3205
|
function generateMigratePrompt(results, config, detected) {
|
|
3202
3206
|
const sections = [];
|
|
3203
3207
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3204
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3208
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.40.0 repo:sync\` on ${timestamp}_\n`);
|
|
3205
3209
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3206
3210
|
if (changed.length > 0) {
|
|
3207
3211
|
const items = changed.map((r) => {
|
|
@@ -3501,7 +3505,8 @@ async function runCheck(targetDir) {
|
|
|
3501
3505
|
if (!newContent) continue;
|
|
3502
3506
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3503
3507
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3504
|
-
if (
|
|
3508
|
+
if (isBundledFile(r.filePath)) log.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} → ${formatBytes(newContent.length)})`);
|
|
3509
|
+
else if (!existing) {
|
|
3505
3510
|
const lineCount = newContent.split("\n").length - 1;
|
|
3506
3511
|
log.info(` + ${lineCount} new lines`);
|
|
3507
3512
|
} else {
|
|
@@ -3511,6 +3516,14 @@ async function runCheck(targetDir) {
|
|
|
3511
3516
|
}
|
|
3512
3517
|
return 1;
|
|
3513
3518
|
}
|
|
3519
|
+
/** Files that are minified/bundled and produce unreadable diffs. */
|
|
3520
|
+
function isBundledFile(filePath) {
|
|
3521
|
+
return filePath.endsWith("bst-hook.mjs");
|
|
3522
|
+
}
|
|
3523
|
+
function formatBytes(bytes) {
|
|
3524
|
+
if (bytes < 1024) return `${String(bytes)} B`;
|
|
3525
|
+
return `${(bytes / 1024).toFixed(1)} kB`;
|
|
3526
|
+
}
|
|
3514
3527
|
const normalize = (line) => line.trimEnd();
|
|
3515
3528
|
function lineDiff(oldText, newText) {
|
|
3516
3529
|
const oldLines = oldText.split("\n").map(normalize);
|
|
@@ -3818,6 +3831,30 @@ async function ensureRelease(executor, conn, tag) {
|
|
|
3818
3831
|
//#endregion
|
|
3819
3832
|
//#region src/release/version.ts
|
|
3820
3833
|
const BRANCH = "changeset-release/main";
|
|
3834
|
+
/** Pattern matching a dateless version heading like `## 1.2.3` or `## 1.2.3-beta.0`. */
|
|
3835
|
+
const DATELESS_VERSION_RE = /^## (\d+\.\d+\.\d+(?:-[^\s)]+)?)$/gm;
|
|
3836
|
+
/** Format today's date as YYYY-MM-DD. */
|
|
3837
|
+
function todayIso() {
|
|
3838
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3839
|
+
}
|
|
3840
|
+
/** Add today's date to dateless version headings in a changelog string. */
|
|
3841
|
+
function stampDates(content, date) {
|
|
3842
|
+
return content.replace(DATELESS_VERSION_RE, `## $1 (${date})`);
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* Stamp release dates on CHANGELOGs for packages that were just versioned.
|
|
3846
|
+
* Only modifies headings that don't already have a date (idempotent).
|
|
3847
|
+
*/
|
|
3848
|
+
function stampChangelogDates(executor, cwd, changedDirs, isMonorepo) {
|
|
3849
|
+
const date = todayIso();
|
|
3850
|
+
const changelogPaths = isMonorepo ? changedDirs.map((dir) => path.join(cwd, "packages", dir, "CHANGELOG.md")) : [path.join(cwd, "CHANGELOG.md")];
|
|
3851
|
+
for (const changelogPath of changelogPaths) {
|
|
3852
|
+
const content = executor.readFile(changelogPath);
|
|
3853
|
+
if (!content) continue;
|
|
3854
|
+
const updated = stampDates(content, date);
|
|
3855
|
+
if (updated !== content) executor.writeFile(changelogPath, updated);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3821
3858
|
/** Extract the latest changelog entry (content between first and second `## ` heading). */
|
|
3822
3859
|
function extractLatestEntry(changelog) {
|
|
3823
3860
|
const lines = changelog.split("\n");
|
|
@@ -3905,6 +3942,12 @@ async function runVersionMode(executor, config) {
|
|
|
3905
3942
|
debugExec(config, "pnpm changeset version", versionResult);
|
|
3906
3943
|
if (originalConfig) executor.writeFile(changesetConfigPath, originalConfig);
|
|
3907
3944
|
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
3945
|
+
const packagesAfterVersion = executor.listWorkspacePackages(config.cwd);
|
|
3946
|
+
if (packagesBefore.length > 0 || packagesAfterVersion.length > 0) {
|
|
3947
|
+
const beforeMap = new Map(packagesBefore.map((pkg) => [pkg.name, pkg.version]));
|
|
3948
|
+
const changedDirs = packagesAfterVersion.filter((pkg) => beforeMap.get(pkg.name) !== pkg.version).map((pkg) => pkg.dir);
|
|
3949
|
+
stampChangelogDates(executor, config.cwd, changedDirs, true);
|
|
3950
|
+
} else stampChangelogDates(executor, config.cwd, [], false);
|
|
3908
3951
|
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm", ["install", "--no-frozen-lockfile"], { cwd: config.cwd }));
|
|
3909
3952
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
3910
3953
|
debug(config, `PR title: ${title}`);
|
|
@@ -5173,11 +5216,122 @@ const dockerCheckCommand = defineCommand({
|
|
|
5173
5216
|
}
|
|
5174
5217
|
});
|
|
5175
5218
|
//#endregion
|
|
5219
|
+
//#region src/commands/hooks-audit.ts
|
|
5220
|
+
const AuditEntrySchema = z.object({
|
|
5221
|
+
timestamp: z.string(),
|
|
5222
|
+
sessionId: z.string(),
|
|
5223
|
+
cwd: z.string().optional(),
|
|
5224
|
+
command: z.string(),
|
|
5225
|
+
decision: z.enum([
|
|
5226
|
+
"allow",
|
|
5227
|
+
"deny",
|
|
5228
|
+
"passthrough"
|
|
5229
|
+
]),
|
|
5230
|
+
rule: z.string().nullable(),
|
|
5231
|
+
reason: z.string().nullable()
|
|
5232
|
+
});
|
|
5233
|
+
const write = (msg = "") => console.log(msg);
|
|
5234
|
+
function parseAuditLog(logPath) {
|
|
5235
|
+
if (!existsSync(logPath)) return [];
|
|
5236
|
+
const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
|
|
5237
|
+
const entries = [];
|
|
5238
|
+
for (const line of lines) try {
|
|
5239
|
+
const result = AuditEntrySchema.safeParse(JSON.parse(line));
|
|
5240
|
+
if (result.success) entries.push(result.data);
|
|
5241
|
+
} catch (_error) {}
|
|
5242
|
+
return entries;
|
|
5243
|
+
}
|
|
5244
|
+
/** Count occurrences of each unique command, sorted descending. */
|
|
5245
|
+
function topCommands(entries, limit) {
|
|
5246
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5247
|
+
for (const e of entries) {
|
|
5248
|
+
const existing = counts.get(e.command);
|
|
5249
|
+
if (existing) existing.count++;
|
|
5250
|
+
else counts.set(e.command, {
|
|
5251
|
+
command: e.command,
|
|
5252
|
+
count: 1,
|
|
5253
|
+
rule: e.rule
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
return [...counts.values()].toSorted((a, b) => b.count - a.count).slice(0, limit);
|
|
5257
|
+
}
|
|
5258
|
+
function printSection(title, entries, limit) {
|
|
5259
|
+
const top = topCommands(entries, limit);
|
|
5260
|
+
if (top.length === 0) return;
|
|
5261
|
+
write(title);
|
|
5262
|
+
for (const { command, count, rule } of top) {
|
|
5263
|
+
const ruleSuffix = rule ? ` [${rule}]` : "";
|
|
5264
|
+
write(` ${String(count).padStart(4)}x ${command}${ruleSuffix}`);
|
|
5265
|
+
}
|
|
5266
|
+
write();
|
|
5267
|
+
}
|
|
5268
|
+
const hooksAuditCommand = defineCommand({
|
|
5269
|
+
meta: {
|
|
5270
|
+
name: "hooks:audit",
|
|
5271
|
+
description: "Summarize the bst-hook audit log"
|
|
5272
|
+
},
|
|
5273
|
+
args: {
|
|
5274
|
+
dir: {
|
|
5275
|
+
type: "positional",
|
|
5276
|
+
description: "Project directory (default: current directory)",
|
|
5277
|
+
required: false
|
|
5278
|
+
},
|
|
5279
|
+
tail: {
|
|
5280
|
+
type: "string",
|
|
5281
|
+
description: "Show the last N raw entries"
|
|
5282
|
+
},
|
|
5283
|
+
decision: {
|
|
5284
|
+
type: "string",
|
|
5285
|
+
description: "Filter by decision: allow, deny, or passthrough"
|
|
5286
|
+
},
|
|
5287
|
+
json: {
|
|
5288
|
+
type: "boolean",
|
|
5289
|
+
description: "Output raw JSON lines"
|
|
5290
|
+
}
|
|
5291
|
+
},
|
|
5292
|
+
async run({ args }) {
|
|
5293
|
+
const targetDir = path.resolve(args.dir ?? ".");
|
|
5294
|
+
let entries = parseAuditLog(path.join(targetDir, ".claude/hooks/bst-hook.audit.local.jsonl"));
|
|
5295
|
+
if (entries.length === 0) {
|
|
5296
|
+
write("No audit log entries found.");
|
|
5297
|
+
return;
|
|
5298
|
+
}
|
|
5299
|
+
const decisionFilter = args.decision;
|
|
5300
|
+
if (decisionFilter) entries = entries.filter((e) => e.decision === decisionFilter);
|
|
5301
|
+
if (args.json === true) {
|
|
5302
|
+
const tail = args.tail ? Number.parseInt(args.tail, 10) : entries.length;
|
|
5303
|
+
for (const entry of entries.slice(-tail)) write(JSON.stringify(entry));
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
5306
|
+
if (args.tail) {
|
|
5307
|
+
const tailCount = Number.parseInt(args.tail, 10);
|
|
5308
|
+
for (const entry of entries.slice(-tailCount)) {
|
|
5309
|
+
const ts = new Date(entry.timestamp).toLocaleString();
|
|
5310
|
+
const rule = entry.rule ? ` [${entry.rule}]` : "";
|
|
5311
|
+
write(`${ts} ${entry.decision.padEnd(12)}${entry.command}${rule}`);
|
|
5312
|
+
}
|
|
5313
|
+
return;
|
|
5314
|
+
}
|
|
5315
|
+
const counts = {
|
|
5316
|
+
allow: 0,
|
|
5317
|
+
deny: 0,
|
|
5318
|
+
passthrough: 0
|
|
5319
|
+
};
|
|
5320
|
+
for (const e of entries) counts[e.decision]++;
|
|
5321
|
+
write(`${String(entries.length)} entries: ${String(counts.allow)} allow, ${String(counts.deny)} deny, ${String(counts.passthrough)} passthrough`);
|
|
5322
|
+
write();
|
|
5323
|
+
const denied = entries.filter((e) => e.decision === "deny");
|
|
5324
|
+
const passthrough = entries.filter((e) => e.decision === "passthrough");
|
|
5325
|
+
printSection("Denied commands:", denied, 10);
|
|
5326
|
+
printSection("Passthrough commands:", passthrough, 10);
|
|
5327
|
+
}
|
|
5328
|
+
});
|
|
5329
|
+
//#endregion
|
|
5176
5330
|
//#region src/bin.ts
|
|
5177
5331
|
const main = defineCommand({
|
|
5178
5332
|
meta: {
|
|
5179
5333
|
name: "bst",
|
|
5180
|
-
version: "0.
|
|
5334
|
+
version: "0.40.0",
|
|
5181
5335
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5182
5336
|
},
|
|
5183
5337
|
subCommands: {
|
|
@@ -5190,10 +5344,11 @@ const main = defineCommand({
|
|
|
5190
5344
|
"release:simple": releaseSimpleCommand,
|
|
5191
5345
|
"docker:publish": publishDockerCommand,
|
|
5192
5346
|
"docker:build": dockerBuildCommand,
|
|
5193
|
-
"docker:check": dockerCheckCommand
|
|
5347
|
+
"docker:check": dockerCheckCommand,
|
|
5348
|
+
"hooks:audit": hooksAuditCommand
|
|
5194
5349
|
}
|
|
5195
5350
|
});
|
|
5196
|
-
console.log(`@bensandee/tooling v0.
|
|
5351
|
+
console.log(`@bensandee/tooling v0.40.0`);
|
|
5197
5352
|
async function run() {
|
|
5198
5353
|
await runMain(main);
|
|
5199
5354
|
process.exit(process.exitCode ?? 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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.
|
|
63
|
+
"@bensandee/config": "0.10.0",
|
|
64
|
+
"@bensandee/claude-hooks": "0.2.0"
|
|
64
65
|
},
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "tsdown",
|