@seanmozeik/tripwire 0.6.6 → 0.7.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/src/cli.ts CHANGED
@@ -1,49 +1,35 @@
1
1
  #!/usr/bin/env bun
2
- // `tripwire test '<command>'` pipe a synthetic event through the
3
- // Dispatcher and pretty-print the decision. Indispensable for tuning
4
- // Rules without going through Claude Code.
5
- //
6
- // `tripwire install <target>` install tripwire hooks for AI agents.
7
- //
8
- // Usage:
9
- // Bun src/cli.ts test 'rm -rf /etc'
10
- // Bun src/cli.ts test --tool=Read --path=.env
11
- // Bun src/cli.ts test --post --tool=Bash --stdout='ghp_<token>'
12
- // Bun src/cli.ts install claude
13
- // Bun src/cli.ts install codex
14
- // Bun src/cli.ts install pi
15
- // Bun src/cli.ts install all
16
-
17
- // oxlint-disable-next-line unicorn/import-style
18
- import { dirname } from 'node:path';
2
+ // CLI commands for testing policy and installing agent integrations.
3
+ // Usage examples:
4
+ // Bun src/main.ts test 'rm -rf /etc'
5
+ // Bun src/main.ts test --tool=Read --path=.env
6
+ // Bun src/main.ts test --post --tool=Bash --stdout='ghp_<token>'
7
+ // Bun src/main.ts install claude
8
+ // Bun src/main.ts install codex
9
+ // Bun src/main.ts install cursor
10
+ // Bun src/main.ts install pi
11
+ // Bun src/main.ts install all
19
12
 
20
13
  import { BunServices } from '@effect/platform-bun';
21
- import { file } from 'bun';
22
14
  import { Effect, Option } from 'effect';
23
15
  import { Argument, Command, Flag } from 'effect/unstable/cli';
24
16
 
25
17
  import pkg from '../package.json' with { type: 'json' };
26
- import { installAll, installClaude, installCodex, installPi } from './lib/install';
27
-
28
- // Resolve tripwire-hook path at runtime using process.argv
29
- // This works in both script mode (bun run) and compiled/bundled mode
30
- const runtimeSelf = (): string => {
31
- const isBunCli = /\/bun(?<ext>\.exe)?$/.test(process.argv[0] ?? '');
32
- return isBunCli ? process.argv[1]! : process.argv[0]!;
33
- };
18
+ import {
19
+ installAll,
20
+ installClaude,
21
+ installCodex,
22
+ installCursor,
23
+ installOhMyPi,
24
+ installPi,
25
+ } from './lib/install';
34
26
 
