code-aide 1.7.3__tar.gz → 1.10.0__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.
Files changed (48) hide show
  1. {code_aide-1.7.3 → code_aide-1.10.0}/.gitignore +1 -0
  2. {code_aide-1.7.3 → code_aide-1.10.0}/PKG-INFO +1 -1
  3. code_aide-1.10.0/specs/unify-upgrade-eligibility-with-shared-evaluator.md +87 -0
  4. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/__init__.py +1 -1
  5. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/commands_actions.py +33 -34
  6. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/commands_tools.py +101 -101
  7. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/config.py +20 -3
  8. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/data/tools.json +0 -1
  9. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/detection.py +99 -34
  10. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/install.py +18 -29
  11. code_aide-1.10.0/src/code_aide/install_types.py +90 -0
  12. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/operations.py +47 -24
  13. code_aide-1.10.0/src/code_aide/status.py +418 -0
  14. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/versions.py +36 -6
  15. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_commands_actions.py +77 -11
  16. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_commands_tools.py +120 -11
  17. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_config.py +41 -0
  18. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_install.py +3 -36
  19. code_aide-1.10.0/tests/test_install_types.py +61 -0
  20. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_operations.py +74 -12
  21. code_aide-1.10.0/tests/test_status.py +355 -0
  22. code_aide-1.7.3/src/code_aide/status.py +0 -116
  23. code_aide-1.7.3/tests/test_status.py +0 -76
  24. {code_aide-1.7.3 → code_aide-1.10.0}/.github/workflows/ci.yml +0 -0
  25. {code_aide-1.7.3 → code_aide-1.10.0}/.github/workflows/publish.yml +0 -0
  26. {code_aide-1.7.3 → code_aide-1.10.0}/.gitlab-ci.yml +0 -0
  27. {code_aide-1.7.3 → code_aide-1.10.0}/.pre-commit-config.yaml +0 -0
  28. {code_aide-1.7.3 → code_aide-1.10.0}/AGENTS.md +0 -0
  29. {code_aide-1.7.3 → code_aide-1.10.0}/CLAUDE.md +0 -0
  30. {code_aide-1.7.3 → code_aide-1.10.0}/LICENSE +0 -0
  31. {code_aide-1.7.3 → code_aide-1.10.0}/README.md +0 -0
  32. {code_aide-1.7.3 → code_aide-1.10.0}/TODO.md +0 -0
  33. {code_aide-1.7.3 → code_aide-1.10.0}/pyproject.toml +0 -0
  34. {code_aide-1.7.3 → code_aide-1.10.0}/specs/auto-migrate-deprecated-installs.md +0 -0
  35. {code_aide-1.7.3 → code_aide-1.10.0}/specs/claude-native-installer-migration.md +0 -0
  36. {code_aide-1.7.3 → code_aide-1.10.0}/specs/missing-coding-llm-cli-tools.md +0 -0
  37. {code_aide-1.7.3 → code_aide-1.10.0}/specs/pre-commit-uv-setup.md +0 -0
  38. {code_aide-1.7.3 → code_aide-1.10.0}/specs/remove-bundled-version-baseline.md +0 -0
  39. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/__main__.py +0 -0
  40. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/console.py +0 -0
  41. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/constants.py +0 -0
  42. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/entry.py +0 -0
  43. {code_aide-1.7.3 → code_aide-1.10.0}/src/code_aide/prereqs.py +0 -0
  44. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_console.py +0 -0
  45. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_constants.py +0 -0
  46. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_detection.py +0 -0
  47. {code_aide-1.7.3 → code_aide-1.10.0}/tests/test_versions.py +0 -0
  48. {code_aide-1.7.3 → code_aide-1.10.0}/uv.lock +0 -0
@@ -12,3 +12,4 @@
12
12
  __pycache__/
13
13
  build/
14
14
  dist/
