contextzip 0.4.0__tar.gz → 0.4.2__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.
Files changed (50) hide show
  1. {contextzip-0.4.0 → contextzip-0.4.2}/PKG-INFO +3 -1
  2. {contextzip-0.4.0 → contextzip-0.4.2}/README.md +2 -0
  3. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/__init__.py +1 -1
  4. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/applier.py +7 -5
  5. contextzip-0.4.2/contextzip/cleanup.py +207 -0
  6. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli.py +179 -230
  7. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli_ai.py +6 -35
  8. contextzip-0.4.2/contextzip/cli_display.py +426 -0
  9. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli_onboard.py +13 -35
  10. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/config.py +3 -1
  11. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/packager.py +197 -45
  12. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/project_config.py +81 -11
  13. contextzip-0.4.2/contextzip/redact.py +175 -0
  14. contextzip-0.4.2/contextzip/report.py +174 -0
  15. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/watcher.py +30 -65
  16. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/persist.py +2 -2
  17. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/server.py +9 -24
  18. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/PKG-INFO +3 -1
  19. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/SOURCES.txt +3 -0
  20. {contextzip-0.4.0 → contextzip-0.4.2}/pyproject.toml +1 -1
  21. contextzip-0.4.0/contextzip/cli_display.py +0 -454
  22. {contextzip-0.4.0 → contextzip-0.4.2}/LICENSE +0 -0
  23. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/__init__.py +0 -0
  24. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/gemini.py +0 -0
  25. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/heuristic.py +0 -0
  26. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/selector.py +0 -0
  27. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/api.py +0 -0
  28. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/clipboard.py +0 -0
  29. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/detector.py +0 -0
  30. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/error_parser.py +0 -0
  31. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/filters.py +0 -0
  32. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/git.py +0 -0
  33. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/__init__.py +0 -0
  34. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/base.py +0 -0
  35. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/__init__.py +0 -0
  36. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/node.py +0 -0
  37. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/python.py +0 -0
  38. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/go.py +0 -0
  39. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/node.py +0 -0
  40. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/python.py +0 -0
  41. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/ruby.py +0 -0
  42. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/rust.py +0 -0
  43. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/__init__.py +0 -0
  44. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/assets.py +0 -0
  45. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/suggestions.py +0 -0
  46. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/dependency_links.txt +0 -0
  47. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/entry_points.txt +0 -0
  48. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/requires.txt +0 -0
  49. {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/top_level.txt +0 -0
  50. {contextzip-0.4.0 → contextzip-0.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Intelligently package your codebase for AI tools
5
5
  Author-email: Deepesh <akadeepesh@gmail.com>
6
6
  License-Expression: MIT
@@ -27,6 +27,8 @@ Requires-Dist: pathspec>=0.12
27
27
  Requires-Dist: httpx>=0.27
28
28
  Dynamic: license-file
29
29
 
30
+ <img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" />
31
+
30
32
  # contextzip
31
33
 
32
34
  > Package exactly the right parts of your codebase and paste it into any AI tool — in one command.
@@ -1,3 +1,5 @@
1
+ <img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" />
2
+
1
3
  # contextzip
2
4
 
3
5
  > Package exactly the right parts of your codebase and paste it into any AI tool — in one command.
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.2"
4
4
 
5
5
  from contextzip.api import (
6
6
  FileCollection,
@@ -291,10 +291,12 @@ def find_latest_manifest(
291
291
  Resolve which manifest to diff against.
292
292
 
293
293
  An explicit path always wins (if it exists). Otherwise picks the most
294
- recently modified *.manifest.json under .contextzip/output/ — in the
295
- normal one-round-trip-at-a-time flow, that's the zip you most recently
296
- generated, which is almost always the right baseline. Returns None if
297
- nothing is found (apply-zip still works, just more conservatively).
294
+ recently modified *.manifest.json anywhere under .contextzip/output/
295
+ — including its per-mode subfolders (output/codebase/, output/git-changes/,
296
+ output/prompt/) — since in the normal one-round-trip-at-a-time flow,
297
+ that's the zip you most recently generated regardless of which mode
298
+ produced it, which is almost always the right baseline. Returns None
299
+ if nothing is found (apply-zip still works, just more conservatively).
298
300
  """
299
301
  if explicit_manifest is not None:
300
302
  path = Path(explicit_manifest).expanduser().resolve()
@@ -304,7 +306,7 @@ def find_latest_manifest(
304
306
  if not out_dir.is_dir():
305
307
  return None
306
308
 
307
- candidates = [p for p in out_dir.glob(f"*{_MANIFEST_SUFFIX}") if p.is_file()]
309
+ candidates = [p for p in out_dir.rglob(f"*{_MANIFEST_SUFFIX}") if p.is_file()]
308
310
  if not candidates:
309
311
  return None
310
312
  return max(candidates, key=lambda p: p.stat().st_mtime)
@@ -0,0 +1,207 @@
1
+ """
2
+ cleanup.py — Automatic workspace housekeeping for contextzip.
3
+
4
+ .contextzip/ accumulates disposable artifacts over time: a zip+manifest+report
5
+ set every run, a timestamped backup folder every risky `apply-zip`, and (until
6
+ pruned) old archived applied-zips. None of it is precious — every zip is
7
+ trivially reproducible by just re-running contextzip, and a backup only
8
+ matters until the apply that made it is confirmed good. So cleanup here is
9
+ deliberately brutal rather than cautious: it runs automatically after every
10
+ successful command (see `_auto_cleanup` in cli.py), with no prompt and no
11
+ separate command to remember, and keeps only the most recent
12
+ `cleanup.keep_recent` item(s) in each category — everything else is deleted
13
+ immediately, every time.
14
+
15
+ The heavier caution used elsewhere in contextzip (backing up before overwrite
16
+ in apply-zip, never inferring deletions) is reserved for the project's own
17
+ files, where losing something can mean losing real work. Nothing here ever
18
+ touches the project — only disposable metadata under .contextzip/ that costs
19
+ nothing to regenerate.
20
+
21
+ Never touched, under any settings:
22
+ - .contextzip/config.json (project preferences)
23
+ - .contextzip/inbox/*.zip (zips waiting to be applied — never assumed
24
+ stale just because they've been sitting there; only inbox/applied/,
25
+ the archive of zips already consumed, is ever pruned)
26
+ - anything outside .contextzip/
27
+
28
+ Kept only-the-most-recent-N, everything older deleted immediately:
29
+ - zip/manifest/report sets under .contextzip/output/<mode>/, per mode
30
+ folder
31
+ - timestamped folders under .contextzip/backups/
32
+ - archived zips under .contextzip/inbox/applied/ (this is also governed
33
+ by `applied_zip_retention`, already enforced on every apply-zip run —
34
+ scanning it here too just catches the case where zips piled up before
35
+ that setting existed, or the setting was since lowered)
36
+
37
+ Scanning only ever touches .contextzip/ directory listings and file mtimes —
38
+ no hashing, no reading file contents — so this stays fast even called on
39
+ every command, on a large project.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import shutil
45
+ from dataclasses import dataclass, field
46
+ from pathlib import Path
47
+
48
+
49
+ _OUTPUT_DIRNAME = "output"
50
+ _BACKUPS_DIRNAME = "backups"
51
+ _INBOX_DIRNAME = "inbox"
52
+ _APPLIED_DIRNAME = "applied"
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Data model
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ @dataclass
61
+ class CleanupItem:
62
+ path: Path
63
+ kind: str # "zip-set" | "backup" | "applied-zip"
64
+ size_bytes: int
65
+
66
+
67
+ @dataclass
68
+ class CleanupPlan:
69
+ workspace: Path
70
+ items: list[CleanupItem] = field(default_factory=list)
71
+
72
+ @property
73
+ def total_bytes(self) -> int:
74
+ return sum(i.size_bytes for i in self.items)
75
+
76
+ @property
77
+ def is_empty(self) -> bool:
78
+ return not self.items
79
+
80
+
81
+ @dataclass
82
+ class CleanupResult:
83
+ removed: list[Path] = field(default_factory=list)
84
+ failed: list[tuple[Path, str]] = field(default_factory=list)
85
+ freed_bytes: int = 0
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Helpers
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ def _size_of(path: Path) -> int:
94
+ """Size of a file, or total size of everything under a directory."""
95
+ if path.is_file():
96
+ try:
97
+ return path.stat().st_size
98
+ except OSError:
99
+ return 0
100
+ if not path.is_dir():
101
+ return 0
102
+ total = 0
103
+ for p in path.rglob("*"):
104
+ if p.is_file():
105
+ try:
106
+ total += p.stat().st_size
107
+ except OSError:
108
+ pass
109
+ return total
110
+
111
+
112
+ def _zip_set_files(zip_path: Path) -> list[Path]:
113
+ """The zip and any sidecar manifest/report files that sit next to it."""
114
+ files = [zip_path]
115
+ for suffix in (".manifest.json", ".report.txt", ".apply-report.txt"):
116
+ sidecar = zip_path.with_name(zip_path.stem + suffix)
117
+ if sidecar.is_file():
118
+ files.append(sidecar)
119
+ return files
120
+
121
+
122
+ def _newest_first(paths: list[Path]) -> list[Path]:
123
+ return sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True)
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Scan
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ def scan(
132
+ project_dir: Path,
133
+ *,
134
+ keep_recent: int = 1,
135
+ applied_zip_retention: int = 1,
136
+ ) -> CleanupPlan:
137
+ """
138
+ Build a plan of everything beyond the most recent `keep_recent` item(s)
139
+ per category. Read-only — never deletes anything itself; pass the
140
+ result to `execute()` to actually do so.
141
+ """
142
+ from contextzip.packager import _workspace_dir
143
+
144
+ workspace, _is_git_repo = _workspace_dir(project_dir)
145
+ items: list[CleanupItem] = []
146
+ keep = max(0, keep_recent)
147
+
148
+ # ── output/ — per-mode subfolders, plus any flat legacy files left
149
+ # behind by a contextzip version predating the per-mode split ──────
150
+ output_root = workspace / _OUTPUT_DIRNAME
151
+ if output_root.is_dir():
152
+ for mode_dir in sorted(p for p in output_root.iterdir() if p.is_dir()):
153
+ zips = _newest_first([p for p in mode_dir.glob("*.zip") if p.is_file()])
154
+ for zp in zips[keep:]:
155
+ for f in _zip_set_files(zp):
156
+ items.append(CleanupItem(path=f, kind="zip-set", size_bytes=_size_of(f)))
157
+
158
+ legacy_zips = _newest_first([p for p in output_root.glob("*.zip") if p.is_file()])
159
+ for zp in legacy_zips[keep:]:
160
+ for f in _zip_set_files(zp):
161
+ items.append(CleanupItem(path=f, kind="zip-set", size_bytes=_size_of(f)))
162
+
163
+ # ── backups/<timestamp>/ ─────────────────────────────────────────────
164
+ backups_root = workspace / _BACKUPS_DIRNAME
165
+ if backups_root.is_dir():
166
+ backups = _newest_first([p for p in backups_root.iterdir() if p.is_dir()])
167
+ for backup_dir in backups[keep:]:
168
+ items.append(
169
+ CleanupItem(path=backup_dir, kind="backup", size_bytes=_size_of(backup_dir))
170
+ )
171
+
172
+ # ── inbox/applied/ ───────────────────────────────────────────────────
173
+ applied_root = workspace / _INBOX_DIRNAME / _APPLIED_DIRNAME
174
+ if applied_root.is_dir():
175
+ applied_zips = _newest_first([p for p in applied_root.glob("*.zip") if p.is_file()])
176
+ retain = max(1, applied_zip_retention)
177
+ for zp in applied_zips[retain:]:
178
+ items.append(CleanupItem(path=zp, kind="applied-zip", size_bytes=_size_of(zp)))
179
+
180
+ return CleanupPlan(workspace=workspace, items=items)
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Execute
185
+ # ---------------------------------------------------------------------------
186
+
187
+
188
+ def execute(plan: CleanupPlan) -> CleanupResult:
189
+ """
190
+ Delete everything in *plan*, immediately, no confirmation. Best-effort:
191
+ one failure doesn't stop the rest, and every failure is reported rather
192
+ than silently swallowed.
193
+ """
194
+ result = CleanupResult()
195
+ for item in plan.items:
196
+ try:
197
+ if item.path.is_dir():
198
+ shutil.rmtree(item.path)
199
+ elif item.path.is_file():
200
+ item.path.unlink()
201
+ else:
202
+ continue # already gone — nothing to do, not a failure
203
+ result.removed.append(item.path)
204
+ result.freed_bytes += item.size_bytes
205
+ except OSError as exc:
206
+ result.failed.append((item.path, str(exc)))
207
+ return result