Coverage for src / agent / cli / update_commands.py: 85%
81 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
1# Copyright 2025-2026 Microsoft Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""CLI commands for managing agent updates."""
17import logging
18import os
19import sys
21import typer
23from agent.cli.utils import get_console
25console = get_console()
26logger = logging.getLogger(__name__)
28# Windows file lock error indicator
29WINDOWS_FILE_LOCK_ERROR = "os error 32"
32def _get_manual_update_command(installer: str) -> str:
33 """Get the manual update command for the given installer.
35 Args:
36 installer: The package manager ("uv" or "pipx").
38 Returns:
39 The command string to run manually.
40 """
41 from agent.config.constants import PACKAGE_NAME, PACKAGE_REGISTRY_URL
43 if installer == "uv":
44 return f"uv tool upgrade {PACKAGE_NAME} --extra-index-url {PACKAGE_REGISTRY_URL}"
45 elif installer == "pipx":
46 return f'pipx upgrade {PACKAGE_NAME} --pip-args "--extra-index-url {PACKAGE_REGISTRY_URL}"'
47 return ""
50def _show_windows_update_instructions(installer: str) -> None:
51 """Show instructions for updating on Windows.
53 Args:
54 installer: The package manager ("uv" or "pipx").
55 """
56 cmd = _get_manual_update_command(installer)
57 console.print(
58 "\n[bold yellow]Windows Update Instructions[/bold yellow]\n\n"
59 "On Windows, the running executable cannot replace itself.\n"
60 "Please run the following command from a [bold]separate terminal[/bold]:\n"
61 )
62 console.print(f" [bold cyan]{cmd}[/bold cyan]\n")
65def update_agent(
66 check_only: bool = False,
67 force: bool = False,
68 show_changelog: bool = True,
69 skip_skills: bool = False,
70 token: str | None = None,
71) -> None:
72 """Check for and install updates from GitLab Package Registry.
74 Args:
75 check_only: Only check for updates, don't install.
76 force: Force reinstall even if on latest version.
77 show_changelog: Display changelog for new version.
78 skip_skills: Skip updating bundled skills.
79 token: GitLab token for API access.
80 """
81 from agent.config.constants import OSDU_GITLAB_URL
82 from agent.update import (
83 compare_versions,
84 detect_installer,
85 display_changelog,
86 fetch_latest_version,
87 fetch_releases,
88 format_changelog,
89 get_current_version,
90 update_bundled_skills,
91 upgrade_package,
92 )
94 try:
95 # Get current version
96 current = get_current_version()
97 console.print(f"[dim white]Current version: v{current}[/dim white]")
99 # Detect installer
100 installer = detect_installer()
101 if not installer:
102 console.print(
103 "[bold red]Could not detect installation method[/bold red]\n\n"
104 "osdu-agent must be installed via 'uv tool install' or 'pipx install'\n"
105 f"See: {OSDU_GITLAB_URL}/osdu/ui/osdu-agent#quick-setup"
106 )
107 raise typer.Exit(1)
109 console.print(f"[dim white]Installer: {installer}[/dim white]")
111 # Get GitLab token for API access (from env or parameter)
112 gitlab_token = token or os.environ.get("GITLAB_TOKEN")
114 # Check latest version
115 console.print("[dim white]Checking for updates...[/dim white]")
116 latest = fetch_latest_version(gitlab_token)
118 if not latest:
119 console.print(
120 "[bold yellow]Could not check for updates[/bold yellow]\n"
121 "Unable to reach GitLab Package Registry"
122 )
123 raise typer.Exit(1)
125 console.print(f"[dim white]Latest version: v{latest}[/dim white]")
127 # Compare versions
128 comparison = compare_versions(current, latest)
130 if comparison >= 0 and not force:
131 console.print(f"\n[bold green]Already on latest version (v{current})[/bold green]")
133 # Still check for skill updates if not skipping
134 if not skip_skills:
135 skill_success, skill_message = update_bundled_skills()
136 if not skill_success:
137 console.print(f"[yellow]{skill_message}[/yellow]")
139 raise typer.Exit(0)
141 if check_only:
142 if comparison < 0:
143 console.print(
144 f"\n[bold cyan]Update available: v{current} -> v{latest}[/bold cyan]\n"
145 "Run 'osdu-agent update' to install"
146 )
147 raise typer.Exit(0)
149 # Show changelog
150 if show_changelog and comparison < 0:
151 releases = fetch_releases(gitlab_token, limit=10)
152 if releases:
153 changelog = format_changelog(releases, current, latest)
154 console.print()
155 display_changelog(changelog)
156 console.print()
158 # Perform upgrade
159 action = "Reinstalling" if force and comparison >= 0 else "Upgrading"
160 console.print(f"[bold cyan]{action} v{current} -> v{latest}...[/bold cyan]")
162 success, output = upgrade_package(installer)
164 if success:
165 console.print(f"\n[bold green]Successfully updated to v{latest}[/bold green]")
167 # Update bundled skills if not skipping
168 if not skip_skills:
169 skill_success, skill_message = update_bundled_skills()
170 if skill_success:
171 console.print(f"[green]{skill_message}[/green]")
172 else:
173 console.print(f"[yellow]Skill update warning: {skill_message}[/yellow]")
175 console.print(
176 "\n[dim white]Restart your terminal or run 'osdu-agent --version' "
177 "to verify[/dim white]"
178 )
179 else:
180 console.print("\n[bold red]Update failed[/bold red]")
181 if output:
182 console.print(f"[dim white]{output}[/dim white]")
184 # Check for Windows file lock error and show helpful instructions
185 if sys.platform == "win32" and WINDOWS_FILE_LOCK_ERROR in output:
186 _show_windows_update_instructions(installer)
188 raise typer.Exit(1)
190 except KeyboardInterrupt:
191 console.print("\n[bold yellow]Update interrupted by user[/bold yellow]")
192 raise typer.Exit(1)
193 except typer.Exit:
194 raise
195 except Exception as e:
196 console.print(f"\n[bold red]Update failed: {e}[/bold red]")
197 raise typer.Exit(1)