execdiff 0.0.1__tar.gz → 0.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execdiff
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: Passive execution tracing for file and package changes.
5
5
  Requires-Python: >=3.8
6
6
  License-File: LICENSE
@@ -0,0 +1,193 @@
1
+ # ExecDiff
2
+
3
+ See what AI-generated code will change before running it.
4
+
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ AI coding tools and agents today can:
10
+
11
+ - install dependencies
12
+ - create files
13
+ - modify configs
14
+ - run migrations
15
+ - delete project files
16
+
17
+ All automatically.
18
+
19
+ When something breaks after execution, tools cannot answer:
20
+
21
+ > What exactly changed because of this action?
22
+
23
+ Git tracks source code changes —
24
+ but it does **not** track execution side effects like:
25
+
26
+ - newly installed Python packages
27
+ - runtime-created files
28
+ - deleted files
29
+ - modified configs
30
+
31
+ So tools often fall back to:
32
+
33
+ > regenerate and try again
34
+
35
+ ---
36
+
37
+ ## Solution
38
+
39
+ ExecDiff allows tools to run AI-generated code and observe:
40
+
41
+ > what changed in the workspace because of that execution
42
+
43
+ It detects:
44
+
45
+ - files created
46
+ - files modified
47
+ - files deleted
48
+ - Python packages installed
49
+
50
+ inside a specific workspace
51
+ during a specific execution window.
52
+
53
+ ---
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install execdiff
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Example
64
+
65
+ Create a test script:
66
+
67
+ ```python
68
+ import execdiff
69
+ import json
70
+ import os
71
+
72
+ os.makedirs("workspace", exist_ok=True)
73
+
74
+ diff = execdiff.run_traced(
75
+ "touch workspace/test.txt",
76
+ workspace="workspace"
77
+ )
78
+
79
+ print(json.dumps(diff, indent=2))
80
+ ```
81
+
82
+ Run:
83
+
84
+ ```bash
85
+ python test.py
86
+ ```
87
+
88
+ ---
89
+
90
+ ## API Reference
91
+
92
+ ### `start_action_trace(workspace=".")`
93
+
94
+ Start tracing a workspace for changes. Must be called before any operations.
95
+
96
+ ```python
97
+ import execdiff
98
+
99
+ execdiff.start_action_trace(workspace="./my_workspace")
100
+ # ... your code that makes changes ...
101
+ ```
102
+
103
+ ### `stop_action_trace()`
104
+
105
+ Stop tracing and return a diff of all changes detected. Automatically logs to `.execdiff/logs/actions.jsonl`.
106
+
107
+ ```python
108
+ diff = execdiff.stop_action_trace()
109
+ # Returns: {"files": {...}, "packages": {...}}
110
+ ```
111
+
112
+ ### `last_action_summary(workspace=".")`
113
+
114
+ Get a human-readable summary of the last action trace without parsing JSON.
115
+
116
+ ```python
117
+ summary = execdiff.last_action_summary(workspace=".")
118
+ print(summary)
119
+ ```
120
+
121
+ Output example:
122
+ ```
123
+ Last AI Action:
124
+
125
+ Created:
126
+ - output.txt
127
+ - data.json
128
+
129
+ Installed:
130
+ - requests==2.32.0
131
+ ```
132
+
133
+ ### `snapshot_workspace_state(workspace)`
134
+
135
+ Take a full metadata snapshot of the workspace (files with mtime/size, installed packages).
136
+
137
+ ```python
138
+ state = execdiff.snapshot_workspace_state(workspace=".")
139
+ # Returns: {"files": {...}, "packages": {...}}
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Output Format
145
+
146
+ ### Diff Structure
147
+
148
+ ```json
149
+ {
150
+ "files": {
151
+ "created": [{"path": "file.txt", "mtime": 123.45, "size": 1024}],
152
+ "modified": [{"path": "config.yaml", "before_mtime": 123, "after_mtime": 124, "before_size": 512, "after_size": 1024}],
153
+ "deleted": [{"path": "old_file.txt", "mtime": 123.45, "size": 256}]
154
+ },
155
+ "packages": {
156
+ "installed": [{"name": "requests", "version": "2.32.0"}],
157
+ "upgraded": [{"name": "django", "before_version": "3.2", "after_version": "4.0"}],
158
+ "removed": [{"name": "deprecated_lib", "version": "1.0"}]
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Log File
164
+
165
+ All action traces are automatically persisted to `.execdiff/logs/actions.jsonl`:
166
+
167
+ ```json
168
+ {
169
+ "timestamp": "2026-02-18T18:19:35.872838",
170
+ "workspace": "/path/to/workspace",
171
+ "diff": {...}
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Use Cases
178
+
179
+ ExecDiff can help AI coding tools:
180
+
181
+ - preview changes before applying generated code
182
+ - detect unintended file or dependency changes
183
+ - explain execution impact to users
184
+ - debug failed automation
185
+ - build undo / rollback systems
186
+
187
+ ---
188
+
189
+ ## License
190
+
191
+ MIT
192
+
193
+ ````
@@ -0,0 +1,371 @@
1
+ import sysconfig
2
+ import re
3
+ import json
4
+ from datetime import datetime
5
+
6
+ # --- Full Workspace Metadata Snapshot and Action Trace ---
7
+ _action_trace_before = None
8
+
9
+ def snapshot_workspace_state(workspace):
10
+ """
11
+ Take a full snapshot of the workspace state: files (mtime, size) and installed packages (name, version).
12
+ Returns:
13
+ dict: {"files": {relpath: {"mtime": float, "size": int}}, "packages": {name: {"version": str}}}
14
+ """
15
+ # File snapshot
16
+ files = {}
17
+ for root, dirs, filelist in os.walk(workspace):
18
+ for fname in filelist:
19
+ fpath = os.path.join(root, fname)
20
+ relpath = os.path.relpath(fpath, workspace)
21
+ try:
22
+ files[relpath] = {
23
+ "mtime": os.path.getmtime(fpath),
24
+ "size": os.path.getsize(fpath)
25
+ }
26
+ except (OSError, IOError):
27
+ pass
28
+
29
+ # Package snapshot
30
+ packages = {}
31
+ site_packages = sysconfig.get_paths()["purelib"]
32
+ dist_info_re = re.compile(r"^(?P<name>.+?)-(?P<version>[^-]+)\.dist-info$")
33
+ try:
34
+ for entry in os.listdir(site_packages):
35
+ m = dist_info_re.match(entry)
36
+ if m:
37
+ name = m.group("name").replace('_', '-')
38
+ version = m.group("version")
39
+ packages[name.lower()] = {"version": version}
40
+ except Exception:
41
+ pass
42
+
43
+ return {"files": files, "packages": packages}
44
+
45
+
46
+ def start_action_trace(workspace="."):
47
+ """
48
+ Take and store a full workspace metadata snapshot for later diffing.
49
+ """
50
+ global _action_trace_before, _workspace
51
+ _workspace = workspace
52
+ _action_trace_before = snapshot_workspace_state(workspace)
53
+
54
+
55
+ def stop_action_trace():
56
+ """
57
+ Take a new snapshot and compute diff (files: created/modified/deleted, packages: installed/removed/upgraded).
58
+ Returns:
59
+ dict: {"files": {...}, "packages": {...}}
60
+ """
61
+ global _action_trace_before, _workspace
62
+ if _action_trace_before is None:
63
+ raise RuntimeError("start_action_trace() must be called before stop_action_trace()")
64
+ after = snapshot_workspace_state(_workspace)
65
+ before = _action_trace_before
66
+
67
+ # File diffs
68
+ before_files = before["files"]
69
+ after_files = after["files"]
70
+ created = []
71
+ modified = []
72
+ deleted = []
73
+ for f in after_files:
74
+ if f not in before_files:
75
+ created.append({"path": f, **after_files[f]})
76
+ else:
77
+ b, a = before_files[f], after_files[f]
78
+ if b["mtime"] != a["mtime"] or b["size"] != a["size"]:
79
+ modified.append({"path": f, "before_mtime": b["mtime"], "after_mtime": a["mtime"], "before_size": b["size"], "after_size": a["size"]})
80
+ for f in before_files:
81
+ if f not in after_files:
82
+ deleted.append({"path": f, **before_files[f]})
83
+
84
+ # Package diffs
85
+ before_pkgs = before["packages"]
86
+ after_pkgs = after["packages"]
87
+ installed = []
88
+ removed = []
89
+ upgraded = []
90
+ for name in after_pkgs:
91
+ if name not in before_pkgs:
92
+ installed.append({"name": name, "version": after_pkgs[name]["version"]})
93
+ else:
94
+ if before_pkgs[name]["version"] != after_pkgs[name]["version"]:
95
+ upgraded.append({"name": name, "before_version": before_pkgs[name]["version"], "after_version": after_pkgs[name]["version"]})
96
+ for name in before_pkgs:
97
+ if name not in after_pkgs:
98
+ removed.append({"name": name, "version": before_pkgs[name]["version"]})
99
+
100
+ diff = {
101
+ "files": {
102
+ "created": created,
103
+ "modified": modified,
104
+ "deleted": deleted
105
+ },
106
+ "packages": {
107
+ "installed": installed,
108
+ "removed": removed,
109
+ "upgraded": upgraded
110
+ }
111
+ }
112
+
113
+ _persist_action_log(diff)
114
+ return diff
115
+
116
+
117
+ def _persist_action_log(diff):
118
+ """
119
+ Persist the action trace diff to global logs directory.
120
+ Uses EXECDIFF_LOG_DIR env var, or defaults to ~/.execdiff/logs/
121
+ """
122
+ # Get log directory from env or use home directory
123
+ log_base = os.environ.get('EXECDIFF_LOG_DIR') or os.path.expanduser('~/.execdiff/logs')
124
+ log_file = os.path.join(log_base, "actions.jsonl")
125
+
126
+ try:
127
+ os.makedirs(log_base, exist_ok=True)
128
+ except Exception:
129
+ return
130
+
131
+ try:
132
+ entry = {
133
+ "timestamp": datetime.utcnow().isoformat(),
134
+ "workspace": os.path.abspath(_workspace),
135
+ "diff": diff
136
+ }
137
+ with open(log_file, "a") as f:
138
+ f.write(json.dumps(entry) + "\n")
139
+ except Exception:
140
+ pass
141
+ """Minimal passive execution tracing library for file system snapshots."""
142
+
143
+ import os
144
+ import subprocess
145
+ import time
146
+
147
+
148
+ # Module-level variables to store the initial snapshot, workspace, package snapshot, and execution window
149
+ _initial_snapshot = None
150
+ _workspace = "."
151
+ _initial_packages = None
152
+ _execution_start_time = None
153
+ _execution_end_time = None
154
+
155
+
156
+ def start_trace(workspace="."):
157
+ """
158
+ Snapshot all files in the specified workspace directory recursively.
159
+ Stores the snapshot in a module-level variable for later comparison.
160
+
161
+ Args:
162
+ workspace (str): The workspace directory to trace. Defaults to ".".
163
+ """
164
+ global _initial_snapshot, _workspace, _initial_packages, _execution_start_time
165
+ _workspace = workspace
166
+ _initial_snapshot = _take_snapshot()
167
+ _initial_packages = _snapshot_packages()
168
+ _execution_start_time = time.time()
169
+
170
+
171
+ def stop_trace():
172
+ """
173
+ Take a new snapshot and compare with the previous one.
174
+
175
+ Returns:
176
+ dict: A dictionary containing detailed information about created, modified, and deleted files:
177
+ {
178
+ "files": {
179
+ "created": [{"path": <file_path>, "mtime": <modified_time>}, ...],
180
+ "modified": [{"path": <file_path>, "before_mtime": <mtime>, "after_mtime": <mtime>}, ...],
181
+ "deleted": [{"path": <file_path>, "before_mtime": <mtime>}, ...]
182
+ }
183
+ }
184
+ """
185
+ global _execution_end_time
186
+ if _initial_snapshot is None or _initial_packages is None or _execution_start_time is None:
187
+ raise RuntimeError("start_trace() must be called before stop_trace()")
188
+
189
+ _execution_end_time = time.time()
190
+ current_snapshot = _take_snapshot()
191
+ current_packages = _snapshot_packages()
192
+
193
+ # Only include files whose mtime falls within the execution window
194
+ def in_window(mtime):
195
+ return _execution_start_time <= mtime <= _execution_end_time
196
+
197
+ # Find newly created files
198
+ created_files = []
199
+ for file_path in sorted(set(current_snapshot.keys()) - set(_initial_snapshot.keys())):
200
+ mtime = current_snapshot[file_path]
201
+ if in_window(mtime):
202
+ created_files.append({
203
+ "path": file_path,
204
+ "mtime": mtime
205
+ })
206
+
207
+ # Find modified files (files that existed before but have different mtime)
208
+ modified_files = []
209
+ for file_path in sorted(_initial_snapshot.keys()):
210
+ if file_path in current_snapshot:
211
+ before_mtime = _initial_snapshot[file_path]
212
+ after_mtime = current_snapshot[file_path]
213
+ if after_mtime != before_mtime and in_window(after_mtime):
214
+ modified_files.append({
215
+ "path": file_path,
216
+ "before_mtime": before_mtime,
217
+ "after_mtime": after_mtime
218
+ })
219
+
220
+ # Find deleted files (files that existed before but don't exist now)
221
+ deleted_files = []
222
+ for file_path in sorted(set(_initial_snapshot.keys()) - set(current_snapshot.keys())):
223
+ before_mtime = _initial_snapshot[file_path]
224
+ if in_window(before_mtime):
225
+ deleted_files.append({
226
+ "path": file_path,
227
+ "before_mtime": before_mtime
228
+ })
229
+
230
+ # Find newly installed packages using pip freeze
231
+ installed_packages = []
232
+ new_pkgs = current_packages - _initial_packages
233
+ for pkg in sorted(new_pkgs):
234
+ if "==" in pkg:
235
+ name, version = pkg.split("==", 1)
236
+ installed_packages.append({"name": name, "version": version})
237
+
238
+ return {
239
+ "files": {
240
+ "created": created_files,
241
+ "modified": modified_files,
242
+ "deleted": deleted_files
243
+ },
244
+ "packages": {
245
+ "installed": installed_packages
246
+ }
247
+ }
248
+
249
+ def run_traced(command, workspace="."):
250
+ """
251
+ Trace the effects of running a shell command in a subprocess.
252
+
253
+ Args:
254
+ command (list or str): The command to run (as for subprocess.run)
255
+ workspace (str): The workspace directory to trace. Defaults to ".".
256
+
257
+ Returns:
258
+ dict: The diff as returned by stop_trace().
259
+ """
260
+ start_trace(workspace)
261
+ subprocess.run(command, shell=isinstance(command, str))
262
+ return stop_trace()
263
+
264
+
265
+ def _snapshot_packages():
266
+ """
267
+ Take a snapshot of installed Python packages using pip freeze.
268
+ Returns:
269
+ set: Set of 'package==version' strings.
270
+ """
271
+ try:
272
+ result = subprocess.run(["python3", "-m", "pip", "freeze"], capture_output=True, text=True, check=True)
273
+ lines = result.stdout.strip().split("\n")
274
+ return set(line for line in lines if line and not line.startswith("-") and "==" in line)
275
+ except Exception:
276
+ return set()
277
+
278
+
279
+ def _take_snapshot():
280
+ """
281
+ Take a snapshot of all files in the workspace directory recursively.
282
+
283
+ Returns:
284
+ dict: A dictionary mapping relative file paths to their last modified time.
285
+ """
286
+ file_dict = {}
287
+
288
+ for root, dirs, files in os.walk(_workspace):
289
+ for file in files:
290
+ file_path = os.path.join(root, file)
291
+ relative_path = os.path.relpath(file_path, _workspace)
292
+ try:
293
+ mtime = os.path.getmtime(file_path)
294
+ file_dict[relative_path] = mtime
295
+ except (OSError, IOError):
296
+ # Skip files that can't be accessed
297
+ pass
298
+
299
+ return file_dict
300
+
301
+
302
+ def last_action_summary(workspace="."):
303
+ """
304
+ Read the latest action trace from global logs and return a human-readable summary.
305
+ Uses EXECDIFF_LOG_DIR env var, or defaults to ~/.execdiff/logs/
306
+ Returns:
307
+ str: Human-readable summary of the last AI action, or a message if no log exists.
308
+ """
309
+ log_base = os.environ.get('EXECDIFF_LOG_DIR') or os.path.expanduser('~/.execdiff/logs')
310
+ log_file = os.path.join(log_base, "actions.jsonl")
311
+
312
+ if not os.path.exists(log_file):
313
+ return "No action history found."
314
+
315
+ try:
316
+ # Read the last line
317
+ with open(log_file, "r") as f:
318
+ lines = f.readlines()
319
+ if not lines:
320
+ return "No action history found."
321
+ last_line = lines[-1].strip()
322
+
323
+ entry = json.loads(last_line)
324
+ diff = entry.get("diff", {})
325
+ files = diff.get("files", {})
326
+ packages = diff.get("packages", {})
327
+
328
+ # Build summary
329
+ summary_lines = ["Last AI Action:\n"]
330
+
331
+ # Packages
332
+ pkg_installed = packages.get("installed", [])
333
+ if pkg_installed:
334
+ summary_lines.append("Installed:")
335
+ for pkg in pkg_installed:
336
+ summary_lines.append(f"- {pkg['name']}=={pkg['version']}")
337
+
338
+ pkg_upgraded = packages.get("upgraded", [])
339
+ if pkg_upgraded:
340
+ summary_lines.append("Upgraded:")
341
+ for pkg in pkg_upgraded:
342
+ summary_lines.append(f"- {pkg['name']}: {pkg['before_version']} → {pkg['after_version']}")
343
+
344
+ pkg_removed = packages.get("removed", [])
345
+ if pkg_removed:
346
+ summary_lines.append("Removed:")
347
+ for pkg in pkg_removed:
348
+ summary_lines.append(f"- {pkg['name']}")
349
+
350
+ # Files
351
+ file_modified = files.get("modified", [])
352
+ if file_modified:
353
+ summary_lines.append("Modified:")
354
+ for f in file_modified:
355
+ summary_lines.append(f"- {f['path']}")
356
+
357
+ file_created = files.get("created", [])
358
+ if file_created:
359
+ summary_lines.append("Created:")
360
+ for f in file_created:
361
+ summary_lines.append(f"- {f['path']}")
362
+
363
+ file_deleted = files.get("deleted", [])
364
+ if file_deleted:
365
+ summary_lines.append("Deleted:")
366
+ for f in file_deleted:
367
+ summary_lines.append(f"- {f['path']}")
368
+
369
+ return "\n".join(summary_lines) if len(summary_lines) > 1 else "No changes detected."
370
+ except Exception:
371
+ return "Error reading action history."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execdiff
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: Passive execution tracing for file and package changes.
5
5
  Requires-Python: >=3.8
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "execdiff"
7
- version = "0.0.1"
7
+ version = "0.0.3"
8
8
  description = "Passive execution tracing for file and package changes."
9
9
  requires-python = ">=3.8"
10
10
 
execdiff-0.0.1/README.md DELETED
@@ -1,85 +0,0 @@
1
- # execdiff
2
- Track what changes when AI-generated code runs
3
-
4
- ## Features
5
- - Trace file system changes (created, modified, deleted files)
6
- - Detect newly installed Python packages
7
- - Track changes only within a specified workspace directory
8
- - Execution window: only changes made during the traced execution are reported
9
- - Simple API, no classes
10
-
11
- ## API Usage
12
-
13
- ### Basic Tracing
14
- ```python
15
- import execdiff
16
-
17
- execdiff.start_trace(workspace=".")
18
- # ... your code that makes changes ...
19
- diff = execdiff.stop_trace()
20
- print(diff)
21
- ```
22
-
23
- ### Trace a Subprocess Command
24
- ```python
25
- import execdiff
26
- import json
27
-
28
- diff = execdiff.run_traced(["touch", "example.txt"])
29
- print(json.dumps(diff, indent=2))
30
- ```
31
-
32
- ### Diff Output Structure
33
- The result is a dictionary with:
34
- - `files`: created, modified, and deleted files (with mtimes, only those changed during execution)
35
- - `packages`: newly installed Python packages (name and version)
36
-
37
- Example:
38
- ```json
39
- {
40
- "files": {
41
- "created": [ {"path": "example.txt", "mtime": 1234567890.0} ],
42
- "modified": [],
43
- "deleted": []
44
- },
45
- "packages": {
46
- "installed": [ {"name": "requests", "version": "2.32.0"} ]
47
- }
48
- }
49
- ```
50
-
51
- ## Installation
52
- This package is self-contained and requires only Python 3. No external dependencies are needed for core tracing features. For package detection, ensure `pip` is available in your environment.
53
-
54
- Clone the repository and use the code directly, or copy `execdiff/` into your project.
55
-
56
- ## Running the Test
57
- To verify the MVP end-to-end:
58
-
59
- ```bash
60
- python3 test.py
61
- ```
62
-
63
- You should see output like:
64
-
65
- ```
66
- {
67
- "files": {
68
- "created": [
69
- {"path": "test_workspace/ai_created.txt", "mtime": ...}
70
- ],
71
- "modified": [],
72
- "deleted": []
73
- },
74
- "packages": {
75
- "installed": []
76
- }
77
- }
78
- ```
79
-
80
- ## Contributing
81
- - Please open issues or pull requests for bugs, ideas, or improvements.
82
- - Keep the implementation simple and function-based (no classes).
83
-
84
- ## License
85
- MIT
@@ -1,159 +0,0 @@
1
- """Minimal passive execution tracing library for file system snapshots."""
2
-
3
- import os
4
- import subprocess
5
- import time
6
-
7
-
8
- # Module-level variables to store the initial snapshot, workspace, package snapshot, and execution window
9
- _initial_snapshot = None
10
- _workspace = "."
11
- _initial_packages = None
12
- _execution_start_time = None
13
- _execution_end_time = None
14
-
15
-
16
- def start_trace(workspace="."):
17
- """
18
- Snapshot all files in the specified workspace directory recursively.
19
- Stores the snapshot in a module-level variable for later comparison.
20
-
21
- Args:
22
- workspace (str): The workspace directory to trace. Defaults to ".".
23
- """
24
- global _initial_snapshot, _workspace, _initial_packages, _execution_start_time
25
- _workspace = workspace
26
- _initial_snapshot = _take_snapshot()
27
- _initial_packages = _snapshot_packages()
28
- _execution_start_time = time.time()
29
-
30
-
31
- def stop_trace():
32
- """
33
- Take a new snapshot and compare with the previous one.
34
-
35
- Returns:
36
- dict: A dictionary containing detailed information about created, modified, and deleted files:
37
- {
38
- "files": {
39
- "created": [{"path": <file_path>, "mtime": <modified_time>}, ...],
40
- "modified": [{"path": <file_path>, "before_mtime": <mtime>, "after_mtime": <mtime>}, ...],
41
- "deleted": [{"path": <file_path>, "before_mtime": <mtime>}, ...]
42
- }
43
- }
44
- """
45
- global _execution_end_time
46
- if _initial_snapshot is None or _initial_packages is None or _execution_start_time is None:
47
- raise RuntimeError("start_trace() must be called before stop_trace()")
48
-
49
- _execution_end_time = time.time()
50
- current_snapshot = _take_snapshot()
51
- current_packages = _snapshot_packages()
52
-
53
- # Only include files whose mtime falls within the execution window
54
- def in_window(mtime):
55
- return _execution_start_time <= mtime <= _execution_end_time
56
-
57
- # Find newly created files
58
- created_files = []
59
- for file_path in sorted(set(current_snapshot.keys()) - set(_initial_snapshot.keys())):
60
- mtime = current_snapshot[file_path]
61
- if in_window(mtime):
62
- created_files.append({
63
- "path": file_path,
64
- "mtime": mtime
65
- })
66
-
67
- # Find modified files (files that existed before but have different mtime)
68
- modified_files = []
69
- for file_path in sorted(_initial_snapshot.keys()):
70
- if file_path in current_snapshot:
71
- before_mtime = _initial_snapshot[file_path]
72
- after_mtime = current_snapshot[file_path]
73
- if after_mtime != before_mtime and in_window(after_mtime):
74
- modified_files.append({
75
- "path": file_path,
76
- "before_mtime": before_mtime,
77
- "after_mtime": after_mtime
78
- })
79
-
80
- # Find deleted files (files that existed before but don't exist now)
81
- deleted_files = []
82
- for file_path in sorted(set(_initial_snapshot.keys()) - set(current_snapshot.keys())):
83
- before_mtime = _initial_snapshot[file_path]
84
- if in_window(before_mtime):
85
- deleted_files.append({
86
- "path": file_path,
87
- "before_mtime": before_mtime
88
- })
89
-
90
- # Find newly installed packages using pip freeze
91
- installed_packages = []
92
- new_pkgs = current_packages - _initial_packages
93
- for pkg in sorted(new_pkgs):
94
- if "==" in pkg:
95
- name, version = pkg.split("==", 1)
96
- installed_packages.append({"name": name, "version": version})
97
-
98
- return {
99
- "files": {
100
- "created": created_files,
101
- "modified": modified_files,
102
- "deleted": deleted_files
103
- },
104
- "packages": {
105
- "installed": installed_packages
106
- }
107
- }
108
-
109
- def run_traced(command, workspace="."):
110
- """
111
- Trace the effects of running a shell command in a subprocess.
112
-
113
- Args:
114
- command (list or str): The command to run (as for subprocess.run)
115
- workspace (str): The workspace directory to trace. Defaults to ".".
116
-
117
- Returns:
118
- dict: The diff as returned by stop_trace().
119
- """
120
- start_trace(workspace)
121
- subprocess.run(command, shell=isinstance(command, str))
122
- return stop_trace()
123
-
124
-
125
- def _snapshot_packages():
126
- """
127
- Take a snapshot of installed Python packages using pip freeze.
128
- Returns:
129
- set: Set of 'package==version' strings.
130
- """
131
- try:
132
- result = subprocess.run(["python3", "-m", "pip", "freeze"], capture_output=True, text=True, check=True)
133
- lines = result.stdout.strip().split("\n")
134
- return set(line for line in lines if line and not line.startswith("-") and "==" in line)
135
- except Exception:
136
- return set()
137
-
138
-
139
- def _take_snapshot():
140
- """
141
- Take a snapshot of all files in the workspace directory recursively.
142
-
143
- Returns:
144
- dict: A dictionary mapping relative file paths to their last modified time.
145
- """
146
- file_dict = {}
147
-
148
- for root, dirs, files in os.walk(_workspace):
149
- for file in files:
150
- file_path = os.path.join(root, file)
151
- relative_path = os.path.relpath(file_path, _workspace)
152
- try:
153
- mtime = os.path.getmtime(file_path)
154
- file_dict[relative_path] = mtime
155
- except (OSError, IOError):
156
- # Skip files that can't be accessed
157
- pass
158
-
159
- return file_dict
File without changes
File without changes