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 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 check_and_install_updates_async
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
- check_and_install_updates_async(
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, we'll show notification at the end
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 check_and_install_updates_async
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
- check_and_install_updates_async(
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
- """Override quit action to show update notification."""
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]:
@@ -1,125 +1,133 @@
1
- """Auto-update functionality for shotgun-sh CLI."""
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 get_cache_file() -> Path:
28
- """Get the path to the update cache file.
13
+ def detect_installation_method() -> str:
14
+ """Detect how shotgun-sh was installed.
29
15
 
30
16
  Returns:
31
- Path to the cache file in the shotgun home directory.
17
+ Installation method: 'pipx', 'pip', 'venv', or 'unknown'.
32
18
  """
33
- return get_shotgun_home() / "check-update.json"
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
- class UpdateCache(BaseModel):
37
- """Model for update check cache data."""
40
+ # Check for user installation
41
+ import site
38
42
 
39
- last_check: datetime = Field(description="Last time update check was performed")
40
- latest_version: str = Field(description="Latest version available on PyPI")
41
- current_version: str = Field(description="Current installed version at check time")
42
- update_available: bool = Field(
43
- default=False, description="Whether an update is available"
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
- Args:
51
- version_str: Version string to check. If None, uses current version.
57
+ def perform_auto_update(no_update_check: bool = False) -> None:
58
+ """Perform automatic update if installed via pipx.
52
59
 
53
- Returns:
54
- True if version contains 'dev', False otherwise.
60
+ Args:
61
+ no_update_check: If True, skip the update.
55
62
  """
56
- check_version = version_str or __version__
57
- return "dev" in check_version.lower()
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
- def load_cache() -> UpdateCache | None:
61
- """Load the update check cache from disk.
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
- Returns:
64
- UpdateCache model if cache exists and is valid, None otherwise.
65
- """
66
- cache_file = get_cache_file()
67
- if not cache_file.exists():
68
- return None
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
- try:
71
- with open(cache_file) as f:
72
- data = json.load(f)
73
- return UpdateCache.model_validate(data)
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 save_cache(cache_data: UpdateCache) -> None:
80
- """Save update check cache to disk.
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
- cache_data: UpdateCache model containing cache data to save.
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
- try:
88
- # Ensure the parent directory exists
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
- with open(cache_file, "w") as f:
92
- json.dump(cache_data.model_dump(mode="json"), f, indent=2, default=str)
93
- except (OSError, PermissionError) as e:
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
- def should_check_for_updates(no_update_check: bool = False) -> bool:
98
- """Determine if we should check for updates.
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
- Args:
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
- # Skip if development version
111
- if is_dev_version():
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
- # Check cache to see if enough time has passed
116
- cache = load_cache()
117
- if not cache:
118
- return True
123
+ Args:
124
+ version_str: Version string to check. If None, uses current version.
119
125
 
120
- now = datetime.now(timezone.utc)
121
- time_since_check = now - cache.last_check
122
- return time_since_check >= UPDATE_CHECK_INTERVAL
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=REQUEST_TIMEOUT) as client:
133
- response = client.get(PYPI_API_URL)
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 the actual update of shotgun-sh.
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 (with confirmation).
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 auto-update development version. Use --force to override."
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
- # Log output for debugging
267
- if result.stdout:
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
- # Only use stderr for error message, stdout often contains normal progress
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
- "UpdateCache",
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
- "detect_installation_method",
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -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=lFx8IsLIfMvOw6lMJGnzg8o9HKcuj59VxhjNgetMZP0,5854
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=AAoFQYw6A9_kVxB0c0p7g-BNdplexfZIQbrGDQ1m4KI,4407
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=7zdNV1GbEKrUrduibDZ6OeUcuyz87Pihr3GKayL5Kxk,23944
127
- shotgun_sh-0.1.6.dist-info/METADATA,sha256=-PbbQuJfK6Wfn-eC2LKft13vdiEpFcT1QMtHPGv0m1w,11191
128
- shotgun_sh-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- shotgun_sh-0.1.6.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
130
- shotgun_sh-0.1.6.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
131
- shotgun_sh-0.1.6.dist-info/RECORD,,
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,,