contextzip 0.4.2__tar.gz → 0.4.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.
Files changed (50) hide show
  1. {contextzip-0.4.2 → contextzip-0.4.3}/PKG-INFO +1 -1
  2. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/__init__.py +1 -1
  3. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/applier.py +21 -3
  4. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli.py +85 -0
  5. contextzip-0.4.3/contextzip/updater.py +165 -0
  6. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/assets.py +11 -0
  7. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/PKG-INFO +1 -1
  8. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/SOURCES.txt +1 -0
  9. {contextzip-0.4.2 → contextzip-0.4.3}/pyproject.toml +1 -1
  10. {contextzip-0.4.2 → contextzip-0.4.3}/LICENSE +0 -0
  11. {contextzip-0.4.2 → contextzip-0.4.3}/README.md +0 -0
  12. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/__init__.py +0 -0
  13. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/gemini.py +0 -0
  14. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/heuristic.py +0 -0
  15. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/selector.py +0 -0
  16. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/api.py +0 -0
  17. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cleanup.py +0 -0
  18. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_ai.py +0 -0
  19. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_display.py +0 -0
  20. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_onboard.py +0 -0
  21. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/clipboard.py +0 -0
  22. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/config.py +0 -0
  23. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/detector.py +0 -0
  24. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/error_parser.py +0 -0
  25. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/filters.py +0 -0
  26. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/git.py +0 -0
  27. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/packager.py +0 -0
  28. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/project_config.py +0 -0
  29. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/redact.py +0 -0
  30. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/report.py +0 -0
  31. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/__init__.py +0 -0
  32. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/base.py +0 -0
  33. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/__init__.py +0 -0
  34. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/node.py +0 -0
  35. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/python.py +0 -0
  36. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/go.py +0 -0
  37. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/node.py +0 -0
  38. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/python.py +0 -0
  39. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/ruby.py +0 -0
  40. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/rust.py +0 -0
  41. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/watcher.py +0 -0
  42. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/__init__.py +0 -0
  43. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/persist.py +0 -0
  44. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/server.py +0 -0
  45. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/suggestions.py +0 -0
  46. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/dependency_links.txt +0 -0
  47. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/entry_points.txt +0 -0
  48. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/requires.txt +0 -0
  49. {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/top_level.txt +0 -0
  50. {contextzip-0.4.2 → contextzip-0.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Intelligently package your codebase for AI tools
5
5
  Author-email: Deepesh <akadeepesh@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.3"
4
4
 
5
5
  from contextzip.api import (
6
6
  FileCollection,
@@ -446,8 +446,24 @@ def build_plan(
446
446
  strip_prefix: str | None = None
447
447
  wrapper_note: str | None = None
448
448
  candidate = _detect_common_wrapper(raw_names)
449
- if candidate and not (project_dir / candidate).is_dir():
449
+ if candidate:
450
450
  if manifest_files:
451
+ # Decide using the manifest — actual ground truth about what
452
+ # paths this project has — rather than whether a directory of
453
+ # that name merely happens to exist on disk. That on-disk check
454
+ # used to gate this whole branch, which backfired on the most
455
+ # common real case: an AI tool wraps the whole project in a
456
+ # folder named after the project itself (e.g. "contextzip/"),
457
+ # and the project's own top-level package/dir happens to share
458
+ # that exact name — so the wrapper looked "legitimate" and was
459
+ # never stripped, dumping a nested copy of the project instead
460
+ # of updating the real files. Comparing manifest match-rates
461
+ # with vs without stripping gets both cases right: a genuine
462
+ # wrapper matches far better once stripped, while a zip whose
463
+ # top-level folder is an intentional part of the project (e.g.
464
+ # updates confined to an existing "docs/" folder) matches
465
+ # better *unstripped*, since those paths are already in the
466
+ # manifest as-is.
451
467
  unstripped_rate = _match_rate(raw_names, manifest_files)
452
468
  stripped_names = [n[len(candidate) + 1:] for n in raw_names]
453
469
  stripped_rate = _match_rate(stripped_names, manifest_files)
@@ -462,10 +478,12 @@ def build_plan(
462
478
  f"project manifest after stripping vs {unstripped_rate:.0%} "
463
479
  "before."
464
480
  )
465
- else:
481
+ elif not (project_dir / candidate).is_dir():
466
482
  # No manifest to confirm against, so this is a softer call —
467
483
  # still strip (matches how `tar` and GitHub's own zip downloads
468
- # behave), but say so plainly since it's not manifest-verified.
484
+ # behave) as long as the project doesn't already have a real
485
+ # folder of that name, but say so plainly since it's not
486
+ # manifest-verified.
469
487
  strip_prefix = candidate
470
488
  wrapper_note = (
471
489
  f"Removed wrapping folder '{candidate}/' present in every zip "
@@ -74,6 +74,7 @@ from contextzip.cli_ai import (
74
74
  )
75
75
  from contextzip.cli_onboard import onboard_api_key
76
76
  from contextzip import cleanup as cleanup_mod
77
+ from contextzip import updater as updater_mod
77
78
 
78
79
  console = Console()
79
80
 
@@ -688,6 +689,90 @@ def cmd_config(
688
689
  console.print()
689
690
 
690
691
 
692
+ # ---------------------------------------------------------------------------
693
+ # Subcommand: update
694
+ # ---------------------------------------------------------------------------
695
+
696
+
697
+ @main.command("update")
698
+ @click.option(
699
+ "--check",
700
+ "check_only",
701
+ is_flag=True,
702
+ default=False,
703
+ help="Only check for a newer version — don't install anything.",
704
+ )
705
+ @click.option(
706
+ "--yes",
707
+ "-y",
708
+ is_flag=True,
709
+ default=False,
710
+ help="Skip the confirmation prompt and update immediately if available.",
711
+ )
712
+ def cmd_update(check_only: bool, yes: bool) -> None:
713
+ """
714
+ Check PyPI for a newer contextzip release and update in place.
715
+
716
+ \b
717
+ EXAMPLES
718
+ contextzip update # check, then confirm before updating
719
+ contextzip update --yes # update immediately if one is available
720
+ contextzip update --check # only report the current/latest version
721
+ cz update # same command, short alias
722
+
723
+ \b
724
+ HOW IT WORKS
725
+ contextzip cuts frequent releases, so this beats remembering
726
+ `pip install --upgrade contextzip` by hand. It detects whether this
727
+ install came from pip, pipx, or uv tool and re-invokes the matching
728
+ one, upgrading the same interpreter that's currently running —
729
+ never a possibly-different "pip" found on PATH.
730
+ """
731
+ console.print()
732
+ with console.status("[cyan]Checking for updates…[/]", spinner="dots"):
733
+ check = updater_mod.check_latest_version()
734
+
735
+ if check.latest is None:
736
+ err(f"Could not check for updates: {check.error}")
737
+ info(f"You're on {check.current}. Try again later, or update manually:")
738
+ info(" pip install --upgrade contextzip")
739
+ console.print()
740
+ raise SystemExit(1)
741
+
742
+ if not check.update_available:
743
+ ok(f"You're up to date — contextzip {check.current}")
744
+ console.print()
745
+ return
746
+
747
+ info(f"Update available: {check.current} → {check.latest}")
748
+
749
+ if check_only:
750
+ info("Run contextzip update to install it.")
751
+ console.print()
752
+ return
753
+
754
+ if not yes:
755
+ proceed = click.confirm(f" Update to {check.latest} now?", default=True)
756
+ if not proceed:
757
+ info("Not updating.")
758
+ console.print()
759
+ return
760
+
761
+ with console.status(f"[cyan]Updating to {check.latest}…[/]", spinner="dots"):
762
+ result = updater_mod.run_update()
763
+
764
+ console.print()
765
+ if result.ok:
766
+ ok(f"Updated to contextzip {check.latest}", f"via {result.method}")
767
+ info("Restart any running contextzip/watch sessions to pick it up.")
768
+ else:
769
+ err(f"Update via {result.method} failed")
770
+ if result.output:
771
+ console.print(f"[dim]{result.output}[/]")
772
+ info("You can also update manually: pip install --upgrade contextzip")
773
+ console.print()
774
+
775
+
691
776
  # Core execution logic (shared by main command + all subcommands)
692
777
  # ---------------------------------------------------------------------------
693
778
 
@@ -0,0 +1,165 @@
1
+ """
2
+ updater.py — Self-update support for `contextzip update` / `cz update`.
3
+
4
+ contextzip cuts frequent releases, so re-running
5
+ `pip install --upgrade contextzip` by hand every time gets old fast.
6
+ This module:
7
+
8
+ 1. Asks PyPI's JSON API for the latest published version.
9
+ 2. Compares it against the version actually running (contextzip.__version__).
10
+ 3. If newer, re-invokes pip (or pipx, if that's how contextzip was
11
+ installed) to upgrade in place.
12
+
13
+ Never raises on network failure — a broken connection or PyPI hiccup
14
+ should degrade to "couldn't check right now", not crash the CLI.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ from dataclasses import dataclass
23
+
24
+ from contextzip import __version__ as installed_version
25
+
26
+ PYPI_URL = "https://pypi.org/pypi/contextzip/json"
27
+ _TIMEOUT_SECS = 5
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Version comparison (no `packaging` dependency)
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ def _parse_version(v: str) -> tuple[int, ...]:
36
+ """
37
+ Turn "1.2.3" into (1, 2, 3) for comparison. Ignores any pre-release /
38
+ build suffix after the numeric core (e.g. "1.2.3rc1" -> (1, 2, 3)) —
39
+ good enough for "is there a newer release" without pulling in the
40
+ `packaging` library just for this.
41
+ """
42
+ core = v.strip().lstrip("v")
43
+ parts: list[int] = []
44
+ for chunk in core.split("."):
45
+ digits = ""
46
+ for ch in chunk:
47
+ if ch.isdigit():
48
+ digits += ch
49
+ else:
50
+ break
51
+ parts.append(int(digits) if digits else 0)
52
+ return tuple(parts) if parts else (0,)
53
+
54
+
55
+ def is_newer(latest: str, current: str) -> bool:
56
+ """True if *latest* is a strictly newer version than *current*."""
57
+ a, b = _parse_version(latest), _parse_version(current)
58
+ length = max(len(a), len(b))
59
+ a = a + (0,) * (length - len(a))
60
+ b = b + (0,) * (length - len(b))
61
+ return a > b
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # PyPI lookup
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ @dataclass
70
+ class VersionCheck:
71
+ current: str
72
+ latest: str | None # None if the lookup failed
73
+ error: str | None = None
74
+
75
+ @property
76
+ def update_available(self) -> bool:
77
+ return bool(self.latest) and is_newer(self.latest, self.current)
78
+
79
+
80
+ def check_latest_version(timeout: float = _TIMEOUT_SECS) -> VersionCheck:
81
+ """
82
+ Query PyPI for the latest published contextzip version.
83
+
84
+ Uses httpx (already a hard dependency) but never lets a network
85
+ problem escape as an exception — callers always get a VersionCheck
86
+ back, with `.error` set on failure.
87
+ """
88
+ try:
89
+ import httpx
90
+
91
+ resp = httpx.get(PYPI_URL, timeout=timeout)
92
+ resp.raise_for_status()
93
+ data = resp.json()
94
+ latest = data.get("info", {}).get("version")
95
+ if not latest:
96
+ return VersionCheck(
97
+ current=installed_version, latest=None, error="Malformed PyPI response"
98
+ )
99
+ return VersionCheck(current=installed_version, latest=latest)
100
+ except Exception as exc: # noqa: BLE001 — any network/parsing failure
101
+ return VersionCheck(
102
+ current=installed_version, latest=None, error=f"{type(exc).__name__}: {exc}"
103
+ )
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Install-method detection
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ def _installed_via_pipx() -> bool:
112
+ """
113
+ Best-effort guess at whether this contextzip was installed with pipx
114
+ rather than plain pip — pipx installs live under a `pipx/venvs/...`
115
+ path, which shows up in the interpreter's own executable path.
116
+ """
117
+ exe = sys.executable.replace("\\", "/").lower()
118
+ return "pipx" in exe
119
+
120
+
121
+ def _installed_via_uv_tool() -> bool:
122
+ exe = sys.executable.replace("\\", "/").lower()
123
+ return "/uv/tools/" in exe or "\\uv\\tools\\" in sys.executable.lower()
124
+
125
+
126
+ @dataclass
127
+ class UpdateResult:
128
+ ok: bool
129
+ method: str
130
+ output: str
131
+
132
+
133
+ def run_update(timeout: float = 120) -> UpdateResult:
134
+ """
135
+ Upgrade the installed contextzip package to the latest PyPI release.
136
+
137
+ Picks the tool that matches how contextzip was installed:
138
+ - pipx, if this interpreter lives inside a pipx venv and the pipx
139
+ executable is on PATH
140
+ - uv tool, if this interpreter lives inside a uv tool install
141
+ - otherwise `python -m pip install --upgrade contextzip`, using
142
+ the *currently running* interpreter's pip — never a bare "pip"
143
+ off PATH, which could silently target the wrong Python.
144
+ """
145
+ if _installed_via_pipx() and shutil.which("pipx"):
146
+ cmd = ["pipx", "upgrade", "contextzip"]
147
+ method = "pipx"
148
+ elif _installed_via_uv_tool() and shutil.which("uv"):
149
+ cmd = ["uv", "tool", "upgrade", "contextzip"]
150
+ method = "uv tool"
151
+ else:
152
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "contextzip"]
153
+ method = "pip"
154
+
155
+ try:
156
+ proc = subprocess.run(
157
+ cmd,
158
+ capture_output=True,
159
+ text=True,
160
+ timeout=timeout,
161
+ )
162
+ output = (proc.stdout or "") + (proc.stderr or "")
163
+ return UpdateResult(ok=proc.returncode == 0, method=method, output=output.strip())
164
+ except Exception as exc: # noqa: BLE001
165
+ return UpdateResult(ok=False, method=method, output=f"{type(exc).__name__}: {exc}")
@@ -1148,6 +1148,17 @@ INDEX_HTML = r"""<!DOCTYPE html>
1148
1148
  document.getElementById("overlay").classList.add("show");
1149
1149
  setTimeout(function () {
1150
1150
  api("/api/shutdown", { method: "GET" }).catch(function () {});
1151
+ // Try to close the tab automatically and hop back to the terminal.
1152
+ // Browsers only allow script-initiated close on windows/tabs that
1153
+ // were opened by a script, or ones whose entire session history is
1154
+ // just this one page — which this config-UI tab always is, since
1155
+ // it's a single page with no navigation. If the browser still
1156
+ // blocks it (some do, regardless), window.close() is a silent
1157
+ // no-op and the overlay's "you can close this tab" text — already
1158
+ // on screen — is the fallback the user follows manually.
1159
+ setTimeout(function () {
1160
+ try { window.close(); } catch (e) { /* ignored — fallback is the overlay text */ }
1161
+ }, 300);
1151
1162
  }, 900);
1152
1163
  }).catch(function () {
1153
1164
  btn.disabled = false;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Intelligently package your codebase for AI tools
5
5
  Author-email: Deepesh <akadeepesh@gmail.com>
6
6
  License-Expression: MIT
@@ -19,6 +19,7 @@ contextzip/packager.py
19
19
  contextzip/project_config.py
20
20
  contextzip/redact.py
21
21
  contextzip/report.py
22
+ contextzip/updater.py
22
23
  contextzip/watcher.py
23
24
  contextzip.egg-info/PKG-INFO
24
25
  contextzip.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "contextzip"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Intelligently package your codebase for AI tools"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes