shotgun-sh 0.1.7__tar.gz → 0.1.8.dev1__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/PKG-INFO +1 -1
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/pyproject.toml +1 -1
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/build_constants.py +2 -2
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/main.py +4 -14
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/app.py +3 -13
- shotgun_sh-0.1.8.dev1/src/shotgun/utils/update_checker.py +375 -0
- shotgun_sh-0.1.7/src/shotgun/utils/update_checker.py +0 -692
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/.gitignore +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/LICENSE +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/README.md +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/hatch_build.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/agent_manager.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/common.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/config/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/config/constants.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/config/manager.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/config/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/config/provider.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/conversation_history.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/conversation_manager.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/export.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/compaction.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/constants.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/context_extraction.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/history_building.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/history_processors.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/message_utils.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/token_counting.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/history/token_estimation.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/messages.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/plan.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/research.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/specify.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tasks.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/file_management.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/user_interaction.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/web_search/openai.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/agents/tools/web_search/utils.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/codebase/commands.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/codebase/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/config.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/export.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/plan.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/research.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/specify.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/tasks.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/update.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/cli/utils.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/change_detector.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/code_retrieval.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/cypher_models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/ingestor.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/language_config.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/manager.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/nl_query.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/core/parser_loader.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/codebase/service.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/logging_config.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/posthog_telemetry.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/export.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/plan.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/research.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/specify.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/agents/tasks.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/history/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/history/summarization.j2 +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/prompts/loader.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/py.typed +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sdk/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sdk/codebase.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sdk/exceptions.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sdk/models.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sdk/services.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/sentry_telemetry.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/telemetry.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/commands/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/components/prompt_input.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/components/spinner.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/components/splash.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/components/vertical_tail.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat.tcss +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/chat_screen/history.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/directory_setup.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/provider_config.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/screens/splash.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/styles.tcss +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/utils/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/tui/utils/mode_progress.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/utils/__init__.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/utils/env_utils.py +0 -0
- {shotgun_sh-0.1.7 → shotgun_sh-0.1.8.dev1}/src/shotgun/utils/file_system_utils.py +0 -0
|
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
|
|
|
12
12
|
POSTHOG_PROJECT_ID = '191396'
|
|
13
13
|
|
|
14
14
|
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
-
LOGFIRE_ENABLED = ''
|
|
16
|
-
LOGFIRE_TOKEN = ''
|
|
15
|
+
LOGFIRE_ENABLED = 'true'
|
|
16
|
+
LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
|
|
17
17
|
|
|
18
18
|
# Build metadata
|
|
19
19
|
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
|
@@ -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 check_for_updates_async
|
|
26
26
|
|
|
27
27
|
# Load environment variables from .env file
|
|
28
28
|
load_dotenv()
|
|
@@ -52,7 +52,6 @@ logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
|
|
|
52
52
|
|
|
53
53
|
# Global variable to store update notification
|
|
54
54
|
_update_notification: str | None = None
|
|
55
|
-
_update_progress: str | None = None
|
|
56
55
|
|
|
57
56
|
|
|
58
57
|
def _update_callback(notification: str) -> None:
|
|
@@ -61,13 +60,6 @@ def _update_callback(notification: str) -> None:
|
|
|
61
60
|
_update_notification = notification
|
|
62
61
|
|
|
63
62
|
|
|
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
|
-
|
|
71
63
|
app = typer.Typer(
|
|
72
64
|
name="shotgun",
|
|
73
65
|
help="Shotgun - AI-powered CLI tool for research, planning, and task management",
|
|
@@ -129,12 +121,10 @@ def main(
|
|
|
129
121
|
"""Shotgun - AI-powered CLI tool."""
|
|
130
122
|
logger.debug("Starting shotgun CLI application")
|
|
131
123
|
|
|
132
|
-
# Start async update check
|
|
124
|
+
# Start async update check (non-blocking)
|
|
133
125
|
if not ctx.resilient_parsing:
|
|
134
|
-
|
|
135
|
-
callback=_update_callback,
|
|
136
|
-
no_update_check=no_update_check,
|
|
137
|
-
progress_callback=_update_progress_callback,
|
|
126
|
+
check_for_updates_async(
|
|
127
|
+
callback=_update_callback, no_update_check=no_update_check
|
|
138
128
|
)
|
|
139
129
|
|
|
140
130
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
@@ -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 check_for_updates_async
|
|
13
13
|
|
|
14
14
|
from .screens.chat import ChatScreen
|
|
15
15
|
from .screens.directory_setup import DirectorySetupScreen
|
|
@@ -37,26 +37,16 @@ class ShotgunApp(App[None]):
|
|
|
37
37
|
self.no_update_check = no_update_check
|
|
38
38
|
self.continue_session = continue_session
|
|
39
39
|
self.update_notification: str | None = None
|
|
40
|
-
self.update_progress: str | None = None
|
|
41
40
|
|
|
42
|
-
# Start async update check
|
|
41
|
+
# Start async update check
|
|
43
42
|
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
|
-
)
|
|
43
|
+
check_for_updates_async(callback=self._update_callback)
|
|
49
44
|
|
|
50
45
|
def _update_callback(self, notification: str) -> None:
|
|
51
46
|
"""Store update notification to show later."""
|
|
52
47
|
self.update_notification = notification
|
|
53
48
|
logger.debug(f"Update notification received: {notification}")
|
|
54
49
|
|
|
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}")
|
|
59
|
-
|
|
60
50
|
def on_mount(self) -> None:
|
|
61
51
|
self.theme = "gruvbox"
|
|
62
52
|
# Track TUI startup
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Auto-update functionality for shotgun-sh CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from packaging import version
|
|
13
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
14
|
+
|
|
15
|
+
from shotgun import __version__
|
|
16
|
+
from shotgun.logging_config import get_logger
|
|
17
|
+
from shotgun.utils.file_system_utils import get_shotgun_home
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
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
|
+
|
|
27
|
+
def get_cache_file() -> Path:
|
|
28
|
+
"""Get the path to the update cache file.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Path to the cache file in the shotgun home directory.
|
|
32
|
+
"""
|
|
33
|
+
return get_shotgun_home() / "check-update.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UpdateCache(BaseModel):
|
|
37
|
+
"""Model for update check cache data."""
|
|
38
|
+
|
|
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
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_dev_version(version_str: str | None = None) -> bool:
|
|
48
|
+
"""Check if the current or given version is a development version.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
version_str: Version string to check. If None, uses current version.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if version contains 'dev', False otherwise.
|
|
55
|
+
"""
|
|
56
|
+
check_version = version_str or __version__
|
|
57
|
+
return "dev" in check_version.lower()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_cache() -> UpdateCache | None:
|
|
61
|
+
"""Load the update check cache from disk.
|
|
62
|
+
|
|
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
|
|
69
|
+
|
|
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
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_cache(cache_data: UpdateCache) -> None:
|
|
80
|
+
"""Save update check cache to disk.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
cache_data: UpdateCache model containing cache data to save.
|
|
84
|
+
"""
|
|
85
|
+
cache_file = get_cache_file()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Ensure the parent directory exists
|
|
89
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
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}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def should_check_for_updates(no_update_check: bool = False) -> bool:
|
|
98
|
+
"""Determine if we should check for updates.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
no_update_check: If True, skip update checks.
|
|
102
|
+
|
|
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
|
+
|
|
110
|
+
# Skip if development version
|
|
111
|
+
if is_dev_version():
|
|
112
|
+
logger.debug("Skipping update check for development version")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Check cache to see if enough time has passed
|
|
116
|
+
cache = load_cache()
|
|
117
|
+
if not cache:
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
now = datetime.now(timezone.utc)
|
|
121
|
+
time_since_check = now - cache.last_check
|
|
122
|
+
return time_since_check >= UPDATE_CHECK_INTERVAL
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_latest_version() -> str | None:
|
|
126
|
+
"""Fetch the latest version from PyPI.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Latest version string if successful, None otherwise.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
|
|
133
|
+
response = client.get(PYPI_API_URL)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
data = response.json()
|
|
137
|
+
latest = data.get("info", {}).get("version")
|
|
138
|
+
|
|
139
|
+
if latest:
|
|
140
|
+
logger.debug(f"Latest version from PyPI: {latest}")
|
|
141
|
+
return str(latest)
|
|
142
|
+
|
|
143
|
+
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as e:
|
|
144
|
+
logger.debug(f"Failed to fetch latest version: {e}")
|
|
145
|
+
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compare_versions(current: str, latest: str) -> bool:
|
|
150
|
+
"""Compare version strings to determine if update is available.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
current: Current version string.
|
|
154
|
+
latest: Latest available version string.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
True if latest version is newer than current, False otherwise.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
current_v = version.parse(current)
|
|
161
|
+
latest_v = version.parse(latest)
|
|
162
|
+
return latest_v > current_v
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug(f"Error comparing versions: {e}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
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
|
+
def get_update_command(method: str) -> list[str]:
|
|
213
|
+
"""Get the appropriate update command based on installation method.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
method: Installation method ('pipx', 'pip', 'venv', or 'unknown').
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Command list to execute for updating.
|
|
220
|
+
"""
|
|
221
|
+
commands = {
|
|
222
|
+
"pipx": ["pipx", "upgrade", "shotgun-sh"],
|
|
223
|
+
"pip": [sys.executable, "-m", "pip", "install", "--upgrade", "shotgun-sh"],
|
|
224
|
+
"venv": [sys.executable, "-m", "pip", "install", "--upgrade", "shotgun-sh"],
|
|
225
|
+
"unknown": [sys.executable, "-m", "pip", "install", "--upgrade", "shotgun-sh"],
|
|
226
|
+
}
|
|
227
|
+
return commands.get(method, commands["unknown"])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
231
|
+
"""Perform the actual update of shotgun-sh.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
force: If True, update even if it's a dev version (with confirmation).
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Tuple of (success, message).
|
|
238
|
+
"""
|
|
239
|
+
# Check if dev version and not forced
|
|
240
|
+
if is_dev_version() and not force:
|
|
241
|
+
return False, "Cannot auto-update development version. Use --force to override."
|
|
242
|
+
|
|
243
|
+
# Get latest version
|
|
244
|
+
latest = get_latest_version()
|
|
245
|
+
if not latest:
|
|
246
|
+
return False, "Failed to fetch latest version from PyPI"
|
|
247
|
+
|
|
248
|
+
# Check if update is needed
|
|
249
|
+
if not compare_versions(__version__, latest):
|
|
250
|
+
return False, f"Already at latest version ({__version__})"
|
|
251
|
+
|
|
252
|
+
# Detect installation method
|
|
253
|
+
method = detect_installation_method()
|
|
254
|
+
command = get_update_command(method)
|
|
255
|
+
|
|
256
|
+
# Perform update
|
|
257
|
+
try:
|
|
258
|
+
logger.info(f"Updating shotgun-sh using {method}...")
|
|
259
|
+
logger.debug(f"Running command: {' '.join(command)}")
|
|
260
|
+
|
|
261
|
+
result = subprocess.run(command, capture_output=True, text=True, timeout=60) # noqa: S603
|
|
262
|
+
|
|
263
|
+
if result.returncode == 0:
|
|
264
|
+
message = f"Successfully updated from {__version__} to {latest}"
|
|
265
|
+
logger.info(message)
|
|
266
|
+
|
|
267
|
+
# Clear cache to trigger fresh check next time
|
|
268
|
+
cache_file = get_cache_file()
|
|
269
|
+
if cache_file.exists():
|
|
270
|
+
cache_file.unlink()
|
|
271
|
+
|
|
272
|
+
return True, message
|
|
273
|
+
else:
|
|
274
|
+
error_msg = f"Update failed: {result.stderr or result.stdout}"
|
|
275
|
+
logger.error(error_msg)
|
|
276
|
+
return False, error_msg
|
|
277
|
+
|
|
278
|
+
except subprocess.TimeoutExpired:
|
|
279
|
+
return False, "Update command timed out"
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return False, f"Update failed: {e}"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def format_update_notification(current: str, latest: str) -> str:
|
|
285
|
+
"""Format a user-friendly update notification message.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
current: Current version.
|
|
289
|
+
latest: Latest available version.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Formatted notification string.
|
|
293
|
+
"""
|
|
294
|
+
return f"Update available: {current} → {latest}. Run 'shotgun update' to upgrade."
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def check_for_updates_sync(no_update_check: bool = False) -> str | None:
|
|
298
|
+
"""Synchronously check for updates and return notification if available.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
no_update_check: If True, skip update checks.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Update notification string if update available, None otherwise.
|
|
305
|
+
"""
|
|
306
|
+
if not should_check_for_updates(no_update_check):
|
|
307
|
+
# Check cache for existing notification
|
|
308
|
+
cache = load_cache()
|
|
309
|
+
if cache and cache.update_available:
|
|
310
|
+
current = cache.current_version
|
|
311
|
+
latest = cache.latest_version
|
|
312
|
+
if compare_versions(current, latest):
|
|
313
|
+
return format_update_notification(current, latest)
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
latest_version = get_latest_version()
|
|
317
|
+
if not latest_version:
|
|
318
|
+
return None
|
|
319
|
+
latest = latest_version # Type narrowing - we know it's not None here
|
|
320
|
+
|
|
321
|
+
# Update cache
|
|
322
|
+
now = datetime.now(timezone.utc)
|
|
323
|
+
update_available = compare_versions(__version__, latest)
|
|
324
|
+
|
|
325
|
+
cache_data = UpdateCache(
|
|
326
|
+
last_check=now,
|
|
327
|
+
latest_version=latest,
|
|
328
|
+
current_version=__version__,
|
|
329
|
+
update_available=update_available,
|
|
330
|
+
)
|
|
331
|
+
save_cache(cache_data)
|
|
332
|
+
|
|
333
|
+
if update_available:
|
|
334
|
+
return format_update_notification(__version__, latest)
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def check_for_updates_async(
|
|
340
|
+
callback: Callable[[str], None] | None = None, no_update_check: bool = False
|
|
341
|
+
) -> threading.Thread:
|
|
342
|
+
"""Asynchronously check for updates in a background thread.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
callback: Optional callback function to call with notification string.
|
|
346
|
+
no_update_check: If True, skip update checks.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The thread object that was started.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def _check_updates() -> None:
|
|
353
|
+
try:
|
|
354
|
+
notification = check_for_updates_sync(no_update_check)
|
|
355
|
+
if notification and callback:
|
|
356
|
+
callback(notification)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.debug(f"Error in async update check: {e}")
|
|
359
|
+
|
|
360
|
+
thread = threading.Thread(target=_check_updates, daemon=True)
|
|
361
|
+
thread.start()
|
|
362
|
+
return thread
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
__all__ = [
|
|
366
|
+
"UpdateCache",
|
|
367
|
+
"is_dev_version",
|
|
368
|
+
"should_check_for_updates",
|
|
369
|
+
"get_latest_version",
|
|
370
|
+
"detect_installation_method",
|
|
371
|
+
"perform_update",
|
|
372
|
+
"check_for_updates_async",
|
|
373
|
+
"check_for_updates_sync",
|
|
374
|
+
"format_update_notification",
|
|
375
|
+
]
|