veto-leash 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli.js +27 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/watchdog/index.d.ts +1 -1
  4. package/dist/watchdog/index.d.ts.map +1 -1
  5. package/dist/watchdog/index.js +6 -1
  6. package/dist/watchdog/index.js.map +1 -1
  7. package/dist/wrapper/daemon.d.ts +3 -1
  8. package/dist/wrapper/daemon.d.ts.map +1 -1
  9. package/dist/wrapper/daemon.js +9 -1
  10. package/dist/wrapper/daemon.js.map +1 -1
  11. package/dist/wrapper/sessions.d.ts +29 -0
  12. package/dist/wrapper/sessions.d.ts.map +1 -0
  13. package/dist/wrapper/sessions.js +101 -0
  14. package/dist/wrapper/sessions.js.map +1 -0
  15. package/package.json +11 -3
  16. package/IMPLEMENTATION_PLAN.md +0 -2194
  17. package/src/audit/index.ts +0 -172
  18. package/src/cli.ts +0 -503
  19. package/src/cloud/index.ts +0 -139
  20. package/src/compiler/builtins.ts +0 -137
  21. package/src/compiler/cache.ts +0 -51
  22. package/src/compiler/index.ts +0 -59
  23. package/src/compiler/llm.ts +0 -83
  24. package/src/compiler/prompt.ts +0 -37
  25. package/src/config/loader.ts +0 -126
  26. package/src/config/schema.ts +0 -136
  27. package/src/matcher.ts +0 -89
  28. package/src/native/aider.ts +0 -150
  29. package/src/native/claude-code.ts +0 -308
  30. package/src/native/cursor.ts +0 -131
  31. package/src/native/index.ts +0 -233
  32. package/src/native/opencode.ts +0 -310
  33. package/src/native/windsurf.ts +0 -231
  34. package/src/types.ts +0 -48
  35. package/src/ui/colors.ts +0 -50
  36. package/src/watchdog/index.ts +0 -82
  37. package/src/watchdog/restore.ts +0 -74
  38. package/src/watchdog/snapshot.ts +0 -209
  39. package/src/watchdog/watcher.ts +0 -150
  40. package/src/wrapper/daemon.ts +0 -133
  41. package/src/wrapper/shims.ts +0 -409
  42. package/src/wrapper/spawn.ts +0 -47
  43. package/tsconfig.json +0 -20
