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,231 +0,0 @@
1
- // src/native/windsurf.ts
2
- // Windsurf native hook integration (Cascade Hooks)
3
- // Very similar to Claude Code - supports pre_write_code, pre_run_command hooks
4
-
5
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
6
- import { join } from 'path';
7
- import { homedir } from 'os';
8
- import type { Policy } from '../types.js';
9
- import { COLORS, SYMBOLS } from '../ui/colors.js';
10
-
11
- const WINDSURF_USER_CONFIG = join(homedir(), '.codeium', 'windsurf', 'hooks.json');
12
- const WINDSURF_WORKSPACE_CONFIG = '.windsurf/hooks.json';
13
- const VETO_SCRIPTS_DIR = join(homedir(), '.codeium', 'windsurf', 'veto-leash');
14
-
15
- interface WindsurfHooksConfig {
16
- hooks?: {
17
- pre_write_code?: Array<{ command: string; show_output?: boolean }>;
18
- pre_run_command?: Array<{ command: string; show_output?: boolean }>;
19
- pre_read_code?: Array<{ command: string; show_output?: boolean }>;
20
- [key: string]: unknown;
21
- };
22
- }
23
-
24
- /**
25
- * Install veto-leash as Windsurf Cascade hooks
26
- */
27
- export async function installWindsurfHooks(
28
- target: 'user' | 'workspace' = 'user'
29
- ): Promise<void> {
30
- console.log(`\n${COLORS.info}Installing veto-leash for Windsurf (${target})...${COLORS.reset}\n`);
31
-
32
- // Create scripts directory and validator
33
- mkdirSync(VETO_SCRIPTS_DIR, { recursive: true });
34
- mkdirSync(join(VETO_SCRIPTS_DIR, 'policies'), { recursive: true });
35
-
36
- const validatorPath = join(VETO_SCRIPTS_DIR, 'validator.py');
37
- writeFileSync(validatorPath, VALIDATOR_SCRIPT, { mode: 0o755 });
38
- console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Created validator: ${validatorPath}`);
39
-
40
- // Determine config path
41
- const configPath = target === 'user' ? WINDSURF_USER_CONFIG : WINDSURF_WORKSPACE_CONFIG;
42
- const configDir = target === 'user'
43
- ? join(homedir(), '.codeium', 'windsurf')
44
- : '.windsurf';
45
-
46
- // Load existing config
47
- let config: WindsurfHooksConfig = { hooks: {} };
48
- if (existsSync(configPath)) {
49
- try {
50
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
51
- } catch {
52
- // Start fresh
53
- }
54
- }
55
-
56
- if (!config.hooks) config.hooks = {};
57
-
58
- // Add veto-leash hooks
59
- const hookCommand = `python3 "${validatorPath}"`;
60
-
61
- // Helper to add hook if not exists
62
- const addHook = (hookName: string) => {
63
- if (!config.hooks![hookName]) {
64
- config.hooks![hookName] = [];
65
- }
66
- const hooks = config.hooks![hookName] as Array<{ command: string; show_output?: boolean }>;
67
- const exists = hooks.some(h => h.command.includes('veto-leash'));
68
- if (!exists) {
69
- hooks.unshift({ command: hookCommand, show_output: true });
70
- }
71
- };
72
-
73
- addHook('pre_write_code');
74
- addHook('pre_run_command');
75
-
76
- // Write config
77
- mkdirSync(configDir, { recursive: true });
78
- writeFileSync(configPath, JSON.stringify(config, null, 2));
79
- console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Updated: ${configPath}`);
80
-
81
- console.log(`\n${COLORS.success}${SYMBOLS.success} veto-leash installed for Windsurf${COLORS.reset}\n`);
82
- console.log(`Add policies with: ${COLORS.dim}leash add "don't delete test files"${COLORS.reset}\n`);
83
- }
84
-
85
- /**
86
- * Add a policy for Windsurf
87
- */
88
- export async function addWindsurfPolicy(policy: Policy, name: string): Promise<void> {
89
- const policiesDir = join(VETO_SCRIPTS_DIR, 'policies');
90
- mkdirSync(policiesDir, { recursive: true });
91
-
92
- const policyFile = join(policiesDir, `${name}.json`);
93
- writeFileSync(policyFile, JSON.stringify(policy, null, 2));
94
- console.log(`${COLORS.success}${SYMBOLS.success}${COLORS.reset} Windsurf policy saved: ${policyFile}`);
95
- }
96
-
97
- /**
98
- * Uninstall veto-leash from Windsurf
99
- */
100
- export async function uninstallWindsurfHooks(
101
- target: 'user' | 'workspace' = 'user'
102
- ): Promise<void> {
103
- const configPath = target === 'user' ? WINDSURF_USER_CONFIG : WINDSURF_WORKSPACE_CONFIG;
104
-
105
- if (!existsSync(configPath)) {
106
- console.log(`${COLORS.dim}No Windsurf config found at ${configPath}${COLORS.reset}`);
107
- return;
108
- }
109
-
110
- try {
111
- const config: WindsurfHooksConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
112
-
113
- if (config.hooks) {
114
- for (const hookName of Object.keys(config.hooks)) {
115
- const hooks = config.hooks[hookName];
116
- if (Array.isArray(hooks)) {
117
- config.hooks[hookName] = hooks.filter(
118
- (h: { command: string }) => !h.command.includes('veto-leash')
119
- );
120
- }
121
- }
122
- }
123
-
124
- writeFileSync(configPath, JSON.stringify(config, null, 2));
125
- console.log(`${COLORS.success}${SYMBOLS.success} Removed veto-leash from Windsurf${COLORS.reset}`);
126
- } catch {
127
- console.log(`${COLORS.warning}${SYMBOLS.warning} Could not parse Windsurf config${COLORS.reset}`);
128
- }
129
- }
130
-
131
- /**
132
- * Python validator script for Windsurf Cascade hooks.
133
- * Handles pre_write_code and pre_run_command events.
134
- */
135
- const VALIDATOR_SCRIPT = `#!/usr/bin/env python3
136
- """
137
- veto-leash validator for Windsurf Cascade hooks.
138
- Exit code 2 blocks the action.
139
- """
140
-
141
- import json
142
- import sys
143
- import os
144
- import re
145
- from pathlib import Path
146
- from fnmatch import fnmatch
147
-
148
- POLICIES_DIR = Path(__file__).parent / "policies"
149
-
150
- def load_policies():
151
- policies = []
152
- if POLICIES_DIR.exists():
153
- for f in POLICIES_DIR.glob("*.json"):
154
- try:
155
- policies.append(json.loads(f.read_text()))
156
- except:
157
- pass
158
- return policies
159
-
160
- def normalize_path(p):
161
- return str(p).replace("\\\\", "/")
162
-
163
- def matches_pattern(target, pattern):
164
- target = normalize_path(target)
165
- pattern = normalize_path(pattern)
166
- basename = os.path.basename(target)
167
- return fnmatch(target, pattern) or fnmatch(basename, pattern)
168
-
169
- def is_protected(target, policy):
170
- matches_include = any(matches_pattern(target, p) for p in policy.get("include", []))
171
- if not matches_include:
172
- return False
173
- matches_exclude = any(matches_pattern(target, p) for p in policy.get("exclude", []))
174
- return not matches_exclude
175
-
176
- def parse_command_targets(command, action):
177
- targets = []
178
- if action == "delete":
179
- rm_match = re.search(r'\\brm\\s+(?:-[rfiv]+\\s+)*(.+)', command)
180
- if rm_match:
181
- for arg in rm_match.group(1).split():
182
- if not arg.startswith('-'):
183
- targets.append(arg)
184
- git_rm = re.search(r'\\bgit\\s+rm\\s+(?:-[rf]+\\s+)*(.+)', command)
185
- if git_rm:
186
- for arg in git_rm.group(1).split():
187
- if not arg.startswith('-'):
188
- targets.append(arg)
189
- return targets
190
-
191
- def main():
192
- try:
193
- input_data = json.load(sys.stdin)
194
- except:
195
- sys.exit(0)
196
-
197
- action_name = input_data.get("agent_action_name", "")
198
- tool_info = input_data.get("tool_info", {})
199
- policies = load_policies()
200
-
201
- if not policies:
202
- sys.exit(0)
203
-
204
- targets = []
205
-
206
- if action_name == "pre_write_code":
207
- file_path = tool_info.get("file_path", "")
208
- if file_path:
209
- targets.append(file_path)
210
-
211
- elif action_name == "pre_run_command":
212
- command = tool_info.get("command_line", "")
213
- for policy in policies:
214
- action = policy.get("action", "modify")
215
- targets.extend(parse_command_targets(command, action))
216
-
217
- for target in targets:
218
- for policy in policies:
219
- if is_protected(target, policy):
220
- desc = policy.get("description", "Protected file")
221
- print(f"veto-leash: BLOCKED", file=sys.stderr)
222
- print(f" Target: {target}", file=sys.stderr)
223
- print(f" Policy: {desc}", file=sys.stderr)
224
- print(f" Filesystem unchanged.", file=sys.stderr)
225
- sys.exit(2)
226
-
227
- sys.exit(0)
228
-
229
- if __name__ == "__main__":
230
- main()
231
- `;
package/src/types.ts DELETED
@@ -1,48 +0,0 @@
1
- // src/types.ts
2
-
3
- export interface Policy {
4
- action: 'delete' | 'modify' | 'execute' | 'read';
5
- include: string[];
6
- exclude: string[];
7
- description: string;
8
- }
9
-
10
- export interface CheckRequest {
11
- action: string;
12
- target: string;
13
- }
14
-
15
- export interface CheckResponse {
16
- allowed: boolean;
17
- reason?: string;
18
- }
19
-
20
- export interface SessionState {
21
- pid: number;
22
- agent: string;
23
- policy: Policy;
24
- startTime: Date;
25
- blockedCount: number;
26
- allowedCount: number;
27
- blockedActions: Array<{ time: Date; action: string; target: string }>;
28
- }
29
-
30
- export interface Config {
31
- failClosed: boolean;
32
- fallbackToBuiltins: boolean;
33
- warnBroadPatterns: boolean;
34
- maxSnapshotFiles: number;
35
- maxMemoryCacheSize: number;
36
- auditLog: boolean;
37
- verbose: boolean;
38
- }
39
-
40
- export const DEFAULT_CONFIG: Config = {
41
- failClosed: true,
42
- fallbackToBuiltins: true,
43
- warnBroadPatterns: true,
44
- maxSnapshotFiles: 10000,
45
- maxMemoryCacheSize: 100 * 1024,
46
- auditLog: false,
47
- verbose: false,
48
- };
package/src/ui/colors.ts DELETED
@@ -1,50 +0,0 @@
1
- // src/ui/colors.ts
2
-
3
- const isTTY = process.stdout.isTTY && process.stderr.isTTY;
4
- const noColor = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
5
-
6
- function color(code: string): string {
7
- return isTTY && !noColor ? code : '';
8
- }
9
-
10
- export const COLORS = {
11
- success: color('\x1b[32m'),
12
- error: color('\x1b[31m'),
13
- warning: color('\x1b[33m'),
14
- info: color('\x1b[36m'),
15
- dim: color('\x1b[90m'),
16
- bold: color('\x1b[1m'),
17
- reset: color('\x1b[0m'),
18
- };
19
-
20
- export const SYMBOLS = {
21
- success: '\u2713',
22
- error: '\u2717',
23
- blocked: '\u26D4',
24
- warning: '\u26A0',
25
- arrow: '\u2192',
26
- bullet: '\u2022',
27
- };
28
-
29
- const SPINNER_FRAMES = ['\u25D0', '\u25D3', '\u25D1', '\u25D2'];
30
-
31
- export function createSpinner(message: string): { stop: () => void } {
32
- if (!isTTY) {
33
- console.log(message);
34
- return { stop: () => {} };
35
- }
36
-
37
- let i = 0;
38
- const interval = setInterval(() => {
39
- process.stdout.write(
40
- `\r${COLORS.dim}${SPINNER_FRAMES[i++ % 4]} ${message}${COLORS.reset}`
41
- );
42
- }, 100);
43
-
44
- return {
45
- stop: () => {
46
- clearInterval(interval);
47
- process.stdout.write('\r\x1b[K'); // Clear line
48
- },
49
- };
50
- }
@@ -1,82 +0,0 @@
1
- // src/watchdog/index.ts
2
- // Watchdog mode orchestrator
3
-
4
- import type { FSWatcher } from 'chokidar';
5
- import type { Policy } from '../types.js';
6
- import { createSnapshot, cleanupSnapshot, generateSessionId, type Snapshot } from './snapshot.js';
7
- import { createWatcher, type WatcherStats } from './watcher.js';
8
- import { COLORS, SYMBOLS } from '../ui/colors.js';
9
-
10
- export interface WatchdogSession {
11
- sessionId: string;
12
- rootDir: string;
13
- policy: Policy;
14
- snapshot: Snapshot;
15
- watcher: FSWatcher;
16
- stats: WatcherStats;
17
- startTime: Date;
18
- }
19
-
20
- /**
21
- * Start watchdog mode - monitors and auto-restores protected files
22
- */
23
- export async function startWatchdog(
24
- rootDir: string,
25
- policy: Policy
26
- ): Promise<WatchdogSession> {
27
- const sessionId = generateSessionId();
28
- const startTime = new Date();
29
-
30
- // Create snapshot of protected files
31
- const snapshot = await createSnapshot(rootDir, policy, sessionId);
32
-
33
- // Start filesystem watcher
34
- const { watcher, stats } = createWatcher({
35
- rootDir,
36
- policy,
37
- snapshot,
38
- });
39
-
40
- return {
41
- sessionId,
42
- rootDir,
43
- policy,
44
- snapshot,
45
- watcher,
46
- stats,
47
- startTime,
48
- };
49
- }
50
-
51
- /**
52
- * Stop watchdog mode and print summary
53
- */
54
- export async function stopWatchdog(session: WatchdogSession): Promise<void> {
55
- const duration = Date.now() - session.startTime.getTime();
56
- const minutes = Math.floor(duration / 60000);
57
- const seconds = Math.floor((duration % 60000) / 1000);
58
-
59
- // Close watcher
60
- await session.watcher.close();
61
-
62
- // Print summary
63
- console.log(`\n${COLORS.success}${SYMBOLS.success} veto-leash watchdog ended${COLORS.reset}\n`);
64
- console.log(` Duration: ${minutes}m ${seconds}s`);
65
- console.log(` Files protected: ${session.snapshot.files.size}`);
66
- console.log(` Auto-restored: ${session.stats.restored}`);
67
-
68
- if (session.stats.events.length > 0) {
69
- console.log(`\n Recent events:`);
70
- for (const event of session.stats.events.slice(-5)) {
71
- console.log(` ${SYMBOLS.bullet} ${event.action} ${event.path}`);
72
- }
73
- }
74
- console.log('');
75
-
76
- // Cleanup snapshot (optional - keep for debugging)
77
- // cleanupSnapshot(session.sessionId);
78
- }
79
-
80
- export { createSnapshot, cleanupSnapshot, generateSessionId } from './snapshot.js';
81
- export { restoreFile, restoreAll } from './restore.js';
82
- export { createWatcher } from './watcher.js';
@@ -1,74 +0,0 @@
1
- // src/watchdog/restore.ts
2
- // File restoration from snapshots
3
-
4
- import { existsSync, writeFileSync, mkdirSync } from 'fs';
5
- import { join, dirname } from 'path';
6
- import type { Snapshot } from './snapshot.js';
7
- import { getSnapshotContent } from './snapshot.js';
8
- import { normalizePath } from '../matcher.js';
9
-
10
- /**
11
- * Restore a file from snapshot
12
- * Returns true if restored, false if no snapshot exists
13
- */
14
- export function restoreFile(
15
- snapshot: Snapshot,
16
- filePath: string,
17
- rootDir: string
18
- ): boolean {
19
- const normalizedPath = normalizePath(filePath);
20
- const content = getSnapshotContent(snapshot, normalizedPath);
21
-
22
- if (!content) return false;
23
-
24
- const fullPath = join(rootDir, normalizedPath);
25
- const dir = dirname(fullPath);
26
-
27
- // Ensure directory exists
28
- if (!existsSync(dir)) {
29
- mkdirSync(dir, { recursive: true });
30
- }
31
-
32
- // Write restored content
33
- writeFileSync(fullPath, content);
34
-
35
- return true;
36
- }
37
-
38
- /**
39
- * Restore all files from snapshot
40
- * Returns count of restored files
41
- */
42
- export function restoreAll(snapshot: Snapshot, rootDir: string): number {
43
- let restored = 0;
44
-
45
- for (const [filePath] of snapshot.files) {
46
- const fullPath = join(rootDir, filePath);
47
-
48
- // Only restore if file is missing or different
49
- if (!existsSync(fullPath)) {
50
- if (restoreFile(snapshot, filePath, rootDir)) {
51
- restored++;
52
- }
53
- }
54
- }
55
-
56
- return restored;
57
- }
58
-
59
- /**
60
- * Check what files would be restored
61
- */
62
- export function previewRestore(snapshot: Snapshot, rootDir: string): string[] {
63
- const toRestore: string[] = [];
64
-
65
- for (const [filePath] of snapshot.files) {
66
- const fullPath = join(rootDir, filePath);
67
-
68
- if (!existsSync(fullPath)) {
69
- toRestore.push(filePath);
70
- }
71
- }
72
-
73
- return toRestore;
74
- }
@@ -1,209 +0,0 @@
1
- // src/watchdog/snapshot.ts
2
- // File snapshot system for watchdog mode
3
-
4
- import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'fs';
5
- import { join, dirname, relative } from 'path';
6
- import { homedir } from 'os';
7
- import { createHash } from 'crypto';
8
- import { glob } from 'glob';
9
- import type { Policy } from '../types.js';
10
- import { isProtected, normalizePath } from '../matcher.js';
11
-
12
- const SNAPSHOT_DIR = join(homedir(), '.config', 'veto-leash', 'snapshots');
13
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB max per file
14
- const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100MB max total
15
-
16
- export interface Snapshot {
17
- sessionId: string;
18
- rootDir: string;
19
- files: Map<string, SnapshotEntry>;
20
- createdAt: Date;
21
- }
22
-
23
- export interface SnapshotEntry {
24
- path: string;
25
- hash: string;
26
- size: number;
27
- snapshotPath: string;
28
- }
29
-
30
- /**
31
- * Create a snapshot of all files matching the policy
32
- */
33
- export async function createSnapshot(
34
- rootDir: string,
35
- policy: Policy,
36
- sessionId: string
37
- ): Promise<Snapshot> {
38
- const sessionDir = join(SNAPSHOT_DIR, sessionId);
39
- mkdirSync(sessionDir, { recursive: true });
40
-
41
- const snapshot: Snapshot = {
42
- sessionId,
43
- rootDir: normalizePath(rootDir),
44
- files: new Map(),
45
- createdAt: new Date(),
46
- };
47
-
48
- // Find all files matching include patterns
49
- const matchedFiles = await findMatchingFiles(rootDir, policy);
50
-
51
- let totalSize = 0;
52
-
53
- for (const filePath of matchedFiles) {
54
- const fullPath = join(rootDir, filePath);
55
-
56
- if (!existsSync(fullPath)) continue;
57
-
58
- const stat = statSync(fullPath);
59
- if (!stat.isFile()) continue;
60
- if (stat.size > MAX_FILE_SIZE) continue;
61
- if (totalSize + stat.size > MAX_TOTAL_SIZE) break;
62
-
63
- const content = readFileSync(fullPath);
64
- const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
65
-
66
- // Store with hash prefix for deduplication
67
- const snapshotPath = join(sessionDir, hash);
68
- if (!existsSync(snapshotPath)) {
69
- writeFileSync(snapshotPath, content);
70
- }
71
-
72
- snapshot.files.set(filePath, {
73
- path: filePath,
74
- hash,
75
- size: stat.size,
76
- snapshotPath,
77
- });
78
-
79
- totalSize += stat.size;
80
- }
81
-
82
- // Write manifest
83
- const manifest = {
84
- sessionId,
85
- rootDir: snapshot.rootDir,
86
- createdAt: snapshot.createdAt.toISOString(),
87
- files: Array.from(snapshot.files.entries()).map(([path, entry]) => ({
88
- path,
89
- hash: entry.hash,
90
- size: entry.size,
91
- })),
92
- };
93
-
94
- writeFileSync(join(sessionDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
95
-
96
- return snapshot;
97
- }
98
-
99
- /**
100
- * Find all files matching the policy's include patterns
101
- */
102
- async function findMatchingFiles(rootDir: string, policy: Policy): Promise<string[]> {
103
- const allFiles: string[] = [];
104
-
105
- for (const pattern of policy.include) {
106
- const matches = await glob(pattern, {
107
- cwd: rootDir,
108
- dot: true,
109
- nodir: true,
110
- ignore: ['node_modules/**', '.git/**'],
111
- });
112
- allFiles.push(...matches);
113
- }
114
-
115
- // Dedupe and filter by policy
116
- const unique = [...new Set(allFiles)];
117
- return unique.filter(f => isProtected(f, policy));
118
- }
119
-
120
- /**
121
- * Get the snapshot content for a file
122
- */
123
- export function getSnapshotContent(snapshot: Snapshot, filePath: string): Buffer | null {
124
- const entry = snapshot.files.get(normalizePath(filePath));
125
- if (!entry) return null;
126
-
127
- if (!existsSync(entry.snapshotPath)) return null;
128
-
129
- return readFileSync(entry.snapshotPath);
130
- }
131
-
132
- /**
133
- * Check if a file has been modified since snapshot
134
- */
135
- export function hasChanged(snapshot: Snapshot, filePath: string, rootDir: string): boolean {
136
- const normalizedPath = normalizePath(filePath);
137
- const entry = snapshot.files.get(normalizedPath);
138
-
139
- if (!entry) return false; // Not in snapshot, can't detect change
140
-
141
- const fullPath = join(rootDir, normalizedPath);
142
-
143
- if (!existsSync(fullPath)) return true; // Deleted
144
-
145
- const content = readFileSync(fullPath);
146
- const currentHash = createHash('sha256').update(content).digest('hex').slice(0, 16);
147
-
148
- return currentHash !== entry.hash;
149
- }
150
-
151
- /**
152
- * Load an existing snapshot from disk
153
- */
154
- export function loadSnapshot(sessionId: string): Snapshot | null {
155
- const sessionDir = join(SNAPSHOT_DIR, sessionId);
156
- const manifestPath = join(sessionDir, 'manifest.json');
157
-
158
- if (!existsSync(manifestPath)) return null;
159
-
160
- try {
161
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
162
- const snapshot: Snapshot = {
163
- sessionId: manifest.sessionId,
164
- rootDir: manifest.rootDir,
165
- files: new Map(),
166
- createdAt: new Date(manifest.createdAt),
167
- };
168
-
169
- for (const file of manifest.files) {
170
- snapshot.files.set(file.path, {
171
- path: file.path,
172
- hash: file.hash,
173
- size: file.size,
174
- snapshotPath: join(sessionDir, file.hash),
175
- });
176
- }
177
-
178
- return snapshot;
179
- } catch {
180
- return null;
181
- }
182
- }
183
-
184
- /**
185
- * Clean up a snapshot session
186
- */
187
- export function cleanupSnapshot(sessionId: string): void {
188
- const sessionDir = join(SNAPSHOT_DIR, sessionId);
189
-
190
- if (!existsSync(sessionDir)) return;
191
-
192
- try {
193
- const files = readdirSync(sessionDir);
194
- for (const file of files) {
195
- unlinkSync(join(sessionDir, file));
196
- }
197
- // Remove directory
198
- require('fs').rmdirSync(sessionDir);
199
- } catch {
200
- // Ignore cleanup errors
201
- }
202
- }
203
-
204
- /**
205
- * Generate a unique session ID
206
- */
207
- export function generateSessionId(): string {
208
- return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
209
- }