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.
- {contextzip-0.4.2 → contextzip-0.4.3}/PKG-INFO +1 -1
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/__init__.py +1 -1
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/applier.py +21 -3
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli.py +85 -0
- contextzip-0.4.3/contextzip/updater.py +165 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/assets.py +11 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/PKG-INFO +1 -1
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/SOURCES.txt +1 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/pyproject.toml +1 -1
- {contextzip-0.4.2 → contextzip-0.4.3}/LICENSE +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/README.md +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/ai/selector.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/api.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cleanup.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_ai.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_display.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/clipboard.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/config.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/detector.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/error_parser.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/filters.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/git.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/packager.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/project_config.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/redact.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/report.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/base.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/go.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/node.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/python.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/rules/rust.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/watcher.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/persist.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/server.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.4.2 → contextzip-0.4.3}/setup.cfg +0 -0
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
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;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|