35
- const dispatchBin = async (): Promise<string> => {
36
- const cliPath = runtimeSelf();
37
- const cliDir = dirname(cliPath);
38
- // Try tripwire-hook in same directory first (installed scenario)
39
- const installedPath = `${cliDir}/tripwire-hook`;
40
- try {
41
- await file(installedPath).text();
42
- return installedPath;
43
- } catch {
44
- // Fallback to development scenario: tripwire-hook in ../dist relative to CLI
45
- return `${cliDir}/tripwire.js`;
27
+ const hookCommand = (): string[] => {
28
+ const isBunCli = /\/bun(?<ext>\.exe)?$/.test(process.execPath);
29
+ if (isBunCli) {
30
+ return [process.execPath, new URL('main.ts', import.meta.url).pathname, '--tripwire-hook'];
46
31
  }
32
+ return [process.execPath, '--tripwire-hook'];
47
33
  };
48
34
 
49
35
  interface BuiltEvent {
@@ -60,7 +46,7 @@ const buildToolInput = (
60
46
  command: string | undefined,
61
47
  path: string | undefined,
62
48
  content: string | undefined,
63
- ): unknown => {
49
+ ) => {
64
50
  if (tool === 'Bash') {
65
51
  return { command: command ?? '' };
66
52
  }
@@ -73,7 +59,7 @@ const buildToolInput = (
73
59
  if (tool === 'Edit' || tool === 'MultiEdit') {
74
60
  return { file_path: path ?? '', old_string: '', new_string: content ?? '' };
75
61
  }
76
- return undefined;
62
+ return null;
77
63
  };
78
64
 
79
65
  interface EventParams {
@@ -89,12 +75,13 @@ interface EventParams {
89
75
  const buildEvent = (params: EventParams): BuiltEvent => {
90
76
  const { tool, post, command, path, stdout, stderr, content } = params;
91
77
  const eventName = post ? 'PostToolUse' : 'PreToolUse';
78
+ const toolInput = buildToolInput(tool, command, path, content);
92
79
  const event: BuiltEvent = {
93
80
  hook_event_name: eventName,
94
81
  tool_name: tool,
95
82
  cwd: process.cwd(),
96
83
  session_id: 'tripwire-cli-test',
97
- tool_input: buildToolInput(tool, command, path, content),
84
+ ...(toolInput !== null && { tool_input: toolInput }),
98
85
  };
99
86
  if (post) {
100
87
  event.tool_response =
@@ -103,6 +90,14 @@ const buildEvent = (params: EventParams): BuiltEvent => {
103
90
  return event;
104
91
  };
105
92
 
93
+ const prettyJson = (output: string): string => {
94
+ try {
95
+ return JSON.stringify(JSON.parse(output) as unknown, null, 2);
96
+ } catch {
97
+ return output;
98
+ }
99
+ };
100
+
106
101
  const runTest = (config: {
107
102
  readonly command: string | undefined;
108
103
  readonly content: string | undefined;
@@ -112,11 +107,10 @@ const runTest = (config: {
112
107
  readonly stdout: string | undefined;
113
108
  readonly tool: string;
114
109
  }): Effect.Effect<void> =>
115
- Effect.gen(function* () {
110
+ Effect.sync(() => {
116
111
  const { command, content, path, post, stderr, stdout, tool } = config;
117
112
  const event = buildEvent({ tool, post, command, path, stdout, stderr, content });
118
- const bin = yield* Effect.promise(() => dispatchBin());
119
- const result = Bun.spawnSync([bin], {
113
+ const result = Bun.spawnSync(hookCommand(), {
120
114
  stdin: new TextEncoder().encode(JSON.stringify(event)),
121
115
  timeout: 10_000,
122
116
  stdout: 'pipe',
@@ -128,12 +122,7 @@ const runTest = (config: {
128
122
  process.exit(1);
129
123
  }
130
124
  const output = new TextDecoder().decode(result.stdout);
131
- try {
132
- const parsed = JSON.parse(output) as unknown;
133
- console.log(JSON.stringify(parsed, null, 2));
134
- } catch {
135
- console.log(output);
136
- }
125
+ console.log(prettyJson(output));
137
126
  });
138
127
 
139
128
  const testCommand = Command.make(
@@ -151,7 +140,10 @@ const testCommand = Command.make(
151
140
  Flag.optional,
152
141
  Flag.withDescription('File path for Read/Write/Edit tools'),
153
142
  ),
154
- post: Flag.boolean('post').pipe(Flag.withDescription('Test PostToolUse instead of PreToolUse')),
143
+ post: Flag.boolean('post').pipe(
144
+ Flag.withDefault(false),
145
+ Flag.withDescription('Test PostToolUse instead of PreToolUse'),
146
+ ),
155
147
  stderr: Flag.string('stderr').pipe(
156
148
  Flag.optional,
157
149
  Flag.withDescription('Stderr for PostToolUse Bash'),
@@ -178,10 +170,10 @@ const testCommand = Command.make(
178
170
  ).pipe(Command.withDescription('Test a synthetic hook event'));
179
171
 
180
172
  const runInstall = (target: string): Effect.Effect<void> =>
181
- Effect.gen(function* () {
182
- if (!['claude', 'codex', 'pi', 'all'].includes(target)) {
173
+ Effect.gen(function* runInstallEffect() {
174
+ if (!['claude', 'codex', 'cursor', 'pi', 'oh-my-pi', 'omp', 'all'].includes(target)) {
183
175
  console.error(`error: unknown target "${target}"`);
184
- console.error('Valid targets: claude, codex, pi, all');
176
+ console.error('Valid targets: claude, codex, cursor, pi, oh-my-pi, all');
185
177
  process.exit(1);
186
178
  }
187
179
 
@@ -206,6 +198,17 @@ const runInstall = (target: string): Effect.Effect<void> =>
206
198
  results = [{ target: 'pi', result }];
207
199
  break;
208
200
  }
201
+ case 'oh-my-pi':
202
+ case 'omp': {
203
+ const result = yield* Effect.promise(() => installOhMyPi());
204
+ results = [{ target: 'oh-my-pi', result }];
205
+ break;
206
+ }
207
+ case 'cursor': {
208
+ const result = yield* Effect.promise(() => installCursor());
209
+ results = [{ target: 'cursor', result }];
210
+ break;
211
+ }
209
212
  case 'all': {
210
213
  const installResults = yield* Effect.promise(() => installAll());
211
214
  results = installResults.map((r) => ({ target: r.target, result: r }));
@@ -237,7 +240,7 @@ const installCommand = Command.make(
237
240
  'install',
238
241
  {
239
242
  target: Argument.string('target').pipe(
240
- Argument.withDescription('Target agent (claude, codex, pi, or all)'),
243
+ Argument.withDescription('Target agent (claude, codex, cursor, pi, oh-my-pi, or all)'),
241
244
  ),
242
245
  },
243
246
  ({ target }) => runInstall(target),
@@ -250,7 +253,7 @@ const app = Command.make('tripwire').pipe(
250
253
 
251
254
  const program = Command.run(app, { version: pkg.version });
252
255
 
253
- const main = async (): Promise<void> => {
256
+ const runCli = async (): Promise<void> => {
254
257
  try {
255
258
  await Effect.runPromise(program.pipe(Effect.provide(BunServices.layer)));
256
259
  } catch (error) {
@@ -260,5 +263,9 @@ const main = async (): Promise<void> => {
260
263
  }
261
264
  };
262
265
 
263
- // oxlint-disable-next-line no-void, unicorn/prefer-top-level-await
264
- void main();
266
+ if (import.meta.main) {
267
+ // oxlint-disable-next-line no-void -- runCli reports failures through process.exitCode.
268
+ void runCli();
269
+ }
270
+
271
+ export { runCli };