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

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. 

14 

15"""CLI commands for managing agent updates.""" 

16 

17import logging 

18import os 

19import sys 

20 

21import typer 

22 

23from agent.cli.utils import get_console 

24 

25console = get_console() 

26logger = logging.getLogger(__name__) 

27 

28# Windows file lock error indicator 

29WINDOWS_FILE_LOCK_ERROR = "os error 32" 

30 

31 

32def _get_manual_update_command(installer: str) -> str: 

33 """Get the manual update command for the given installer. 

34 

35 Args: 

36 installer: The package manager ("uv" or "pipx"). 

37 

38 Returns: 

39 The command string to run manually. 

40 """ 

41 from agent.config.constants import PACKAGE_NAME, PACKAGE_REGISTRY_URL 

42 

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 "" 

48 

49 

50def _show_windows_update_instructions(installer: str) -> None: 

51 """Show instructions for updating on Windows. 

52 

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") 

63 

64 

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. 

73 

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 ) 

93 

94 try: 

95 # Get current version 

96 current = get_current_version() 

97 console.print(f"[dim white]Current version: v{current}[/dim white]") 

98 

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) 

108 

109 console.print(f"[dim white]Installer: {installer}[/dim white]") 

110 

111 # Get GitLab token for API access (from env or parameter) 

112 gitlab_token = token or os.environ.get("GITLAB_TOKEN") 

113 

114 # Check latest version 

115 console.print("[dim white]Checking for updates...[/dim white]") 

116 latest = fetch_latest_version(gitlab_token) 

117 

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) 

124 

125 console.print(f"[dim white]Latest version: v{latest}[/dim white]") 

126 

127 # Compare versions 

128 comparison = compare_versions(current, latest) 

129 

130 if comparison >= 0 and not force: 

131 console.print(f"\n[bold green]Already on latest version (v{current})[/bold green]") 

132 

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]") 

138 

139 raise typer.Exit(0) 

140 

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) 

148 

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() 

157 

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]") 

161 

162 success, output = upgrade_package(installer) 

163 

164 if success: 

165 console.print(f"\n[bold green]Successfully updated to v{latest}[/bold green]") 

166 

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]") 

174 

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]") 

183 

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) 

187 

188 raise typer.Exit(1) 

189 

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)