15
+ archive/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-aide
3
- Version: 1.7.3
3
+ Version: 1.10.0
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
@@ -0,0 +1,87 @@
1
+ # Unify upgrade eligibility with a shared evaluator
2
+
3
+ ## Summary
4
+
5
+ Introduce a single evaluator class that computes tool state once and exposes
6
+ normalized decisions for both `status` and default `upgrade`. Use typed
7
+ `Enum` values, not strings, for all decision outcomes and display states so
8
+ callers cannot drift by comparing ad hoc literals.
9
+
10
+ ## Implementation changes
11
+
12
+ - Add a small evaluator in `src/code_aide/status.py` as the single source of
13
+ truth for:
14
+ - install presence
15
+ - detected install method
16
+ - package-manager metadata
17
+ - version relationship to catalog/package state
18
+ - normalized outcome for both status rendering and default `upgrade`
19
+ - Define typed enums for evaluator output:
20
+ - `UpgradeDecision`: `CURRENT`, `UPGRADE`, `MIGRATION`, `PACKAGE_MANAGED`,
21
+ `NOT_INSTALLED`, `UNKNOWN`
22
+ - `VersionDisplayState`: `UP_TO_DATE`, `OUTDATED`, `UNAVAILABLE`
23
+ - optional compact-state enum if needed, otherwise derive compact text
24
+ from `UpgradeDecision`
25
+ - Return a typed result object/dataclass from the evaluator containing:
26
+ - `decision: UpgradeDecision`
27
+ - `version_state: VersionDisplayState`
28
+ - `actionable_by_upgrade: bool`
29
+ - optional package/upstream fields needed by full status
30
+ - Keep all decision rules inside the evaluator:
31
+ - exact catalog match => `CURRENT`
32
+ - installed newer than catalog => `CURRENT`
33
+ - deprecated detected install (`npm`/`brew_npm` vs configured non-matching
34
+ method) => `MIGRATION`
35
+ - Homebrew outdated => `UPGRADE`
36
+ - Homebrew current => `CURRENT`
37
+ - system-managed install => `PACKAGE_MANAGED`, never auto-selected by
38
+ default `upgrade`
39
+ - missing latest-version metadata or missing CLI version => `UNKNOWN`, not
40
+ auto-selected
41
+ - Refactor `cmd_status` and compact status in
42
+ `src/code_aide/commands_tools.py` to consume evaluator results only.
43
+ - Refactor default-selection logic in `cmd_upgrade` in
44
+ `src/code_aide/commands_actions.py` to use the same evaluator result.
45
+ - Keep explicit `code-aide upgrade <tool>` behavior unchanged; the evaluator
46
+ governs automatic selection and status semantics.
47
+
48
+ ## Public interfaces and types
49
+
50
+ - Add an internal evaluator class, for example `ToolUpgradeEvaluator`.
51
+ - Add internal enums and a typed result/dataclass; no caller should branch
52
+ on raw strings after this refactor.
53
+ - Do not change CLI flags, command names, or user-facing command flow.
54
+
55
+ ## Test plan
56
+
57
+ - Add evaluator-focused unit tests covering enum outputs for:
58
+ - installed version equals catalog latest
59
+ - installed version newer than catalog latest
60
+ - installed version older than catalog latest
61
+ - Homebrew tool with `outdated=True`
62
+ - Homebrew tool with `outdated=False`
63
+ - system-managed tool with newer upstream metadata
64
+ - deprecated npm install requiring migration
65
+ - missing version metadata or unknown state
66
+ - Add command-level regressions proving shared behavior:
67
+ - full `status` says `up to date` for newer-than-catalog installs and
68
+ default `upgrade` does not auto-select them
69
+ - full/compact `status` and default `upgrade` both treat Homebrew current
70
+ installs as non-upgradeable
71
+ - system-managed installs are not auto-selected by default `upgrade`
72
+ - migration-required installs still appear as actionable for default
73
+ `upgrade`
74
+ - Run the targeted suite:
75
+ - `tests/test_status.py`
76
+ - `tests/test_commands_tools.py`
77
+ - `tests/test_commands_actions.py`
78
+ - any existing detection/operations tests touched by the refactor
79
+
80
+ ## Assumptions
81
+
82
+ - The evaluator, enums, and result dataclass remain internal APIs.
83
+ - Full `status` still shows migration warnings separately from upgrade
84
+ footers, but both are derived from the same enum-backed evaluator result.
85
+ - `Upgradeable` in the footer means `selected by default code-aide upgrade`.
86
+ - Display text remains string-based at the CLI boundary only; decision logic
87
+ uses enums throughout.
@@ -1,3 +1,3 @@
1
1
  """code-aide - Manage AI coding CLI tools."""
