code-aide 1.7.0__tar.gz → 1.7.1__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.
- {code_aide-1.7.0 → code_aide-1.7.1}/PKG-INFO +1 -1
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/__init__.py +1 -1
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/commands_actions.py +34 -9
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/commands_tools.py +44 -1
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/detection.py +67 -6
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/install.py +14 -2
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/operations.py +90 -23
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/status.py +30 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_commands_actions.py +78 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_commands_tools.py +51 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_detection.py +13 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_install.py +57 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_operations.py +71 -9
- {code_aide-1.7.0 → code_aide-1.7.1}/.github/workflows/ci.yml +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/.github/workflows/publish.yml +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/.gitignore +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/.gitlab-ci.yml +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/.pre-commit-config.yaml +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/AGENTS.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/CLAUDE.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/LICENSE +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/README.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/TODO.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/pyproject.toml +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/__main__.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/config.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/console.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/constants.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/data/tools.json +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/entry.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/prereqs.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/src/code_aide/versions.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_config.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_console.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_constants.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_status.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/tests/test_versions.py +0 -0
- {code_aide-1.7.0 → code_aide-1.7.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-aide
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: Manage AI coding CLI tools (Claude, Copilot, Cursor, Gemini, Amp, Codex)
|
|
5
5
|
Project-URL: Homepage, https://github.com/dajobe/code-aide
|
|
6
6
|
Project-URL: Repository, https://github.com/dajobe/code-aide
|
|
@@ -5,10 +5,19 @@ import sys
|
|
|
5
5
|
from typing import Any, Dict, List
|
|
6
6
|
|
|
7
7
|
from code_aide.constants import TOOLS
|
|
8
|
-
from code_aide.detection import
|
|
8
|
+
from code_aide.detection import (
|
|
9
|
+
detect_install_method,
|
|
10
|
+
get_brew_package_info,
|
|
11
|
+
is_deprecated_install,
|
|
12
|
+
)
|
|
9
13
|
from code_aide.install import install_tool
|
|
10
14
|
from code_aide.console import error, info, success, warning
|
|
11
|
-
from code_aide.operations import
|
|
15
|
+
from code_aide.operations import (
|
|
16
|
+
UpgradeResult,
|
|
17
|
+
remove_tool,
|
|
18
|
+
upgrade_tool,
|
|
19
|
+
validate_tools,
|
|
20
|
+
)
|
|
12
21
|
from code_aide.prereqs import (
|
|
13
22
|
check_path_directories,
|
|
14
23
|
check_prerequisites,
|
|
@@ -131,6 +140,13 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
|
|
131
140
|
latest = config.get("latest_version")
|
|
132
141
|
if not latest:
|
|
133
142
|
continue
|
|
143
|
+
install_info = detect_install_method(name)
|
|
144
|
+
if install_info["method"] in ("brew_formula", "brew_cask"):
|
|
145
|
+
pkg_info = get_brew_package_info(
|
|
146
|
+
install_info["method"], install_info["detail"]
|
|
147
|
+
)
|
|
148
|
+
if pkg_info.get("outdated") is False:
|
|
149
|
+
continue
|
|
134
150
|
status = get_tool_status(name, config)
|
|
135
151
|
if status["version"] and status_version_matches_latest(
|
|
136
152
|
status["version"], latest
|
|
@@ -145,7 +161,8 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
|
|
145
161
|
|
|
146
162
|
validate_tools(tools_to_upgrade)
|
|
147
163
|
|
|
148
|
-
|
|
164
|
+
updated = []
|
|
165
|
+
unchanged = []
|
|
149
166
|
failed = []
|
|
150
167
|
skipped = []
|
|
151
168
|
|
|
@@ -157,8 +174,11 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
|
|
157
174
|
skipped.append(tool)
|
|
158
175
|
continue
|
|
159
176
|
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
result = upgrade_tool(tool)
|
|
178
|
+
if result == UpgradeResult.CHANGED:
|
|
179
|
+
updated.append(tool)
|
|
180
|
+
elif result == UpgradeResult.UNCHANGED:
|
|
181
|
+
unchanged.append(tool)
|
|
162
182
|
else:
|
|
163
183
|
failed.append(tool)
|
|
164
184
|
|
|
@@ -167,8 +187,11 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
|
|
167
187
|
info("Upgrade Summary")
|
|
168
188
|
print("=" * 42)
|
|
169
189
|
|
|
170
|
-
if
|
|
171
|
-
success(f"Successfully
|
|
190
|
+
if updated:
|
|
191
|
+
success(f"Successfully updated: {', '.join(updated)}")
|
|
192
|
+
|
|
193
|
+
if unchanged:
|
|
194
|
+
info(f"No package-manager change: {', '.join(unchanged)}")
|
|
172
195
|
|
|
173
196
|
if skipped:
|
|
174
197
|
warning(f"Skipped (not installed): {', '.join(skipped)}")
|
|
@@ -177,9 +200,11 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
|
|
177
200
|
error(f"Failed to upgrade: {', '.join(failed)}")
|
|
178
201
|
sys.exit(1)
|
|
179
202
|
|
|
180
|
-
if
|
|
203
|
+
if updated:
|
|
181
204
|
success("All upgrades completed successfully!")
|
|
182
|
-
elif
|
|
205
|
+
elif unchanged and not updated and not failed:
|
|
206
|
+
info("No tools changed version during the upgrade attempt")
|
|
207
|
+
elif skipped and not updated:
|
|
183
208
|
info(
|
|
184
209
|
"No tools were upgraded (all were either not installed or already up to date)"
|
|
185
210
|
)
|
|
@@ -9,12 +9,17 @@ from code_aide.constants import Colors, PACKAGE_MANAGERS, TOOLS
|
|
|
9
9
|
from code_aide.detection import (
|
|
10
10
|
format_install_method,
|
|
11
11
|
format_migration_warning,
|
|
12
|
+
get_brew_package_info,
|
|
12
13
|
get_system_package_info,
|
|
13
14
|
detect_install_method,
|
|
14
15
|
)
|
|
15
16
|
from code_aide.console import command_exists, info, warning
|
|
16
17
|
from code_aide.prereqs import detect_package_manager, is_tool_installed
|
|
17
|
-
from code_aide.status import
|
|
18
|
+
from code_aide.status import (
|
|
19
|
+
get_tool_status,
|
|
20
|
+
print_brew_version_status,
|
|
21
|
+
print_system_version_status,
|
|
22
|
+
)
|
|
18
23
|
from code_aide.versions import (
|
|
19
24
|
extract_version_from_string,
|
|
20
25
|
status_version_matches_latest,
|
|
@@ -128,6 +133,44 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
128
133
|
print_system_version_status(
|
|
129
134
|
status["version"], latest_version, pkg_info
|
|
130
135
|
)
|
|
136
|
+
elif install_info["method"] in ("brew_formula", "brew_cask"):
|
|
137
|
+
pkg_info = get_brew_package_info(
|
|
138
|
+
install_info["method"], install_info["detail"]
|
|
139
|
+
)
|
|
140
|
+
if pkg_info.get("available_version"):
|
|
141
|
+
print_brew_version_status(
|
|
142
|
+
status["version"], latest_version, pkg_info
|
|
143
|
+
)
|
|
144
|
+
if pkg_info.get("outdated"):
|
|
145
|
+
outdated_count += 1
|
|
146
|
+
elif latest_version:
|
|
147
|
+
if status_version_matches_latest(
|
|
148
|
+
status["version"], latest_version
|
|
149
|
+
):
|
|
150
|
+
version_annotation = (
|
|
151
|
+
f" Version: {status['version']} "
|
|
152
|
+
f"{Colors.GREEN}(up to date){Colors.NC}"
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
installed_ver = extract_version_from_string(
|
|
156
|
+
status["version"]
|
|
157
|
+
)
|
|
158
|
+
if installed_ver and version_is_newer(
|
|
159
|
+
installed_ver, latest_version
|
|
160
|
+
):
|
|
161
|
+
version_annotation = (
|
|
162
|
+
f" Version: {status['version']} "
|
|
163
|
+
f"{Colors.YELLOW}(newer than configured "
|
|
164
|
+
f"{latest_version}){Colors.NC}"
|
|
165
|
+
)
|
|
166
|
+
config_outdated.append(tool_name)
|
|
167
|
+
else:
|
|
168
|
+
version_annotation = (
|
|
169
|
+
f" Version: {status['version']} "
|
|
170
|
+
f"{Colors.YELLOW}(latest: {latest_version}){Colors.NC}"
|
|
171
|
+
)
|
|
172
|
+
outdated_count += 1
|
|
173
|
+
print(version_annotation)
|
|
131
174
|
elif latest_version:
|
|
132
175
|
if status_version_matches_latest(status["version"], latest_version):
|
|
133
176
|
version_annotation = (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Install-method and system package detection helpers."""
|
|
2
2
|
|
|
3
3
|
import glob as globmod
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
import shutil
|
|
@@ -41,12 +42,6 @@ def detect_install_method(tool_name: str) -> Dict[str, Optional[str]]:
|
|
|
41
42
|
match = re.search(r"/node_modules/((?:@[^/]+/)?[^/]+)", real_path)
|
|
42
43
|
if match:
|
|
43
44
|
npm_package = match.group(1)
|
|
44
|
-
|
|
45
|
-
if command_path.startswith("/opt/homebrew/bin/") or command_path.startswith(
|
|
46
|
-
"/usr/local/bin/"
|
|
47
|
-
):
|
|
48
|
-
return {"method": "brew_npm", "detail": npm_package}
|
|
49
|
-
|
|
50
45
|
return {"method": "npm", "detail": npm_package}
|
|
51
46
|
|
|
52
47
|
system_prefixes = ("/opt/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/")
|
|
@@ -163,6 +158,72 @@ def get_system_package_info(binary_path: str) -> Dict[str, Optional[str]]:
|
|
|
163
158
|
return result
|
|
164
159
|
|
|
165
160
|
|
|
161
|
+
def get_brew_package_info(
|
|
162
|
+
method: str, package_name: Optional[str]
|
|
163
|
+
) -> Dict[str, Optional[str]]:
|
|
164
|
+
"""Get package version info for a Homebrew-managed tool."""
|
|
165
|
+
result: Dict[str, Optional[str]] = {
|
|
166
|
+
"package": package_name,
|
|
167
|
+
"installed_version": None,
|
|
168
|
+
"available_version": None,
|
|
169
|
+
"available_date": None,
|
|
170
|
+
"outdated": None,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if method not in ("brew_formula", "brew_cask") or not package_name:
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
if not command_exists("brew"):
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
command = ["brew", "info", "--json=v2"]
|
|
180
|
+
if method == "brew_cask":
|
|
181
|
+
command.append("--cask")
|
|
182
|
+
command.append(package_name)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
proc = subprocess.run(
|
|
186
|
+
command,
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
timeout=10,
|
|
190
|
+
check=False,
|
|
191
|
+
stdin=subprocess.DEVNULL,
|
|
192
|
+
)
|
|
193
|
+
if proc.returncode != 0 or not proc.stdout.strip():
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
payload = json.loads(proc.stdout)
|
|
197
|
+
if method == "brew_formula":
|
|
198
|
+
formulae = payload.get("formulae", [])
|
|
199
|
+
if not formulae:
|
|
200
|
+
return result
|
|
201
|
+
formula = formulae[0]
|
|
202
|
+
installed = formula.get("installed", [])
|
|
203
|
+
if installed:
|
|
204
|
+
result["installed_version"] = installed[-1].get("version")
|
|
205
|
+
linked_keg = formula.get("linked_keg")
|
|
206
|
+
if linked_keg:
|
|
207
|
+
result["installed_version"] = linked_keg
|
|
208
|
+
result["available_version"] = formula.get("versions", {}).get("stable")
|
|
209
|
+
result["outdated"] = bool(formula.get("outdated"))
|
|
210
|
+
else:
|
|
211
|
+
casks = payload.get("casks", [])
|
|
212
|
+
if not casks:
|
|
213
|
+
return result
|
|
214
|
+
cask = casks[0]
|
|
215
|
+
installed_version = cask.get("installed")
|
|
216
|
+
if isinstance(installed_version, list):
|
|
217
|
+
installed_version = installed_version[0] if installed_version else None
|
|
218
|
+
result["installed_version"] = installed_version
|
|
219
|
+
result["available_version"] = cask.get("version")
|
|
220
|
+
result["outdated"] = bool(cask.get("outdated"))
|
|
221
|
+
except Exception:
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
166
227
|
def format_install_method(method: Optional[str], detail: Optional[str]) -> str:
|
|
167
228
|
"""Format detected local install method for display."""
|
|
168
229
|
if method == "brew_formula":
|
|
@@ -234,7 +234,7 @@ def install_direct_download(
|
|
|
234
234
|
return False
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
def install_tool(tool_name: str, dryrun: bool = False) -> bool:
|
|
237
|
+
def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> bool:
|
|
238
238
|
"""Install a tool based on its configuration."""
|
|
239
239
|
tool_config = TOOLS.get(tool_name)
|
|
240
240
|
if not tool_config:
|
|
@@ -246,13 +246,25 @@ def install_tool(tool_name: str, dryrun: bool = False) -> bool:
|
|
|
246
246
|
else:
|
|
247
247
|
info(f"Installing {tool_config['name']}...")
|
|
248
248
|
|
|
249
|
-
if command_exists(tool_config["command"]):
|
|
249
|
+
if command_exists(tool_config["command"]) and not force:
|
|
250
250
|
tool_path = shutil.which(tool_config["command"])
|
|
251
251
|
if dryrun:
|
|
252
252
|
info(f"{tool_config['command']} already installed at {tool_path}")
|
|
253
253
|
else:
|
|
254
254
|
warning(f"{tool_config['command']} already installed at {tool_path}")
|
|
255
255
|
return True
|
|
256
|
+
if command_exists(tool_config["command"]) and force:
|
|
257
|
+
tool_path = shutil.which(tool_config["command"])
|
|
258
|
+
if dryrun:
|
|
259
|
+
info(
|
|
260
|
+
f"[DRYRUN] Would reinstall {tool_config['command']} despite existing "
|
|
261
|
+
f"binary at {tool_path}"
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
info(
|
|
265
|
+
f"Reinstalling {tool_config['command']} despite existing binary at "
|
|
266
|
+
f"{tool_path}"
|
|
267
|
+
)
|
|
256
268
|
|
|
257
269
|
try:
|
|
258
270
|
install_type = tool_config["install_type"]
|
|
@@ -5,7 +5,8 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
-
from
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Dict, List
|
|
9
10
|
|
|
10
11
|
from code_aide.constants import TOOLS
|
|
11
12
|
from code_aide.detection import (
|
|
@@ -16,10 +17,60 @@ from code_aide.detection import (
|
|
|
16
17
|
from code_aide.install import install_direct_download, install_tool, run_install_script
|
|
17
18
|
from code_aide.console import error, info, run_command, success, warning
|
|
18
19
|
from code_aide.prereqs import is_tool_installed
|
|
20
|
+
from code_aide.status import get_tool_status
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
"""
|
|
23
|
+
class UpgradeResult(Enum):
|
|
24
|
+
"""Possible outcomes from `upgrade_tool()`.
|
|
25
|
+
|
|
26
|
+
Values:
|
|
27
|
+
- `CHANGED`: The upgrade or migration changed the detected install state.
|
|
28
|
+
- `UNCHANGED`: The upgrade command ran, but the detected install state did
|
|
29
|
+
not change.
|
|
30
|
+
- `FAILED`: The upgrade or migration failed.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
CHANGED = "changed"
|
|
34
|
+
UNCHANGED = "unchanged"
|
|
35
|
+
FAILED = "failed"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_upgrade_snapshot(
|
|
39
|
+
tool_name: str, tool_config: Dict[str, str]
|
|
40
|
+
) -> Dict[str, str]:
|
|
41
|
+
"""Capture install method and version before/after a change."""
|
|
42
|
+
install_info = detect_install_method(tool_name)
|
|
43
|
+
status = get_tool_status(tool_name, tool_config)
|
|
44
|
+
return {
|
|
45
|
+
"method": install_info["method"],
|
|
46
|
+
"detail": install_info["detail"],
|
|
47
|
+
"version": status.get("version"),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _upgrade_result_from_snapshots(
|
|
52
|
+
tool_config: Dict[str, str], before: Dict[str, str], after: Dict[str, str]
|
|
53
|
+
) -> UpgradeResult:
|
|
54
|
+
"""Classify whether an upgrade actually changed the installed tool."""
|
|
55
|
+
if before == after:
|
|
56
|
+
version = after.get("version") or "unknown"
|
|
57
|
+
info(
|
|
58
|
+
f"{tool_config['name']} did not change after the upgrade attempt "
|
|
59
|
+
f"(current version: {version})"
|
|
60
|
+
)
|
|
61
|
+
return UpgradeResult.UNCHANGED
|
|
62
|
+
success(f"{tool_config['name']} upgraded successfully")
|
|
63
|
+
return UpgradeResult.CHANGED
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _migrate_install_method(tool_name: str) -> UpgradeResult:
|
|
67
|
+
"""Migrate a tool from a deprecated install method to the configured one.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
- `UpgradeResult.CHANGED` when the tool is successfully migrated.
|
|
71
|
+
- `UpgradeResult.FAILED` when removal, reinstall, or post-check verification
|
|
72
|
+
fails.
|
|
73
|
+
"""
|
|
23
74
|
tool_config = TOOLS[tool_name]
|
|
24
75
|
install_info = detect_install_method(tool_name)
|
|
25
76
|
old_label = format_install_method(install_info["method"], install_info["detail"])
|
|
@@ -36,30 +87,47 @@ def _migrate_install_method(tool_name: str) -> bool:
|
|
|
36
87
|
f"Failed to remove old {old_label} install of {tool_config['name']}. "
|
|
37
88
|
"Migration aborted."
|
|
38
89
|
)
|
|
39
|
-
return
|
|
90
|
+
return UpgradeResult.FAILED
|
|
40
91
|
|
|
41
|
-
if not install_tool(tool_name):
|
|
92
|
+
if not install_tool(tool_name, force=True):
|
|
42
93
|
error(f"Failed to install {tool_config['name']} via {new_label}.")
|
|
43
94
|
error(
|
|
44
95
|
f"The old {old_label} install has been removed. "
|
|
45
96
|
f"To recover, run: code-aide install {tool_name}"
|
|
46
97
|
)
|
|
47
|
-
return
|
|
98
|
+
return UpgradeResult.FAILED
|
|
99
|
+
|
|
100
|
+
after = detect_install_method(tool_name)
|
|
101
|
+
if after["method"] != tool_config["install_type"]:
|
|
102
|
+
detected_label = format_install_method(after["method"], after["detail"])
|
|
103
|
+
error(
|
|
104
|
+
f"Migration did not complete: {tool_config['name']} is still detected as "
|
|
105
|
+
f"{detected_label}."
|
|
106
|
+
)
|
|
107
|
+
return UpgradeResult.FAILED
|
|
48
108
|
|
|
49
109
|
success(f"{tool_config['name']} migrated from {old_label} to {new_label}")
|
|
50
|
-
return
|
|
110
|
+
return UpgradeResult.CHANGED
|
|
111
|
+
|
|
51
112
|
|
|
113
|
+
def upgrade_tool(tool_name: str) -> UpgradeResult:
|
|
114
|
+
"""Upgrade a tool based on its configuration.
|
|
52
115
|
|
|
53
|
-
|
|
54
|
-
|
|
116
|
+
Returns:
|
|
117
|
+
- `UpgradeResult.CHANGED` when the installed tool changed version or install
|
|
118
|
+
method.
|
|
119
|
+
- `UpgradeResult.UNCHANGED` when the upgrade command ran but the detected
|
|
120
|
+
install state did not change.
|
|
121
|
+
- `UpgradeResult.FAILED` when the upgrade could not be completed.
|
|
122
|
+
"""
|
|
55
123
|
tool_config = TOOLS.get(tool_name)
|
|
56
124
|
if not tool_config:
|
|
57
125
|
error(f"Unknown tool: {tool_name}")
|
|
58
|
-
return
|
|
126
|
+
return UpgradeResult.FAILED
|
|
59
127
|
|
|
60
128
|
if not is_tool_installed(tool_name):
|
|
61
129
|
warning(f"{tool_config['name']} is not installed. Use 'install' command first.")
|
|
62
|
-
return
|
|
130
|
+
return UpgradeResult.FAILED
|
|
63
131
|
|
|
64
132
|
if is_deprecated_install(tool_name):
|
|
65
133
|
return _migrate_install_method(tool_name)
|
|
@@ -67,62 +135,61 @@ def upgrade_tool(tool_name: str) -> bool:
|
|
|
67
135
|
install_info = detect_install_method(tool_name)
|
|
68
136
|
method = install_info["method"]
|
|
69
137
|
detail = install_info["detail"]
|
|
138
|
+
before = _get_upgrade_snapshot(tool_name, tool_config)
|
|
70
139
|
|
|
71
140
|
info(f"Upgrading {tool_config['name']} (installed via {method})...")
|
|
72
141
|
|
|
73
142
|
try:
|
|
74
143
|
if method == "brew_formula":
|
|
75
144
|
run_command(["brew", "upgrade", detail], check=True, capture=False)
|
|
76
|
-
success(f"{tool_config['name']} upgraded successfully")
|
|
77
145
|
|
|
78
146
|
elif method == "brew_cask":
|
|
79
147
|
run_command(
|
|
80
148
|
["brew", "upgrade", "--cask", detail], check=True, capture=False
|
|
81
149
|
)
|
|
82
|
-
success(f"{tool_config['name']} upgraded successfully")
|
|
83
150
|
|
|
84
151
|
elif method in ("npm", "brew_npm"):
|
|
85
152
|
npm_package = detail or tool_config.get("npm_package")
|
|
86
153
|
if not npm_package:
|
|
87
154
|
error(f"No npm package configured for {tool_config['name']}")
|
|
88
|
-
return
|
|
155
|
+
return UpgradeResult.FAILED
|
|
89
156
|
run_command(["npm", "install", "-g", f"{npm_package}@latest"], check=True)
|
|
90
|
-
success(f"{tool_config['name']} upgraded successfully")
|
|
91
157
|
|
|
92
158
|
elif method == "script":
|
|
93
159
|
install_url = tool_config["install_url"]
|
|
94
160
|
expected_sha256 = tool_config.get("install_sha256")
|
|
95
161
|
if run_install_script(install_url, tool_config["name"], expected_sha256):
|
|
96
|
-
|
|
162
|
+
pass
|
|
97
163
|
else:
|
|
98
|
-
return
|
|
164
|
+
return UpgradeResult.FAILED
|
|
99
165
|
|
|
100
166
|
elif method == "direct_download":
|
|
101
167
|
if not install_direct_download(tool_name, tool_config):
|
|
102
|
-
return
|
|
168
|
+
return UpgradeResult.FAILED
|
|
103
169
|
|
|
104
170
|
elif method == "system":
|
|
105
171
|
error(
|
|
106
172
|
f"{tool_config['name']} is managed by the system package manager. "
|
|
107
173
|
"Use your package manager to upgrade it."
|
|
108
174
|
)
|
|
109
|
-
return
|
|
175
|
+
return UpgradeResult.FAILED
|
|
110
176
|
|
|
111
177
|
else:
|
|
112
178
|
error(
|
|
113
179
|
f"Don't know how to upgrade {tool_config['name']} "
|
|
114
180
|
f"(install method: {method})"
|
|
115
181
|
)
|
|
116
|
-
return
|
|
182
|
+
return UpgradeResult.FAILED
|
|
117
183
|
|
|
118
|
-
|
|
184
|
+
after = _get_upgrade_snapshot(tool_name, tool_config)
|
|
185
|
+
return _upgrade_result_from_snapshots(tool_config, before, after)
|
|
119
186
|
|
|
120
187
|
except subprocess.CalledProcessError as exc:
|
|
121
188
|
error(f"Failed to upgrade {tool_config['name']}: {exc.stderr}")
|
|
122
|
-
return
|
|
189
|
+
return UpgradeResult.FAILED
|
|
123
190
|
except Exception as exc:
|
|
124
191
|
error(f"Failed to upgrade {tool_config['name']}: {exc}")
|
|
125
|
-
return
|
|
192
|
+
return UpgradeResult.FAILED
|
|
126
193
|
|
|
127
194
|
|
|
128
195
|
def remove_tool(tool_name: str) -> bool:
|
|
@@ -51,6 +51,36 @@ def print_system_version_status(
|
|
|
51
51
|
print(f" Packaged: {avail_ver} ({pkg_name}{date_suffix})")
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def print_brew_version_status(
|
|
55
|
+
cli_version: str,
|
|
56
|
+
latest_version: Optional[str],
|
|
57
|
+
pkg_info: Dict[str, Optional[str]],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Print version status for a Homebrew-managed tool."""
|
|
60
|
+
avail_ver = pkg_info.get("available_version")
|
|
61
|
+
outdated = pkg_info.get("outdated")
|
|
62
|
+
|
|
63
|
+
if outdated:
|
|
64
|
+
print(
|
|
65
|
+
f" Version: {cli_version} {Colors.YELLOW}(Homebrew has {avail_ver})"
|
|
66
|
+
f"{Colors.NC}"
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
print(f" Version: {cli_version} {Colors.GREEN}(up to date){Colors.NC}")
|
|
70
|
+
|
|
71
|
+
if avail_ver:
|
|
72
|
+
pkg_name = pkg_info.get("package") or "Homebrew"
|
|
73
|
+
if latest_version and not status_version_matches_latest(
|
|
74
|
+
avail_ver, latest_version
|
|
75
|
+
):
|
|
76
|
+
print(
|
|
77
|
+
f" Packaged: {avail_ver} ({pkg_name}) "
|
|
78
|
+
f"{Colors.YELLOW}(upstream: {latest_version}){Colors.NC}"
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
print(f" Packaged: {avail_ver} ({pkg_name})")
|
|
82
|
+
|
|
83
|
+
|
|
54
84
|
def get_tool_status(tool_name: str, tool_config: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
85
|
"""Get status information for a specific tool."""
|
|
56
86
|
status_info = {
|
|
@@ -8,6 +8,7 @@ from unittest import mock
|
|
|
8
8
|
|
|
9
9
|
from code_aide import commands_actions
|
|
10
10
|
from code_aide import entry
|
|
11
|
+
from code_aide.operations import UpgradeResult
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class TestCmdInstall(unittest.TestCase):
|
|
@@ -136,3 +137,80 @@ class TestUpgradeNoArgsParsing(unittest.TestCase):
|
|
|
136
137
|
(args,) = mock_upgrade.call_args[0]
|
|
137
138
|
self.assertEqual(args.command, "upgrade")
|
|
138
139
|
self.assertEqual(args.tools, [])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestCmdUpgrade(unittest.TestCase):
|
|
143
|
+
"""Tests for cmd_upgrade output handling."""
|
|
144
|
+
|
|
145
|
+
def test_unchanged_upgrades_are_not_reported_as_updated(self):
|
|
146
|
+
tools = {
|
|
147
|
+
"test": {
|
|
148
|
+
"name": "Test Tool",
|
|
149
|
+
"command": "test",
|
|
150
|
+
"latest_version": "2.0.0",
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
args = type("Args", (), {"tools": ["test"]})()
|
|
154
|
+
|
|
155
|
+
with (
|
|
156
|
+
mock.patch.dict(commands_actions.TOOLS, tools, clear=True),
|
|
157
|
+
mock.patch.object(commands_actions, "validate_tools"),
|
|
158
|
+
mock.patch.object(commands_actions, "is_tool_installed", return_value=True),
|
|
159
|
+
mock.patch.object(
|
|
160
|
+
commands_actions,
|
|
161
|
+
"upgrade_tool",
|
|
162
|
+
return_value=UpgradeResult.UNCHANGED,
|
|
163
|
+
),
|
|
164
|
+
):
|
|
165
|
+
buf = io.StringIO()
|
|
166
|
+
with contextlib.redirect_stdout(buf):
|
|
167
|
+
commands_actions.cmd_upgrade(args)
|
|
168
|
+
|
|
169
|
+
output = buf.getvalue()
|
|
170
|
+
self.assertIn("No package-manager change: test", output)
|
|
171
|
+
self.assertNotIn("Successfully updated: test", output)
|
|
172
|
+
|
|
173
|
+
def test_default_upgrade_skips_brew_tool_when_homebrew_not_outdated(self):
|
|
174
|
+
tools = {
|
|
175
|
+
"brewtool": {
|
|
176
|
+
"name": "Brew Tool",
|
|
177
|
+
"command": "brewtool",
|
|
178
|
+
"latest_version": "9.9.9",
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
args = type("Args", (), {"tools": []})()
|
|
182
|
+
|
|
183
|
+
with (
|
|
184
|
+
mock.patch.dict(commands_actions.TOOLS, tools, clear=True),
|
|
185
|
+
mock.patch.object(commands_actions, "validate_tools"),
|
|
186
|
+
mock.patch.object(commands_actions, "is_tool_installed", return_value=True),
|
|
187
|
+
mock.patch.object(
|
|
188
|
+
commands_actions, "is_deprecated_install", return_value=False
|
|
189
|
+
),
|
|
190
|
+
mock.patch.object(
|
|
191
|
+
commands_actions,
|
|
192
|
+
"detect_install_method",
|
|
193
|
+
return_value={"method": "brew_formula", "detail": "brewtool"},
|
|
194
|
+
),
|
|
195
|
+
mock.patch.object(
|
|
196
|
+
commands_actions,
|
|
197
|
+
"get_brew_package_info",
|
|
198
|
+
return_value={
|
|
199
|
+
"package": "brewtool",
|
|
200
|
+
"installed_version": "1.0.0",
|
|
201
|
+
"available_version": "1.0.0",
|
|
202
|
+
"available_date": None,
|
|
203
|
+
"outdated": False,
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
mock.patch.object(commands_actions, "get_tool_status") as mock_status,
|
|
207
|
+
mock.patch.object(commands_actions, "upgrade_tool") as mock_upgrade,
|
|
208
|
+
):
|
|
209
|
+
buf = io.StringIO()
|
|
210
|
+
with contextlib.redirect_stdout(buf):
|
|
211
|
+
commands_actions.cmd_upgrade(args)
|
|
212
|
+
|
|
213
|
+
output = buf.getvalue()
|
|
214
|
+
self.assertIn("All installed tools are up to date", output)
|
|
215
|
+
mock_status.assert_not_called()
|
|
216
|
+
mock_upgrade.assert_not_called()
|
|
@@ -80,6 +80,57 @@ class TestCmdStatus(unittest.TestCase):
|
|
|
80
80
|
self.assertIn("Example Tool", output)
|
|
81
81
|
self.assertIn("tool(s) can be upgraded", output)
|
|
82
82
|
|
|
83
|
+
def test_brew_current_upstream_lag_does_not_count_as_upgradeable(self):
|
|
84
|
+
tools = {
|
|
85
|
+
"x": {
|
|
86
|
+
"name": "Example Tool",
|
|
87
|
+
"command": "example",
|
|
88
|
+
"install_type": "npm",
|
|
89
|
+
"latest_version": "2.0.0",
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
status = {
|
|
93
|
+
"installed": True,
|
|
94
|
+
"version": "1.0.0",
|
|
95
|
+
"user": None,
|
|
96
|
+
"usage": None,
|
|
97
|
+
"errors": [],
|
|
98
|
+
}
|
|
99
|
+
args = type("Args", (), {})()
|
|
100
|
+
with (
|
|
101
|
+
mock.patch.dict(commands_tools.TOOLS, tools, clear=True),
|
|
102
|
+
mock.patch.object(commands_tools, "get_tool_status", return_value=status),
|
|
103
|
+
mock.patch.object(
|
|
104
|
+
commands_tools.shutil, "which", return_value="/opt/homebrew/bin/example"
|
|
105
|
+
),
|
|
106
|
+
mock.patch.object(
|
|
107
|
+
commands_tools,
|
|
108
|
+
"detect_install_method",
|
|
109
|
+
return_value={"method": "brew_formula", "detail": "example"},
|
|
110
|
+
),
|
|
111
|
+
mock.patch.object(
|
|
112
|
+
commands_tools,
|
|
113
|
+
"get_brew_package_info",
|
|
114
|
+
return_value={
|
|
115
|
+
"package": "example",
|
|
116
|
+
"installed_version": "1.0.0",
|
|
117
|
+
"available_version": "1.0.0",
|
|
118
|
+
"available_date": None,
|
|
119
|
+
"outdated": False,
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
mock.patch.object(
|
|
123
|
+
commands_tools, "format_migration_warning", return_value=None
|
|
124
|
+
),
|
|
125
|
+
):
|
|
126
|
+
buf = io.StringIO()
|
|
127
|
+
with contextlib.redirect_stdout(buf):
|
|
128
|
+
commands_tools.cmd_status(args)
|
|
129
|
+
output = buf.getvalue()
|
|
130
|
+
self.assertIn("Packaged: 1.0.0 (example)", output)
|
|
131
|
+
self.assertIn("upstream: 2.0.0", output)
|
|
132
|
+
self.assertNotIn("tool(s) can be upgraded", output)
|
|
133
|
+
|
|
83
134
|
def test_status_shows_migration_warning(self):
|
|
84
135
|
"""cmd_status shows migration warning for deprecated install."""
|
|
85
136
|
tools = {
|
|
@@ -57,6 +57,19 @@ class TestDetectInstallMethod(unittest.TestCase):
|
|
|
57
57
|
{"method": "npm", "detail": "@google/gemini-cli"},
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
@mock.patch.object(cli_detection.os.path, "realpath")
|
|
61
|
+
@mock.patch.object(cli_detection.shutil, "which")
|
|
62
|
+
def test_detects_npm_under_homebrew_prefix_as_npm(self, mock_which, mock_realpath):
|
|
63
|
+
mock_which.return_value = "/opt/homebrew/bin/copilot"
|
|
64
|
+
mock_realpath.return_value = (
|
|
65
|
+
"/opt/homebrew/lib/node_modules/@github/copilot/bin/copilot.js"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
cli_detection.detect_install_method("copilot"),
|
|
70
|
+
{"method": "npm", "detail": "@github/copilot"},
|
|
71
|
+
)
|
|
72
|
+
|
|
60
73
|
@mock.patch.object(cli_detection.os.path, "realpath")
|
|
61
74
|
@mock.patch.object(cli_detection.shutil, "which")
|
|
62
75
|
def test_detects_system_package_opt(self, mock_which, mock_realpath):
|
|
@@ -176,3 +176,60 @@ class TestInstallDirectDownload(unittest.TestCase):
|
|
|
176
176
|
self.assertTrue(os.path.isdir(os.path.dirname(install_dir)))
|
|
177
177
|
self.assertTrue(os.path.exists(os.path.join(install_dir, "test-bin")))
|
|
178
178
|
self.assertTrue(os.path.islink(os.path.join(bin_dir, "test")))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestInstallTool(unittest.TestCase):
|
|
182
|
+
"""Tests for install_tool behavior."""
|
|
183
|
+
|
|
184
|
+
def test_force_reinstalls_even_when_binary_exists(self):
|
|
185
|
+
tool_config = {
|
|
186
|
+
"name": "Test Tool",
|
|
187
|
+
"command": "test-tool",
|
|
188
|
+
"install_type": "npm",
|
|
189
|
+
"npm_package": "test-tool",
|
|
190
|
+
"next_steps": "Run test-tool",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
with (
|
|
194
|
+
mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True),
|
|
195
|
+
mock.patch.object(cli_install, "command_exists", return_value=True),
|
|
196
|
+
mock.patch.object(
|
|
197
|
+
cli_install.shutil, "which", return_value="/usr/local/bin/test-tool"
|
|
198
|
+
),
|
|
199
|
+
mock.patch.object(cli_install, "run_command") as mock_run,
|
|
200
|
+
):
|
|
201
|
+
result = cli_install.install_tool("test", force=True)
|
|
202
|
+
|
|
203
|
+
self.assertTrue(result)
|
|
204
|
+
mock_run.assert_called_once_with(
|
|
205
|
+
["npm", "install", "-g", "test-tool"], check=True
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def test_force_dryrun_reports_reinstall_when_binary_exists(self):
|
|
209
|
+
tool_config = {
|
|
210
|
+
"name": "Test Tool",
|
|
211
|
+
"command": "test-tool",
|
|
212
|
+
"install_type": "npm",
|
|
213
|
+
"npm_package": "test-tool",
|
|
214
|
+
"next_steps": "Run test-tool",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
with (
|
|
218
|
+
mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True),
|
|
219
|
+
mock.patch.object(cli_install, "command_exists", return_value=True),
|
|
220
|
+
mock.patch.object(
|
|
221
|
+
cli_install.shutil, "which", return_value="/usr/local/bin/test-tool"
|
|
222
|
+
),
|
|
223
|
+
mock.patch.object(cli_install, "info") as mock_info,
|
|
224
|
+
mock.patch.object(cli_install, "run_command") as mock_run,
|
|
225
|
+
):
|
|
226
|
+
result = cli_install.install_tool("test", dryrun=True, force=True)
|
|
227
|
+
|
|
228
|
+
self.assertTrue(result)
|
|
229
|
+
mock_run.assert_not_called()
|
|
230
|
+
mock_info.assert_any_call("[DRYRUN] Checking Test Tool...")
|
|
231
|
+
mock_info.assert_any_call(
|
|
232
|
+
"[DRYRUN] Would reinstall test-tool despite existing binary at "
|
|
233
|
+
"/usr/local/bin/test-tool"
|
|
234
|
+
)
|
|
235
|
+
mock_info.assert_any_call("[DRYRUN] Would install npm package: test-tool")
|
|
@@ -7,6 +7,7 @@ from unittest import mock
|
|
|
7
7
|
|
|
8
8
|
from code_aide import commands_actions as cli_commands_actions
|
|
9
9
|
from code_aide import operations as cli_operations
|
|
10
|
+
from code_aide.operations import UpgradeResult
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TestRemoveToolDirectDownload(unittest.TestCase):
|
|
@@ -137,13 +138,16 @@ class TestMigrateInstallMethod(unittest.TestCase):
|
|
|
137
138
|
mock.patch.object(
|
|
138
139
|
cli_operations,
|
|
139
140
|
"detect_install_method",
|
|
140
|
-
|
|
141
|
+
side_effect=[
|
|
142
|
+
{"method": "npm", "detail": "test-pkg"},
|
|
143
|
+
{"method": "script", "detail": None},
|
|
144
|
+
],
|
|
141
145
|
),
|
|
142
146
|
):
|
|
143
147
|
result = cli_operations.upgrade_tool("test")
|
|
144
|
-
self.
|
|
148
|
+
self.assertEqual(result, UpgradeResult.CHANGED)
|
|
145
149
|
mock_remove.assert_called_once_with("test")
|
|
146
|
-
mock_install.assert_called_once_with("test")
|
|
150
|
+
mock_install.assert_called_once_with("test", force=True)
|
|
147
151
|
|
|
148
152
|
def test_upgrade_normal_when_not_deprecated(self):
|
|
149
153
|
"""upgrade_tool does normal upgrade when not deprecated."""
|
|
@@ -163,14 +167,33 @@ class TestMigrateInstallMethod(unittest.TestCase):
|
|
|
163
167
|
mock.patch.object(
|
|
164
168
|
cli_operations,
|
|
165
169
|
"detect_install_method",
|
|
166
|
-
|
|
170
|
+
side_effect=[
|
|
171
|
+
{"method": "script", "detail": "native installer"},
|
|
172
|
+
{"method": "script", "detail": "native installer"},
|
|
173
|
+
],
|
|
167
174
|
),
|
|
168
175
|
mock.patch.object(
|
|
169
176
|
cli_operations, "run_install_script", return_value=True
|
|
170
177
|
) as mock_script,
|
|
178
|
+
mock.patch.object(
|
|
179
|
+
cli_operations,
|
|
180
|
+
"_get_upgrade_snapshot",
|
|
181
|
+
side_effect=[
|
|
182
|
+
{
|
|
183
|
+
"method": "script",
|
|
184
|
+
"detail": "native installer",
|
|
185
|
+
"version": "1.0.0",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"method": "script",
|
|
189
|
+
"detail": "native installer",
|
|
190
|
+
"version": "2.0.0",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
),
|
|
171
194
|
):
|
|
172
195
|
result = cli_operations.upgrade_tool("test")
|
|
173
|
-
self.
|
|
196
|
+
self.assertEqual(result, UpgradeResult.CHANGED)
|
|
174
197
|
mock_script.assert_called_once()
|
|
175
198
|
|
|
176
199
|
def test_migration_fails_on_remove(self):
|
|
@@ -197,7 +220,7 @@ class TestMigrateInstallMethod(unittest.TestCase):
|
|
|
197
220
|
),
|
|
198
221
|
):
|
|
199
222
|
result = cli_operations.upgrade_tool("test")
|
|
200
|
-
self.
|
|
223
|
+
self.assertEqual(result, UpgradeResult.FAILED)
|
|
201
224
|
mock_install.assert_not_called()
|
|
202
225
|
|
|
203
226
|
def test_migration_fails_on_install(self):
|
|
@@ -222,7 +245,42 @@ class TestMigrateInstallMethod(unittest.TestCase):
|
|
|
222
245
|
),
|
|
223
246
|
):
|
|
224
247
|
result = cli_operations.upgrade_tool("test")
|
|
225
|
-
self.
|
|
248
|
+
self.assertEqual(result, UpgradeResult.FAILED)
|
|
249
|
+
|
|
250
|
+
def test_upgrade_reports_unchanged_when_version_does_not_change(self):
|
|
251
|
+
"""Upgrade result is unchanged when package manager leaves version as-is."""
|
|
252
|
+
tool_config = {
|
|
253
|
+
"name": "Test Tool",
|
|
254
|
+
"command": "test-tool",
|
|
255
|
+
"install_type": "npm",
|
|
256
|
+
"npm_package": "test-tool",
|
|
257
|
+
}
|
|
258
|
+
with (
|
|
259
|
+
mock.patch.dict(cli_operations.TOOLS, {"test": tool_config}),
|
|
260
|
+
mock.patch.object(cli_operations, "is_tool_installed", return_value=True),
|
|
261
|
+
mock.patch.object(
|
|
262
|
+
cli_operations, "is_deprecated_install", return_value=False
|
|
263
|
+
),
|
|
264
|
+
mock.patch.object(
|
|
265
|
+
cli_operations,
|
|
266
|
+
"detect_install_method",
|
|
267
|
+
return_value={"method": "npm", "detail": "test-tool"},
|
|
268
|
+
),
|
|
269
|
+
mock.patch.object(cli_operations, "run_command") as mock_run,
|
|
270
|
+
mock.patch.object(
|
|
271
|
+
cli_operations,
|
|
272
|
+
"_get_upgrade_snapshot",
|
|
273
|
+
side_effect=[
|
|
274
|
+
{"method": "npm", "detail": "test-tool", "version": "1.0.0"},
|
|
275
|
+
{"method": "npm", "detail": "test-tool", "version": "1.0.0"},
|
|
276
|
+
],
|
|
277
|
+
),
|
|
278
|
+
):
|
|
279
|
+
result = cli_operations.upgrade_tool("test")
|
|
280
|
+
self.assertEqual(result, UpgradeResult.UNCHANGED)
|
|
281
|
+
mock_run.assert_called_once_with(
|
|
282
|
+
["npm", "install", "-g", "test-tool@latest"], check=True
|
|
283
|
+
)
|
|
226
284
|
|
|
227
285
|
|
|
228
286
|
class TestCmdUpgradeDefaultSelection(unittest.TestCase):
|
|
@@ -268,7 +326,9 @@ class TestCmdUpgradeDefaultSelection(unittest.TestCase):
|
|
|
268
326
|
cli_commands_actions, "get_tool_status", side_effect=_status
|
|
269
327
|
),
|
|
270
328
|
mock.patch.object(
|
|
271
|
-
cli_commands_actions,
|
|
329
|
+
cli_commands_actions,
|
|
330
|
+
"upgrade_tool",
|
|
331
|
+
return_value=UpgradeResult.CHANGED,
|
|
272
332
|
) as mock_upgrade,
|
|
273
333
|
):
|
|
274
334
|
cli_commands_actions.cmd_upgrade(args)
|
|
@@ -301,7 +361,9 @@ class TestCmdUpgradeDefaultSelection(unittest.TestCase):
|
|
|
301
361
|
return_value={"version": "1.0.0"},
|
|
302
362
|
),
|
|
303
363
|
mock.patch.object(
|
|
304
|
-
cli_commands_actions,
|
|
364
|
+
cli_commands_actions,
|
|
365
|
+
"upgrade_tool",
|
|
366
|
+
return_value=UpgradeResult.CHANGED,
|
|
305
367
|
) as mock_upgrade,
|
|
306
368
|
):
|
|
307
369
|
cli_commands_actions.cmd_upgrade(args)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|