@seanmozeik/tripwire 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tripwire-cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun @bytecode @bun-cjs
|
|
3
|
-
(function(exports, require, module, __filename, __dirname) {var S=require("path"),U=require("@effect/platform-bun"),C=globalThis.Bun,Q=require("effect"),x=require("effect/unstable/cli");var W={name:"@seanmozeik/tripwire",version:"0.6.
|
|
3
|
+
(function(exports, require, module, __filename, __dirname) {var S=require("path"),U=require("@effect/platform-bun"),C=globalThis.Bun,Q=require("effect"),x=require("effect/unstable/cli");var W={name:"@seanmozeik/tripwire",version:"0.6.5",description:"Opinionated hooks dispatcher for AI coding agents with configurable safety rules",license:"MIT",bin:{tripwire:"./dist/tripwire-cli.js","tripwire-hook":"./dist/tripwire.js"},files:["dist","package.json","src","README.md"],type:"module",exports:{".":"./src/index.ts"},publishConfig:{access:"public"},scripts:{build:"bun scripts/build.ts",prepublishOnly:"bun run build",check:"bun run format && bun run lint:fix && bun run typecheck",format:"oxfmt --write .",lint:"oxlint --tsconfig tsconfig.oxlint.json .","lint:fix":"oxlint --format agent --tsconfig tsconfig.oxlint.json --fix .",test:"bun test",typecheck:"tsc --noEmit"},dependencies:{"@effect/platform-bun":"^4.0.0-beta.78",effect:"^4.0.0-beta.78","shell-quote":"^1.8.4"},devDependencies:{"@types/bun":"^1.3.14","@types/shell-quote":"^1.7.5",oxfmt:"^0.53.0",oxlint:"^1.68.0","oxlint-tsgolint":"^0.23.0",typescript:"^6.0.3"},engines:{bun:">=1.0"}};var N=require("os"),z=globalThis.Bun,X="tripwire-hook",$=(b)=>{if(!b)return[[{hooks:[{type:"command",command:X}]}],!1];let w=!1,q=b.map((L)=>({hooks:L.hooks.map((D)=>{if(D.command===X||D.command.endsWith("/tripwire-hook")){if(D.command!==X)return w=!0,{...D,command:X};return D}return D})}));if(q.some((L)=>L.hooks.some((D)=>D.command===X)))return[q,!w];return[[...q,{hooks:[{type:"command",command:X}]}],!1]},v=async()=>{let b=`${N.homedir()}/.claude/settings.json`,w=z.file(b);try{let q=await w.text(),y=JSON.parse(q);y.hooks??={};let[G,L]=$(y.hooks.PreToolUse),[D,j]=$(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await w.write(`${JSON.stringify(y,null,2)}
|
|
4
4
|
`),{success:!0,message:`Updated ${b}`}}catch(q){let y=q instanceof Error?q.message:String(q);if(y.includes("No such file"))return{success:!1,message:`Config file not found: ${b}`};return{success:!1,message:`Failed to update Claude config: ${y}`}}},J=async()=>{let b=`${N.homedir()}/.pi/agent/settings.json`,w=z.file(b);try{let q=await w.text(),y=JSON.parse(q);y.hooks??={};let[G,L]=$(y.hooks.PreToolUse),[D,j]=$(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await w.write(`${JSON.stringify(y,null,2)}
|
|
5
5
|
`),{success:!0,message:`Updated ${b}`}}catch(q){let y=q instanceof Error?q.message:String(q);if(y.includes("No such file"))return{success:!1,message:`Config file not found: ${b}`};return{success:!1,message:`Failed to update pi config: ${y}`}}},K=async()=>{let b=`${N.homedir()}/.codex/config.toml`,w=`${N.homedir()}/.codex/hooks.json`,q=z.file(w),y=z.file(b),G=!1,L=!1;try{let D=await q.text(),j=JSON.parse(D);j.hooks??={};let[Y,V]=$(j.hooks.PreToolUse),[B,A]=$(j.hooks.PostToolUse);if(j.hooks.PreToolUse=Y,j.hooks.PostToolUse=B,!V||!A)G=!0;let Z=(R)=>{return R?.map((E)=>({hooks:E.hooks.map((M)=>{if(M.command===X&&M.timeout===void 0)return{...M,timeout:10};return M})}))??[]};if(j.hooks.PreToolUse=Z(j.hooks.PreToolUse),j.hooks.PostToolUse=Z(j.hooks.PostToolUse),G)await q.write(`${JSON.stringify(j,null,2)}
|
|
6
6
|
`)}catch(D){let j=D instanceof Error?D.message:String(D);if(j.includes("No such file"))return{success:!1,message:`Config file not found: ${w}`};return{success:!1,message:`Failed to update Codex hooks.json: ${j}`}}try{let j=await y.text();if(j.includes("hooks = true"));else if(L=!0,j.includes("[features]")){let Y=j.indexOf("[features]"),V=j.indexOf(`
|
package/dist/tripwire-cli.js.jsc
CHANGED
|
Binary file
|
package/dist/tripwire.js
CHANGED
|
@@ -81,7 +81,7 @@ ${YA($,Z)}
|
|
|
81
81
|
Flagged targets:
|
|
82
82
|
${G}
|
|
83
83
|
|
|
84
|
-
If raw \`rm\` is genuinely needed, append \` # tripwire-allow: <reason>\` to the command.`)};var bn=(Q)=>Q==="-x"||Q==="-xf"||Q==="-xzf"||Q==="-xjf"||Q==="-xJf"||Q==="-xvf"||Q==="-xvzf"||Q==="-xvjf"||Q==="--extract"||/^-[xvzjJtf]+$/.test(Q),gn=(Q)=>{for(let X=0;X<Q.tokens.length;X++){let Y=Q.tokens[X];if(Y==="-C"||Y==="--directory")return Q.tokens[X+1]??null;if(Y.startsWith("--directory="))return Y.slice(12)}return null},MA=(Q)=>{return Q==="/"||/^(?<home>~|\$HOME|\$\{HOME\})$/.test(Q)},AA=(Q,X)=>{if(q1(X))return I("bash-tar-explosion");for(let Y of Q){if(Y.head!=="tar")continue;if(!(Y.flags.some(bn)||Y.tokens.includes("--extract")))continue;let $=gn(Y);if($!==null&&MA($))return m("tar-extract-to-root",`tar -x with -C ${$} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory (e.g. ./tmp/extract) and inspect before moving anything elsewhere.`)}for(let Y of Q){if(Y.head!=="unzip")continue;for(let J=0;J<Y.tokens.length;J++)if(Y.tokens[J]==="-d"){let $=Y.tokens[J+1];if($!==void 0&&MA($))return m("unzip-to-root",`unzip -d ${$} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory.`)}}return I("bash-tar-explosion")};var hn=[{rule:"use-bun-not-npm",action:"deny",message:"Use `bun` instead of `npm`. Translations: `npm install` \u2192 `bun install`, `npm install <pkg>` \u2192 `bun add <pkg>`, `npm install -D <pkg>` \u2192 `bun add -d <pkg>`, `npm run X` \u2192 `bun run X` (or `bun X` for bin scripts), `npm test` \u2192 `bun test`. If you genuinely need npm (publishing to a registry that requires it, working in a different repo), append ` # tripwire-allow: <reason>` to the command.",fires:(Q)=>Q.head==="npm"},{rule:"use-bunx-not-npx",action:"deny",message:"Use `bunx` instead of `npx`. Same usage shape, faster, no implicit npm cache.",fires:(Q)=>Q.head==="npx"},{rule:"use-bun-not-pnpm",action:"deny",message:"Use `bun` instead of `pnpm`. The user is bun-only across their repos.",fires:(Q)=>Q.head==="pnpm"},{rule:"use-bun-not-yarn",action:"deny",message:"Use `bun` instead of `yarn`. The user is bun-only.",fires:(Q)=>Q.head==="yarn"},{rule:"use-uv-not-pip",action:"deny",message:"Use `uv` instead of `pip`. Translations: `pip install <pkg>` \u2192 `uv add <pkg>` (project dependency) or `uv pip install <pkg>` (env-only escape hatch). `pip freeze` \u2192 `uv pip freeze`. `pip list` \u2192 `uv pip list`. The user is uv-only across Python repos.",fires:(Q)=>Q.head==="pip"||Q.head==="pip3"},{rule:"use-uv-sync-not-venv",action:"deny",message:"`python -m venv` creates a bare venv; use `uv sync` instead. uv sync creates the venv AND installs from pyproject.toml + uv.lock in one atomic step. To activate: `source .venv/bin/activate` after.",fires:(Q)=>(Q.head==="python"||Q.head==="python3")&&Q.tokens.includes("-m")&&Q.tokens.includes("venv")},{rule:"uv-sync-over-uv-venv",action:"deny",message:"Use `uv sync` instead of `uv venv`. `uv venv` creates an empty venv that you then have to populate; `uv sync` creates the venv AND resolves+installs from pyproject.toml + uv.lock in one step. The only reason to use `uv venv` standalone is when there is no pyproject.toml \u2014 and in that case, `uv init` first.",fires:(Q)=>Q.head==="uv"&&Q.tokens[1]==="venv"},{rule:"use-bun-patch-not-patch-package",action:"deny",message:"Use `bun patch` instead of `patch-package`. Bun has built-in patch support that integrates with bun.lock; patch-package is npm-era and produces patches in a different format.",fires:(Q)=>Q.head==="patch-package"},{rule:"consider-fd",action:"warn",message:'Consider `fd` instead of `find`. Faster, simpler syntax, respects .gitignore by default. Examples: `find . -name "*.ts"` \u2192 `fd -e ts`, `find . -type f -name "X"` \u2192 `fd -t f X`, `find PATH ...` \u2192 `fd ... PATH`. The user has both installed; either works.',fires:(Q)=>Q.head==="find"},{rule:"consider-rg",action:"warn",message:
|
|
84
|
+
If raw \`rm\` is genuinely needed, append \` # tripwire-allow: <reason>\` to the command.`)};var bn=(Q)=>Q==="-x"||Q==="-xf"||Q==="-xzf"||Q==="-xjf"||Q==="-xJf"||Q==="-xvf"||Q==="-xvzf"||Q==="-xvjf"||Q==="--extract"||/^-[xvzjJtf]+$/.test(Q),gn=(Q)=>{for(let X=0;X<Q.tokens.length;X++){let Y=Q.tokens[X];if(Y==="-C"||Y==="--directory")return Q.tokens[X+1]??null;if(Y.startsWith("--directory="))return Y.slice(12)}return null},MA=(Q)=>{return Q==="/"||/^(?<home>~|\$HOME|\$\{HOME\})$/.test(Q)},AA=(Q,X)=>{if(q1(X))return I("bash-tar-explosion");for(let Y of Q){if(Y.head!=="tar")continue;if(!(Y.flags.some(bn)||Y.tokens.includes("--extract")))continue;let $=gn(Y);if($!==null&&MA($))return m("tar-extract-to-root",`tar -x with -C ${$} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory (e.g. ./tmp/extract) and inspect before moving anything elsewhere.`)}for(let Y of Q){if(Y.head!=="unzip")continue;for(let J=0;J<Y.tokens.length;J++)if(Y.tokens[J]==="-d"){let $=Y.tokens[J+1];if($!==void 0&&MA($))return m("unzip-to-root",`unzip -d ${$} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory.`)}}return I("bash-tar-explosion")};var hn=[{rule:"use-bun-not-npm",action:"deny",message:"Use `bun` instead of `npm`. Translations: `npm install` \u2192 `bun install`, `npm install <pkg>` \u2192 `bun add <pkg>`, `npm install -D <pkg>` \u2192 `bun add -d <pkg>`, `npm run X` \u2192 `bun run X` (or `bun X` for bin scripts), `npm test` \u2192 `bun test`. If you genuinely need npm (publishing to a registry that requires it, working in a different repo), append ` # tripwire-allow: <reason>` to the command.",fires:(Q)=>Q.head==="npm"},{rule:"use-bunx-not-npx",action:"deny",message:"Use `bunx` instead of `npx`. Same usage shape, faster, no implicit npm cache.",fires:(Q)=>Q.head==="npx"},{rule:"use-bun-not-pnpm",action:"deny",message:"Use `bun` instead of `pnpm`. The user is bun-only across their repos.",fires:(Q)=>Q.head==="pnpm"},{rule:"use-bun-not-yarn",action:"deny",message:"Use `bun` instead of `yarn`. The user is bun-only.",fires:(Q)=>Q.head==="yarn"},{rule:"use-uv-not-pip",action:"deny",message:"Use `uv` instead of `pip`. Translations: `pip install <pkg>` \u2192 `uv add <pkg>` (project dependency) or `uv pip install <pkg>` (env-only escape hatch). `pip freeze` \u2192 `uv pip freeze`. `pip list` \u2192 `uv pip list`. The user is uv-only across Python repos.",fires:(Q)=>Q.head==="pip"||Q.head==="pip3"},{rule:"use-uv-sync-not-venv",action:"deny",message:"`python -m venv` creates a bare venv; use `uv sync` instead. uv sync creates the venv AND installs from pyproject.toml + uv.lock in one atomic step. To activate: `source .venv/bin/activate` after.",fires:(Q)=>(Q.head==="python"||Q.head==="python3")&&Q.tokens.includes("-m")&&Q.tokens.includes("venv")},{rule:"uv-sync-over-uv-venv",action:"deny",message:"Use `uv sync` instead of `uv venv`. `uv venv` creates an empty venv that you then have to populate; `uv sync` creates the venv AND resolves+installs from pyproject.toml + uv.lock in one step. The only reason to use `uv venv` standalone is when there is no pyproject.toml \u2014 and in that case, `uv init` first.",fires:(Q)=>Q.head==="uv"&&Q.tokens[1]==="venv"},{rule:"use-bun-patch-not-patch-package",action:"deny",message:"Use `bun patch` instead of `patch-package`. Bun has built-in patch support that integrates with bun.lock; patch-package is npm-era and produces patches in a different format.",fires:(Q)=>Q.head==="patch-package"},{rule:"consider-fd",action:"warn",message:'Consider `fd` instead of `find`. Faster, simpler syntax, respects .gitignore by default. Examples: `find . -name "*.ts"` \u2192 `fd -e ts`, `find . -type f -name "X"` \u2192 `fd -t f X`, `find PATH ...` \u2192 `fd ... PATH`. The user has both installed; either works.',fires:(Q)=>Q.head==="find"},{rule:"consider-rg",action:"warn",message:'Consider `rg` (ripgrep) instead of `grep`. Faster; recurses by default; respects .gitignore by default (use `-u`/`-uu` to include ignored/hidden files); searches the CWD when no path is given. NOT a clean flag-for-flag swap; some flags differ or are unneeded. Carry over fine: `-i`, `-n`, `-v`, `-l`, `-c`. WATCH OUT: `-r` is NOT recursive in rg \u2014 it means `--replace` (rg already recurses), so `rg -rn "PATTERN" dir/` silently parses as `--replace=n` and rewrites every match to the literal "n" while exiting 0. Drop the `-r`: `grep -rn PATTERN .` \u2192 `rg -n PATTERN`. And `grep --include`/`--exclude` become `rg -g GLOB`/`-g !GLOB`.',fires:(Q)=>Q.head==="grep"||Q.head==="egrep"||Q.head==="fgrep"},{rule:"consider-btop",action:"warn",message:"Consider `btop` instead of `top`. Better UI, more info, modern.",fires:(Q)=>Q.head==="top"},{rule:"consider-dust",action:"warn",message:"Consider `dust` instead of `du -sh`. Sorted, colorful, faster.",fires:(Q)=>Q.head==="du"},{rule:"consider-duf",action:"warn",message:"Consider `duf` instead of `df -h`. Better formatting, more readable.",fires:(Q)=>Q.head==="df"},{rule:"consider-procs",action:"warn",message:"Consider `procs` instead of `ps aux`. Better filtering and output.",fires:(Q)=>Q.head==="ps"}],wA=(Q,X)=>{if(q1(X))return I("bash-tool-policy");for(let Y of Q)for(let J of hn)if(J.fires(Y))return J.action==="deny"?m(J.rule,J.message):J6(J.rule,J.message);return I("bash-tool-policy")};var mn="If this is intentional, append ` # tripwire-allow: <reason>` to the command.",dn=new Map([["add","create"],["new","create"],["edit","update"],["set","update"],["rm","delete"],["del","delete"],["remove","delete"]]),CA=(Q)=>dn.get(Q)??Q,TA=(Q)=>{let X=Q.lastIndexOf("/");return X===-1?Q:Q.slice(X+1)},pn=(Q,X)=>Q.some((Y)=>Y===X||Y.startsWith(`${X}=`)),cn=(Q,X)=>{for(let Y=0;Y<Q.length;Y++){let J=Q[Y];if(J===X)return Q[Y+1]??"";if(J.startsWith(`${X}=`))return J.slice(X.length+1)}return null},fn=(Q)=>{let X=[],Y=Q.tokens.slice(1);for(let J=0;J<Y.length;J++){let $=Y[J];if($.startsWith("-")){if(!$.includes("=")&&Y[J+1]!==void 0&&!Y[J+1].startsWith("-"))J++;continue}X.push($)}return X},IA=(Q,X)=>{let Y=X.pattern,J=L9(Y);if(J.length===0)return!1;let $=J[0].tokens,Z=$[0];if(Z===void 0)return!1;let G=$.slice(1);for(let K of Q){if(TA(K.head)!==TA(Z))continue;if(G.length>0){let _=fn(K);if(!G.every((q,D)=>_[D]!==void 0&&CA(_[D])===CA(q)))continue}if((X.requiresFlags??[]).some((_)=>!pn(K.tokens,_)))continue;if(!(X.forbidsFlagValues??[]).every((_)=>{let V=cn(K.tokens,_.flag);return V!==null&&_.values.includes(V)}))continue;if(G.length===0&&X.requiresFlags===void 0&&X.forbidsFlagValues===void 0)return!0;return!0}return!1},jA=(Q,X,Y,J)=>{if(q1(X))return I("config-custom");for(let $ of J)if(IA(Q,$))return I("config-custom");for(let $ of Y)if(IA(Q,$)){let Z=$.message.includes("tripwire-allow")?$.message:`${$.message} ${mn}`;return $.action==="ask"?o0("config-custom",Z):m("config-custom",Z)}return I("config-custom")};var EA=require("fs"),SA=(Q,X)=>{let Y=new Set(Q.split(`
|
|
85
85
|
`).map((J)=>J.trim()).filter((J)=>J.length>0));return X.split(`
|
|
86
86
|
`).filter((J)=>J.trim().length>0).filter((J)=>!Y.has(J.trim()))},xA=(Q)=>{try{return EA.readFileSync(Q,"utf8")}catch{return""}};var un=[/\bTODO\s*:/i,/\bFIXME\s*:/i,/\bXXX\s*:/i,/\bHACK\s*:/i,/\bfor now\b/i,/\bnot implemented\b/i,/\bNotImplementedError\b/,/\btemp fix\b/i,/\bfallback\b/i,/\bplaceholder\b/i,/\bbackwards?[ -]?compat(?<ibility>ibility)?\b/i,/\bfor later\b/i,/\blater on\b/i,/\bget back to\b/i,/\bI'?ll fix\b/i,/\bto be implemented\b/i,/\bnot yet (?<state>implemented|done)\b/i,/\bstubbed\b/i],ln=/\.(?<ext>ts|tsx|js|jsx|mjs|cjs|py|rs|go|rb|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|bash|lua|ex|exs|clj|scala|dart)$/i,rn=/(?<prefix>^|\/)(?<dir>__tests__|tests?|spec|fixtures?|mocks?|__mocks__|stories)(?<suffix>\/|$)|\.(?<ext>test|spec|fixture|mock|stories)\.[^/]+$/i,on=/tripwire-allow\b/,nn=(Q)=>{if(on.test(Q))return!1;for(let X of un)if(X.test(Q))return!0;return!1},GW=(Q)=>{let X=Q.file_path;if(!ln.test(X)||rn.test(X))return I("lazy-code");let Y="content"in Q?Q.content:Q.new_string,J="content"in Q?xA(X):Q.old_string,$=[];for(let K of SA(J,Y))if(nn(K))$.push(K.slice(0,200));if($.length===0)return I("lazy-code");let Z=$.slice(0,3).map((K)=>` \u2022 ${K}`).join(`
|
|
87
87
|
`),G=$.length>3?`
|
package/dist/tripwire.js.jsc
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -88,7 +88,7 @@ const POLICIES: readonly Policy[] = [
|
|
|
88
88
|
rule: 'consider-rg',
|
|
89
89
|
action: 'warn',
|
|
90
90
|
message:
|
|
91
|
-
'Consider `rg` (ripgrep) instead of `grep`. Faster
|
|
91
|
+
'Consider `rg` (ripgrep) instead of `grep`. Faster; recurses by default; respects .gitignore by default (use `-u`/`-uu` to include ignored/hidden files); searches the CWD when no path is given. NOT a clean flag-for-flag swap; some flags differ or are unneeded. Carry over fine: `-i`, `-n`, `-v`, `-l`, `-c`. WATCH OUT: `-r` is NOT recursive in rg — it means `--replace` (rg already recurses), so `rg -rn "PATTERN" dir/` silently parses as `--replace=n` and rewrites every match to the literal "n" while exiting 0. Drop the `-r`: `grep -rn PATTERN .` → `rg -n PATTERN`. And `grep --include`/`--exclude` become `rg -g GLOB`/`-g !GLOB`.',
|
|
92
92
|
fires: (seg) => seg.head === 'grep' || seg.head === 'egrep' || seg.head === 'fgrep',
|
|
93
93
|
},
|
|
94
94
|
{
|