@bensandee/tooling 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/bin.mjs +324 -140
- package/package.json +2 -2
- package/tooling.schema.json +1 -1
package/README.md
CHANGED
|
@@ -89,13 +89,14 @@ To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first
|
|
|
89
89
|
|
|
90
90
|
### Release management
|
|
91
91
|
|
|
92
|
-
| Command | Description
|
|
93
|
-
| -------------------------------- |
|
|
94
|
-
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`.
|
|
95
|
-
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`.
|
|
96
|
-
| `tooling release:trigger` | Trigger a release workflow.
|
|
97
|
-
| `tooling forgejo:create-release` | Create a Forgejo release from a tag.
|
|
98
|
-
| `tooling changesets:merge` | Merge a changesets version PR.
|
|
92
|
+
| Command | Description |
|
|
93
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
94
|
+
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
|
|
95
|
+
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. |
|
|
96
|
+
| `tooling release:trigger` | Trigger a release workflow. |
|
|
97
|
+
| `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
|
|
98
|
+
| `tooling changesets:merge` | Merge a changesets version PR. |
|
|
99
|
+
| `tooling webhook:send <url>` | POST a structured JSON release payload with HMAC-SHA256 auth. Flags: `--tag`, `--repository`, `--server-url`, `--actor`, `--prerelease`. Secret from `WEBHOOK_SECRET` env var. |
|
|
99
100
|
|
|
100
101
|
#### `release:simple`
|
|
101
102
|
|
package/dist/bin.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { parse } from "jsonc-parser";
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
13
|
import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
14
|
+
import { createHmac } from "node:crypto";
|
|
14
15
|
import picomatch from "picomatch";
|
|
15
16
|
import { tmpdir } from "node:os";
|
|
16
17
|
//#region src/types.ts
|
|
@@ -626,7 +627,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
626
627
|
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
|
|
627
628
|
registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
|
|
628
629
|
registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" }),
|
|
629
|
-
postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default:
|
|
630
|
+
postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)" }),
|
|
630
631
|
postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" })
|
|
631
632
|
});
|
|
632
633
|
/** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
|
|
@@ -1571,7 +1572,7 @@ async function generatePackageJson(ctx) {
|
|
|
1571
1572
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1572
1573
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1573
1574
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
|
|
1574
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1575
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.45.0";
|
|
1575
1576
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1576
1577
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1577
1578
|
"@changesets/cli": "2.30.0",
|
|
@@ -2181,16 +2182,14 @@ async function generateGitignore(ctx) {
|
|
|
2181
2182
|
//#region src/generators/release-ci.ts
|
|
2182
2183
|
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
2183
2184
|
"if [ -n \"$POST_RELEASE_HOOK_URL\" ]; then",
|
|
2184
|
-
"
|
|
2185
|
-
" $",
|
|
2186
|
-
"{POST_RELEASE_HOOK_SECRET:+-H \"Authorization: Bearer $POST_RELEASE_HOOK_SECRET\"}",
|
|
2185
|
+
" pnpm dlx @bensandee/tooling webhook:send",
|
|
2187
2186
|
" \"$POST_RELEASE_HOOK_URL\"; fi"
|
|
2188
2187
|
].join("");
|
|
2189
|
-
/** Build env vars for hook secrets to inject into a
|
|
2188
|
+
/** Build env vars for hook secrets to inject into a workflow step. */
|
|
2190
2189
|
function hookSecretEnv(secrets) {
|
|
2191
2190
|
const env = {
|
|
2192
2191
|
POST_RELEASE_HOOK_URL: actionsExpr("secrets.POST_RELEASE_HOOK_URL"),
|
|
2193
|
-
|
|
2192
|
+
WEBHOOK_SECRET: actionsExpr("secrets.POST_RELEASE_HOOK_SECRET")
|
|
2194
2193
|
};
|
|
2195
2194
|
for (const name of secrets ?? []) env[name] = actionsExpr(`secrets.${name}`);
|
|
2196
2195
|
return env;
|
|
@@ -2707,7 +2706,7 @@ function writeIfChanged(ctx, filePath, content, label) {
|
|
|
2707
2706
|
};
|
|
2708
2707
|
}
|
|
2709
2708
|
async function generateClaudeHooks(ctx) {
|
|
2710
|
-
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.1\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync — do not edit manually.\n */\nimport*as e from\"node:fs\";import{existsSync as t,readFileSync as n,readdirSync as r,statSync as i}from\"node:fs\";import*as a from\"node:path\";import{dirname as o,join as s,resolve as c}from\"node:path\";import{fileURLToPath as l}from\"node:url\";Object.freeze({status:`aborted`});function u(e,t,n){function r(n,r){if(n._zod||Object.defineProperty(n,`_zod`,{value:{def:r,constr:o,traits:new Set},enumerable:!1}),n._zod.traits.has(e))return;n._zod.traits.add(e),t(n,r);let i=o.prototype,a=Object.keys(i);for(let e=0;e<a.length;e++){let t=a[e];t in n||(n[t]=i[t].bind(n))}}let i=n?.Parent??Object;class a extends i{}Object.defineProperty(a,`name`,{value:e});function o(e){var t;let i=n?.Parent?new a:this;r(i,e),(t=i._zod).deferred??(t.deferred=[]);for(let e of i._zod.deferred)e();return i}return Object.defineProperty(o,`init`,{value:r}),Object.defineProperty(o,Symbol.hasInstance,{value:t=>n?.Parent&&t instanceof n.Parent?!0:t?._zod?.traits?.has(e)}),Object.defineProperty(o,`name`,{value:e}),o}var d=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const f={};function p(e){return e&&Object.assign(f,e),f}function ee(e,t){return typeof t==`bigint`?t.toString():t}function te(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function ne(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const re=Symbol(`evaluating`);function m(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==re)return r===void 0&&(r=re,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const h=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ie(e){if(g(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(g(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function ae(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function _(e){let t=e;if(!t)return{};if(typeof t==`string`)return{error:()=>t};if(t?.message!==void 0){if(t?.error!==void 0)throw Error(\"Cannot specify both `message` and `error` params\");t.error=t.message}return delete t.message,typeof t.error==`string`?{...t,error:()=>t.error}:t}function oe(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function v(e,t=0){if(e.aborted===!0)return!0;for(let n=t;n<e.issues.length;n++)if(e.issues[n]?.continue!==!0)return!0;return!1}function y(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function b(e){return typeof e==`string`?e:e?.message}function x(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=b(e.inst?._zod.def?.error?.(e))??b(t?.error?.(e))??b(n.customError?.(e))??b(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const S=(e,t)=>{e.name=`$ZodError`,Object.defineProperty(e,`_zod`,{value:e._zod,enumerable:!1}),Object.defineProperty(e,`issues`,{value:t,enumerable:!1}),e.message=JSON.stringify(t,ee,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},se=u(`$ZodError`,S),C=u(`$ZodError`,S,{Parent:Error}),ce=(e=>(t,n,r,i)=>{let a=r?Object.assign(r,{async:!1}):{async:!1},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise)throw new d;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,p())));throw h(t,i?.callee),t}return o.value})(C),le=(e=>async(t,n,r,i)=>{let a=r?Object.assign(r,{async:!0}):{async:!0},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise&&(o=await o),o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,p())));throw h(t,i?.callee),t}return o.value})(C),w=(e=>(t,n,r)=>{let i=r?{...r,async:!1}:{async:!1},a=t._zod.run({value:n,issues:[]},i);if(a instanceof Promise)throw new d;return a.issues.length?{success:!1,error:new(e??se)(a.issues.map(e=>x(e,i,p())))}:{success:!0,data:a.value}})(C),ue=(e=>async(t,n,r)=>{let i=r?Object.assign(r,{async:!0}):{async:!0},a=t._zod.run({value:n,issues:[]},i);return a instanceof Promise&&(a=await a),a.issues.length?{success:!1,error:new e(a.issues.map(e=>x(e,i,p())))}:{success:!0,data:a.value}})(C),de=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},fe=/^-?\\d+(?:\\.\\d+)?$/,pe={major:4,minor:3,patch:6},T=u(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=pe;let r=[...e._zod.def.checks??[]];e._zod.traits.has(`$ZodCheck`)&&r.unshift(e);for(let t of r)for(let n of t._zod.onattach)n(e);if(r.length===0)(n=e._zod).deferred??(n.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{let t=(e,t,n)=>{let r=v(e),i;for(let a of t){if(a._zod.def.when){if(!a._zod.def.when(e))continue}else if(r)continue;let t=e.issues.length,o=a._zod.check(e);if(o instanceof Promise&&n?.async===!1)throw new d;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=v(e,t))});else{if(e.issues.length===t)continue;r||=v(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(v(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new d;return o.then(t=>e._zod.parse(t,a))}return e._zod.parse(o,a)};e._zod.run=(i,a)=>{if(a.skipChecks)return e._zod.parse(i,a);if(a.direction===`backward`){let t=e._zod.parse({value:i.value,issues:[]},{...a,skipChecks:!0});return t instanceof Promise?t.then(e=>n(e,i,a)):n(t,i,a)}let o=e._zod.parse(i,a);if(o instanceof Promise){if(a.async===!1)throw new d;return o.then(e=>t(e,r,a))}return t(o,r,a)}}m(e,`~standard`,()=>({validate:t=>{try{let n=w(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return ue(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=u(`$ZodString`,(e,t)=>{T.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??de(e._zod.bag),e._zod.parse=(n,r)=>{if(t.coerce)try{n.value=String(n.value)}catch{}return typeof n.value==`string`||n.issues.push({expected:`string`,code:`invalid_type`,input:n.value,inst:e}),n}}),he=u(`$ZodUnknown`,(e,t)=>{T.init(e,t),e._zod.parse=e=>e});function E(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...y(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function ge(e){let t=Object.keys(e.shape);for(let n of t)if(!e.shape?.[n]?._zod?.traits?.has(`$ZodType`))throw Error(`Invalid element at key \"${n}\": expected a Zod schema`);let n=oe(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function _e(e,t,n,r,i,a){let o=[],s=i.keySet,c=i.catchall._zod,l=c.def.type,u=c.optout===`optional`;for(let i in t){if(s.has(i))continue;if(l===`never`){o.push(i);continue}let a=c.run({value:t[i],issues:[]},r);a instanceof Promise?e.push(a.then(e=>E(e,n,i,t,u))):E(a,n,i,t,u)}return o.length&&n.issues.push({code:`unrecognized_keys`,keys:o,input:t,inst:a}),e.length?Promise.all(e).then(()=>n):n}const ve=u(`$ZodObject`,(e,t)=>{if(T.init(e,t),!Object.getOwnPropertyDescriptor(t,`shape`)?.get){let e=t.shape;Object.defineProperty(t,`shape`,{get:()=>{let n={...e};return Object.defineProperty(t,`shape`,{value:n}),n}})}let n=te(()=>ge(t));m(e._zod,`propValues`,()=>{let e=t.shape,n={};for(let t in e){let r=e[t]._zod;if(r.values){n[t]??(n[t]=new Set);for(let e of r.values)n[t].add(e)}}return n});let r=g,i=t.catchall,a;e._zod.parse=(t,o)=>{a??=n.value;let s=t.value;if(!r(s))return t.issues.push({expected:`object`,code:`invalid_type`,input:s,inst:e}),t;t.value={};let c=[],l=a.shape;for(let e of a.keys){let n=l[e],r=n._zod.optout===`optional`,i=n._zod.run({value:s[e],issues:[]},o);i instanceof Promise?c.push(i.then(n=>E(n,t,e,s,r))):E(i,t,e,s,r)}return i?_e(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),ye=u(`$ZodRecord`,(e,t)=>{T.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!ie(i))return n.issues.push({expected:`record`,code:`invalid_type`,input:i,inst:e}),n;let a=[],o=t.keyType._zod.values;if(o){n.value={};let s=new Set;for(let e of o)if(typeof e==`string`||typeof e==`number`||typeof e==`symbol`){s.add(typeof e==`number`?e.toString():e);let o=t.valueType._zod.run({value:i[e],issues:[]},r);o instanceof Promise?a.push(o.then(t=>{t.issues.length&&n.issues.push(...y(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...y(e,o.issues)),n.value[e]=o.value)}let c;for(let e in i)s.has(e)||(c??=[],c.push(e));c&&c.length>0&&n.issues.push({code:`unrecognized_keys`,input:i,inst:e,keys:c})}else{n.value={};for(let o of Reflect.ownKeys(i)){if(o===`__proto__`)continue;let s=t.keyType._zod.run({value:o,issues:[]},r);if(s instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);if(typeof o==`string`&&fe.test(o)&&s.issues.length){let e=t.keyType._zod.run({value:Number(o),issues:[]},r);if(e instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);e.issues.length===0&&(s=e)}if(s.issues.length){t.mode===`loose`?n.value[o]=i[o]:n.issues.push({code:`invalid_key`,origin:`record`,issues:s.issues.map(e=>x(e,r,p())),input:o,path:[o],inst:e});continue}let c=t.valueType._zod.run({value:i[o],issues:[]},r);c instanceof Promise?a.push(c.then(e=>{e.issues.length&&n.issues.push(...y(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...y(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function be(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const xe=u(`$ZodOptional`,(e,t)=>{T.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,m(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),m(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${ne(e.source)})?$`):void 0}),e._zod.parse=(e,n)=>{if(t.innerType._zod.optin===`optional`){let r=t.innerType._zod.run(e,n);return r instanceof Promise?r.then(t=>be(t,e.value)):be(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var Se,Ce=class{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){let n=t[0];return this._map.set(e,n),n&&typeof n==`object`&&`id`in n&&this._idmap.set(n.id,e),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){let t=this._map.get(e);return t&&typeof t==`object`&&`id`in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){let t=e._zod.parent;if(t){let n={...this.get(t)??{}};delete n.id;let r={...n,...this._map.get(e)};return Object.keys(r).length?r:void 0}return this._map.get(e)}has(e){return this._map.has(e)}};function we(){return new Ce}(Se=globalThis).__zod_globalRegistry??(Se.__zod_globalRegistry=we()),globalThis.__zod_globalRegistry;function Te(e,t){return new e({type:`string`,..._(t)})}function Ee(e){return new e({type:`unknown`})}const D=u(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);T.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>ce(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>w(e,t,n),e.parseAsync=async(t,n)=>le(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>ue(e,t,n),e.check=(...n)=>e.clone({...t,checks:[...t.checks??[],...n.map(e=>typeof e==`function`?{_zod:{check:e,def:{check:`custom`},onattach:[]}}:e)]},{parent:!0}),e.with=e.check,e.clone=(t,n)=>ae(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),De=u(`ZodMiniString`,(e,t)=>{me.init(e,t),D.init(e,t)});function O(e){return Te(De,e)}const Oe=u(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),D.init(e,t)});function k(){return Ee(Oe)}const ke=u(`ZodMiniObject`,(e,t)=>{ve.init(e,t),D.init(e,t),m(e,`shape`,()=>t.shape)});function A(e,t){return new ke({type:`object`,shape:e??{},..._(t)})}const Ae=u(`ZodMiniRecord`,(e,t)=>{ye.init(e,t),D.init(e,t)});function j(e,t,n){return new Ae({type:`record`,keyType:e,valueType:t,..._(n)})}const je=u(`ZodMiniOptional`,(e,t)=>{xe.init(e,t),D.init(e,t)});function M(e){return new je({type:`optional`,innerType:e})}const Me=A({session_id:O(),transcript_path:O(),cwd:O(),permission_mode:O(),hook_event_name:O(),tool_name:O(),tool_input:j(O(),k())});async function Ne(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Me.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Pe=A({command:O(),description:M(O())}),Fe=A({file_path:O()}),Ie=A({pattern:O(),path:M(O())});function Le(e){let t=Pe.safeParse(e);return t.success?t.data:null}function Re(e){let t=Fe.safeParse(e);return t.success?t.data:null}function ze(e){let t=Ie.safeParse(e);return t.success?t.data:null}function N(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Be(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ve(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let He=!1;function Ue(t){try{let n=Be();He||=(Ve(n),!0);let r={timestamp:new Date().toISOString(),...t};e.appendFileSync(n,JSON.stringify(r)+`\n`)}catch(e){process.stderr.write(`[bst-hook] audit write failed: ${e instanceof Error?e.message:String(e)}\\n`)}}function P(e){let t=[],n=``,r=0,i=!1;for(;r<e.length;){let a=e[r];if(a===`'`){for(i=!0,r++;r<e.length&&e[r]!==`'`;)n+=e[r],r++;if(r>=e.length)return{ok:!1,error:`unterminated single quote`};r++}else if(a===`\"`){for(i=!0,r++;r<e.length&&e[r]!==`\"`;){if(e[r]===`\\\\`&&r+1<e.length){let t=e[r+1];if(t===`\"`||t===`\\\\`||t===`$`||t===\"`\"){n+=t,r+=2;continue}}n+=e[r],r++}if(r>=e.length)return{ok:!1,error:`unterminated double quote`};r++}else a===`\\\\`&&r+1<e.length?(i=!0,n+=e[r+1],r+=2):a===` `||a===` `?(i&&=(t.push(n),n=``,!1),r++):(i=!0,n+=a,r++)}return i&&t.push(n),{ok:!0,tokens:t}}function We(e){if(!e||!e.trim())return[];let t=[],n=``,r=0,i=!1,a=!1;for(;r<e.length;){let o=e[r];if(o===`'`&&!a){i=!i,n+=o,r++;continue}if(o===`\"`&&!i){a=!a,n+=o,r++;continue}if(o===`\\\\`&&!i&&r+1<e.length){n+=o+e[r+1],r+=2;continue}if(!i&&!a){if(o===`&`&&r+1<e.length&&e[r+1]===`&`){F(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){F(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){F(t,n,`||`),n=``,r+=2;continue}if(o===`|`){F(t,n,`|`),n=``,r++;continue}if(o===`;`){F(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function F(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const Ge=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ke(e,t){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!Ge.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!Ge.has(n)){if(n.includes(`<`)&&!n.includes(`<(`))return{safe:!1,reason:`input redirect`};if(n.includes(`>`)){if(n.endsWith(`>/dev/null`)||n.match(/^(&>>|&>|>>|>|1>|2>)$/)&&e[t+1]===`/dev/null`)continue;return{safe:!1,reason:`output redirect to file`}}}}return{safe:!0}}const qe=[`.sample`,`.example`,`.template`],Je=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Ye(e){return qe.some(t=>e.endsWith(t))}function I(e){return Ye(a.basename(e))?!1:Je.test(e)}function Xe(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&I(e))}const Ze=new Set([`status`,`log`,`diff`,`show`,`blame`,`grep`,`shortlog`,`range-diff`,`describe`,`branch`,`remote`,`fetch`,`ls-files`,`ls-tree`,`ls-remote`,`show-ref`,`for-each-ref`,`rev-parse`,`rev-list`,`cat-file`,`diff-tree`,`name-rev`,`merge-base`,`check-ignore`]),Qe=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),$e=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),et=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function L(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(et.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function tt(e){for(let t of e.slice(1))if($e.has(t)||t===`-o`||t.startsWith(`--output`)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`)||t.startsWith(`-o=`)||t.startsWith(`--output=`))return!1;let t=L(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Ze.has(t)}function nt(e){let t=L(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return Qe.has(t)}const rt=new Set([`push`,`pop`,`apply`,`show`]);function R(e){let t=L(e);if(!t)return!1;if(t===`add`)return!0;if(t===`checkout`)return e.slice(e.indexOf(`checkout`)+1).some(e=>e===`-b`||e===`-B`);if(t===`switch`)return!0;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===void 0?!0:rt.has(t)}return!1}const z=new Set(`install.i.add.remove.rm.uninstall.un.update.up.upgrade.list.ls.ll.la.why.view.info.show.outdated.audit.licenses.init.create.link.unlink.pack.publish.config.set.get.store.cache.rebuild.rb.prune.dedupe.dlx.exec.run.patch.patch-commit.recursive.-r`.split(`.`)),it=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function at(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!z.has(e))){n=e;break}return n?it.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const ot=new Set([`view`,`info`,`show`]);function st(e){let t=e[1];return t!==void 0&&ot.has(t)}function ct(e){let t=e[1],n=`This project uses pnpm.`;return t?t===`install`||t===`i`||t===`ci`?e.slice(2).some(e=>!e.startsWith(`-`))?`${n} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`:`${n} Use \\`pnpm install\\` instead of npm.`:t===`add`?`${n} Use \\`pnpm add <pkg>\\` instead of npm.`:t===`uninstall`||t===`remove`||t===`rm`||t===`un`||t===`r`?`${n} Use \\`pnpm remove <pkg>\\` instead of npm.`:t===`run`||t===`run-script`?`${n} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`:t===`test`||t===`t`||t===`tst`?`${n} Use \\`pnpm test\\` instead of npm.`:t===`start`?`${n} Use \\`pnpm start\\` instead of npm.`:t===`stop`?`${n} Use \\`pnpm stop\\` instead of npm.`:t===`restart`?`${n} Use \\`pnpm restart\\` instead of npm.`:t===`exec`?`${n} Use \\`pnpm exec <cmd>\\` instead of npm.`:t===`list`||t===`ls`||t===`ll`||t===`la`?`${n} Use \\`pnpm list\\` instead of npm.`:t===`why`?`${n} Use \\`pnpm why\\` instead of npm.`:t===`outdated`?`${n} Use \\`pnpm outdated\\` instead of npm.`:t===`audit`?`${n} Use \\`pnpm audit\\` instead of npm.`:t===`update`||t===`up`||t===`upgrade`?`${n} Use \\`pnpm update\\` instead of npm.`:t===`init`?`${n} Use \\`pnpm init\\` instead of npm.`:t===`publish`?`${n} Use \\`pnpm publish\\` instead of npm.`:t===`link`?`${n} Use \\`pnpm link\\` instead of npm.`:t===`unlink`?`${n} Use \\`pnpm unlink\\` instead of npm.`:t===`pack`?`${n} Use \\`pnpm pack\\` instead of npm.`:t===`cache`?`${n} Use \\`pnpm store\\` instead of \\`npm cache\\`.`:t===`config`||t===`set`||t===`get`?`${n} Use \\`pnpm config\\` instead of npm.`:`${n} Use the equivalent pnpm command instead of npm.`:`${n} Use \\`pnpm\\` instead of npm.`}function B(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`)){if(n===`-t`||n.startsWith(`--target-directory`))return!1;continue}if(a.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=a.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+a.sep))return!1}return!0}const lt=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function ut(e){return!e.some(e=>lt.has(e))}function dt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ft(e){let t=e.findIndex(e=>a.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),i=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=i.includes(`--recursive`);return o||s}function pt(e){return e.some(e=>e===`777`||e===`0777`)}function mt(e){let t=e.indexOf(`push`);if(t===-1)return!1;let n=e.slice(t+1);return n.some(e=>e===`--force`||e===`-f`&&!n.includes(`--force-with-lease`))}function ht(e){if(!(e.includes(`reset`)&&e.includes(`--hard`)))return!1;if(e.some(e=>e===`main`||e===`master`))return!0;let t=e.filter(e=>e!==`git`&&e!==`reset`&&!e.startsWith(`-`));return t.length===0||t.includes(`HEAD`)}const gt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ft(e),reason:\"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"},{name:`sudo`,match:/^sudo$/,action:`deny`,reason:`Don't use sudo. If you need elevated permissions, ask me to do it manually.`},{name:`git-force-push`,match:/^git$/,action:`deny`,when:e=>mt(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>ht(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>nt(e)&&!R(e),reason:`Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>pt(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-read`,match:/^(cat|head|tail|less|more|grep)$/,action:`deny`,when:e=>Xe(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`npx-redirect`,match:/^npx$/,action:`deny`,reason:\"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"}],V=[{name:`vitest`,match:/^vitest$/,action:`allow`},{name:`jest`,match:/^jest$/,action:`allow`},{name:`tsc`,match:/^tsc$/,action:`allow`},{name:`tsdown`,match:/^tsdown$/,action:`allow`},{name:`oxlint`,match:/^oxlint$/,action:`allow`},{name:`oxfmt`,match:/^oxfmt$/,action:`allow`},{name:`eslint`,match:/^eslint$/,action:`allow`},{name:`prettier`,match:/^prettier$/,action:`allow`},{name:`biome`,match:/^biome$/,action:`allow`},{name:`knip`,match:/^knip$/,action:`allow`},{name:`cat`,match:/^cat$/,action:`allow`},{name:`head`,match:/^head$/,action:`allow`},{name:`tail`,match:/^tail$/,action:`allow`},{name:`grep`,match:/^grep$/,action:`allow`},{name:`ls`,match:/^ls$/,action:`allow`},{name:`wc`,match:/^wc$/,action:`allow`},{name:`uniq`,match:/^uniq$/,action:`allow`},{name:`diff`,match:/^diff$/,action:`allow`},{name:`echo`,match:/^echo$/,action:`allow`},{name:`pwd`,match:/^pwd$/,action:`allow`},{name:`which`,match:/^which$/,action:`allow`},{name:`dirname`,match:/^dirname$/,action:`allow`},{name:`basename`,match:/^basename$/,action:`allow`},{name:`realpath`,match:/^realpath$/,action:`allow`},{name:`npm`,match:/^npm$/,action:`allow`,when:e=>st(e)},{name:`test`,match:/^test$/,action:`allow`},{name:`bracket-test`,match:/^\\[$/,action:`allow`},{name:`cd`,match:/^cd$/,action:`allow`},{name:`true`,match:/^true$/,action:`allow`},{name:`colon`,match:/^:$/,action:`allow`},{name:`find`,match:/^find$/,action:`allow`,when:e=>ut(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>dt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>tt(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>R(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>at(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>B(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>B(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>B(e,t)}],_t=A({scripts:j(O(),k())});function H(e,t){let n=Tt(t);if(!n)return null;let r=n[e];if(typeof r!=`string`)return null;let i=n[`pre${e}`],a=n[`post${e}`];return{pre:typeof i==`string`?i:null,main:r,post:typeof a==`string`?a:null}}function vt(e,t,n){let r=xt(t,n);if(!r)return null;let i=H(e,r);return i?{...i,pkgDir:r}:null}function yt(e,t){let n=U(t);if(!n)return null;let r=[];for(let t of n){let n=H(e,t);n&&r.push({...n,pkgDir:t})}return r}function bt(e){let n=c(e);for(;;){if(t(s(n,`pnpm-workspace.yaml`)))return n;let e=o(n);if(e===n)return null;n=e}}function U(e){let t=St(e);if(!t)return null;let n=[];for(let a of t){if(!a.endsWith(`/*`))continue;let t=c(e,a.slice(0,-2)),o;try{o=r(t)}catch{continue}for(let e of o){let r=s(t,e);try{if(!i(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function xt(e,t){let n=U(t);if(!n)return null;for(let t of n){let n=wt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function St(e){try{let t=n(s(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=t.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<t.length;e++){let n=t[e],r=/^\\s+-\\s+(.+)$/.exec(n);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const Ct=A({name:O()});function wt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Ct.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Tt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=_t.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}const Et=new Set([`LD_PRELOAD`,`LD_LIBRARY_PATH`,`DYLD_INSERT_LIBRARIES`,`NODE_OPTIONS`,`PYTHONPATH`,`PYTHONSTARTUP`,`PERL5OPT`,`PERL5LIB`,`RUBYOPT`,`RUBYLIB`,`BASH_ENV`,`ENV`,`npm_config_prefix`,`npm_config_registry`,`EDITOR`,`VISUAL`]),Dt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function W(e){return Et.has(e)?!0:Dt.some(t=>e.startsWith(t))}function Ot(e,t){let n=P(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return a.isAbsolute(i)?i:a.resolve(t.cwd,i)}function G(e,t){if(t.depth>=5)return{action:`passthrough`,rule:null,reason:`max evaluation depth exceeded`};let n=e.trim();if(!n)return{action:`passthrough`,rule:null,reason:null};if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=We(n);if(r.length===0)return{action:`passthrough`,rule:null,reason:null};let i=null,a=null,o=t;for(let e of r){if(e.operator===`&&`){let t=Ot(e.command,o);t&&(o={...o,cwd:t})}let t=kt(e.command,o);if(t.action===`deny`)return t;t.action===`passthrough`&&(i||=t),t.action===`allow`&&(a=t)}return i||(r.length===1&&a?a:{action:`allow`,rule:`all-segments`,reason:null})}function K(e,t){if(e.pre){let n=G(e.pre,t);if(n.action!==`allow`)return n}let n=G(e.main,t);if(n.action!==`allow`)return n;if(e.post){let n=G(e.post,t);if(n.action!==`allow`)return n}return`allow`}function kt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=P(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let i=r.tokens;if(i.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<i.length&&/^\\w+=/.test(i[o]);){let e=i[o].split(`=`)[0];if(W(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=i.length)return{action:`allow`,rule:`env-only`,reason:null};i=i.slice(o);let s=Ke(i,e);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};for(let e=0;e<i.length;e++){let t=i[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(i[e+1]===`/dev/null`)continue;return{action:`passthrough`,rule:null,reason:`output redirect to file`}}if(t===`<`)return{action:`passthrough`,rule:null,reason:`input redirect`}}}let c=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);i=i.filter((e,t)=>!(c.has(e)||(e===`>`||e===`>>`)&&i[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(i[t-1]===`>`||i[t-1]===`>>`)));let l=a.basename(i[0]);if(l===`command`&&i.length>1)return G(i.slice(1).join(` `),{...t,depth:t.depth+1});if(l===`env`){let e=1;for(;e<i.length&&/^\\w+=/.test(i[e]);){let t=i[e].split(`=`)[0];if(W(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:G(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return At(i,t);if(l===`npm`)return Mt(i);if(l===`bash`||l===`sh`||l===`zsh`)return Ft(i,t);if(l===`xargs`)return Lt(i,t);if(l===`node`&&(i.includes(`-e`)||i.includes(`-p`)||i.includes(`--eval`)||i.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(l===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of gt)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of V)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};return{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function At(e,t){let n=e.slice(1),r,i=-1,o=!1,s=!1,c;for(let a=0;a<n.length;a++){let l=n[a];if(l===`--filter`||l.startsWith(`--filter=`))return Nt(e,n,a,t);if(l===`-r`||l===`--recursive`){o=!0;continue}if(l===`-w`||l===`--workspace-root`){s=!0;continue}if(l===`-C`||l===`--dir`){c=n[++a];continue}if(l.startsWith(`-C=`)||l.startsWith(`--dir=`)){c=l.split(`=`,2)[1];continue}if(!(l.startsWith(`-`)&&!z.has(l))){r=l,i=a;break}}if(!r)return{action:`passthrough`,rule:null,reason:null};if(r===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(r===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(r===`exec`){let e=n.slice(i+1);return e.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:G(e.join(` `),{...t,depth:t.depth+1})}let l;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};l=e}else if(!z.has(r))l=r;else{for(let n of V)if(n.match.test(`pnpm`)&&(!n.when||n.when(e,t)))return{action:`allow`,rule:n.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}if(o)return jt(l,t);if(s){let e=bt(t.cwd);return e?q(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return q(l,{...t,cwd:e})}return q(l,t)}function q(e,t){let n=H(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=K(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function jt(e,t){let n=yt(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=K(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function Mt(e){for(let t of V)if(t.match.test(`npm`)&&(!t.when||t.when(e,{cwd:``,depth:0})))return{action:`allow`,rule:t.name,reason:null};return{action:`deny`,rule:`npm-redirect`,reason:ct(e)}}function Nt(e,t,n,r){let i=t[n],a,o;if(i.startsWith(`--filter=`))a=i.slice(9),o=n+1;else{if(a=t[n+1],!a||a.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};o=n+2}if(!a)return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};let s;for(let e=o;e<t.length;e++){let n=t[e];if(!(n.startsWith(`-`)&&!z.has(n))){s=n;break}}if(!s)return{action:`passthrough`,rule:null,reason:`pnpm --filter without subcommand`};if(s===`install`||s===`i`)return t.slice(t.indexOf(s)+1).every(e=>e.startsWith(`-`))?{action:`allow`,rule:`pnpm`,reason:null}:{action:`passthrough`,rule:null,reason:`pnpm --filter install with package arg`};if(s===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(s===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`,`licenses`,`audit`]).has(s))return{action:`allow`,rule:`pnpm`,reason:null};if(s===`exec`){let e=t.indexOf(`exec`,o),n=t.slice(e+1);return n.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:G(n.join(` `),{...r,depth:r.depth+1})}let c=s===`run`?t[t.indexOf(`run`,o)+1]:s;if(!c||c.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter run without script name`};if(z.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=vt(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=K(l,{...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd});return u===`allow`?{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}:u}const Pt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Ft(e,t){let n=e.indexOf(`-c`);if(n===-1)return{action:`passthrough`,rule:null,reason:`shell without -c`};for(let t=1;t<e.length;t++){if(t===n||t===n+1)continue;let r=e[t];if(r.startsWith(`-`)||Pt.has(r))return{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}}let r=e[n+1];return r?e.length>n+2?{action:`passthrough`,rule:null,reason:`shell -c with positional args`}:n===1?G(r,{...t,depth:t.depth+1}):{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}:{action:`passthrough`,rule:null,reason:`shell -c without command`}}const J=new Set([`-I`,`-d`,`-n`,`-P`,`-s`,`-L`,`-E`,`-a`,`--max-args`,`--max-procs`,`--delimiter`,`--replace`,`--max-lines`,`--max-chars`,`--arg-file`,`--process-slot-var`]),It=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function Lt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(J.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(J.has(e)){n++;continue}}if(It.has(t)){n++;continue}return{action:`passthrough`,rule:null,reason:`xargs with unrecognised flags`}}return n>=e.length?{action:`passthrough`,rule:null,reason:`xargs without command`}:G(e.slice(n).join(` `),{...t,depth:t.depth+1})}const Y=new Set([`Read`,`Glob`,`Grep`]),X=new Set([`Write`,`Edit`]),Rt=[{name:`env-file-read`,tools:Y,action:`deny`,when:e=>I(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:X,action:`deny`,when:e=>I(e),reason:`Don't write to .env files. These contain secrets.`},{name:`outside-cwd-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n!==t.cwd&&!n.startsWith(t.cwd+a.sep)},reason:`Don't write files outside the project directory.`}],zt=[{name:`cwd-read`,tools:Y,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}},{name:`cwd-write`,tools:X,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Bt=new Set([`Read`,`Write`,`Edit`]),Vt=new Set([`Glob`,`Grep`]);function Ht(e,t,n){return Bt.has(e)?Re(t)?.file_path??null:Vt.has(e)?ze(t)?.path??n:null}function Ut(e,t,n){let r=Ht(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Rt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of zt)if(t.tools.has(e)&&t.when(r,n))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const Z=await Ne();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);let Q,$;if(Z.tool_name===`Bash`){let e=Le(Z.tool_input);e||process.exit(0),Q=G(e.command,{cwd:Z.cwd,depth:0}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=Ut(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}Ue({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?N(`allow`):Q.action===`deny`&&N(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
|
|
2709
|
+
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.2\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync — do not edit manually.\n */\nimport*as e from\"node:fs\";import{existsSync as t,readFileSync as n,readdirSync as r,statSync as i}from\"node:fs\";import*as a from\"node:path\";import{dirname as o,join as s,resolve as c}from\"node:path\";import{fileURLToPath as l}from\"node:url\";import{execFileSync as u}from\"node:child_process\";Object.freeze({status:`aborted`});function d(e,t,n){function r(n,r){if(n._zod||Object.defineProperty(n,`_zod`,{value:{def:r,constr:o,traits:new Set},enumerable:!1}),n._zod.traits.has(e))return;n._zod.traits.add(e),t(n,r);let i=o.prototype,a=Object.keys(i);for(let e=0;e<a.length;e++){let t=a[e];t in n||(n[t]=i[t].bind(n))}}let i=n?.Parent??Object;class a extends i{}Object.defineProperty(a,`name`,{value:e});function o(e){var t;let i=n?.Parent?new a:this;r(i,e),(t=i._zod).deferred??(t.deferred=[]);for(let e of i._zod.deferred)e();return i}return Object.defineProperty(o,`init`,{value:r}),Object.defineProperty(o,Symbol.hasInstance,{value:t=>n?.Parent&&t instanceof n.Parent?!0:t?._zod?.traits?.has(e)}),Object.defineProperty(o,`name`,{value:e}),o}var f=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const p={};function m(e){return e&&Object.assign(p,e),p}function ee(e,t){return typeof t==`bigint`?t.toString():t}function te(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function ne(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const h=Symbol(`evaluating`);function g(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==h)return r===void 0&&(r=h,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const _=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function v(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function re(e){if(v(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(v(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 y(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 ae(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function b(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 x(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function S(e){return typeof e==`string`?e:e?.message}function C(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=S(e.inst?._zod.def?.error?.(e))??S(t?.error?.(e))??S(n.customError?.(e))??S(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const w=(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})},oe=d(`$ZodError`,w),T=d(`$ZodError`,w,{Parent:Error}),se=(e=>(t,n,r,i)=>{let a=r?Object.assign(r,{async:!1}):{async:!1},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise)throw new f;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>C(e,a,m())));throw _(t,i?.callee),t}return o.value})(T),ce=(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=>C(e,a,m())));throw _(t,i?.callee),t}return o.value})(T),le=(e=>(t,n,r)=>{let i=r?{...r,async:!1}:{async:!1},a=t._zod.run({value:n,issues:[]},i);if(a instanceof Promise)throw new f;return a.issues.length?{success:!1,error:new(e??oe)(a.issues.map(e=>C(e,i,m())))}:{success:!0,data:a.value}})(T),ue=(e=>async(t,n,r)=>{let i=r?Object.assign(r,{async:!0}):{async:!0},a=t._zod.run({value:n,issues:[]},i);return a instanceof Promise&&(a=await a),a.issues.length?{success:!1,error:new e(a.issues.map(e=>C(e,i,m())))}:{success:!0,data:a.value}})(T),de=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},fe=/^-?\\d+(?:\\.\\d+)?$/,pe={major:4,minor:3,patch:6},E=d(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=pe;let r=[...e._zod.def.checks??[]];e._zod.traits.has(`$ZodCheck`)&&r.unshift(e);for(let t of r)for(let n of t._zod.onattach)n(e);if(r.length===0)(n=e._zod).deferred??(n.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{let t=(e,t,n)=>{let r=b(e),i;for(let a of t){if(a._zod.def.when){if(!a._zod.def.when(e))continue}else if(r)continue;let t=e.issues.length,o=a._zod.check(e);if(o instanceof Promise&&n?.async===!1)throw new f;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=b(e,t))});else{if(e.issues.length===t)continue;r||=b(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(b(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new f;return o.then(t=>e._zod.parse(t,a))}return e._zod.parse(o,a)};e._zod.run=(i,a)=>{if(a.skipChecks)return e._zod.parse(i,a);if(a.direction===`backward`){let t=e._zod.parse({value:i.value,issues:[]},{...a,skipChecks:!0});return t instanceof Promise?t.then(e=>n(e,i,a)):n(t,i,a)}let o=e._zod.parse(i,a);if(o instanceof Promise){if(a.async===!1)throw new f;return o.then(e=>t(e,r,a))}return t(o,r,a)}}g(e,`~standard`,()=>({validate:t=>{try{let n=le(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return ue(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=d(`$ZodString`,(e,t)=>{E.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??de(e._zod.bag),e._zod.parse=(n,r)=>{if(t.coerce)try{n.value=String(n.value)}catch{}return typeof n.value==`string`||n.issues.push({expected:`string`,code:`invalid_type`,input:n.value,inst:e}),n}}),he=d(`$ZodUnknown`,(e,t)=>{E.init(e,t),e._zod.parse=e=>e});function D(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...x(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function ge(e){let t=Object.keys(e.shape);for(let n of t)if(!e.shape?.[n]?._zod?.traits?.has(`$ZodType`))throw Error(`Invalid element at key \"${n}\": expected a Zod schema`);let n=ae(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function _e(e,t,n,r,i,a){let o=[],s=i.keySet,c=i.catchall._zod,l=c.def.type,u=c.optout===`optional`;for(let i in t){if(s.has(i))continue;if(l===`never`){o.push(i);continue}let a=c.run({value:t[i],issues:[]},r);a instanceof Promise?e.push(a.then(e=>D(e,n,i,t,u))):D(a,n,i,t,u)}return o.length&&n.issues.push({code:`unrecognized_keys`,keys:o,input:t,inst:a}),e.length?Promise.all(e).then(()=>n):n}const ve=d(`$ZodObject`,(e,t)=>{if(E.init(e,t),!Object.getOwnPropertyDescriptor(t,`shape`)?.get){let e=t.shape;Object.defineProperty(t,`shape`,{get:()=>{let n={...e};return Object.defineProperty(t,`shape`,{value:n}),n}})}let n=te(()=>ge(t));g(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=v,i=t.catchall,a;e._zod.parse=(t,o)=>{a??=n.value;let s=t.value;if(!r(s))return t.issues.push({expected:`object`,code:`invalid_type`,input:s,inst:e}),t;t.value={};let c=[],l=a.shape;for(let e of a.keys){let n=l[e],r=n._zod.optout===`optional`,i=n._zod.run({value:s[e],issues:[]},o);i instanceof Promise?c.push(i.then(n=>D(n,t,e,s,r))):D(i,t,e,s,r)}return i?_e(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),ye=d(`$ZodRecord`,(e,t)=>{E.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(...x(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...x(e,o.issues)),n.value[e]=o.value)}let c;for(let e in i)s.has(e)||(c??=[],c.push(e));c&&c.length>0&&n.issues.push({code:`unrecognized_keys`,input:i,inst:e,keys:c})}else{n.value={};for(let o of Reflect.ownKeys(i)){if(o===`__proto__`)continue;let s=t.keyType._zod.run({value:o,issues:[]},r);if(s instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);if(typeof o==`string`&&fe.test(o)&&s.issues.length){let e=t.keyType._zod.run({value:Number(o),issues:[]},r);if(e instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);e.issues.length===0&&(s=e)}if(s.issues.length){t.mode===`loose`?n.value[o]=i[o]:n.issues.push({code:`invalid_key`,origin:`record`,issues:s.issues.map(e=>C(e,r,m())),input:o,path:[o],inst:e});continue}let c=t.valueType._zod.run({value:i[o],issues:[]},r);c instanceof Promise?a.push(c.then(e=>{e.issues.length&&n.issues.push(...x(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...x(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function be(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const xe=d(`$ZodOptional`,(e,t)=>{E.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,g(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),g(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${ne(e.source)})?$`):void 0}),e._zod.parse=(e,n)=>{if(t.innerType._zod.optin===`optional`){let r=t.innerType._zod.run(e,n);return r instanceof Promise?r.then(t=>be(t,e.value)):be(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var Se,Ce=class{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){let n=t[0];return this._map.set(e,n),n&&typeof n==`object`&&`id`in n&&this._idmap.set(n.id,e),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){let t=this._map.get(e);return t&&typeof t==`object`&&`id`in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){let t=e._zod.parent;if(t){let n={...this.get(t)??{}};delete n.id;let r={...n,...this._map.get(e)};return Object.keys(r).length?r:void 0}return this._map.get(e)}has(e){return this._map.has(e)}};function we(){return new Ce}(Se=globalThis).__zod_globalRegistry??(Se.__zod_globalRegistry=we()),globalThis.__zod_globalRegistry;function Te(e,t){return new e({type:`string`,...y(t)})}function Ee(e){return new e({type:`unknown`})}const O=d(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);E.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>se(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>le(e,t,n),e.parseAsync=async(t,n)=>ce(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>ue(e,t,n),e.check=(...n)=>e.clone({...t,checks:[...t.checks??[],...n.map(e=>typeof e==`function`?{_zod:{check:e,def:{check:`custom`},onattach:[]}}:e)]},{parent:!0}),e.with=e.check,e.clone=(t,n)=>ie(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),De=d(`ZodMiniString`,(e,t)=>{me.init(e,t),O.init(e,t)});function k(e){return Te(De,e)}const Oe=d(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),O.init(e,t)});function A(){return Ee(Oe)}const ke=d(`ZodMiniObject`,(e,t)=>{ve.init(e,t),O.init(e,t),g(e,`shape`,()=>t.shape)});function j(e,t){return new ke({type:`object`,shape:e??{},...y(t)})}const Ae=d(`ZodMiniRecord`,(e,t)=>{ye.init(e,t),O.init(e,t)});function M(e,t,n){return new Ae({type:`record`,keyType:e,valueType:t,...y(n)})}const je=d(`ZodMiniOptional`,(e,t)=>{xe.init(e,t),O.init(e,t)});function N(e){return new je({type:`optional`,innerType:e})}const Me=j({session_id:k(),transcript_path:k(),cwd:k(),permission_mode:k(),hook_event_name:k(),tool_name:k(),tool_input:M(k(),A())});async function Ne(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Me.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Pe=j({command:k(),description:N(k())}),Fe=j({file_path:k()}),Ie=j({pattern:k(),path:N(k())});function Le(e){let t=Pe.safeParse(e);return t.success?t.data:null}function Re(e){let t=Fe.safeParse(e);return t.success?t.data:null}function ze(e){let t=Ie.safeParse(e);return t.success?t.data:null}function P(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Be(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ve(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let He=!1;function Ue(t){try{let n=Be();He||=(Ve(n),!0);let r={timestamp:new Date().toISOString(),...t};e.appendFileSync(n,JSON.stringify(r)+`\n`)}catch(e){process.stderr.write(`[bst-hook] audit write failed: ${e instanceof Error?e.message:String(e)}\\n`)}}function F(e){let t=[],n=``,r=0,i=!1;for(;r<e.length;){let a=e[r];if(a===`'`){for(i=!0,r++;r<e.length&&e[r]!==`'`;)n+=e[r],r++;if(r>=e.length)return{ok:!1,error:`unterminated single quote`};r++}else if(a===`\"`){for(i=!0,r++;r<e.length&&e[r]!==`\"`;){if(e[r]===`\\\\`&&r+1<e.length){let t=e[r+1];if(t===`\"`||t===`\\\\`||t===`$`||t===\"`\"){n+=t,r+=2;continue}}n+=e[r],r++}if(r>=e.length)return{ok:!1,error:`unterminated double quote`};r++}else a===`\\\\`&&r+1<e.length?(i=!0,n+=e[r+1],r+=2):a===` `||a===` `?(i&&=(t.push(n),n=``,!1),r++):(i=!0,n+=a,r++)}return i&&t.push(n),{ok:!0,tokens:t}}function We(e){if(!e||!e.trim())return[];let t=[],n=``,r=0,i=!1,a=!1;for(;r<e.length;){let o=e[r];if(o===`'`&&!a){i=!i,n+=o,r++;continue}if(o===`\"`&&!i){a=!a,n+=o,r++;continue}if(o===`\\\\`&&!i&&r+1<e.length){n+=o+e[r+1],r+=2;continue}if(!i&&!a){if(o===`&`&&r+1<e.length&&e[r+1]===`&`){I(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){I(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){I(t,n,`||`),n=``,r+=2;continue}if(o===`|`){I(t,n,`|`),n=``,r++;continue}if(o===`;`){I(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function I(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const Ge=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ke(e,t,n=!1){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!Ge.has(t))return{safe:!1,reason:`process substitution`}}if(!n)for(let t=0;t<e.length;t++){let n=e[t];if(!Ge.has(n)){if(n.includes(`<`)&&!n.includes(`<(`))return{safe:!1,reason:`input redirect`};if(n.includes(`>`)){if(n.endsWith(`>/dev/null`)||n.match(/^(&>>|&>|>>|>|1>|2>)$/)&&e[t+1]===`/dev/null`)continue;return{safe:!1,reason:`output redirect to file`}}}}return{safe:!0}}const qe=[`.sample`,`.example`,`.template`],Je=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Ye(e){return qe.some(t=>e.endsWith(t))}function L(e){return Ye(a.basename(e))?!1:Je.test(e)}function Xe(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&L(e))}const Ze=new Set([`status`,`log`,`diff`,`show`,`blame`,`grep`,`shortlog`,`range-diff`,`describe`,`branch`,`remote`,`fetch`,`ls-files`,`ls-tree`,`ls-remote`,`show-ref`,`for-each-ref`,`rev-parse`,`rev-list`,`cat-file`,`diff-tree`,`name-rev`,`merge-base`,`check-ignore`]),Qe=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),$e=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),et=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function R(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(et.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function tt(e){for(let t of e.slice(1))if($e.has(t)||t===`-o`||t.startsWith(`--output`)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`)||t.startsWith(`-o=`)||t.startsWith(`--output=`))return!1;let t=R(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Ze.has(t)}function nt(e){let t=R(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return Qe.has(t)}const rt=new Set([`push`,`pop`,`apply`,`show`]);function z(e){let t=R(e);if(!t)return!1;if(t===`add`)return!0;if(t===`checkout`)return e.slice(e.indexOf(`checkout`)+1).some(e=>e===`-b`||e===`-B`);if(t===`switch`)return!0;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===void 0?!0:rt.has(t)}return!1}const B=new Set(`install.i.add.remove.rm.uninstall.un.update.up.upgrade.list.ls.ll.la.why.view.info.show.outdated.audit.licenses.init.create.link.unlink.pack.publish.config.set.get.store.cache.rebuild.rb.prune.dedupe.dlx.exec.run.patch.patch-commit.recursive.-r`.split(`.`)),it=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function at(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!B.has(e))){n=e;break}return n?it.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const ot=new Set([`view`,`info`,`show`]);function st(e){let t=e[1];return t!==void 0&&ot.has(t)}function ct(e){let t=e[1],n=`This project uses pnpm.`;return t?t===`install`||t===`i`||t===`ci`?e.slice(2).some(e=>!e.startsWith(`-`))?`${n} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`:`${n} Use \\`pnpm install\\` instead of npm.`:t===`add`?`${n} Use \\`pnpm add <pkg>\\` instead of npm.`:t===`uninstall`||t===`remove`||t===`rm`||t===`un`||t===`r`?`${n} Use \\`pnpm remove <pkg>\\` instead of npm.`:t===`run`||t===`run-script`?`${n} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`:t===`test`||t===`t`||t===`tst`?`${n} Use \\`pnpm test\\` instead of npm.`:t===`start`?`${n} Use \\`pnpm start\\` instead of npm.`:t===`stop`?`${n} Use \\`pnpm stop\\` instead of npm.`:t===`restart`?`${n} Use \\`pnpm restart\\` instead of npm.`:t===`exec`?`${n} Use \\`pnpm exec <cmd>\\` instead of npm.`:t===`list`||t===`ls`||t===`ll`||t===`la`?`${n} Use \\`pnpm list\\` instead of npm.`:t===`why`?`${n} Use \\`pnpm why\\` instead of npm.`:t===`outdated`?`${n} Use \\`pnpm outdated\\` instead of npm.`:t===`audit`?`${n} Use \\`pnpm audit\\` instead of npm.`:t===`update`||t===`up`||t===`upgrade`?`${n} Use \\`pnpm update\\` instead of npm.`:t===`init`?`${n} Use \\`pnpm init\\` instead of npm.`:t===`publish`?`${n} Use \\`pnpm publish\\` instead of npm.`:t===`link`?`${n} Use \\`pnpm link\\` instead of npm.`:t===`unlink`?`${n} Use \\`pnpm unlink\\` instead of npm.`:t===`pack`?`${n} Use \\`pnpm pack\\` instead of npm.`:t===`cache`?`${n} Use \\`pnpm store\\` instead of \\`npm cache\\`.`:t===`config`||t===`set`||t===`get`?`${n} Use \\`pnpm config\\` instead of npm.`:`${n} Use the equivalent pnpm command instead of npm.`:`${n} Use \\`pnpm\\` instead of npm.`}function V(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`)){if(n===`-t`||n.startsWith(`--target-directory`))return!1;continue}if(a.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=a.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+a.sep))return!1}return!0}const lt=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function ut(e){return!e.some(e=>lt.has(e))}function dt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ft(e){let t=e.findIndex(e=>a.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),i=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=i.includes(`--recursive`);return o||s}function pt(e){return e.some(e=>e===`777`||e===`0777`)}function mt(e){let t=e.indexOf(`push`);if(t===-1)return!1;let n=e.slice(t+1);return n.some(e=>e===`--force`||e===`-f`&&!n.includes(`--force-with-lease`))}function ht(e){if(!(e.includes(`reset`)&&e.includes(`--hard`)))return!1;if(e.some(e=>e===`main`||e===`master`))return!0;let t=e.filter(e=>e!==`git`&&e!==`reset`&&!e.startsWith(`-`));return t.length===0||t.includes(`HEAD`)}const gt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ft(e),reason:\"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"},{name:`sudo`,match:/^sudo$/,action:`deny`,reason:`Don't use sudo. If you need elevated permissions, ask me to do it manually.`},{name:`git-force-push`,match:/^git$/,action:`deny`,when:e=>mt(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>ht(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>nt(e)&&!z(e),reason:`Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>pt(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-read`,match:/^(cat|head|tail|less|more|grep)$/,action:`deny`,when:e=>Xe(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`npx-redirect`,match:/^npx$/,action:`deny`,reason:\"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"}],H=[{name:`vitest`,match:/^vitest$/,action:`allow`},{name:`jest`,match:/^jest$/,action:`allow`},{name:`tsc`,match:/^tsc$/,action:`allow`},{name:`tsdown`,match:/^tsdown$/,action:`allow`},{name:`oxlint`,match:/^oxlint$/,action:`allow`},{name:`oxfmt`,match:/^oxfmt$/,action:`allow`},{name:`eslint`,match:/^eslint$/,action:`allow`},{name:`prettier`,match:/^prettier$/,action:`allow`},{name:`biome`,match:/^biome$/,action:`allow`},{name:`knip`,match:/^knip$/,action:`allow`},{name:`cat`,match:/^cat$/,action:`allow`},{name:`head`,match:/^head$/,action:`allow`},{name:`tail`,match:/^tail$/,action:`allow`},{name:`grep`,match:/^grep$/,action:`allow`},{name:`ls`,match:/^ls$/,action:`allow`},{name:`wc`,match:/^wc$/,action:`allow`},{name:`uniq`,match:/^uniq$/,action:`allow`},{name:`diff`,match:/^diff$/,action:`allow`},{name:`echo`,match:/^echo$/,action:`allow`},{name:`pwd`,match:/^pwd$/,action:`allow`},{name:`which`,match:/^which$/,action:`allow`},{name:`dirname`,match:/^dirname$/,action:`allow`},{name:`basename`,match:/^basename$/,action:`allow`},{name:`realpath`,match:/^realpath$/,action:`allow`},{name:`npm`,match:/^npm$/,action:`allow`,when:e=>st(e)},{name:`test`,match:/^test$/,action:`allow`},{name:`bracket-test`,match:/^\\[$/,action:`allow`},{name:`cd`,match:/^cd$/,action:`allow`},{name:`true`,match:/^true$/,action:`allow`},{name:`colon`,match:/^:$/,action:`allow`},{name:`find`,match:/^find$/,action:`allow`,when:e=>ut(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>dt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>tt(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>z(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>at(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>V(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>V(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>V(e,t)}],_t=j({scripts:M(k(),A())});function U(e,t){let n=Tt(t);if(!n)return null;let r=n[e];if(typeof r!=`string`)return null;let i=n[`pre${e}`],a=n[`post${e}`];return{pre:typeof i==`string`?i:null,main:r,post:typeof a==`string`?a:null}}function vt(e,t,n){let r=xt(t,n);if(!r)return null;let i=U(e,r);return i?{...i,pkgDir:r}:null}function yt(e,t){let n=W(t);if(!n)return null;let r=[];for(let t of n){let n=U(e,t);n&&r.push({...n,pkgDir:t})}return r}function bt(e){let n=c(e);for(;;){if(t(s(n,`pnpm-workspace.yaml`)))return n;let e=o(n);if(e===n)return null;n=e}}function W(e){let t=St(e);if(!t)return null;let n=[];for(let a of t){if(!a.endsWith(`/*`))continue;let t=c(e,a.slice(0,-2)),o;try{o=r(t)}catch{continue}for(let e of o){let r=s(t,e);try{if(!i(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function xt(e,t){let n=W(t);if(!n)return null;for(let t of n){let n=wt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function St(e){try{let t=n(s(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=t.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<t.length;e++){let n=t[e],r=/^\\s+-\\s+(.+)$/.exec(n);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const Ct=j({name:k()});function wt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Ct.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Tt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=_t.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}function Et(e){try{return u(`git`,[`ls-files`,`--error-unmatch`,`package.json`],{cwd:e,stdio:`ignore`}),u(`git`,[`diff`,`--quiet`,`HEAD`,`--`,`package.json`],{cwd:e,stdio:`ignore`}),!0}catch{return!1}}const Dt=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`]),Ot=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function G(e){return Dt.has(e)?!0:Ot.some(t=>e.startsWith(t))}function kt(e,t){let n=F(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return a.isAbsolute(i)?i:a.resolve(t.cwd,i)}function K(e,t){if(t.depth>=5)return{action:`passthrough`,rule:null,reason:`max evaluation depth exceeded`};let n=e.trim();if(!n)return{action:`passthrough`,rule:null,reason:null};if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=We(n);if(r.length===0)return{action:`passthrough`,rule:null,reason:null};let i=null,a=null,o=t;for(let e of r){if(e.operator===`&&`){let t=kt(e.command,o);t&&(o={...o,cwd:t})}let t=At(e.command,o);if(t.action===`deny`)return t;t.action===`passthrough`&&(i||=t),t.action===`allow`&&(a=t)}return i||(r.length===1&&a?a:{action:`allow`,rule:`all-segments`,reason:null})}function q(e,t){let n=e.pkgDir??t.cwd,r=t.insideResolvedScript??Et(n)?{...t,insideResolvedScript:!0}:t;if(e.pre){let t=K(e.pre,r);if(t.action!==`allow`)return t}let i=K(e.main,r);if(i.action!==`allow`)return i;if(e.post){let t=K(e.post,r);if(t.action!==`allow`)return t}return`allow`}function At(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=F(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let i=r.tokens;if(i.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<i.length&&/^\\w+=/.test(i[o]);){let e=i[o].split(`=`)[0];if(G(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=i.length)return{action:`allow`,rule:`env-only`,reason:null};i=i.slice(o);let s=Ke(i,e,t.insideResolvedScript);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};if(!t.insideResolvedScript)for(let e=0;e<i.length;e++){let t=i[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(i[e+1]===`/dev/null`)continue;return{action:`passthrough`,rule:null,reason:`output redirect to file`}}if(t===`<`)return{action:`passthrough`,rule:null,reason:`input redirect`}}}let c=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);i=i.filter((e,t)=>!(c.has(e)||(e===`>`||e===`>>`)&&i[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(i[t-1]===`>`||i[t-1]===`>>`)));let l=a.basename(i[0]);if(l===`command`&&i.length>1)return K(i.slice(1).join(` `),{...t,depth:t.depth+1});if(l===`env`){let e=1;for(;e<i.length&&/^\\w+=/.test(i[e]);){let t=i[e].split(`=`)[0];if(G(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:K(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return jt(i,t);if(l===`npm`)return Nt(i);if(l===`bash`||l===`sh`||l===`zsh`)return It(i,t);if(l===`xargs`)return Rt(i,t);if(l===`node`&&(i.includes(`-e`)||i.includes(`-p`)||i.includes(`--eval`)||i.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(l===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of gt)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of H)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};return t.insideResolvedScript?{action:`allow`,rule:`resolved-script`,reason:null}:{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function jt(e,t){let n=e.slice(1),r,i=-1,o=!1,s=!1,c;for(let a=0;a<n.length;a++){let l=n[a];if(l===`--filter`||l.startsWith(`--filter=`))return Pt(e,n,a,t);if(l===`-r`||l===`--recursive`){o=!0;continue}if(l===`-w`||l===`--workspace-root`){s=!0;continue}if(l===`-C`||l===`--dir`){c=n[++a];continue}if(l.startsWith(`-C=`)||l.startsWith(`--dir=`)){c=l.split(`=`,2)[1];continue}if(!(l.startsWith(`-`)&&!B.has(l))){r=l,i=a;break}}if(!r)return{action:`passthrough`,rule:null,reason:null};if(r===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(r===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(r===`exec`){let e=n.slice(i+1);return e.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:K(e.join(` `),{...t,depth:t.depth+1})}let l;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};l=e}else if(!B.has(r))l=r;else{for(let n of H)if(n.match.test(`pnpm`)&&(!n.when||n.when(e,t)))return{action:`allow`,rule:n.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}if(o)return Mt(l,t);if(s){let e=bt(t.cwd);return e?J(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return J(l,{...t,cwd:e})}return J(l,t)}function J(e,t){let n=U(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=q(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function Mt(e,t){let n=yt(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=q(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function Nt(e){for(let t of H)if(t.match.test(`npm`)&&(!t.when||t.when(e,{cwd:``,depth:0})))return{action:`allow`,rule:t.name,reason:null};return{action:`deny`,rule:`npm-redirect`,reason:ct(e)}}function Pt(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(`-`)&&!B.has(n))){s=n;break}}if(!s)return{action:`passthrough`,rule:null,reason:`pnpm --filter without subcommand`};if(s===`install`||s===`i`)return t.slice(t.indexOf(s)+1).every(e=>e.startsWith(`-`))?{action:`allow`,rule:`pnpm`,reason:null}:{action:`passthrough`,rule:null,reason:`pnpm --filter install with package arg`};if(s===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(s===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`,`licenses`,`audit`]).has(s))return{action:`allow`,rule:`pnpm`,reason:null};if(s===`exec`){let e=t.indexOf(`exec`,o),n=t.slice(e+1);return n.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:K(n.join(` `),{...r,depth:r.depth+1})}let c=s===`run`?t[t.indexOf(`run`,o)+1]:s;if(!c||c.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter run without script name`};if(B.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=vt(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=q(l,{...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd});return u===`allow`?{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}:u}const Ft=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function It(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(`-`)||Ft.has(r))return{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}}let r=e[n+1];return r?e.length>n+2?{action:`passthrough`,rule:null,reason:`shell -c with positional args`}:n===1?K(r,{...t,depth:t.depth+1}):{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}:{action:`passthrough`,rule:null,reason:`shell -c without command`}}const Y=new Set([`-I`,`-d`,`-n`,`-P`,`-s`,`-L`,`-E`,`-a`,`--max-args`,`--max-procs`,`--delimiter`,`--replace`,`--max-lines`,`--max-chars`,`--arg-file`,`--process-slot-var`]),Lt=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function Rt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(Y.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(Y.has(e)){n++;continue}}if(Lt.has(t)){n++;continue}return{action:`passthrough`,rule:null,reason:`xargs with unrecognised flags`}}return n>=e.length?{action:`passthrough`,rule:null,reason:`xargs without command`}:K(e.slice(n).join(` `),{...t,depth:t.depth+1})}const zt=new Set([`Read`,`Glob`,`Grep`]),X=new Set([`Write`,`Edit`]),Bt=[{name:`env-file-read`,tools:zt,action:`deny`,when:e=>L(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:X,action:`deny`,when:e=>L(e),reason:`Don't write to .env files. These contain secrets.`},{name:`outside-cwd-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n!==t.cwd&&!n.startsWith(t.cwd+a.sep)},reason:`Don't write files outside the project directory.`}],Vt=[{name:`cwd-read`,tools:zt,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}},{name:`cwd-write`,tools:X,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Ht=new Set([`Read`,`Write`,`Edit`]),Ut=new Set([`Glob`,`Grep`]);function Wt(e,t,n){return Ht.has(e)?Re(t)?.file_path??null:Ut.has(e)?ze(t)?.path??n:null}function Gt(e,t,n){let r=Wt(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Bt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of Vt)if(t.tools.has(e)&&t.when(r,n))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const Z=await Ne();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);let Q,$;if(Z.tool_name===`Bash`){let e=Le(Z.tool_input);e||process.exit(0),Q=K(e.command,{cwd:Z.cwd,depth:0}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=Gt(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}Ue({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?P(`allow`):Q.action===`deny`&&P(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
|
|
2711
2710
|
}
|
|
2712
2711
|
//#endregion
|
|
2713
2712
|
//#region src/generators/claude-settings.ts
|
|
@@ -3333,7 +3332,7 @@ async function runGenerators(ctx) {
|
|
|
3333
3332
|
function generateMigratePrompt(results, config, detected) {
|
|
3334
3333
|
const sections = [];
|
|
3335
3334
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3336
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3335
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.45.0 repo:sync\` on ${timestamp}_\n`);
|
|
3337
3336
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3338
3337
|
if (changed.length > 0) {
|
|
3339
3338
|
const items = changed.map((r) => {
|
|
@@ -4240,6 +4239,244 @@ async function runVersionMode(executor, config) {
|
|
|
4240
4239
|
};
|
|
4241
4240
|
}
|
|
4242
4241
|
//#endregion
|
|
4242
|
+
//#region src/release/connection.ts
|
|
4243
|
+
const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
|
|
4244
|
+
/**
|
|
4245
|
+
* Resolve the hosting platform and connection details.
|
|
4246
|
+
*
|
|
4247
|
+
* Priority:
|
|
4248
|
+
* 1. Environment variables (FORGEJO_SERVER_URL, FORGEJO_REPOSITORY, RELEASE_TOKEN)
|
|
4249
|
+
* 2. `repository` field in package.json (server URL and owner/repo parsed from the URL)
|
|
4250
|
+
*
|
|
4251
|
+
* For Forgejo, a token is always required. Accepts RELEASE_TOKEN (preferred) or
|
|
4252
|
+
* FORGEJO_TOKEN (legacy, but conflicts with Forgejo Actions' internal auth).
|
|
4253
|
+
* If the repository URL hostname is `github.com`, returns `{ type: "github" }`.
|
|
4254
|
+
*/
|
|
4255
|
+
function resolveConnection(cwd) {
|
|
4256
|
+
const serverUrl = process.env["FORGEJO_SERVER_URL"];
|
|
4257
|
+
const repository = process.env["FORGEJO_REPOSITORY"];
|
|
4258
|
+
const token = process.env["RELEASE_TOKEN"] ?? process.env["FORGEJO_TOKEN"];
|
|
4259
|
+
if (serverUrl && repository && token) return {
|
|
4260
|
+
type: "forgejo",
|
|
4261
|
+
conn: {
|
|
4262
|
+
serverUrl,
|
|
4263
|
+
repository,
|
|
4264
|
+
token
|
|
4265
|
+
}
|
|
4266
|
+
};
|
|
4267
|
+
const parsed = parseRepositoryUrl(cwd);
|
|
4268
|
+
if (parsed === null) {
|
|
4269
|
+
if (serverUrl) {
|
|
4270
|
+
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
4271
|
+
if (!token) throw new FatalError("RELEASE_TOKEN environment variable is required");
|
|
4272
|
+
}
|
|
4273
|
+
return { type: "github" };
|
|
4274
|
+
}
|
|
4275
|
+
if (parsed.hostname === "github.com") return { type: "github" };
|
|
4276
|
+
const resolvedToken = token;
|
|
4277
|
+
if (!resolvedToken) throw new FatalError("RELEASE_TOKEN environment variable is required (server URL and repository were resolved from package.json)");
|
|
4278
|
+
return {
|
|
4279
|
+
type: "forgejo",
|
|
4280
|
+
conn: {
|
|
4281
|
+
serverUrl: serverUrl ?? `${parsed.protocol}//${parsed.hostname}`,
|
|
4282
|
+
repository: repository ?? parsed.repository,
|
|
4283
|
+
token: resolvedToken
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
function parseRepositoryUrl(cwd) {
|
|
4288
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
4289
|
+
let raw;
|
|
4290
|
+
try {
|
|
4291
|
+
raw = readFileSync(pkgPath, "utf-8");
|
|
4292
|
+
} catch {
|
|
4293
|
+
return null;
|
|
4294
|
+
}
|
|
4295
|
+
const pkg = z.object({ repository: RepositorySchema.optional() }).safeParse(JSON.parse(raw));
|
|
4296
|
+
if (!pkg.success) return null;
|
|
4297
|
+
const repo = pkg.data.repository;
|
|
4298
|
+
if (!repo) return null;
|
|
4299
|
+
return parseGitUrl(typeof repo === "string" ? repo : repo.url);
|
|
4300
|
+
}
|
|
4301
|
+
function parseGitUrl(urlStr) {
|
|
4302
|
+
try {
|
|
4303
|
+
const url = new URL(urlStr);
|
|
4304
|
+
const pathname = url.pathname.replace(/\.git$/, "").replace(/^\//, "");
|
|
4305
|
+
if (!pathname.includes("/")) return null;
|
|
4306
|
+
return {
|
|
4307
|
+
protocol: url.protocol,
|
|
4308
|
+
hostname: url.hostname,
|
|
4309
|
+
repository: pathname
|
|
4310
|
+
};
|
|
4311
|
+
} catch {
|
|
4312
|
+
return null;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Configure the git remote origin to use a Forgejo token for push auth.
|
|
4317
|
+
*
|
|
4318
|
+
* This is necessary because `actions/checkout` configures git with the automatic
|
|
4319
|
+
* GITHUB_TOKEN, which may not have push permissions. Using RELEASE_TOKEN ensures
|
|
4320
|
+
* pushes use the explicitly provided token.
|
|
4321
|
+
*/
|
|
4322
|
+
function configureGitAuth(executor, conn, cwd) {
|
|
4323
|
+
const host = conn.serverUrl.replace(/^https?:\/\//, "");
|
|
4324
|
+
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4325
|
+
executor.exec("git", [
|
|
4326
|
+
"remote",
|
|
4327
|
+
"set-url",
|
|
4328
|
+
"origin",
|
|
4329
|
+
authUrl
|
|
4330
|
+
], { cwd });
|
|
4331
|
+
}
|
|
4332
|
+
/** Resolve the CI bot actor name for the given platform. */
|
|
4333
|
+
function ciActorName(platform) {
|
|
4334
|
+
return platform === "github" ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4335
|
+
}
|
|
4336
|
+
/** Resolve the CI bot email for the given platform. */
|
|
4337
|
+
function ciActorEmail(platform) {
|
|
4338
|
+
const name = ciActorName(platform);
|
|
4339
|
+
return platform === "github" ? `${name}@users.noreply.github.com` : `${name}@noreply.localhost`;
|
|
4340
|
+
}
|
|
4341
|
+
/** Configure git user.name and user.email for CI bot commits. */
|
|
4342
|
+
function configureGitIdentity(executor, platform, cwd) {
|
|
4343
|
+
executor.exec("git", [
|
|
4344
|
+
"config",
|
|
4345
|
+
"user.name",
|
|
4346
|
+
ciActorName(platform)
|
|
4347
|
+
], { cwd });
|
|
4348
|
+
executor.exec("git", [
|
|
4349
|
+
"config",
|
|
4350
|
+
"user.email",
|
|
4351
|
+
ciActorEmail(platform)
|
|
4352
|
+
], { cwd });
|
|
4353
|
+
}
|
|
4354
|
+
//#endregion
|
|
4355
|
+
//#region src/release/webhook.ts
|
|
4356
|
+
/** Build a webhook payload from changesets publish context. */
|
|
4357
|
+
function buildChangesetsPayload(options) {
|
|
4358
|
+
const tagName = options.tags.join(", ");
|
|
4359
|
+
const actor = options.actor ?? ciActorName(options.platform ?? "forgejo");
|
|
4360
|
+
return {
|
|
4361
|
+
action: "published",
|
|
4362
|
+
release: {
|
|
4363
|
+
tag_name: tagName,
|
|
4364
|
+
name: tagName,
|
|
4365
|
+
draft: false,
|
|
4366
|
+
prerelease: false,
|
|
4367
|
+
author: { login: actor }
|
|
4368
|
+
},
|
|
4369
|
+
repository: {
|
|
4370
|
+
full_name: options.repository,
|
|
4371
|
+
html_url: `${options.serverUrl}/${options.repository}`
|
|
4372
|
+
},
|
|
4373
|
+
sender: { login: actor }
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
/** Build a webhook payload from simple release context. */
|
|
4377
|
+
function buildSimplePayload(options) {
|
|
4378
|
+
const repository = options.repository ?? "";
|
|
4379
|
+
const serverUrl = options.serverUrl ?? "https://github.com";
|
|
4380
|
+
const actor = options.actor ?? ciActorName(options.platform ?? "github");
|
|
4381
|
+
return {
|
|
4382
|
+
action: "published",
|
|
4383
|
+
release: {
|
|
4384
|
+
tag_name: options.tag,
|
|
4385
|
+
name: options.tag,
|
|
4386
|
+
draft: false,
|
|
4387
|
+
prerelease: options.prerelease ?? false,
|
|
4388
|
+
author: { login: actor }
|
|
4389
|
+
},
|
|
4390
|
+
repository: {
|
|
4391
|
+
full_name: repository,
|
|
4392
|
+
html_url: repository ? `${serverUrl}/${repository}` : ""
|
|
4393
|
+
},
|
|
4394
|
+
sender: { login: actor }
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
/** Build a webhook payload from GitHub Actions environment variables. */
|
|
4398
|
+
function buildPayloadFromEnv() {
|
|
4399
|
+
const repository = process.env["GITHUB_REPOSITORY"];
|
|
4400
|
+
const ref = process.env["GITHUB_REF"];
|
|
4401
|
+
const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
|
|
4402
|
+
const actor = process.env["GITHUB_ACTOR"] ?? "github-actions[bot]";
|
|
4403
|
+
if (!repository || !ref) return void 0;
|
|
4404
|
+
const tagName = ref.startsWith("refs/tags/") ? ref.slice(10) : ref;
|
|
4405
|
+
return {
|
|
4406
|
+
action: "published",
|
|
4407
|
+
release: {
|
|
4408
|
+
tag_name: tagName,
|
|
4409
|
+
name: tagName,
|
|
4410
|
+
draft: false,
|
|
4411
|
+
prerelease: false,
|
|
4412
|
+
author: { login: actor }
|
|
4413
|
+
},
|
|
4414
|
+
repository: {
|
|
4415
|
+
full_name: repository,
|
|
4416
|
+
html_url: `${serverUrl}/${repository}`
|
|
4417
|
+
},
|
|
4418
|
+
sender: { login: actor }
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4422
|
+
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
4423
|
+
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
4424
|
+
*/
|
|
4425
|
+
function computeSignature(secret, data) {
|
|
4426
|
+
const hmac = createHmac("sha256", secret);
|
|
4427
|
+
hmac.update(data);
|
|
4428
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
4432
|
+
*
|
|
4433
|
+
* Sends a JSON-serialized release payload (or empty string if no payload).
|
|
4434
|
+
* If a secret is provided, computes the HMAC-SHA256 signature of the body
|
|
4435
|
+
* and includes it in the `X-Hub-Signature-256` header.
|
|
4436
|
+
*
|
|
4437
|
+
* Never throws — returns a result object indicating success or failure.
|
|
4438
|
+
*/
|
|
4439
|
+
async function sendWebhook(options) {
|
|
4440
|
+
const { url, secret, payload, fetcher = globalThis.fetch } = options;
|
|
4441
|
+
const body = payload ? JSON.stringify(payload) : "";
|
|
4442
|
+
const headers = { "Content-Type": "application/json" };
|
|
4443
|
+
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
4444
|
+
try {
|
|
4445
|
+
const response = await fetcher(url, {
|
|
4446
|
+
method: "POST",
|
|
4447
|
+
headers,
|
|
4448
|
+
body
|
|
4449
|
+
});
|
|
4450
|
+
if (!response.ok) return {
|
|
4451
|
+
ok: false,
|
|
4452
|
+
status: response.status,
|
|
4453
|
+
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
4454
|
+
};
|
|
4455
|
+
return {
|
|
4456
|
+
ok: true,
|
|
4457
|
+
status: response.status
|
|
4458
|
+
};
|
|
4459
|
+
} catch (error) {
|
|
4460
|
+
return {
|
|
4461
|
+
ok: false,
|
|
4462
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Send a webhook and log the result. Intended for CLI usage.
|
|
4468
|
+
* Never throws.
|
|
4469
|
+
*/
|
|
4470
|
+
async function sendWebhookWithLogging(url, secret, payload) {
|
|
4471
|
+
const result = await sendWebhook({
|
|
4472
|
+
url,
|
|
4473
|
+
secret,
|
|
4474
|
+
payload
|
|
4475
|
+
});
|
|
4476
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4477
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4478
|
+
}
|
|
4479
|
+
//#endregion
|
|
4243
4480
|
//#region src/release/hook.ts
|
|
4244
4481
|
const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
|
|
4245
4482
|
/** Read the postReleaseHook command override from .tooling.json at runtime. */
|
|
@@ -4259,12 +4496,12 @@ function loadHookCommand(executor, cwd) {
|
|
|
4259
4496
|
* Execute the post-release hook after a successful publish.
|
|
4260
4497
|
*
|
|
4261
4498
|
* Uses the `postReleaseHook` command from `.tooling.json` if configured,
|
|
4262
|
-
* otherwise
|
|
4263
|
-
* `
|
|
4499
|
+
* otherwise POSTs to `POST_RELEASE_HOOK_URL` with HMAC-SHA256 authentication
|
|
4500
|
+
* using `WEBHOOK_SECRET`. Both env vars must be set by the CI workflow.
|
|
4264
4501
|
*
|
|
4265
4502
|
* Failures are logged as warnings — they never fail the release.
|
|
4266
4503
|
*/
|
|
4267
|
-
function runPostReleaseHook(executor, config) {
|
|
4504
|
+
async function runPostReleaseHook(executor, config, payload) {
|
|
4268
4505
|
if (config.dryRun) {
|
|
4269
4506
|
debug(config, "Skipping post-release hook (dry-run)");
|
|
4270
4507
|
return;
|
|
@@ -4280,22 +4517,16 @@ function runPostReleaseHook(executor, config) {
|
|
|
4280
4517
|
debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
|
|
4281
4518
|
return;
|
|
4282
4519
|
}
|
|
4283
|
-
const hookSecret = process.env["
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
const result = executor.exec("curl", args, { cwd: config.cwd });
|
|
4294
|
-
if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
|
|
4295
|
-
else log.info("Post-release hook completed successfully");
|
|
4296
|
-
} catch (error) {
|
|
4297
|
-
log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4298
|
-
}
|
|
4520
|
+
const hookSecret = process.env["WEBHOOK_SECRET"];
|
|
4521
|
+
debug(config, `Running post-release hook: POST ${hookUrl}`);
|
|
4522
|
+
const result = await sendWebhook({
|
|
4523
|
+
url: hookUrl,
|
|
4524
|
+
secret: hookSecret || void 0,
|
|
4525
|
+
payload,
|
|
4526
|
+
fetcher: executor.fetch.bind(executor)
|
|
4527
|
+
});
|
|
4528
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4529
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4299
4530
|
}
|
|
4300
4531
|
function executeHookCommand(executor, config, command) {
|
|
4301
4532
|
try {
|
|
@@ -4406,120 +4637,17 @@ async function runPublishMode(executor, config) {
|
|
|
4406
4637
|
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4407
4638
|
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4408
4639
|
}
|
|
4409
|
-
runPostReleaseHook(executor, config
|
|
4640
|
+
await runPostReleaseHook(executor, config, buildChangesetsPayload({
|
|
4641
|
+
tags: allTags,
|
|
4642
|
+
repository: config.repository,
|
|
4643
|
+
serverUrl: config.serverUrl
|
|
4644
|
+
}));
|
|
4410
4645
|
return {
|
|
4411
4646
|
mode: "publish",
|
|
4412
4647
|
tags: allTags
|
|
4413
4648
|
};
|
|
4414
4649
|
}
|
|
4415
4650
|
//#endregion
|
|
4416
|
-
//#region src/release/connection.ts
|
|
4417
|
-
const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
|
|
4418
|
-
/**
|
|
4419
|
-
* Resolve the hosting platform and connection details.
|
|
4420
|
-
*
|
|
4421
|
-
* Priority:
|
|
4422
|
-
* 1. Environment variables (FORGEJO_SERVER_URL, FORGEJO_REPOSITORY, RELEASE_TOKEN)
|
|
4423
|
-
* 2. `repository` field in package.json (server URL and owner/repo parsed from the URL)
|
|
4424
|
-
*
|
|
4425
|
-
* For Forgejo, a token is always required. Accepts RELEASE_TOKEN (preferred) or
|
|
4426
|
-
* FORGEJO_TOKEN (legacy, but conflicts with Forgejo Actions' internal auth).
|
|
4427
|
-
* If the repository URL hostname is `github.com`, returns `{ type: "github" }`.
|
|
4428
|
-
*/
|
|
4429
|
-
function resolveConnection(cwd) {
|
|
4430
|
-
const serverUrl = process.env["FORGEJO_SERVER_URL"];
|
|
4431
|
-
const repository = process.env["FORGEJO_REPOSITORY"];
|
|
4432
|
-
const token = process.env["RELEASE_TOKEN"] ?? process.env["FORGEJO_TOKEN"];
|
|
4433
|
-
if (serverUrl && repository && token) return {
|
|
4434
|
-
type: "forgejo",
|
|
4435
|
-
conn: {
|
|
4436
|
-
serverUrl,
|
|
4437
|
-
repository,
|
|
4438
|
-
token
|
|
4439
|
-
}
|
|
4440
|
-
};
|
|
4441
|
-
const parsed = parseRepositoryUrl(cwd);
|
|
4442
|
-
if (parsed === null) {
|
|
4443
|
-
if (serverUrl) {
|
|
4444
|
-
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
4445
|
-
if (!token) throw new FatalError("RELEASE_TOKEN environment variable is required");
|
|
4446
|
-
}
|
|
4447
|
-
return { type: "github" };
|
|
4448
|
-
}
|
|
4449
|
-
if (parsed.hostname === "github.com") return { type: "github" };
|
|
4450
|
-
const resolvedToken = token;
|
|
4451
|
-
if (!resolvedToken) throw new FatalError("RELEASE_TOKEN environment variable is required (server URL and repository were resolved from package.json)");
|
|
4452
|
-
return {
|
|
4453
|
-
type: "forgejo",
|
|
4454
|
-
conn: {
|
|
4455
|
-
serverUrl: serverUrl ?? `${parsed.protocol}//${parsed.hostname}`,
|
|
4456
|
-
repository: repository ?? parsed.repository,
|
|
4457
|
-
token: resolvedToken
|
|
4458
|
-
}
|
|
4459
|
-
};
|
|
4460
|
-
}
|
|
4461
|
-
function parseRepositoryUrl(cwd) {
|
|
4462
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
4463
|
-
let raw;
|
|
4464
|
-
try {
|
|
4465
|
-
raw = readFileSync(pkgPath, "utf-8");
|
|
4466
|
-
} catch {
|
|
4467
|
-
return null;
|
|
4468
|
-
}
|
|
4469
|
-
const pkg = z.object({ repository: RepositorySchema.optional() }).safeParse(JSON.parse(raw));
|
|
4470
|
-
if (!pkg.success) return null;
|
|
4471
|
-
const repo = pkg.data.repository;
|
|
4472
|
-
if (!repo) return null;
|
|
4473
|
-
return parseGitUrl(typeof repo === "string" ? repo : repo.url);
|
|
4474
|
-
}
|
|
4475
|
-
function parseGitUrl(urlStr) {
|
|
4476
|
-
try {
|
|
4477
|
-
const url = new URL(urlStr);
|
|
4478
|
-
const pathname = url.pathname.replace(/\.git$/, "").replace(/^\//, "");
|
|
4479
|
-
if (!pathname.includes("/")) return null;
|
|
4480
|
-
return {
|
|
4481
|
-
protocol: url.protocol,
|
|
4482
|
-
hostname: url.hostname,
|
|
4483
|
-
repository: pathname
|
|
4484
|
-
};
|
|
4485
|
-
} catch {
|
|
4486
|
-
return null;
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
/**
|
|
4490
|
-
* Configure the git remote origin to use a Forgejo token for push auth.
|
|
4491
|
-
*
|
|
4492
|
-
* This is necessary because `actions/checkout` configures git with the automatic
|
|
4493
|
-
* GITHUB_TOKEN, which may not have push permissions. Using RELEASE_TOKEN ensures
|
|
4494
|
-
* pushes use the explicitly provided token.
|
|
4495
|
-
*/
|
|
4496
|
-
function configureGitAuth(executor, conn, cwd) {
|
|
4497
|
-
const host = conn.serverUrl.replace(/^https?:\/\//, "");
|
|
4498
|
-
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4499
|
-
executor.exec("git", [
|
|
4500
|
-
"remote",
|
|
4501
|
-
"set-url",
|
|
4502
|
-
"origin",
|
|
4503
|
-
authUrl
|
|
4504
|
-
], { cwd });
|
|
4505
|
-
}
|
|
4506
|
-
/** Configure git user.name and user.email for CI bot commits. */
|
|
4507
|
-
function configureGitIdentity(executor, platform, cwd) {
|
|
4508
|
-
const isGitHub = platform === "github";
|
|
4509
|
-
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4510
|
-
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4511
|
-
executor.exec("git", [
|
|
4512
|
-
"config",
|
|
4513
|
-
"user.name",
|
|
4514
|
-
name
|
|
4515
|
-
], { cwd });
|
|
4516
|
-
executor.exec("git", [
|
|
4517
|
-
"config",
|
|
4518
|
-
"user.email",
|
|
4519
|
-
email
|
|
4520
|
-
], { cwd });
|
|
4521
|
-
}
|
|
4522
|
-
//#endregion
|
|
4523
4651
|
//#region src/commands/release-changesets.ts
|
|
4524
4652
|
const releaseForgejoCommand = defineCommand({
|
|
4525
4653
|
meta: {
|
|
@@ -4830,7 +4958,16 @@ async function runSimpleRelease(executor, config) {
|
|
|
4830
4958
|
}
|
|
4831
4959
|
let releaseCreated = false;
|
|
4832
4960
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
4833
|
-
if (pushed)
|
|
4961
|
+
if (pushed) {
|
|
4962
|
+
const conn = config.platform?.type === "forgejo" ? config.platform.conn : void 0;
|
|
4963
|
+
await runPostReleaseHook(executor, config, buildSimplePayload({
|
|
4964
|
+
tag,
|
|
4965
|
+
repository: conn?.repository,
|
|
4966
|
+
serverUrl: conn?.serverUrl,
|
|
4967
|
+
prerelease: Boolean(config.prerelease),
|
|
4968
|
+
platform: config.platform?.type
|
|
4969
|
+
}));
|
|
4970
|
+
}
|
|
4834
4971
|
return {
|
|
4835
4972
|
version,
|
|
4836
4973
|
tag,
|
|
@@ -5955,11 +6092,57 @@ const forgejoSecretsCommand = defineCommand({
|
|
|
5955
6092
|
}
|
|
5956
6093
|
});
|
|
5957
6094
|
//#endregion
|
|
6095
|
+
//#region src/commands/webhook-send.ts
|
|
6096
|
+
const webhookSendCommand = defineCommand({
|
|
6097
|
+
meta: {
|
|
6098
|
+
name: "webhook:send",
|
|
6099
|
+
description: "Send a POST request to a webhook URL with GitHub-style HMAC-SHA256 authentication"
|
|
6100
|
+
},
|
|
6101
|
+
args: {
|
|
6102
|
+
url: {
|
|
6103
|
+
type: "positional",
|
|
6104
|
+
description: "Webhook URL to POST to",
|
|
6105
|
+
required: true
|
|
6106
|
+
},
|
|
6107
|
+
tag: {
|
|
6108
|
+
type: "string",
|
|
6109
|
+
description: "Release tag name (e.g. v1.2.3). Falls back to GITHUB_REF env var."
|
|
6110
|
+
},
|
|
6111
|
+
repository: {
|
|
6112
|
+
type: "string",
|
|
6113
|
+
description: "Repository in owner/repo format. Falls back to GITHUB_REPOSITORY env var."
|
|
6114
|
+
},
|
|
6115
|
+
"server-url": {
|
|
6116
|
+
type: "string",
|
|
6117
|
+
description: "Server URL (e.g. https://github.com). Falls back to GITHUB_SERVER_URL env var."
|
|
6118
|
+
},
|
|
6119
|
+
actor: {
|
|
6120
|
+
type: "string",
|
|
6121
|
+
description: "User or bot that triggered the release. Falls back to GITHUB_ACTOR env var."
|
|
6122
|
+
},
|
|
6123
|
+
prerelease: {
|
|
6124
|
+
type: "boolean",
|
|
6125
|
+
description: "Mark the release as a prerelease"
|
|
6126
|
+
}
|
|
6127
|
+
},
|
|
6128
|
+
async run({ args }) {
|
|
6129
|
+
const secret = process.env["WEBHOOK_SECRET"];
|
|
6130
|
+
const payload = args.tag || args.repository ? buildSimplePayload({
|
|
6131
|
+
tag: args.tag ?? "",
|
|
6132
|
+
repository: args.repository ?? process.env["GITHUB_REPOSITORY"] ?? "",
|
|
6133
|
+
serverUrl: args["server-url"] ?? process.env["GITHUB_SERVER_URL"] ?? "https://github.com",
|
|
6134
|
+
actor: args.actor ?? process.env["GITHUB_ACTOR"] ?? "github-actions[bot]",
|
|
6135
|
+
prerelease: args.prerelease ?? false
|
|
6136
|
+
}) : buildPayloadFromEnv();
|
|
6137
|
+
await sendWebhookWithLogging(args.url, secret || void 0, payload);
|
|
6138
|
+
}
|
|
6139
|
+
});
|
|
6140
|
+
//#endregion
|
|
5958
6141
|
//#region src/bin.ts
|
|
5959
6142
|
const main = defineCommand({
|
|
5960
6143
|
meta: {
|
|
5961
6144
|
name: "bst",
|
|
5962
|
-
version: "0.
|
|
6145
|
+
version: "0.45.0",
|
|
5963
6146
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5964
6147
|
},
|
|
5965
6148
|
subCommands: {
|
|
@@ -5975,10 +6158,11 @@ const main = defineCommand({
|
|
|
5975
6158
|
"docker:check": dockerCheckCommand,
|
|
5976
6159
|
"hooks:audit": hooksAuditCommand,
|
|
5977
6160
|
"setup:secrets": setupSecretsCommand,
|
|
5978
|
-
"forgejo:secrets": forgejoSecretsCommand
|
|
6161
|
+
"forgejo:secrets": forgejoSecretsCommand,
|
|
6162
|
+
"webhook:send": webhookSendCommand
|
|
5979
6163
|
}
|
|
5980
6164
|
});
|
|
5981
|
-
console.log(`@bensandee/tooling v0.
|
|
6165
|
+
console.log(`@bensandee/tooling v0.45.0`);
|
|
5982
6166
|
async function run() {
|
|
5983
6167
|
await runMain(main);
|
|
5984
6168
|
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.45.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"prettier": "3.8.1",
|
|
55
55
|
"release-it": "19.2.4",
|
|
56
56
|
"tsdown": "0.21.6",
|
|
57
|
-
"@bensandee/claude-hooks": "0.6.
|
|
57
|
+
"@bensandee/claude-hooks": "0.6.2",
|
|
58
58
|
"@bensandee/config": "0.10.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
package/tooling.schema.json
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"type": "string"
|
|
171
171
|
},
|
|
172
172
|
"postReleaseHook": {
|
|
173
|
-
"description": "Override the post-release hook command (default:
|
|
173
|
+
"description": "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)",
|
|
174
174
|
"type": "string"
|
|
175
175
|
},
|
|
176
176
|
"postReleaseHookSecrets": {
|