@@ -1,150 +0,0 @@
1
- // src/watchdog/watcher.ts
2
- // Filesystem watcher using chokidar
3
-
4
- import chokidar from 'chokidar';
5
- import type { FSWatcher } from 'chokidar';
6
- import type { Policy } from '../types.js';
7
- import type { Snapshot } from './snapshot.js';
8
- import { isProtected, normalizePath } from '../matcher.js';
9
- import { restoreFile } from './restore.js';
10
- import { COLORS, SYMBOLS } from '../ui/colors.js';
11
- import { logRestored } from '../audit/index.js';
12
-
13
- export interface WatcherStats {
14
- restored: number;
15
- blocked: number;
16
- events: Array<{ time: Date; event: string; path: string; action: 'restored' | 'blocked' }>;
17
- }
18
-
19
- export interface WatcherOptions {
20
- rootDir: string;
21
- policy: Policy;
22
- snapshot: Snapshot;
23
- onRestore?: (path: string) => void;
24
- onBlock?: (path: string, event: string) => void;
25
- }
26
-
27
- /**
28
- * Create a filesystem watcher that auto-restores protected files
29
- */
30
- export function createWatcher(options: WatcherOptions): { watcher: FSWatcher; stats: WatcherStats } {
31
- const { rootDir, policy, snapshot, onRestore, onBlock } = options;
32
-
33
- const stats: WatcherStats = {
34
- restored: 0,
35
- blocked: 0,
36
- events: [],
37
- };
38
-
39
- // Build glob patterns for chokidar
40
- const watchPatterns = policy.include.map(p => {
41
- // Ensure patterns work with chokidar
42
- if (p.startsWith('**/')) return p;
43
- if (p.startsWith('/')) return p.slice(1);
44
- return p;
45
- });
46
-
47
- const watcher = chokidar.watch(watchPatterns, {
48
- cwd: rootDir,
49
- ignored: [
50
- 'node_modules/**',
51
- '.git/**',
52
- ...policy.exclude,
53
- ],
54
- persistent: true,
55
- ignoreInitial: true,
56
- awaitWriteFinish: {
57
- stabilityThreshold: 100,
58
- pollInterval: 50,
59
- },
60
- });
61
-
62
- // Handle file deletion - restore immediately
63
- watcher.on('unlink', (path) => {
64
- const normalizedPath = normalizePath(path);
65
-
66
- if (!isProtected(normalizedPath, policy)) return;
67
-
68
- const restored = restoreFile(snapshot, normalizedPath, rootDir);
69
-
70
- if (restored) {
71
- stats.restored++;
72
- stats.events.push({
73
- time: new Date(),
74
- event: 'unlink',
75
- path: normalizedPath,
76
- action: 'restored',
77
- });
78
-
79
- printRestored('delete', normalizedPath, policy.description);
80
- logRestored(normalizedPath, 'delete', policy.description);
81
- onRestore?.(normalizedPath);
82
- }
83
- });
84
-
85
- // Handle file modification - restore if content changed
86
- watcher.on('change', (path) => {
87
- const normalizedPath = normalizePath(path);
88
-
89
- // Only act on modify policies
90
- if (policy.action !== 'modify') return;
91
- if (!isProtected(normalizedPath, policy)) return;
92
-
93
- const restored = restoreFile(snapshot, normalizedPath, rootDir);
94
-
95
- if (restored) {
96
- stats.restored++;
97
- stats.events.push({
98
- time: new Date(),
99
- event: 'change',
100
- path: normalizedPath,
101
- action: 'restored',
102
- });
103
-
104
- printRestored('modify', normalizedPath, policy.description);
105
- logRestored(normalizedPath, 'modify', policy.description);
106
- onRestore?.(normalizedPath);
107
- }
108
- });
109
-
110
- // Handle directory deletion
111
- watcher.on('unlinkDir', (path) => {
112
- const normalizedPath = normalizePath(path);
113
-
114
- // Find all files in snapshot that were under this directory
115
- for (const [filePath] of snapshot.files) {
116
- if (filePath.startsWith(normalizedPath + '/')) {
117
- const restored = restoreFile(snapshot, filePath, rootDir);
118
-
119
- if (restored) {
120
- stats.restored++;
121
- stats.events.push({
122
- time: new Date(),
123
- event: 'unlinkDir',
124
- path: filePath,
125
- action: 'restored',
126
- });
127
-
128
- printRestored('delete', filePath, policy.description);
129
- logRestored(filePath, 'delete', policy.description);
130
- onRestore?.(filePath);
131
- }
132
- }
133
- }
134
- });
135
-
136
- watcher.on('error', (err) => {
137
- const message = err instanceof Error ? err.message : String(err);
138
- console.error(`${COLORS.error}${SYMBOLS.error} Watcher error: ${message}${COLORS.reset}`);
139
- });
140
-
141
- return { watcher, stats };
142
- }
143
-
144
- function printRestored(action: string, path: string, policyDesc: string): void {
145
- console.log(`\n${COLORS.warning}${SYMBOLS.blocked} RESTORED${COLORS.reset}`);
146
- console.log(` ${COLORS.dim}Action:${COLORS.reset} ${action}`);
147
- console.log(` ${COLORS.dim}Target:${COLORS.reset} ${path}`);
148
- console.log(` ${COLORS.dim}Policy:${COLORS.reset} ${policyDesc}`);
149
- console.log(`\n File automatically restored from snapshot.\n`);
150
- }
@@ -1,133 +0,0 @@
1
- // src/wrapper/daemon.ts
2
-
3
- import * as net from 'net';
4
- import type {
5
- Policy,
6
- CheckRequest,
7
- CheckResponse,
8
- SessionState,
9
- } from '../types.js';
10
- import { isProtected } from '../matcher.js';
11
- import { COLORS, SYMBOLS } from '../ui/colors.js';
12
- import { logBlocked } from '../audit/index.js';
13
-
14
- export class VetoDaemon {
15
- private server: net.Server | null = null;
16
- private policy: Policy;
17
- private state: SessionState;
18
-
19
- constructor(policy: Policy, agent: string) {
20
- this.policy = policy;
21
- this.state = {
22
- pid: process.pid,
23
- agent,
24
- policy,
25
- startTime: new Date(),
26
- blockedCount: 0,
27
- allowedCount: 0,
28
- blockedActions: [],
29
- };
30
- }
31
-
32
- async start(): Promise<number> {
33
- return new Promise((resolve, reject) => {
34
- this.server = net.createServer((socket) => {
35
- let buffer = '';
36
-
37
- socket.on('data', (data) => {
38
- buffer += data.toString();
39
- const lines = buffer.split('\n');
40
- buffer = lines.pop() || '';
41
-
42
- for (const line of lines) {
43
- if (!line.trim()) continue;
44
-
45
- try {
46
- const req: CheckRequest = JSON.parse(line);
47
- const res = this.check(req);
48
- socket.write(JSON.stringify(res) + '\n');
49
- } catch {
50
- socket.write('{"allowed":true}\n');
51
- }
52
- }
53
- });
54
-
55
- socket.on('error', () => {
56
- // Ignore socket errors
57
- });
58
- });
59
-
60
- this.server.listen(0, '127.0.0.1', () => {
61
- const addr = this.server!.address() as net.AddressInfo;
62
- resolve(addr.port);
63
- });
64
-
65
- this.server.on('error', reject);
66
- });
67
- }
68
-
69
- check(req: CheckRequest): CheckResponse {
70
- // Action must match policy
71
- if (req.action !== this.policy.action) {
72
- this.state.allowedCount++;
73
- return { allowed: true };
74
- }
75
-
76
- // Check if target is protected
77
- if (isProtected(req.target, this.policy)) {
78
- this.state.blockedCount++;
79
- this.state.blockedActions.push({
80
- time: new Date(),
81
- action: req.action,
82
- target: req.target,
83
- });
84
-
85
- // Log to audit
86
- logBlocked(req.target, req.action, this.policy.description, this.state.agent);
87
-
88
- // Print block notification
89
- console.log(
90
- `\n${COLORS.error}${SYMBOLS.blocked} BLOCKED${COLORS.reset}`
91
- );
92
- console.log(` ${COLORS.dim}Action:${COLORS.reset} ${req.action}`);
93
- console.log(` ${COLORS.dim}Target:${COLORS.reset} ${req.target}`);
94
- console.log(
95
- ` ${COLORS.dim}Policy:${COLORS.reset} ${this.policy.description}`
96
- );
97
- console.log(`\n ${COLORS.success}Filesystem unchanged.${COLORS.reset}\n`);
98
-
99
- return { allowed: false, reason: this.policy.description };
100
- }
101
-
102
- this.state.allowedCount++;
103
- return { allowed: true };
104
- }
105
-
106
- getState(): SessionState {
107
- return this.state;
108
- }
109
-
110
- stop(): void {
111
- // Print session summary
112
- const duration = Date.now() - this.state.startTime.getTime();
113
- const minutes = Math.floor(duration / 60000);
114
- const seconds = Math.floor((duration % 60000) / 1000);
115
-
116
- console.log(
117
- `\n${COLORS.success}${SYMBOLS.success} veto-leash session ended${COLORS.reset}\n`
118
- );
119
- console.log(` Duration: ${minutes}m ${seconds}s`);
120
- console.log(` Blocked: ${this.state.blockedCount} actions`);
121
- console.log(` Allowed: ${this.state.allowedCount} actions`);
122
-
123
- if (this.state.blockedActions.length > 0) {
124
- console.log(`\n Blocked actions:`);
125
- for (const action of this.state.blockedActions.slice(-5)) {
126
- console.log(` ${SYMBOLS.bullet} ${action.action} ${action.target}`);
127
- }
128
- }
129
- console.log('');
130
-
131
- this.server?.close();
132
- }
133
- }
@@ -1,409 +0,0 @@
1
- // src/wrapper/shims.ts
2
-
3
- import { mkdtempSync, writeFileSync, rmSync } from 'fs';
4
- import { join } from 'path';
5
- import { tmpdir } from 'os';
6
- import type { Policy } from '../types.js';
7
-
8
- const IS_WINDOWS = process.platform === 'win32';
9
-
10
- // Unix command mappings
11
- const UNIX_COMMANDS: Record<string, string[]> = {
12
- delete: ['rm', 'unlink', 'rmdir'],
13
- modify: ['mv', 'cp', 'touch', 'chmod', 'chown', 'tee'],
14
- execute: ['node', 'python', 'python3', 'bash', 'sh', 'npx', 'pnpm', 'npm', 'yarn'],
15
- read: ['cat', 'less', 'head', 'tail', 'more'],
16
- };
17
-
18
- // Windows command mappings (PowerShell equivalents)
19
- const WINDOWS_COMMANDS: Record<string, string[]> = {
20
- delete: ['Remove-Item', 'del', 'rd', 'rmdir'],
21
- modify: ['Move-Item', 'Copy-Item', 'mv', 'cp', 'Set-Content'],
22
- execute: ['node', 'python', 'npx', 'pnpm', 'npm', 'yarn'],
23
- read: ['Get-Content', 'cat', 'type'],
24
- };
25
-
26
- export function createWrapperDir(port: number, policy: Policy): string {
27
- const dir = mkdtempSync(join(tmpdir(), 'veto-'));
28
-
29
- // Write the Node shim helper script (cross-platform)
30
- const shimHelper = createNodeShimHelper(port, policy.action);
31
- const shimHelperPath = join(dir, '_veto_check.mjs');
32
- writeFileSync(shimHelperPath, shimHelper, { mode: 0o755 });
33
-
34
- if (IS_WINDOWS) {
35
- createWindowsShims(dir, shimHelperPath, policy);
36
- } else {
37
- createUnixShims(dir, shimHelperPath, policy);
38
- }
39
-
40
- return dir;
41
- }
42
-
43
- function createUnixShims(dir: string, shimHelperPath: string, policy: Policy): void {
44
- const commands = UNIX_COMMANDS[policy.action] || [];
45
- for (const cmd of commands) {
46
- const script = createUnixShim(cmd, shimHelperPath);
47
- writeFileSync(join(dir, cmd), script, { mode: 0o755 });
48
- }
49
-
50
- // Always wrap git for delete/modify actions
51
- if (policy.action === 'delete' || policy.action === 'modify') {
52
- const gitShim = createGitShim(shimHelperPath);
53
- writeFileSync(join(dir, 'git'), gitShim, { mode: 0o755 });
54
- }
55
- }
56
-
57
- function createWindowsShims(dir: string, shimHelperPath: string, policy: Policy): void {
58
- const commands = WINDOWS_COMMANDS[policy.action] || [];
59
-
60
- for (const cmd of commands) {
61
- // Create both .ps1 and .cmd wrappers
62
- const ps1Script = createPowerShellShim(cmd, shimHelperPath);
63
- writeFileSync(join(dir, `${cmd}.ps1`), ps1Script);
64
-
65
- // CMD wrapper that invokes PowerShell
66
- const cmdScript = createCmdWrapper(cmd);
67
- writeFileSync(join(dir, `${cmd}.cmd`), cmdScript);
68
- }
69
-
70
- // Wrap git on Windows
71
- if (policy.action === 'delete' || policy.action === 'modify') {
72
- const gitPs1 = createPowerShellGitShim(shimHelperPath);
73
- writeFileSync(join(dir, 'git.ps1'), gitPs1);
74
- writeFileSync(join(dir, 'git.cmd'), createCmdWrapper('git'));
75
- }
76
- }
77
-
78
- /**
79
- * Node-based shim helper that handles:
80
- * - Directory walking for recursive deletes
81
- * - Proper JSON encoding
82
- * - TCP communication without netcat
83
- * - Path normalization without realpath
84
- */
85
- function createNodeShimHelper(port: number, action: string): string {
86
- return `#!/usr/bin/env node
87
- // veto-leash shim helper - checks files against policy daemon
88
-
89
- import { createConnection } from 'net';
90
- import { statSync, readdirSync } from 'fs';
91
- import { relative, resolve, join } from 'path';
92
-
93
- const PORT = ${port};
94
- const ACTION = '${action}';
95
- const MAX_FILES = 10000;
96
- const MAX_DEPTH = 50;
97
-
98
- // Get all files to check from arguments
99
- const targets = process.argv.slice(2);
100
-
101
- async function checkTarget(target) {
102
- return new Promise((resolve) => {
103
- const socket = createConnection({ port: PORT, host: '127.0.0.1' }, () => {
104
- const relPath = relative(process.cwd(), target) || target;
105
- const req = JSON.stringify({ action: ACTION, target: relPath });
106
- socket.write(req + '\\n');
107
- });
108
-
109
- let data = '';
110
- socket.on('data', (chunk) => {
111
- data += chunk.toString();
112
- if (data.includes('\\n')) {
113
- try {
114
- const resp = JSON.parse(data.trim());
115
- resolve(resp.allowed === true);
116
- } catch {
117
- resolve(false); // Fail closed
118
- }
119
- socket.end();
120
- }
121
- });
122
-
123
- socket.on('error', () => resolve(false)); // Fail closed
124
- socket.setTimeout(1000, () => {
125
- socket.destroy();
126
- resolve(false); // Fail closed on timeout
127
- });
128
- });
129
- }
130
-
131
- // Walk directory recursively and collect all files
132
- function walkDir(dir, depth = 0, files = []) {
133
- if (depth > MAX_DEPTH || files.length > MAX_FILES) return files;
134
-
135
- try {
136
- const entries = readdirSync(dir, { withFileTypes: true });
137
- for (const entry of entries) {
138
- if (files.length > MAX_FILES) break;
139
- const fullPath = join(dir, entry.name);
140
- files.push(fullPath);
141
- if (entry.isDirectory()) {
142
- walkDir(fullPath, depth + 1, files);
143
- }
144
- }
145
- } catch {
146
- // Ignore permission errors
147
- }
148
- return files;
149
- }
150
-
151
- async function main() {
152
- const filesToCheck = [];
153
-
154
- for (const target of targets) {
155
- try {
156
- const resolved = resolve(target);
157
- const stat = statSync(resolved);
158
-
159
- if (stat.isDirectory()) {
160
- // For directories, check all contained files
161
- const contained = walkDir(resolved);
162
- filesToCheck.push(...contained);
163
- } else {
164
- filesToCheck.push(resolved);
165
- }
166
- } catch {
167
- // File doesn't exist, let the real command handle the error
168
- continue;
169
- }
170
- }
171
-
172
- // Check all files
173
- for (const file of filesToCheck) {
174
- const allowed = await checkTarget(file);
175
- if (!allowed) {
176
- process.exit(1);
177
- }
178
- }
179
-
180
- process.exit(0);
181
- }
182
-
183
- main().catch(() => process.exit(1));
184
- `;
185
- }
186
-
187
- /**
188
- * PowerShell shim for Windows
189
- */
190
- function createPowerShellShim(cmd: string, helperPath: string): string {
191
- return `# veto-leash PowerShell shim for ${cmd}
192
- $ErrorActionPreference = "Stop"
193
-
194
- # Find real command
195
- $realCmd = Get-Command ${cmd} -CommandType Application -ErrorAction SilentlyContinue |
196
- Where-Object { $_.Source -notlike "*veto*" } |
197
- Select-Object -First 1
198
-
199
- if (-not $realCmd) {
200
- Write-Error "veto-leash: cannot find real ${cmd}"
201
- exit 127
202
- }
203
-
204
- # Collect file arguments
205
- $files = @()
206
- foreach ($arg in $args) {
207
- if ($arg -notlike "-*" -and (Test-Path $arg -ErrorAction SilentlyContinue)) {
208
- $files += $arg
209
- }
210
- }
211
-
212
- # Check with Node helper
213
- if ($files.Count -gt 0) {
214
- $result = & node "${helperPath.replace(/\\/g, '\\\\')}" @files
215
- if ($LASTEXITCODE -ne 0) {
216
- exit 1
217
- }
218
- }
219
-
220
- # Run real command
221
- & $realCmd.Source @args
222
- exit $LASTEXITCODE
223
- `;
224
- }
225
-
226
- /**
227
- * CMD wrapper that invokes PowerShell script
228
- */
229
- function createCmdWrapper(cmd: string): string {
230
- return `@echo off
231
- powershell -ExecutionPolicy Bypass -File "%~dp0${cmd}.ps1" %*
232
- exit /b %ERRORLEVEL%
233
- `;
234
- }
235
-
236
- /**
237
- * PowerShell git shim for Windows
238
- */
239
- function createPowerShellGitShim(helperPath: string): string {
240
- return `# veto-leash PowerShell git shim
241
- $ErrorActionPreference = "Stop"
242
-
243
- $realGit = Get-Command git -CommandType Application -ErrorAction SilentlyContinue |
244
- Where-Object { $_.Source -notlike "*veto*" } |
245
- Select-Object -First 1
246
-
247
- if (-not $realGit) {
248
- Write-Error "veto-leash: cannot find real git"
249
- exit 127
250
- }
251
-
252
- $subcommand = $args[0]
253
-
254
- switch ($subcommand) {
255
- "rm" {
256
- $files = @()
257
- foreach ($arg in $args[1..$args.Length]) {
258
- if ($arg -notlike "-*" -and (Test-Path $arg -ErrorAction SilentlyContinue)) {
259
- $files += $arg
260
- }
261
- }
262
- if ($files.Count -gt 0) {
263
- & node "${helperPath.replace(/\\/g, '\\\\')}" @files
264
- if ($LASTEXITCODE -ne 0) { exit 1 }
265
- }
266
- }
267
- "checkout" {
268
- if ($args[1] -eq "." -or ($args[1] -eq "--" -and $args[2] -eq ".")) {
269
- Write-Error "veto-leash: 'git checkout .' blocked - would modify protected files"
270
- exit 1
271
- }
272
- }
273
- "reset" {
274
- if ($args -contains "--hard") {
275
- Write-Error "veto-leash: 'git reset --hard' blocked - would modify protected files"
276
- exit 1
277
- }
278
- }
279
- }
280
-
281
- & $realGit.Source @args
282
- exit $LASTEXITCODE
283
- `;
284
- }
285
-
286
- /**
287
- * Bash shim that calls the Node helper for checking,
288
- * then executes the real command if allowed.
289
- */
290
- function createUnixShim(cmd: string, helperPath: string): string {
291
- return `#!/bin/bash
292
- set -e
293
-
294
- # Find real binary (skip our wrapper directory)
295
- WRAPPER_DIR="$(dirname "$0")"
296
- REAL_CMD=$(which -a ${cmd} 2>/dev/null | grep -v "$WRAPPER_DIR" | head -1)
297
-
298
- if [ -z "$REAL_CMD" ]; then
299
- echo "veto-leash: cannot find real ${cmd} binary" >&2
300
- exit 127
301
- fi
302
-
303
- # Collect file arguments (skip flags)
304
- FILES=()
305
- for arg in "$@"; do
306
- [[ "$arg" == -* ]] && continue
307
- FILES+=("$arg")
308
- done
309
-
310
- # Check files with Node helper if any exist
311
- if [ \${#FILES[@]} -gt 0 ]; then
312
- node "${helperPath}" "\${FILES[@]}" || exit 1
313
- fi
314
-
315
- # All approved, run real command
316
- exec "$REAL_CMD" "$@"
317
- `;
318
- }
319
-
320
- /**
321
- * Git shim that handles destructive git commands:
322
- * - git rm: check file args
323
- * - git clean: run dry-run, check candidates
324
- * - git checkout .: block by default (restores files)
325
- * - git reset --hard: block by default
326
- */
327
- function createGitShim(helperPath: string): string {
328
- return `#!/bin/bash
329
- set -e
330
-
331
- # Find real git (skip our wrapper directory)
332
- WRAPPER_DIR="$(dirname "$0")"
333
- REAL_GIT=$(which -a git 2>/dev/null | grep -v "$WRAPPER_DIR" | head -1)
334
-
335
- if [ -z "$REAL_GIT" ]; then
336
- echo "veto-leash: cannot find real git binary" >&2
337
- exit 127
338
- fi
339
-
340
- case "$1" in
341
- rm)
342
- # Check file arguments
343
- FILES=()
344
- for arg in "\${@:2}"; do
345
- [[ "$arg" == -* ]] && continue
346
- FILES+=("$arg")
347
- done
348
- if [ \${#FILES[@]} -gt 0 ]; then
349
- node "${helperPath}" "\${FILES[@]}" || exit 1
350
- fi
351
- ;;
352
-
353
- clean)
354
- # Check if it's a destructive clean
355
- if [[ "$*" == *"-f"* ]] || [[ "$*" == *"-d"* ]] || [[ "$*" == *"-x"* ]]; then
356
- # Get list of files that would be deleted
357
- CANDIDATES=$("$REAL_GIT" clean -n "\${@:2}" 2>/dev/null | sed 's/^Would remove //' || true)
358
- if [ -n "$CANDIDATES" ]; then
359
- echo "$CANDIDATES" | while read -r file; do
360
- [ -n "$file" ] && node "${helperPath}" "$file" || exit 1
361
- done
362
- # If the while loop exited with error, propagate it
363
- if [ \${PIPESTATUS[1]} -ne 0 ]; then
364
- exit 1
365
- fi
366
- fi
367
- fi
368
- ;;
369
-
370
- checkout)
371
- # Block 'git checkout .' and 'git checkout -- .' (restores tracked files)
372
- if [[ "$2" == "." ]] || [[ "$2" == "--" && "$3" == "." ]]; then
373
- echo "veto-leash: 'git checkout .' blocked - would modify protected files" >&2
374
- echo " Use 'git checkout <specific-file>' instead" >&2
375
- exit 1
376
- fi
377
- # Check specific file args
378
- FILES=()
379
- for arg in "\${@:2}"; do
380
- [[ "$arg" == -* ]] && continue
381
- [[ "$arg" == "--" ]] && continue
382
- [ -e "$arg" ] && FILES+=("$arg")
383
- done
384
- if [ \${#FILES[@]} -gt 0 ]; then
385
- node "${helperPath}" "\${FILES[@]}" || exit 1
386
- fi
387
- ;;
388
-
389
- reset)
390
- # Block 'git reset --hard' (destroys uncommitted changes)
391
- if [[ "$*" == *"--hard"* ]]; then
392
- echo "veto-leash: 'git reset --hard' blocked - would modify protected files" >&2
393
- echo " Use 'git stash' or commit your changes first" >&2
394
- exit 1
395
- fi
396
- ;;
397
- esac
398
-
399
- exec "$REAL_GIT" "$@"
400
- `;
401
- }
402
-
403
- export function cleanupWrapperDir(dir: string): void {
404
- try {
405
- rmSync(dir, { recursive: true, force: true });
406
- } catch {
407
- // Ignore cleanup errors
408
- }
409
- }