2
2
 
3
- __version__ = "1.7.3"
3
+ __version__ = "1.10.0"
@@ -5,12 +5,14 @@ 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 (
9
- detect_install_method,
10
- get_brew_package_info,
11
- is_deprecated_install,
12
- )
8
+ from code_aide.detection import detect_install_method, get_brew_package_info
13
9
  from code_aide.install import install_tool
10
+ from code_aide.install_types import (
11
+ InstallMethod,
12
+ InstallType,
13
+ parse_install_method,
14
+ parse_install_type,
15
+ )
14
16
  from code_aide.console import error, info, success, warning
15
17
  from code_aide.operations import (
16
18
  UpgradeResult,
@@ -23,14 +25,13 @@ from code_aide.prereqs import (
23
25
  check_prerequisites,
24
26
  is_tool_installed,
25
27
  )
26
- from code_aide.status import get_tool_status
28
+ from code_aide.status import ToolUpgradeEvaluator
27
29
  from code_aide.versions import (
28
30
  apply_sha256_updates,
29
31
  check_npm_tool,
30
32
  check_script_tool,
31
33
  normalize_version,
32
34
  print_check_results_table,
33
- status_version_matches_latest,
34
35
  )
35
36
  from code_aide.config import (
36
37
  load_bundled_tools,
@@ -131,28 +132,9 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
131
132
  else:
132
133
  tools_to_upgrade = []
133
134
  for name, config in TOOLS.items():
134
- if not is_tool_installed(name):
135
- continue
136
- if is_deprecated_install(name):
137
- if name not in tools_to_upgrade:
138
- tools_to_upgrade.append(name)
139
- continue
140
- latest = config.get("latest_version")
141
- if not latest:
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
150
- status = get_tool_status(name, config)
151
- if status["version"] and status_version_matches_latest(
152
- status["version"], latest
153
- ):
154
- continue
155
- tools_to_upgrade.append(name)
135
+ assessment = ToolUpgradeEvaluator(name, config).evaluate()
136
+ if assessment.actionable_by_upgrade and name not in tools_to_upgrade:
137
+ tools_to_upgrade.append(name)
156
138
  if tools_to_upgrade:
157
139
  info(f"Upgrading out-of-date tools: {', '.join(tools_to_upgrade)}")
158
140
  else:
@@ -282,20 +264,20 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
282
264
  results: List[Dict[str, Any]] = []
283
265
  for name in tool_names:
284
266
  tool_config = tools[name]
285
- install_type = tool_config["install_type"]
267
+ install_type = parse_install_type(tool_config["install_type"])
286
268
 
287
- if install_type == "npm":
269
+ if install_type == InstallType.NPM:
288
270
  results.append(check_npm_tool(name, tool_config, args.verbose))
289
- elif install_type in ("script", "direct_download"):
271
+ elif install_type in (InstallType.SCRIPT, InstallType.DIRECT_DOWNLOAD):
290
272
  results.append(check_script_tool(name, tool_config, args.verbose))
291
273
  else:
292
274
  results.append(
293
275
  {
294
276
  "tool": name,
295
- "type": install_type,
277
+ "type": tool_config["install_type"],
296
278
  "version": "-",
297
279
  "date": "-",
298
- "status": f"unknown type: {install_type}",
280
+ "status": f"unknown type: {tool_config['install_type']}",
299
281
  "update": None,
300
282
  }
301
283
  )
@@ -320,6 +302,23 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
320
302
  tool_entry["latest_date"] = date
321
303
  version_info_changed = True
322
304
 
305
+ # Refresh latest_version from Homebrew for tools installed via brew, so
306
+ # "upstream" in status reflects the correct source for those installs.
307
+ for tool_name in tool_names:
308
+ install_info = detect_install_method(tool_name)
309
+ method = parse_install_method(install_info["method"])
310
+ if method not in (InstallMethod.BREW_FORMULA, InstallMethod.BREW_CASK):
311
+ continue
312
+ pkg_info = get_brew_package_info(method, install_info["detail"])
313
+ avail = pkg_info.get("available_version")
314
+ if not avail:
315
+ continue
316
+ normalized = normalize_version(avail)
317
+ tool_entry = config["tools"][tool_name]
318
+ if tool_entry.get("latest_version") != normalized:
319
+ tool_entry["latest_version"] = normalized
320
+ version_info_changed = True
321
+
323
322
  def _save(tools: dict) -> str:
324
323
  """Save versions to user cache. Returns description."""
325
324
  save_updated_versions(tools)
@@ -1,5 +1,7 @@
1
1
  """Read-only CLI commands: list and status."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import argparse
4
6
  import platform
5
7
  import shutil
@@ -9,21 +11,24 @@ from code_aide.constants import Colors, PACKAGE_MANAGERS, TOOLS
9
11
  from code_aide.detection import (
10
12
  format_install_method,
11
13
  format_migration_warning,
12
- get_brew_package_info,
13
- get_system_package_info,
14
14
  detect_install_method,
15
15
  )
16
16
  from code_aide.console import command_exists, info, warning
17
+ from code_aide.install_types import (
18
+ InstallMethod,
19
+ parse_install_method,
20
+ parse_install_type,
21
+ )
17
22
  from code_aide.prereqs import detect_package_manager, is_tool_installed
18
23
  from code_aide.status import (
19
- get_tool_status,
20
24
  print_brew_version_status,
21
25
  print_system_version_status,
26
+ ToolUpgradeEvaluator,
27
+ UpgradeDecision,
28
+ VersionDisplayState,
22
29
  )
23
30
  from code_aide.versions import (
24
31
  extract_version_from_string,
25
- status_version_matches_latest,
26
- version_is_newer,
27
32
  )
28
33
 
29
34
 
@@ -44,7 +49,9 @@ def cmd_list(args: argparse.Namespace) -> None:
44
49
  print(f"{Colors.BLUE}{tool_config['name']}{Colors.NC}")
45
50
  print(f" Command: {tool_config['command']}")
46
51
  print(f" Status: {status}")
47
- print(f" Managed by: {tool_config['install_type']} (code-aide)")
52
+ install_type = parse_install_type(tool_config["install_type"])
53
+ managed_by = install_type.value if install_type else tool_config["install_type"]
54
+ print(f" Managed by: {managed_by} (code-aide)")
48
55
 
49
56
  if installed:
50
57
  tool_path = shutil.which(tool_config["command"])
@@ -104,31 +111,45 @@ def cmd_list(args: argparse.Namespace) -> None:
104
111
  def _short_install_method(method: str | None) -> str:
105
112
  """Return a short label for an install method."""
106
113
  labels = {
107
- "brew_formula": "brew",
108
- "brew_cask": "cask",
109
- "npm": "npm",
110
- "brew_npm": "brew-npm",
111
- "system": "system",
112
- "script": "script",
113
- "direct_download": "download",
114
+ InstallMethod.BREW_FORMULA: "brew",
115
+ InstallMethod.BREW_CASK: "cask",
116
+ InstallMethod.NPM: "npm",
117
+ InstallMethod.BREW_NPM: "brew-npm",
118
+ InstallMethod.SYSTEM: "system",
119
+ InstallMethod.SCRIPT: "script",
120
+ InstallMethod.DIRECT_DOWNLOAD: "download",
114
121
  }
115
- return labels.get(method or "", method or "unknown")
122
+ install_method = parse_install_method(method)
123
+ if install_method is None:
124
+ return str(method or "unknown")
125
+ return labels.get(install_method, str(install_method))
116
126
 
117
127
 
118
128
  def _compact_version_status(
119
- version: str | None, latest_version: str | None
129
+ installed_version: str | None, version_state: VersionDisplayState
120
130
  ) -> tuple[str, str]:
121
131
  """Return (version_str, status_indicator) for compact display."""
122
- if not version:
132
+ if not installed_version:
123
133
  return ("", "")
124
- ver = extract_version_from_string(version) or version
125
- if not latest_version:
126
- return (ver, "")
127
- if status_version_matches_latest(version, latest_version):
134
+ ver = extract_version_from_string(installed_version) or installed_version
135
+ if version_state == VersionDisplayState.UP_TO_DATE:
128
136
  return (ver, f"{Colors.GREEN}ok{Colors.NC}")
129
- if ver and version_is_newer(ver, latest_version):
130
- return (ver, f"{Colors.YELLOW}newer{Colors.NC}")
131
- return (ver, f"{Colors.YELLOW}old{Colors.NC}")
137
+ if version_state == VersionDisplayState.OUTDATED:
138
+ return (ver, f"{Colors.YELLOW}old{Colors.NC}")
139
+ return (ver, "")
140
+
141
+
142
+ def _generic_version_annotation(
143
+ installed_version: str, latest_version: str | None
144
+ ) -> str:
145
+ """Return the generic version line for non-package-managed status."""
146
+ if not latest_version:
147
+ return f" Version: {installed_version}"
148
+
149
+ return (
150
+ f" Version: {installed_version} "
151
+ f"{Colors.YELLOW}(latest: {latest_version}){Colors.NC}"
152
+ )
132
153
 
133
154
 
134
155
  def cmd_status_compact() -> None:
@@ -136,7 +157,8 @@ def cmd_status_compact() -> None:
136
157
  rows: list[tuple[str, str, str, str, str]] = []
137
158
  for tool_name, tool_config in TOOLS.items():
138
159
  name = tool_config["command"]
139
- status = get_tool_status(tool_name, tool_config)
160
+ assessment = ToolUpgradeEvaluator(tool_name, tool_config).evaluate()
161
+ status = assessment.status
140
162
 
141
163
  if not status["installed"]:
142
164
  opt_in = not tool_config.get("default_install", True)
@@ -148,10 +170,10 @@ def cmd_status_compact() -> None:
148
170
  continue
149
171
 
150
172
  tool_path = shutil.which(tool_config["command"]) or ""
151
- install_info = detect_install_method(tool_name)
152
- method = _short_install_method(install_info["method"])
153
- latest_version = tool_config.get("latest_version")
154
- ver, ver_status = _compact_version_status(status["version"], latest_version)
173
+ method = _short_install_method(assessment.install_method)
174
+ ver, ver_status = _compact_version_status(
175
+ assessment.installed_version, assessment.version_state
176
+ )
155
177
 
156
178
  state = f"{Colors.GREEN}ok{Colors.NC}"
157
179
  if ver_status:
@@ -193,106 +215,90 @@ def cmd_status(args: argparse.Namespace) -> None:
193
215
  print()
194
216
 
195
217
  outdated_count = 0
218
+ outdated_tools: List[str] = []
196
219
  migration_count = 0
197
- config_outdated: List[str] = []
198
220
 
199
221
  for tool_name, tool_config in TOOLS.items():
200
222
  print(f"{Colors.BLUE}{tool_config['name']}{Colors.NC}")
201
223
 
202
- status = get_tool_status(tool_name, tool_config)
224
+ tool_path = shutil.which(tool_config["command"])
225
+ assessment = ToolUpgradeEvaluator(
226
+ tool_name, tool_config, tool_path=tool_path
227
+ ).evaluate()
228
+ status = assessment.status
203
229
 
204
230
  if not status["installed"]:
205
231
  print(f" Status: {Colors.RED}✗ Not installed{Colors.NC}")
206
232
  else:
207
233
  print(f" Status: {Colors.GREEN}✓ Installed{Colors.NC}")
208
234
 
209
- tool_path = shutil.which(tool_config["command"])
210
- install_info = detect_install_method(tool_name)
211
- is_system = install_info["method"] == "system"
212
-
213
235
  if status["version"]:
214
- latest_version = tool_config.get("latest_version")
215
-
216
- if is_system and tool_path:
217
- pkg_info = get_system_package_info(tool_path)
236
+ if (
237
+ assessment.install_method == InstallMethod.SYSTEM
238
+ and tool_path
239
+ and assessment.package_info
240
+ ):
218
241
  print_system_version_status(
219
- status["version"], latest_version, pkg_info
220
- )
221
- elif install_info["method"] in ("brew_formula", "brew_cask"):
222
- pkg_info = get_brew_package_info(
223
- install_info["method"], install_info["detail"]
242
+ status["version"],
243
+ assessment.latest_version,
244
+ assessment.package_info,
224
245
  )
225
- if pkg_info.get("available_version"):
246
+ elif assessment.install_method in (
247
+ InstallMethod.BREW_FORMULA,
248
+ InstallMethod.BREW_CASK,
249
+ ):
250
+ if assessment.package_info and assessment.package_info.get(
251
+ "available_version"
252
+ ):
226
253
  print_brew_version_status(
227
- status["version"], latest_version, pkg_info
254
+ status["version"],
255
+ assessment.latest_version,
256
+ assessment.package_info,
228
257
  )
229
- if pkg_info.get("outdated"):
230
- outdated_count += 1
231
- elif latest_version:
232
- if status_version_matches_latest(
233
- status["version"], latest_version
234
- ):
235
- version_annotation = (
258
+ elif assessment.latest_version:
259
+ if assessment.version_state == VersionDisplayState.UP_TO_DATE:
260
+ print(
236
261
  f" Version: {status['version']} "
237
262
  f"{Colors.GREEN}(up to date){Colors.NC}"
238
263
  )
239
264
  else:
240
- installed_ver = extract_version_from_string(
241
- status["version"]
242
- )
243
- if installed_ver and version_is_newer(
244
- installed_ver, latest_version
245
- ):
246
- version_annotation = (
247
- f" Version: {status['version']} "
248
- f"{Colors.YELLOW}(newer than configured "
249
- f"{latest_version}){Colors.NC}"
250
- )
251
- config_outdated.append(tool_name)
252
- else:
253
- version_annotation = (
254
- f" Version: {status['version']} "
255
- f"{Colors.YELLOW}(latest: {latest_version}){Colors.NC}"
265
+ print(
266
+ _generic_version_annotation(
267
+ status["version"], assessment.latest_version
256
268
  )
257
- outdated_count += 1
258
- print(version_annotation)
259
- elif latest_version:
260
- if status_version_matches_latest(status["version"], latest_version):
261
- version_annotation = (
269
+ )
270
+ else:
271
+ print(f" Version: {status['version']}")
272
+ elif assessment.latest_version:
273
+ if assessment.version_state == VersionDisplayState.UP_TO_DATE:
274
+ print(
262
275
  f" Version: {status['version']} "
263
276
  f"{Colors.GREEN}(up to date){Colors.NC}"
264
277
  )
265
278
  else:
266
- installed_ver = extract_version_from_string(status["version"])
267
- if installed_ver and version_is_newer(
268
- installed_ver, latest_version
269
- ):
270
- version_annotation = (
271
- f" Version: {status['version']} "
272
- f"{Colors.YELLOW}(newer than configured "
273
- f"{latest_version}){Colors.NC}"
274
- )
275
- config_outdated.append(tool_name)
276
- else:
277
- version_annotation = (
278
- f" Version: {status['version']} "
279
- f"{Colors.YELLOW}(latest: {latest_version}){Colors.NC}"
279
+ print(
280
+ _generic_version_annotation(
281
+ status["version"], assessment.latest_version
280
282
  )
281
- outdated_count += 1
282
- print(version_annotation)
283
+ )
283
284
  else:
284
285
  print(f" Version: {status['version']}")
285
286
 
287
+ if assessment.decision == UpgradeDecision.UPGRADE:
288
+ outdated_count += 1
289
+ outdated_tools.append(tool_name)
290
+
286
291
  if tool_path:
287
292
  print(f" Location: {tool_path}")
288
293
  print(
289
294
  " Installed via: "
290
- f"{format_install_method(install_info['method'], install_info['detail'])}"
295
+ f"{format_install_method(assessment.install_method, assessment.install_detail)}"
291
296
  )
292
- migration_msg = format_migration_warning(tool_name)
293
- if migration_msg:
294
- warning(f" {migration_msg}")
297
+ if assessment.decision == UpgradeDecision.MIGRATION:
295
298
  migration_count += 1
299
+ migration_msg = format_migration_warning(tool_name)
300
+ if migration_msg:
301
+ warning(f" {migration_msg}")
296
302
 
297
303
  if status["user"]:
298
304
  print(f" User: {status['user']}")
@@ -306,17 +312,11 @@ def cmd_status(args: argparse.Namespace) -> None:
306
312
 
307
313
  print()
308
314
 
309
- if config_outdated:
310
- tools_str = " ".join(config_outdated)
311
- print(
312
- f"{Colors.YELLOW}Configured version outdated for: "
313
- f"{', '.join(config_outdated)}. Run 'code-aide update-versions "
314
- f"{tools_str}' to update.{Colors.NC}"
315
- )
316
315
  if outdated_count > 0:
316
+ names = ", ".join(outdated_tools)
317
317
  print(
318
318
  f"{Colors.YELLOW}{outdated_count} tool(s) can be upgraded with "
319
- f"'code-aide upgrade'.{Colors.NC}"
319
+ f"'code-aide upgrade': {names}.{Colors.NC}"
320
320
  )
321
321
  if migration_count > 0:
322
322
  print(
@@ -9,6 +9,8 @@ import importlib.resources
9
9
  import json
10
10
  import os
11
11
 
12
+ from code_aide.install_types import InstallType, parse_install_type
13
+
12
14
 
13
15
  def get_config_dir() -> str:
14
16
  """Return XDG config directory for code-aide.
@@ -70,6 +72,14 @@ def merge_cached_versions(tools: dict, cache: dict) -> None:
70
72
  if tool_key in cached_tools:
71
73
  for field in DYNAMIC_FIELDS:
72
74
  if field in cached_tools[tool_key]:
75
+ if (
76
+ field == "install_sha256"
77
+ and parse_install_type(tool_data.get("install_type"))
78
+ == InstallType.DIRECT_DOWNLOAD
79
+ ):
80
+ # Script checksum does not apply to tarball installs; ignore
81
+ # stale cache from older releases or mistaken updates.
82
+ continue
73
83
  tool_data[field] = cached_tools[tool_key][field]
74
84
 
75
85
 
@@ -77,9 +87,10 @@ def load_tools_config() -> dict:
77
87
  """Load tool config: bundled definitions merged with cached versions.
78
88
 
79
89
  Two-layer model: the bundled tools.json provides tool definitions and
80
- install_sha256 checksums. The user's version cache (from
81
- update-versions) provides latest_version, latest_date, and updated
82
- install_sha256 values when present.
90
+ install_sha256 checksums (for script-type tools). The user's version
91
+ cache (from update-versions) provides latest_version, latest_date,
92
+ and updated install_sha256 values when present. Cached install_sha256
93
+ is ignored for direct_download tools (tarball installs).
83
94
  """
84
95
  bundled = load_bundled_tools()
85
96
  cache = load_versions_cache()
@@ -99,6 +110,12 @@ def save_updated_versions(tools: dict) -> None:
99
110
  entry = {}
100
111
  for field in DYNAMIC_FIELDS:
101
112
  if field in tool_data:
113
+ if (
114
+ field == "install_sha256"
115
+ and parse_install_type(tool_data.get("install_type"))
116
+ == InstallType.DIRECT_DOWNLOAD
117
+ ):
118
+ continue
102
119
  entry[field] = tool_data[field]
103
120
  if entry:
104
121
  cache_data["tools"][tool_key] = entry
@@ -6,7 +6,6 @@
6
6
  "command": "agent",
7
7
  "install_type": "direct_download",
8
8
  "install_url": "https://cursor.com/install",
9
- "install_sha256": "7ccf2992a4de12c040854a833db0255a08cdfc91d40c75b78cf66a8746d9bd24",
10
9
  "download_url_template": "https://downloads.cursor.com/lab/{version}/{os}/{arch}/agent-cli-package.tar.gz",
11
10
  "install_dir": "~/.local/share/cursor-agent/versions/{version}",
12
11
  "bin_dir": "~/.local/bin",