shotgun-sh 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/main.py +3 -42
- shotgun/tui/app.py +3 -25
- shotgun/utils/update_checker.py +104 -549
- {shotgun_sh-0.1.6.dist-info → shotgun_sh-0.1.8.dist-info}/METADATA +1 -1
- {shotgun_sh-0.1.6.dist-info → shotgun_sh-0.1.8.dist-info}/RECORD +8 -8
- {shotgun_sh-0.1.6.dist-info → shotgun_sh-0.1.8.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.6.dist-info → shotgun_sh-0.1.8.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.6.dist-info → shotgun_sh-0.1.8.dist-info}/licenses/LICENSE +0 -0
shotgun/main.py
CHANGED
|
@@ -22,7 +22,7 @@ from shotgun.posthog_telemetry import setup_posthog_observability
|
|
|
22
22
|
from shotgun.sentry_telemetry import setup_sentry_observability
|
|
23
23
|
from shotgun.telemetry import setup_logfire_observability
|
|
24
24
|
from shotgun.tui import app as tui_app
|
|
25
|
-
from shotgun.utils.update_checker import
|
|
25
|
+
from shotgun.utils.update_checker import perform_auto_update_async
|
|
26
26
|
|
|
27
27
|
# Load environment variables from .env file
|
|
28
28
|
load_dotenv()
|
|
@@ -50,23 +50,6 @@ logger.debug("Sentry observability enabled: %s", _sentry_enabled)
|
|
|
50
50
|
_posthog_enabled = setup_posthog_observability()
|
|
51
51
|
logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
|
|
52
52
|
|
|
53
|
-
# Global variable to store update notification
|
|
54
|
-
_update_notification: str | None = None
|
|
55
|
-
_update_progress: str | None = None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _update_callback(notification: str) -> None:
|
|
59
|
-
"""Callback to store update notification."""
|
|
60
|
-
global _update_notification
|
|
61
|
-
_update_notification = notification
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _update_progress_callback(progress: str) -> None:
|
|
65
|
-
"""Callback to store update progress."""
|
|
66
|
-
global _update_progress
|
|
67
|
-
_update_progress = progress
|
|
68
|
-
logger.debug(f"Update progress: {progress}")
|
|
69
|
-
|
|
70
53
|
|
|
71
54
|
app = typer.Typer(
|
|
72
55
|
name="shotgun",
|
|
@@ -131,39 +114,17 @@ def main(
|
|
|
131
114
|
|
|
132
115
|
# Start async update check and install (non-blocking)
|
|
133
116
|
if not ctx.resilient_parsing:
|
|
134
|
-
|
|
135
|
-
callback=_update_callback,
|
|
136
|
-
no_update_check=no_update_check,
|
|
137
|
-
progress_callback=_update_progress_callback,
|
|
138
|
-
)
|
|
117
|
+
perform_auto_update_async(no_update_check=no_update_check)
|
|
139
118
|
|
|
140
119
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
141
120
|
logger.debug("Launching shotgun TUI application")
|
|
142
121
|
tui_app.run(no_update_check=no_update_check, continue_session=continue_session)
|
|
143
|
-
|
|
144
|
-
# Show update notification after TUI exits
|
|
145
|
-
if _update_notification:
|
|
146
|
-
from rich.console import Console
|
|
147
|
-
|
|
148
|
-
console = Console()
|
|
149
|
-
console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
|
|
150
|
-
|
|
151
122
|
raise typer.Exit()
|
|
152
123
|
|
|
153
|
-
# For CLI commands,
|
|
154
|
-
# This is handled by registering an atexit handler
|
|
124
|
+
# For CLI commands, register PostHog shutdown handler
|
|
155
125
|
if not ctx.resilient_parsing and ctx.invoked_subcommand is not None:
|
|
156
126
|
import atexit
|
|
157
127
|
|
|
158
|
-
def show_update_notification() -> None:
|
|
159
|
-
if _update_notification:
|
|
160
|
-
from rich.console import Console
|
|
161
|
-
|
|
162
|
-
console = Console()
|
|
163
|
-
console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
|
|
164
|
-
|
|
165
|
-
atexit.register(show_update_notification)
|
|
166
|
-
|
|
167
128
|
# Register PostHog shutdown handler
|
|
168
129
|
def shutdown_posthog() -> None:
|
|
169
130
|
from shotgun.posthog_telemetry import shutdown
|
shotgun/tui/app.py
CHANGED
|
@@ -9,7 +9,7 @@ from shotgun.agents.config import ConfigManager, get_config_manager
|
|
|
9
9
|
from shotgun.logging_config import get_logger
|
|
10
10
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
11
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
-
from shotgun.utils.update_checker import
|
|
12
|
+
from shotgun.utils.update_checker import perform_auto_update_async
|
|
13
13
|
|
|
14
14
|
from .screens.chat import ChatScreen
|
|
15
15
|
from .screens.directory_setup import DirectorySetupScreen
|
|
@@ -36,26 +36,10 @@ class ShotgunApp(App[None]):
|
|
|
36
36
|
self.config_manager: ConfigManager = get_config_manager()
|
|
37
37
|
self.no_update_check = no_update_check
|
|
38
38
|
self.continue_session = continue_session
|
|
39
|
-
self.update_notification: str | None = None
|
|
40
|
-
self.update_progress: str | None = None
|
|
41
39
|
|
|
42
40
|
# Start async update check and install
|
|
43
41
|
if not no_update_check:
|
|
44
|
-
|
|
45
|
-
callback=self._update_callback,
|
|
46
|
-
no_update_check=no_update_check,
|
|
47
|
-
progress_callback=self._update_progress_callback,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def _update_callback(self, notification: str) -> None:
|
|
51
|
-
"""Store update notification to show later."""
|
|
52
|
-
self.update_notification = notification
|
|
53
|
-
logger.debug(f"Update notification received: {notification}")
|
|
54
|
-
|
|
55
|
-
def _update_progress_callback(self, progress: str) -> None:
|
|
56
|
-
"""Store update progress."""
|
|
57
|
-
self.update_progress = progress
|
|
58
|
-
logger.debug(f"Update progress: {progress}")
|
|
42
|
+
perform_auto_update_async(no_update_check=no_update_check)
|
|
59
43
|
|
|
60
44
|
def on_mount(self) -> None:
|
|
61
45
|
self.theme = "gruvbox"
|
|
@@ -98,13 +82,7 @@ class ShotgunApp(App[None]):
|
|
|
98
82
|
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
99
83
|
|
|
100
84
|
async def action_quit(self) -> None:
|
|
101
|
-
"""
|
|
102
|
-
if self.update_notification:
|
|
103
|
-
# Show notification before quitting
|
|
104
|
-
from rich.console import Console
|
|
105
|
-
|
|
106
|
-
console = Console()
|
|
107
|
-
console.print(f"\n[cyan]{self.update_notification}[/cyan]", style="bold")
|
|
85
|
+
"""Quit the application."""
|
|
108
86
|
self.exit()
|
|
109
87
|
|
|
110
88
|
def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
|
shotgun/utils/update_checker.py
CHANGED
|
@@ -1,125 +1,133 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Simple auto-update functionality for shotgun-sh CLI."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import subprocess
|
|
5
4
|
import sys
|
|
6
5
|
import threading
|
|
7
|
-
from collections.abc import Callable
|
|
8
|
-
from datetime import datetime, timedelta, timezone
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
|
|
11
|
-
import httpx
|
|
12
|
-
from packaging import version
|
|
13
|
-
from pydantic import BaseModel, Field, ValidationError
|
|
14
|
-
|
|
15
|
-
from shotgun import __version__
|
|
16
8
|
from shotgun.logging_config import get_logger
|
|
17
|
-
from shotgun.utils.file_system_utils import get_shotgun_home
|
|
18
9
|
|
|
19
10
|
logger = get_logger(__name__)
|
|
20
11
|
|
|
21
|
-
# Configuration constants
|
|
22
|
-
UPDATE_CHECK_INTERVAL = timedelta(hours=24)
|
|
23
|
-
PYPI_API_URL = "https://pypi.org/pypi/shotgun-sh/json"
|
|
24
|
-
REQUEST_TIMEOUT = 5.0 # seconds
|
|
25
|
-
|
|
26
12
|
|
|
27
|
-
def
|
|
28
|
-
"""
|
|
13
|
+
def detect_installation_method() -> str:
|
|
14
|
+
"""Detect how shotgun-sh was installed.
|
|
29
15
|
|
|
30
16
|
Returns:
|
|
31
|
-
|
|
17
|
+
Installation method: 'pipx', 'pip', 'venv', or 'unknown'.
|
|
32
18
|
"""
|
|
33
|
-
|
|
19
|
+
# Check for pipx installation
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["pipx", "list", "--short"], # noqa: S607
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
timeout=5, # noqa: S603
|
|
26
|
+
)
|
|
27
|
+
if "shotgun-sh" in result.stdout:
|
|
28
|
+
logger.debug("Detected pipx installation")
|
|
29
|
+
return "pipx"
|
|
30
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
31
|
+
pass
|
|
34
32
|
|
|
33
|
+
# Check if we're in a virtual environment
|
|
34
|
+
if hasattr(sys, "real_prefix") or (
|
|
35
|
+
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
|
36
|
+
):
|
|
37
|
+
logger.debug("Detected virtual environment installation")
|
|
38
|
+
return "venv"
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
# Check for user installation
|
|
41
|
+
import site
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
user_site = site.getusersitepackages()
|
|
44
|
+
if user_site and Path(user_site).exists():
|
|
45
|
+
shotgun_path = Path(user_site) / "shotgun"
|
|
46
|
+
if shotgun_path.exists() or any(
|
|
47
|
+
p.exists() for p in Path(user_site).glob("shotgun_sh*")
|
|
48
|
+
):
|
|
49
|
+
logger.debug("Detected pip --user installation")
|
|
50
|
+
return "pip"
|
|
45
51
|
|
|
52
|
+
# Default to pip if we can't determine
|
|
53
|
+
logger.debug("Could not detect installation method, defaulting to pip")
|
|
54
|
+
return "pip"
|
|
46
55
|
|
|
47
|
-
def is_dev_version(version_str: str | None = None) -> bool:
|
|
48
|
-
"""Check if the current or given version is a development version.
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
def perform_auto_update(no_update_check: bool = False) -> None:
|
|
58
|
+
"""Perform automatic update if installed via pipx.
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
Args:
|
|
61
|
+
no_update_check: If True, skip the update.
|
|
55
62
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
if no_update_check:
|
|
64
|
+
return
|
|
58
65
|
|
|
66
|
+
try:
|
|
67
|
+
# Only auto-update for pipx installations
|
|
68
|
+
if detect_installation_method() != "pipx":
|
|
69
|
+
logger.debug("Not a pipx installation, skipping auto-update")
|
|
70
|
+
return
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
# Run pipx upgrade quietly
|
|
73
|
+
logger.debug("Running pipx upgrade shotgun-sh --quiet")
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["pipx", "upgrade", "shotgun-sh", "--quiet"], # noqa: S607, S603
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
timeout=30,
|
|
79
|
+
)
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
if result.returncode == 0:
|
|
82
|
+
# Check if there was an actual update (pipx shows output even with --quiet for actual updates)
|
|
83
|
+
if result.stdout and "upgraded" in result.stdout.lower():
|
|
84
|
+
logger.info("Shotgun-sh has been updated to the latest version")
|
|
85
|
+
else:
|
|
86
|
+
# Only log errors at debug level to not annoy users
|
|
87
|
+
logger.debug(f"Auto-update check failed: {result.stderr or result.stdout}")
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
except (json.JSONDecodeError, OSError, PermissionError, ValidationError) as e:
|
|
75
|
-
logger.debug(f"Failed to load cache: {e}")
|
|
76
|
-
return None
|
|
89
|
+
except subprocess.TimeoutExpired:
|
|
90
|
+
logger.debug("Auto-update timed out")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.debug(f"Auto-update error: {e}")
|
|
77
93
|
|
|
78
94
|
|
|
79
|
-
def
|
|
80
|
-
"""
|
|
95
|
+
def perform_auto_update_async(no_update_check: bool = False) -> threading.Thread:
|
|
96
|
+
"""Run auto-update in a background thread.
|
|
81
97
|
|
|
82
98
|
Args:
|
|
83
|
-
|
|
99
|
+
no_update_check: If True, skip the update.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The thread object that was started.
|
|
84
103
|
"""
|
|
85
|
-
cache_file = get_cache_file()
|
|
86
104
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
def _run_update() -> None:
|
|
106
|
+
perform_auto_update(no_update_check)
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
logger.debug(f"Failed to save cache: {e}")
|
|
108
|
+
thread = threading.Thread(target=_run_update, daemon=True)
|
|
109
|
+
thread.start()
|
|
110
|
+
return thread
|
|
95
111
|
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
# Keep these for backward compatibility with the update CLI command
|
|
114
|
+
import httpx # noqa: E402
|
|
115
|
+
from packaging import version # noqa: E402
|
|
99
116
|
|
|
100
|
-
|
|
101
|
-
no_update_check: If True, skip update checks.
|
|
117
|
+
from shotgun import __version__ # noqa: E402
|
|
102
118
|
|
|
103
|
-
Returns:
|
|
104
|
-
True if update check should be performed, False otherwise.
|
|
105
|
-
"""
|
|
106
|
-
# Skip if explicitly disabled
|
|
107
|
-
if no_update_check:
|
|
108
|
-
return False
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
logger.debug("Skipping update check for development version")
|
|
113
|
-
return False
|
|
120
|
+
def is_dev_version(version_str: str | None = None) -> bool:
|
|
121
|
+
"""Check if the current or given version is a development version.
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if not cache:
|
|
118
|
-
return True
|
|
123
|
+
Args:
|
|
124
|
+
version_str: Version string to check. If None, uses current version.
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
Returns:
|
|
127
|
+
True if version contains 'dev', False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
check_version = version_str or __version__
|
|
130
|
+
return "dev" in check_version.lower()
|
|
123
131
|
|
|
124
132
|
|
|
125
133
|
def get_latest_version() -> str | None:
|
|
@@ -129,20 +137,16 @@ def get_latest_version() -> str | None:
|
|
|
129
137
|
Latest version string if successful, None otherwise.
|
|
130
138
|
"""
|
|
131
139
|
try:
|
|
132
|
-
with httpx.Client(timeout=
|
|
133
|
-
response = client.get(
|
|
140
|
+
with httpx.Client(timeout=5.0) as client:
|
|
141
|
+
response = client.get("https://pypi.org/pypi/shotgun-sh/json")
|
|
134
142
|
response.raise_for_status()
|
|
135
|
-
|
|
136
143
|
data = response.json()
|
|
137
144
|
latest = data.get("info", {}).get("version")
|
|
138
|
-
|
|
139
145
|
if latest:
|
|
140
146
|
logger.debug(f"Latest version from PyPI: {latest}")
|
|
141
147
|
return str(latest)
|
|
142
|
-
|
|
143
|
-
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as e:
|
|
148
|
+
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
144
149
|
logger.debug(f"Failed to fetch latest version: {e}")
|
|
145
|
-
|
|
146
150
|
return None
|
|
147
151
|
|
|
148
152
|
|
|
@@ -165,50 +169,6 @@ def compare_versions(current: str, latest: str) -> bool:
|
|
|
165
169
|
return False
|
|
166
170
|
|
|
167
171
|
|
|
168
|
-
def detect_installation_method() -> str:
|
|
169
|
-
"""Detect how shotgun-sh was installed.
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
Installation method: 'pipx', 'pip', 'venv', or 'unknown'.
|
|
173
|
-
"""
|
|
174
|
-
# Check for pipx installation
|
|
175
|
-
try:
|
|
176
|
-
result = subprocess.run(
|
|
177
|
-
["pipx", "list", "--short"], # noqa: S607
|
|
178
|
-
capture_output=True,
|
|
179
|
-
text=True,
|
|
180
|
-
timeout=30, # noqa: S603
|
|
181
|
-
)
|
|
182
|
-
if "shotgun-sh" in result.stdout:
|
|
183
|
-
logger.debug("Detected pipx installation")
|
|
184
|
-
return "pipx"
|
|
185
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
|
186
|
-
pass
|
|
187
|
-
|
|
188
|
-
# Check if we're in a virtual environment
|
|
189
|
-
if hasattr(sys, "real_prefix") or (
|
|
190
|
-
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
|
191
|
-
):
|
|
192
|
-
logger.debug("Detected virtual environment installation")
|
|
193
|
-
return "venv"
|
|
194
|
-
|
|
195
|
-
# Check for user installation
|
|
196
|
-
import site
|
|
197
|
-
|
|
198
|
-
user_site = site.getusersitepackages()
|
|
199
|
-
if user_site and Path(user_site).exists():
|
|
200
|
-
shotgun_path = Path(user_site) / "shotgun"
|
|
201
|
-
if shotgun_path.exists() or any(
|
|
202
|
-
p.exists() for p in Path(user_site).glob("shotgun_sh*")
|
|
203
|
-
):
|
|
204
|
-
logger.debug("Detected pip --user installation")
|
|
205
|
-
return "pip"
|
|
206
|
-
|
|
207
|
-
# Default to pip if we can't determine
|
|
208
|
-
logger.debug("Could not detect installation method, defaulting to pip")
|
|
209
|
-
return "pip"
|
|
210
|
-
|
|
211
|
-
|
|
212
172
|
def get_update_command(method: str) -> list[str]:
|
|
213
173
|
"""Get the appropriate update command based on installation method.
|
|
214
174
|
|
|
@@ -228,17 +188,17 @@ def get_update_command(method: str) -> list[str]:
|
|
|
228
188
|
|
|
229
189
|
|
|
230
190
|
def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
231
|
-
"""Perform
|
|
191
|
+
"""Perform manual update of shotgun-sh (for CLI command).
|
|
232
192
|
|
|
233
193
|
Args:
|
|
234
|
-
force: If True, update even if it's a dev version
|
|
194
|
+
force: If True, update even if it's a dev version.
|
|
235
195
|
|
|
236
196
|
Returns:
|
|
237
197
|
Tuple of (success, message).
|
|
238
198
|
"""
|
|
239
199
|
# Check if dev version and not forced
|
|
240
200
|
if is_dev_version() and not force:
|
|
241
|
-
return False, "Cannot
|
|
201
|
+
return False, "Cannot update development version. Use --force to override."
|
|
242
202
|
|
|
243
203
|
# Get latest version
|
|
244
204
|
latest = get_latest_version()
|
|
@@ -249,9 +209,6 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
|
249
209
|
if not compare_versions(__version__, latest):
|
|
250
210
|
return False, f"Already at latest version ({__version__})"
|
|
251
211
|
|
|
252
|
-
# Store current version for comparison
|
|
253
|
-
current_version = __version__
|
|
254
|
-
|
|
255
212
|
# Detect installation method
|
|
256
213
|
method = detect_installation_method()
|
|
257
214
|
command = get_update_command(method)
|
|
@@ -263,130 +220,12 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
|
263
220
|
|
|
264
221
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60) # noqa: S603
|
|
265
222
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
logger.debug(f"Update stdout: {result.stdout}")
|
|
269
|
-
if result.stderr:
|
|
270
|
-
logger.debug(f"Update stderr: {result.stderr}")
|
|
271
|
-
logger.debug(f"Update return code: {result.returncode}")
|
|
272
|
-
|
|
273
|
-
# Check for success patterns in output (pipx specific)
|
|
274
|
-
output_combined = (result.stdout or "") + (result.stderr or "")
|
|
275
|
-
|
|
276
|
-
# For pipx, check if it mentions successful upgrade or already at latest
|
|
277
|
-
pipx_success_patterns = [
|
|
278
|
-
"successfully upgraded",
|
|
279
|
-
"is already at latest version",
|
|
280
|
-
f"installed package shotgun-sh {latest}",
|
|
281
|
-
"upgrading shotgun-sh...",
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
pipx_success = method == "pipx" and any(
|
|
285
|
-
pattern.lower() in output_combined.lower()
|
|
286
|
-
for pattern in pipx_success_patterns
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
# Verify actual installation by checking version
|
|
290
|
-
update_successful = False
|
|
291
|
-
|
|
292
|
-
# For pipx with return code 0, trust it succeeded
|
|
293
|
-
if method == "pipx" and result.returncode == 0:
|
|
294
|
-
update_successful = True
|
|
295
|
-
logger.debug("Pipx returned 0, trusting update succeeded")
|
|
296
|
-
elif result.returncode == 0 or pipx_success:
|
|
297
|
-
# Give the system a moment to update the package metadata
|
|
298
|
-
import time
|
|
299
|
-
|
|
300
|
-
time.sleep(1)
|
|
301
|
-
|
|
302
|
-
# Try to verify the installed version
|
|
303
|
-
try:
|
|
304
|
-
# For pipx, we need to check differently
|
|
305
|
-
if method == "pipx":
|
|
306
|
-
# Use pipx list to verify the installed version
|
|
307
|
-
verify_result = subprocess.run(
|
|
308
|
-
["pipx", "list", "--json"], # noqa: S607
|
|
309
|
-
capture_output=True,
|
|
310
|
-
text=True,
|
|
311
|
-
timeout=5, # noqa: S603
|
|
312
|
-
)
|
|
313
|
-
if verify_result.returncode == 0:
|
|
314
|
-
try:
|
|
315
|
-
pipx_data = json.loads(verify_result.stdout)
|
|
316
|
-
venvs = pipx_data.get("venvs", {})
|
|
317
|
-
shotgun_info = venvs.get("shotgun-sh", {})
|
|
318
|
-
metadata = shotgun_info.get("metadata", {})
|
|
319
|
-
main_package = metadata.get("main_package", {})
|
|
320
|
-
installed_version = main_package.get("package_version", "")
|
|
321
|
-
if installed_version == latest:
|
|
322
|
-
update_successful = True
|
|
323
|
-
logger.debug(
|
|
324
|
-
f"Pipx verification successful: version {installed_version}"
|
|
325
|
-
)
|
|
326
|
-
except (json.JSONDecodeError, KeyError) as e:
|
|
327
|
-
logger.debug(
|
|
328
|
-
f"Pipx JSON parsing failed: {e}, trusting patterns"
|
|
329
|
-
)
|
|
330
|
-
update_successful = pipx_success
|
|
331
|
-
else:
|
|
332
|
-
# Fallback to checking with command
|
|
333
|
-
import shutil
|
|
334
|
-
|
|
335
|
-
shotgun_path = shutil.which("shotgun")
|
|
336
|
-
if shotgun_path:
|
|
337
|
-
verify_result = subprocess.run( # noqa: S603
|
|
338
|
-
[shotgun_path, "--version"],
|
|
339
|
-
capture_output=True,
|
|
340
|
-
text=True,
|
|
341
|
-
timeout=5,
|
|
342
|
-
)
|
|
343
|
-
if (
|
|
344
|
-
verify_result.returncode == 0
|
|
345
|
-
and latest in verify_result.stdout
|
|
346
|
-
):
|
|
347
|
-
update_successful = True
|
|
348
|
-
logger.debug(
|
|
349
|
-
f"Version verification successful: {verify_result.stdout.strip()}"
|
|
350
|
-
)
|
|
351
|
-
else:
|
|
352
|
-
update_successful = pipx_success
|
|
353
|
-
else:
|
|
354
|
-
# For pip/venv, check with python module
|
|
355
|
-
verify_result = subprocess.run( # noqa: S603
|
|
356
|
-
[sys.executable, "-m", "shotgun", "--version"],
|
|
357
|
-
capture_output=True,
|
|
358
|
-
text=True,
|
|
359
|
-
timeout=5,
|
|
360
|
-
)
|
|
361
|
-
if verify_result.returncode == 0 and latest in verify_result.stdout:
|
|
362
|
-
update_successful = True
|
|
363
|
-
logger.debug(
|
|
364
|
-
f"Version verification successful: {verify_result.stdout.strip()}"
|
|
365
|
-
)
|
|
366
|
-
except Exception as e:
|
|
367
|
-
logger.debug(f"Version verification failed: {e}")
|
|
368
|
-
# If verification fails but initial command succeeded, trust it
|
|
369
|
-
if not update_successful:
|
|
370
|
-
update_successful = result.returncode == 0 or pipx_success
|
|
371
|
-
|
|
372
|
-
if update_successful:
|
|
373
|
-
message = f"Successfully updated from {current_version} to {latest}"
|
|
223
|
+
if result.returncode == 0:
|
|
224
|
+
message = f"Successfully updated from {__version__} to {latest}"
|
|
374
225
|
logger.info(message)
|
|
375
|
-
|
|
376
|
-
# Clear cache to trigger fresh check next time
|
|
377
|
-
cache_file = get_cache_file()
|
|
378
|
-
if cache_file.exists():
|
|
379
|
-
cache_file.unlink()
|
|
380
|
-
|
|
381
226
|
return True, message
|
|
382
227
|
else:
|
|
383
|
-
|
|
384
|
-
if result.stderr:
|
|
385
|
-
error_msg = f"Update failed: {result.stderr}"
|
|
386
|
-
elif result.returncode != 0:
|
|
387
|
-
error_msg = f"Update failed with exit code {result.returncode}: {result.stdout or 'No output'}"
|
|
388
|
-
else:
|
|
389
|
-
error_msg = "Update verification failed but command may have succeeded"
|
|
228
|
+
error_msg = f"Update failed: {result.stderr or result.stdout}"
|
|
390
229
|
logger.error(error_msg)
|
|
391
230
|
return False, error_msg
|
|
392
231
|
|
|
@@ -396,297 +235,13 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
|
396
235
|
return False, f"Update failed: {e}"
|
|
397
236
|
|
|
398
237
|
|
|
399
|
-
def format_update_notification(current: str, latest: str) -> str:
|
|
400
|
-
"""Format a user-friendly update notification message.
|
|
401
|
-
|
|
402
|
-
Args:
|
|
403
|
-
current: Current version.
|
|
404
|
-
latest: Latest available version.
|
|
405
|
-
|
|
406
|
-
Returns:
|
|
407
|
-
Formatted notification string.
|
|
408
|
-
"""
|
|
409
|
-
return f"Update available: {current} → {latest}. Run 'shotgun update' to upgrade."
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def format_update_status(
|
|
413
|
-
status: str, current: str | None = None, latest: str | None = None
|
|
414
|
-
) -> str:
|
|
415
|
-
"""Format update status messages.
|
|
416
|
-
|
|
417
|
-
Args:
|
|
418
|
-
status: Status type ('installing', 'success', 'failed', 'checking').
|
|
419
|
-
current: Current version (optional).
|
|
420
|
-
latest: Latest version (optional).
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
Formatted status message.
|
|
424
|
-
"""
|
|
425
|
-
if status == "checking":
|
|
426
|
-
return "Checking for updates..."
|
|
427
|
-
elif status == "installing" and current and latest:
|
|
428
|
-
return f"Installing update: {current} → {latest}..."
|
|
429
|
-
elif status == "success" and latest:
|
|
430
|
-
return f"✓ Successfully updated to version {latest}. Restart your terminal to use the new version."
|
|
431
|
-
elif status == "failed":
|
|
432
|
-
return "Update failed. Run 'shotgun update' to try manually."
|
|
433
|
-
else:
|
|
434
|
-
return ""
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def check_for_updates_sync(no_update_check: bool = False) -> str | None:
|
|
438
|
-
"""Synchronously check for updates and return notification if available.
|
|
439
|
-
|
|
440
|
-
Args:
|
|
441
|
-
no_update_check: If True, skip update checks.
|
|
442
|
-
|
|
443
|
-
Returns:
|
|
444
|
-
Update notification string if update available, None otherwise.
|
|
445
|
-
"""
|
|
446
|
-
if not should_check_for_updates(no_update_check):
|
|
447
|
-
# Check cache for existing notification
|
|
448
|
-
cache = load_cache()
|
|
449
|
-
if cache and cache.update_available:
|
|
450
|
-
current = cache.current_version
|
|
451
|
-
latest = cache.latest_version
|
|
452
|
-
if compare_versions(current, latest):
|
|
453
|
-
return format_update_notification(current, latest)
|
|
454
|
-
return None
|
|
455
|
-
|
|
456
|
-
latest_version = get_latest_version()
|
|
457
|
-
if not latest_version:
|
|
458
|
-
return None
|
|
459
|
-
latest = latest_version # Type narrowing - we know it's not None here
|
|
460
|
-
|
|
461
|
-
# Update cache
|
|
462
|
-
now = datetime.now(timezone.utc)
|
|
463
|
-
update_available = compare_versions(__version__, latest)
|
|
464
|
-
|
|
465
|
-
cache_data = UpdateCache(
|
|
466
|
-
last_check=now,
|
|
467
|
-
latest_version=latest,
|
|
468
|
-
current_version=__version__,
|
|
469
|
-
update_available=update_available,
|
|
470
|
-
)
|
|
471
|
-
save_cache(cache_data)
|
|
472
|
-
|
|
473
|
-
if update_available:
|
|
474
|
-
return format_update_notification(__version__, latest)
|
|
475
|
-
|
|
476
|
-
return None
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
def check_and_install_updates_sync(no_update_check: bool = False) -> tuple[str, bool]:
|
|
480
|
-
"""Synchronously check for updates and install if available.
|
|
481
|
-
|
|
482
|
-
Args:
|
|
483
|
-
no_update_check: If True, skip update checks and installation.
|
|
484
|
-
|
|
485
|
-
Returns:
|
|
486
|
-
Tuple of (status message, success boolean).
|
|
487
|
-
"""
|
|
488
|
-
if no_update_check:
|
|
489
|
-
return "", False
|
|
490
|
-
|
|
491
|
-
if not should_check_for_updates(no_update_check):
|
|
492
|
-
return "", False
|
|
493
|
-
|
|
494
|
-
# Skip auto-install for development versions
|
|
495
|
-
if is_dev_version():
|
|
496
|
-
logger.debug("Skipping auto-install for development version")
|
|
497
|
-
return "", False
|
|
498
|
-
|
|
499
|
-
latest_version = get_latest_version()
|
|
500
|
-
if not latest_version:
|
|
501
|
-
return "", False
|
|
502
|
-
latest = latest_version # Type narrowing
|
|
503
|
-
|
|
504
|
-
# Check if update is needed
|
|
505
|
-
if not compare_versions(__version__, latest):
|
|
506
|
-
# Already up to date, update cache
|
|
507
|
-
now = datetime.now(timezone.utc)
|
|
508
|
-
cache_data = UpdateCache(
|
|
509
|
-
last_check=now,
|
|
510
|
-
latest_version=latest,
|
|
511
|
-
current_version=__version__,
|
|
512
|
-
update_available=False,
|
|
513
|
-
)
|
|
514
|
-
save_cache(cache_data)
|
|
515
|
-
return "", False
|
|
516
|
-
|
|
517
|
-
# Perform the update
|
|
518
|
-
logger.info(f"Auto-installing update: {__version__} → {latest}")
|
|
519
|
-
success, message = perform_update(force=False)
|
|
520
|
-
|
|
521
|
-
if success:
|
|
522
|
-
# Clear cache on successful update
|
|
523
|
-
cache_file = get_cache_file()
|
|
524
|
-
if cache_file.exists():
|
|
525
|
-
cache_file.unlink()
|
|
526
|
-
return format_update_status("success", latest=latest), True
|
|
527
|
-
else:
|
|
528
|
-
# Update cache to mark that we tried and failed
|
|
529
|
-
# This prevents repeated attempts within the check interval
|
|
530
|
-
now = datetime.now(timezone.utc)
|
|
531
|
-
cache_data = UpdateCache(
|
|
532
|
-
last_check=now,
|
|
533
|
-
latest_version=latest,
|
|
534
|
-
current_version=__version__,
|
|
535
|
-
update_available=True, # Still available, but we failed to install
|
|
536
|
-
)
|
|
537
|
-
save_cache(cache_data)
|
|
538
|
-
logger.warning(f"Auto-update failed: {message}")
|
|
539
|
-
return format_update_status("failed"), False
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def check_for_updates_async(
|
|
543
|
-
callback: Callable[[str], None] | None = None, no_update_check: bool = False
|
|
544
|
-
) -> threading.Thread:
|
|
545
|
-
"""Asynchronously check for updates in a background thread.
|
|
546
|
-
|
|
547
|
-
Args:
|
|
548
|
-
callback: Optional callback function to call with notification string.
|
|
549
|
-
no_update_check: If True, skip update checks.
|
|
550
|
-
|
|
551
|
-
Returns:
|
|
552
|
-
The thread object that was started.
|
|
553
|
-
"""
|
|
554
|
-
|
|
555
|
-
def _check_updates() -> None:
|
|
556
|
-
try:
|
|
557
|
-
notification = check_for_updates_sync(no_update_check)
|
|
558
|
-
if notification and callback:
|
|
559
|
-
callback(notification)
|
|
560
|
-
except Exception as e:
|
|
561
|
-
logger.debug(f"Error in async update check: {e}")
|
|
562
|
-
|
|
563
|
-
thread = threading.Thread(target=_check_updates, daemon=True)
|
|
564
|
-
thread.start()
|
|
565
|
-
return thread
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
def check_and_install_updates_async(
|
|
569
|
-
callback: Callable[[str], None] | None = None,
|
|
570
|
-
no_update_check: bool = False,
|
|
571
|
-
progress_callback: Callable[[str], None] | None = None,
|
|
572
|
-
) -> threading.Thread:
|
|
573
|
-
"""Asynchronously check for updates and install in a background thread.
|
|
574
|
-
|
|
575
|
-
Args:
|
|
576
|
-
callback: Optional callback function to call with final status message.
|
|
577
|
-
no_update_check: If True, skip update checks and installation.
|
|
578
|
-
progress_callback: Optional callback for progress updates.
|
|
579
|
-
|
|
580
|
-
Returns:
|
|
581
|
-
The thread object that was started.
|
|
582
|
-
"""
|
|
583
|
-
|
|
584
|
-
def _check_and_install() -> None:
|
|
585
|
-
try:
|
|
586
|
-
# Send checking status if progress callback provided
|
|
587
|
-
if progress_callback:
|
|
588
|
-
progress_callback(format_update_status("checking"))
|
|
589
|
-
|
|
590
|
-
# Skip if disabled
|
|
591
|
-
if no_update_check:
|
|
592
|
-
return
|
|
593
|
-
|
|
594
|
-
# Skip for dev versions
|
|
595
|
-
if is_dev_version():
|
|
596
|
-
logger.debug("Skipping auto-install for development version")
|
|
597
|
-
return
|
|
598
|
-
|
|
599
|
-
# Check if we should check for updates
|
|
600
|
-
if not should_check_for_updates(no_update_check):
|
|
601
|
-
# Check cache to see if update is still pending
|
|
602
|
-
cache = load_cache()
|
|
603
|
-
if cache and cache.update_available:
|
|
604
|
-
# We have a pending update from a previous check
|
|
605
|
-
# Don't retry installation automatically to avoid repeated failures
|
|
606
|
-
if callback:
|
|
607
|
-
callback(
|
|
608
|
-
format_update_notification(
|
|
609
|
-
cache.current_version, cache.latest_version
|
|
610
|
-
)
|
|
611
|
-
)
|
|
612
|
-
return
|
|
613
|
-
|
|
614
|
-
# Get latest version
|
|
615
|
-
latest_version = get_latest_version()
|
|
616
|
-
if not latest_version:
|
|
617
|
-
return
|
|
618
|
-
latest = latest_version # Type narrowing
|
|
619
|
-
|
|
620
|
-
# Check if update is needed
|
|
621
|
-
if not compare_versions(__version__, latest):
|
|
622
|
-
# Already up to date, update cache
|
|
623
|
-
now = datetime.now(timezone.utc)
|
|
624
|
-
cache_data = UpdateCache(
|
|
625
|
-
last_check=now,
|
|
626
|
-
latest_version=latest,
|
|
627
|
-
current_version=__version__,
|
|
628
|
-
update_available=False,
|
|
629
|
-
)
|
|
630
|
-
save_cache(cache_data)
|
|
631
|
-
logger.debug(f"Already at latest version ({__version__})")
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
# Send installing status
|
|
635
|
-
if progress_callback:
|
|
636
|
-
progress_callback(
|
|
637
|
-
format_update_status(
|
|
638
|
-
"installing", current=__version__, latest=latest
|
|
639
|
-
)
|
|
640
|
-
)
|
|
641
|
-
|
|
642
|
-
# Perform the update
|
|
643
|
-
logger.info(f"Auto-installing update: {__version__} → {latest}")
|
|
644
|
-
success, message = perform_update(force=False)
|
|
645
|
-
|
|
646
|
-
if success:
|
|
647
|
-
# Clear cache on successful update
|
|
648
|
-
cache_file = get_cache_file()
|
|
649
|
-
if cache_file.exists():
|
|
650
|
-
cache_file.unlink()
|
|
651
|
-
|
|
652
|
-
if callback:
|
|
653
|
-
callback(format_update_status("success", latest=latest))
|
|
654
|
-
else:
|
|
655
|
-
# Update cache to mark that we tried and failed
|
|
656
|
-
now = datetime.now(timezone.utc)
|
|
657
|
-
cache_data = UpdateCache(
|
|
658
|
-
last_check=now,
|
|
659
|
-
latest_version=latest,
|
|
660
|
-
current_version=__version__,
|
|
661
|
-
update_available=True,
|
|
662
|
-
)
|
|
663
|
-
save_cache(cache_data)
|
|
664
|
-
logger.warning(f"Auto-update failed: {message}")
|
|
665
|
-
|
|
666
|
-
if callback:
|
|
667
|
-
callback(format_update_status("failed"))
|
|
668
|
-
|
|
669
|
-
except Exception as e:
|
|
670
|
-
logger.debug(f"Error in async update check and install: {e}")
|
|
671
|
-
if callback:
|
|
672
|
-
callback(format_update_status("failed"))
|
|
673
|
-
|
|
674
|
-
thread = threading.Thread(target=_check_and_install, daemon=True)
|
|
675
|
-
thread.start()
|
|
676
|
-
return thread
|
|
677
|
-
|
|
678
|
-
|
|
679
238
|
__all__ = [
|
|
680
|
-
"
|
|
239
|
+
"detect_installation_method",
|
|
240
|
+
"perform_auto_update",
|
|
241
|
+
"perform_auto_update_async",
|
|
681
242
|
"is_dev_version",
|
|
682
|
-
"should_check_for_updates",
|
|
683
243
|
"get_latest_version",
|
|
684
|
-
"
|
|
244
|
+
"compare_versions",
|
|
245
|
+
"get_update_command",
|
|
685
246
|
"perform_update",
|
|
686
|
-
"check_for_updates_async",
|
|
687
|
-
"check_for_updates_sync",
|
|
688
|
-
"check_and_install_updates_async",
|
|
689
|
-
"check_and_install_updates_sync",
|
|
690
|
-
"format_update_notification",
|
|
691
|
-
"format_update_status",
|
|
692
247
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
shotgun/__init__.py,sha256=P40K0fnIsb7SKcQrFnXZ4aREjpWchVDhvM1HxI4cyIQ,104
|
|
2
2
|
shotgun/build_constants.py,sha256=hDFr6eO0lwN0iCqHQ1A5s0D68txR8sYrTJLGa7tSi0o,654
|
|
3
3
|
shotgun/logging_config.py,sha256=UKenihvgH8OA3W0b8ZFcItYaFJVe9MlsMYlcevyW1HY,7440
|
|
4
|
-
shotgun/main.py,sha256=
|
|
4
|
+
shotgun/main.py,sha256=qteehx2FyqPVvSRNawEMorybNiIrYfgVeqtMaASQcPw,4606
|
|
5
5
|
shotgun/posthog_telemetry.py,sha256=usfaJ8VyqckLIbLgoj2yhuNyDh0VWA5EJPRr7a0dyVs,5054
|
|
6
6
|
shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
shotgun/sentry_telemetry.py,sha256=0W0o810ewFpIcdPsi_q4uKLiaP6zDYRRE5MHpIbQIPo,2954
|
|
@@ -102,7 +102,7 @@ shotgun/sdk/exceptions.py,sha256=qBcQv0v7ZTwP7CMcxZST4GqCsfOWtOUjSzGBo0-heqo,412
|
|
|
102
102
|
shotgun/sdk/models.py,sha256=X9nOTUHH0cdkQW1NfnMEDu-QgK9oUsEISh1Jtwr5Am4,5496
|
|
103
103
|
shotgun/sdk/services.py,sha256=J4PJFSxCQ6--u7rb3Ta-9eYtlYcxcbnzrMP6ThyCnw4,705
|
|
104
104
|
shotgun/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
105
|
-
shotgun/tui/app.py,sha256=
|
|
105
|
+
shotgun/tui/app.py,sha256=rNi1a2vIhu385-DylE1rYgVj3_cok65X_z1lEamrFqo,3445
|
|
106
106
|
shotgun/tui/styles.tcss,sha256=ETyyw1bpMBOqTi5RLcAJUScdPWTvAWEqE9YcT0kVs_E,121
|
|
107
107
|
shotgun/tui/commands/__init__.py,sha256=8D5lvtpqMW5-fF7Bg3oJtUzU75cKOv6aUaHYYszydU8,2518
|
|
108
108
|
shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4UgadMXpbdXr40,2229
|
|
@@ -123,9 +123,9 @@ shotgun/tui/utils/mode_progress.py,sha256=lseRRo7kMWLkBzI3cU5vqJmS2ZcCjyRYf9Zwtv
|
|
|
123
123
|
shotgun/utils/__init__.py,sha256=WinIEp9oL2iMrWaDkXz2QX4nYVPAm8C9aBSKTeEwLtE,198
|
|
124
124
|
shotgun/utils/env_utils.py,sha256=8QK5aw_f_V2AVTleQQlcL0RnD4sPJWXlDG46fsHu0d8,1057
|
|
125
125
|
shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
|
|
126
|
-
shotgun/utils/update_checker.py,sha256=
|
|
127
|
-
shotgun_sh-0.1.
|
|
128
|
-
shotgun_sh-0.1.
|
|
129
|
-
shotgun_sh-0.1.
|
|
130
|
-
shotgun_sh-0.1.
|
|
131
|
-
shotgun_sh-0.1.
|
|
126
|
+
shotgun/utils/update_checker.py,sha256=TorvPRLtTVVNrTdKFZCfhrz9CQfkJZa4Mi9vQgsppvM,7698
|
|
127
|
+
shotgun_sh-0.1.8.dist-info/METADATA,sha256=0GSpmkq0hlHDOWeFIjKI0FVWNP-GTADsgTHXmXQT0sY,11191
|
|
128
|
+
shotgun_sh-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
129
|
+
shotgun_sh-0.1.8.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
|
|
130
|
+
shotgun_sh-0.1.8.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
|
|
131
|
+
shotgun_sh-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|