code-aide 1.6.0__tar.gz → 1.7.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 (46) hide show
  1. {code_aide-1.6.0 → code_aide-1.7.0}/PKG-INFO +3 -2
  2. {code_aide-1.6.0 → code_aide-1.7.0}/README.md +2 -1
  3. {code_aide-1.6.0 → code_aide-1.7.0}/TODO.md +19 -18
  4. code_aide-1.7.0/specs/auto-migrate-deprecated-installs.md +128 -0
  5. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/__init__.py +1 -1
  6. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/commands_actions.py +5 -0
  7. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/commands_tools.py +14 -0
  8. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/detection.py +60 -0
  9. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/operations.py +41 -2
  10. code_aide-1.7.0/tests/test_commands_tools.py +163 -0
  11. code_aide-1.7.0/tests/test_detection.py +286 -0
  12. code_aide-1.7.0/tests/test_operations.py +309 -0
  13. code_aide-1.6.0/tests/test_commands_tools.py +0 -78
  14. code_aide-1.6.0/tests/test_detection.py +0 -116
  15. code_aide-1.6.0/tests/test_operations.py +0 -159
  16. {code_aide-1.6.0 → code_aide-1.7.0}/.github/workflows/ci.yml +0 -0
  17. {code_aide-1.6.0 → code_aide-1.7.0}/.github/workflows/publish.yml +0 -0
  18. {code_aide-1.6.0 → code_aide-1.7.0}/.gitignore +0 -0
  19. {code_aide-1.6.0 → code_aide-1.7.0}/.gitlab-ci.yml +0 -0
  20. {code_aide-1.6.0 → code_aide-1.7.0}/.pre-commit-config.yaml +0 -0
  21. {code_aide-1.6.0 → code_aide-1.7.0}/AGENTS.md +0 -0
  22. {code_aide-1.6.0 → code_aide-1.7.0}/CLAUDE.md +0 -0
  23. {code_aide-1.6.0 → code_aide-1.7.0}/LICENSE +0 -0
  24. {code_aide-1.6.0 → code_aide-1.7.0}/pyproject.toml +0 -0
  25. {code_aide-1.6.0 → code_aide-1.7.0}/specs/claude-native-installer-migration.md +0 -0
  26. {code_aide-1.6.0 → code_aide-1.7.0}/specs/missing-coding-llm-cli-tools.md +0 -0
  27. {code_aide-1.6.0 → code_aide-1.7.0}/specs/pre-commit-uv-setup.md +0 -0
  28. {code_aide-1.6.0 → code_aide-1.7.0}/specs/remove-bundled-version-baseline.md +0 -0
  29. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/__main__.py +0 -0
  30. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/config.py +0 -0
  31. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/console.py +0 -0
  32. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/constants.py +0 -0
  33. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/data/tools.json +0 -0
  34. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/entry.py +0 -0
  35. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/install.py +0 -0
  36. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/prereqs.py +0 -0
  37. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/status.py +0 -0
  38. {code_aide-1.6.0 → code_aide-1.7.0}/src/code_aide/versions.py +0 -0
  39. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_commands_actions.py +0 -0
  40. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_config.py +0 -0
  41. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_console.py +0 -0
  42. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_constants.py +0 -0
  43. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_install.py +0 -0
  44. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_status.py +0 -0
  45. {code_aide-1.6.0 → code_aide-1.7.0}/tests/test_versions.py +0 -0
  46. {code_aide-1.6.0 → code_aide-1.7.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-aide
3
- Version: 1.6.0
3
+ Version: 1.7.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
@@ -157,7 +157,8 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
157
157
  - Example: `Fixed timeout handling in status command`
158
158
  5. Tag and push:
159
159
  - `git tag vX.Y.Z`
160
- - `git push origin main --follow-tags`
160
+ - `git push origin main`
161
+ - `git push origin vX.Y.Z`
161
162
  6. Confirm GitHub Actions:
162
163
  - CI should pass.
163
164
  - Publish workflow should upload to PyPI and create GitHub Release notes.
@@ -131,7 +131,8 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
131
131
  - Example: `Fixed timeout handling in status command`
132
132
  5. Tag and push:
133
133
  - `git tag vX.Y.Z`
134
- - `git push origin main --follow-tags`
134
+ - `git push origin main`
135
+ - `git push origin vX.Y.Z`
135
136
  6. Confirm GitHub Actions:
136
137
  - CI should pass.
137
138
  - Publish workflow should upload to PyPI and create GitHub Release notes.
@@ -7,8 +7,8 @@ remove, status, and version metadata).
7
7
 
8
8
  - [x] Standardize `--dryrun` flag: `update-versions` uses `--dry-run` but
9
9
  `install` uses `--dryrun`. Change `update-versions` to `--dryrun`.
10
- - [x] Add missing `success()` message for direct-download installs in non-dryrun
11
- mode (`install.py`, `install_tool()`).
10
+ - [x] Add missing `success()` message for direct-download installs in
11
+ non-dryrun mode (`install.py`, `install_tool()`).
12
12
  - [x] Replace PID-based temp directory naming with `tempfile.mkdtemp()` in
13
13
  `install.py` `install_direct_download()`.
14
14
  - [x] Guard `package.split("/", 1)` in `detection.py`
@@ -18,21 +18,21 @@ remove, status, and version metadata).
18
18
 
19
19
  - [ ] Handle missing `node` cleanly during prerequisite checks
20
20
  (`FileNotFoundError` path in Node version probing).
21
- - [ ] Read tool version output from both stdout and stderr so status does not
22
- miss installed versions.
21
+ - [ ] Read tool version output from both stdout and stderr so status does
22
+ not miss installed versions.
23
23
  - [ ] Make version cache writes atomic (write temp file + rename) to avoid
24
- partial/corrupted `versions.json`. Apply to `save_bundled_versions()` too.
25
- - [ ] Warn when `versions.json` cache contains invalid JSON instead of silently
26
- returning empty data.
24
+ partial/corrupted `versions.json`.
25
+ - [ ] Warn when `versions.json` cache contains invalid JSON instead of
26
+ silently returning empty data.
27
27
  - [ ] Use `os.pathsep` instead of hardcoded `":"` in `prereqs.py`
28
28
  `check_path_directories()`.
29
29
 
30
30
  ## Security and Integrity
31
31
 
32
- - [ ] Add integrity verification for direct-download tarballs (not only install
33
- script SHA256).
34
- - [ ] Extend tool metadata to support tarball checksum/signature fields where
35
- applicable.
32
+ - [ ] Add integrity verification for direct-download tarballs (not only
33
+ install script SHA256).
34
+ - [ ] Extend tool metadata to support tarball checksum/signature fields
35
+ where applicable.
36
36
 
37
37
  ## CLI and Automation UX
38
38
 
@@ -40,21 +40,23 @@ remove, status, and version metadata).
40
40
  - [ ] Add a focused `doctor` command for environment checks (PATH,
41
41
  prerequisites, command health).
42
42
  - [ ] Consider `install --force` for reinstall/repair flows.
43
- - [ ] Add cleanup support for stale direct-download versions no longer in use.
44
- - [ ] Expand `upgrade` help text to mention the default "only out-of-date tools"
45
- behavior.
43
+ - [ ] Add cleanup support for stale direct-download versions no longer in
44
+ use.
45
+ - [ ] Expand `upgrade` help text to mention the default "only out-of-date
46
+ tools" behavior.
46
47
 
47
48
  ## Documentation
48
49
 
49
- - [ ] Document that running `code-aide` with no subcommand defaults to `status`.
50
+ - [ ] Document that running `code-aide` with no subcommand defaults to
51
+ `status`.
50
52
  - [ ] Add `install --dryrun` to the README usage examples.
51
53
  - [ ] Document environment variables and config file paths
52
54
  (`~/.config/code-aide/versions.json`).
53
55
 
54
56
  ## Platform and Package Detection
55
57
 
56
- - [ ] Broaden system package metadata detection beyond Gentoo-specific tooling
57
- where practical.
58
+ - [ ] Broaden system package metadata detection beyond Gentoo-specific
59
+ tooling where practical.
58
60
 
59
61
  ## Maintainability and Tests
60
62
 
@@ -65,6 +67,5 @@ remove, status, and version metadata).
65
67
  - `cmd_remove()` (completely untested)
66
68
  - `fetch_url()` (completely untested)
67
69
  - `check_prerequisites()` / `install_nodejs_npm()` (completely untested)
68
- - `save_bundled_versions()` (untested)
69
70
  - `get_system_package_info()` (completely untested)
70
71
  - [ ] Add tests for prerequisite edge cases and cache write behavior.
@@ -0,0 +1,128 @@
1
+ # Auto-migrate deprecated install methods
2
+
3
+ ## Context
4
+
5
+ Claude Code switched from npm to a native installer. Users who installed
6
+ Claude via npm are stuck: `code-aide upgrade` runs `npm install -g @latest`
7
+ (the detected method) instead of the native script installer (the configured
8
+ method). `code-aide install` exits early because the command already exists.
9
+ There is no migration path. code-aide should detect this and fix it
10
+ automatically.
11
+
12
+ This is a general pattern -- any tool could change install methods in the
13
+ future. The solution should not be Claude-specific.
14
+
15
+ ## Approach
16
+
17
+ A detected install method is "deprecated" when:
18
+
19
+ - Detected method is `npm` or `brew_npm` AND config `install_type` is
20
+ `script` or `direct_download`
21
+
22
+ Methods that are never considered deprecated (user-managed choices):
23
+
24
+ - `brew_formula`, `brew_cask` -- user explicitly chose Homebrew
25
+ - `system` -- managed by system package manager
26
+
27
+ Migration flow: remove old (npm), install new (configured method), with
28
+ clear messaging. If the new install fails after removal, provide recovery
29
+ instructions.
30
+
31
+ ## Changes
32
+
33
+ ### 1. `src/code_aide/data/tools.json` -- add deprecated_npm_package
34
+
35
+ Add a `deprecated_npm_package` field to Claude's entry:
36
+
37
+ ```json
38
+ "claude": {
39
+ "name": "Claude CLI (Claude Code)",
40
+ "install_type": "script",
41
+ "deprecated_npm_package": "@anthropic-ai/claude-code",
42
+ ...
43
+ }
44
+ ```
45
+
46
+ This records the old npm package name explicitly so the migration removal
47
+ step does not rely solely on extracting the package name from the binary
48
+ path.
49
+
50
+ ### 2. `src/code_aide/detection.py` -- add deprecation detection
51
+
52
+ Add two functions:
53
+
54
+ - `is_deprecated_install(tool_name)` -- returns True when the detected
55
+ install method is `npm` or `brew_npm` but the configured `install_type` is
56
+ something else (script, direct_download). Also returns True when
57
+ `deprecated_npm_package` is set and the detected method is npm. Brew
58
+ formula/cask and system installs are never deprecated (user-managed).
59
+ - `format_migration_warning(tool_name)` -- returns a human-readable warning
60
+ string, or None if not deprecated. Uses existing `format_install_method()`
61
+ for labels.
62
+
63
+ ### 3. `src/code_aide/operations.py` -- auto-migrate in upgrade
64
+
65
+ Add `_migrate_install_method()` private function that:
66
+
67
+ 1. Warns about the deprecated method
68
+ 2. Calls `remove_tool()` to remove the old npm install
69
+ 3. Calls `install_tool()` to install via the configured method
70
+ 4. If install fails after remove, prints recovery instructions (`code-aide
71
+ install <tool>` and manual curl command for script types)
72
+
73
+ Modify `upgrade_tool()`: after `detect_install_method()`, check
74
+ `is_deprecated_install()`. If True, call `_migrate_install_method()` instead
75
+ of the normal upgrade branch.
76
+
77
+ Modify `remove_tool()` npm branch: fall back to
78
+ `tool_config.get("deprecated_npm_package")` when looking up the npm package
79
+ name. Current code: `npm_package = detail or
80
+ tool_config.get("npm_package")`. Change to also check
81
+ `deprecated_npm_package`.
82
+
83
+ New imports needed: `is_deprecated_install`, `format_install_method` from
84
+ detection, `install_tool` from install.
85
+
86
+ Note: after `remove_tool()` succeeds, `command_exists()` returns False so
87
+ `install_tool()` proceeds normally with the configured `install_type`.
88
+
89
+ ### 4. `src/code_aide/commands_actions.py` -- include deprecated in auto-upgrade
90
+
91
+ Modify `cmd_upgrade()` no-args path: after building the version-outdated
92
+ list, also add any installed tools with deprecated install methods. Without
93
+ this, a tool whose version is current but install method is deprecated would
94
+ be silently skipped by `code-aide upgrade`.
95
+
96
+ New import: `is_deprecated_install` from detection.
97
+
98
+ ### 5. `src/code_aide/commands_tools.py` -- show warnings in status/list
99
+
100
+ In both `cmd_list()` and `cmd_status()`, after displaying "Installed via:",
101
+ call `format_migration_warning()` and display the warning if present.
102
+
103
+ At the end of `cmd_status()`, add a summary line counting tools that need
104
+ migration.
105
+
106
+ New imports: `format_migration_warning`, `is_deprecated_install` from
107
+ detection.
108
+
109
+ ### 6. Tests
110
+
111
+ - `tests/test_detection.py`: Tests for `is_deprecated_install()` covering
112
+ npm to script (deprecated), npm to npm (not deprecated), brew_formula
113
+ (never deprecated), system (never deprecated), not installed, unknown
114
+ tool, npm to direct_download. Tests for `format_migration_warning()`.
115
+ - `tests/test_operations.py`: Tests for migration in `upgrade_tool()`:
116
+ triggers migration, normal upgrade when not deprecated, migration fails on
117
+ remove, migration fails on install with recovery message.
118
+ - `tests/test_commands_tools.py`: Test that status/list show migration
119
+ warnings for deprecated installs.
120
+
121
+ ## Verification
122
+
123
+ 1. `pre-commit run --all-files` -- formatting passes
124
+ 2. `uv run pytest tests/ -v` -- all tests pass
125
+ 3. `uv run python -m code_aide status` -- shows migration warning for
126
+ npm-installed Claude (if applicable)
127
+ 4. `uv run python -m code_aide upgrade claude` -- performs migration (remove
128
+ npm, install via script)
@@ -1,3 +1,3 @@
1
1
  """code-aide - Manage AI coding CLI tools."""
2
2
 
3
- __version__ = "1.6.0"
3
+ __version__ = "1.7.0"
@@ -5,6 +5,7 @@ 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 is_deprecated_install
8
9
  from code_aide.install import install_tool
9
10
  from code_aide.console import error, info, success, warning
10
11
  from code_aide.operations import remove_tool, upgrade_tool, validate_tools
@@ -123,6 +124,10 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
123
124
  for name, config in TOOLS.items():
124
125
  if not is_tool_installed(name):
125
126
  continue
127
+ if is_deprecated_install(name):
128
+ if name not in tools_to_upgrade:
129
+ tools_to_upgrade.append(name)
130
+ continue
126
131
  latest = config.get("latest_version")
127
132
  if not latest:
128
133
  continue
@@ -8,6 +8,7 @@ from typing import List
8
8
  from code_aide.constants import Colors, PACKAGE_MANAGERS, TOOLS
9
9
  from code_aide.detection import (
10
10
  format_install_method,
11
+ format_migration_warning,
11
12
  get_system_package_info,
12
13
  detect_install_method,
13
14
  )
@@ -48,6 +49,9 @@ def cmd_list(args: argparse.Namespace) -> None:
48
49
  " Installed via: "
49
50
  f"{format_install_method(install_info['method'], install_info['detail'])}"
50
51
  )
52
+ migration_msg = format_migration_warning(tool_name)
53
+ if migration_msg:
54
+ warning(f" {migration_msg}")
51
55
 
52
56
  if tool_config.get("min_node_version"):
53
57
  print(f" Requires: Node.js v{tool_config['min_node_version']}+")
@@ -99,6 +103,7 @@ def cmd_status(args: argparse.Namespace) -> None:
99
103
  print()
100
104
 
101
105
  outdated_count = 0
106
+ migration_count = 0
102
107
  config_outdated: List[str] = []
103
108
 
104
109
  for tool_name, tool_config in TOOLS.items():
@@ -156,6 +161,10 @@ def cmd_status(args: argparse.Namespace) -> None:
156
161
  " Installed via: "
157
162
  f"{format_install_method(install_info['method'], install_info['detail'])}"
158
163
  )
164
+ migration_msg = format_migration_warning(tool_name)
165
+ if migration_msg:
166
+ warning(f" {migration_msg}")
167
+ migration_count += 1
159
168
 
160
169
  if status["user"]:
161
170
  print(f" User: {status['user']}")
@@ -181,3 +190,8 @@ def cmd_status(args: argparse.Namespace) -> None:
181
190
  f"{Colors.YELLOW}{outdated_count} tool(s) can be upgraded with "
182
191
  f"'code-aide upgrade'.{Colors.NC}"
183
192
  )
193
+ if migration_count > 0:
194
+ print(
195
+ f"{Colors.YELLOW}{migration_count} tool(s) need migration to a "
196
+ f"new install method. Run 'code-aide upgrade' to migrate.{Colors.NC}"
197
+ )
@@ -186,3 +186,63 @@ def format_install_method(method: Optional[str], detail: Optional[str]) -> str:
186
186
  if method:
187
187
  return method
188
188
  return "unknown"
189
+
190
+
191
+ # Install methods that are user-managed and never considered deprecated
192
+ _USER_MANAGED_METHODS = frozenset({"brew_formula", "brew_cask", "system"})
193
+
194
+ # Install methods that are considered deprecated when they don't match
195
+ # the configured install_type
196
+ _DEPRECATED_METHODS = frozenset({"npm", "brew_npm"})
197
+
198
+
199
+ def is_deprecated_install(tool_name: str) -> bool:
200
+ """Check if a tool's detected install method is deprecated.
201
+
202
+ Returns True when the detected install method is npm or brew_npm
203
+ but the configured install_type is something else (e.g. script,
204
+ direct_download). Brew formula/cask and system installs are
205
+ user-managed and never considered deprecated.
206
+ """
207
+ tool_config = TOOLS.get(tool_name)
208
+ if not tool_config:
209
+ return False
210
+
211
+ install_info = detect_install_method(tool_name)
212
+ detected = install_info["method"]
213
+
214
+ if not detected:
215
+ return False
216
+
217
+ if detected in _USER_MANAGED_METHODS:
218
+ return False
219
+
220
+ configured = tool_config.get("install_type")
221
+ if not configured:
222
+ return False
223
+
224
+ if detected in _DEPRECATED_METHODS and detected != configured:
225
+ return True
226
+
227
+ return False
228
+
229
+
230
+ def format_migration_warning(tool_name: str) -> Optional[str]:
231
+ """Return a human-readable migration warning, or None if not needed."""
232
+ if not is_deprecated_install(tool_name):
233
+ return None
234
+
235
+ tool_config = TOOLS.get(tool_name)
236
+ if not tool_config:
237
+ return None
238
+
239
+ install_info = detect_install_method(tool_name)
240
+ detected_label = format_install_method(
241
+ install_info["method"], install_info["detail"]
242
+ )
243
+ configured_label = format_install_method(tool_config["install_type"], None)
244
+
245
+ return (
246
+ f"Installed via {detected_label} but configured method is "
247
+ f"{configured_label}. Run 'code-aide upgrade {tool_name}' to migrate."
248
+ )
@@ -8,12 +8,48 @@ import sys
8
8
  from typing import List
9
9
 
10
10
  from code_aide.constants import TOOLS
11
- from code_aide.detection import detect_install_method
12
- from code_aide.install import install_direct_download, run_install_script
11
+ from code_aide.detection import (
12
+ detect_install_method,
13
+ format_install_method,
14
+ is_deprecated_install,
15
+ )
16
+ from code_aide.install import install_direct_download, install_tool, run_install_script
13
17
  from code_aide.console import error, info, run_command, success, warning
14
18
  from code_aide.prereqs import is_tool_installed
15
19
 
16
20
 
21
+ def _migrate_install_method(tool_name: str) -> bool:
22
+ """Migrate a tool from a deprecated install method to the configured one."""
23
+ tool_config = TOOLS[tool_name]
24
+ install_info = detect_install_method(tool_name)
25
+ old_label = format_install_method(install_info["method"], install_info["detail"])
26
+ new_label = format_install_method(tool_config["install_type"], None)
27
+
28
+ warning(
29
+ f"{tool_config['name']} is installed via {old_label} "
30
+ f"but the configured method is {new_label}."
31
+ )
32
+ info(f"Migrating {tool_config['name']} from {old_label} to {new_label}...")
33
+
34
+ if not remove_tool(tool_name):
35
+ error(
36
+ f"Failed to remove old {old_label} install of {tool_config['name']}. "
37
+ "Migration aborted."
38
+ )
39
+ return False
40
+
41
+ if not install_tool(tool_name):
42
+ error(f"Failed to install {tool_config['name']} via {new_label}.")
43
+ error(
44
+ f"The old {old_label} install has been removed. "
45
+ f"To recover, run: code-aide install {tool_name}"
46
+ )
47
+ return False
48
+
49
+ success(f"{tool_config['name']} migrated from {old_label} to {new_label}")
50
+ return True
51
+
52
+
17
53
  def upgrade_tool(tool_name: str) -> bool:
18
54
  """Upgrade a tool based on its configuration."""
19
55
  tool_config = TOOLS.get(tool_name)
@@ -25,6 +61,9 @@ def upgrade_tool(tool_name: str) -> bool:
25
61
  warning(f"{tool_config['name']} is not installed. Use 'install' command first.")
26
62
  return False
27
63
 
64
+ if is_deprecated_install(tool_name):
65
+ return _migrate_install_method(tool_name)
66
+
28
67
  install_info = detect_install_method(tool_name)
29
68
  method = install_info["method"]
30
69
  detail = install_info["detail"]
@@ -0,0 +1,163 @@
1
+ """Unit tests for read-only CLI commands."""
2
+
3
+ import contextlib
4
+ import io
5
+ import unittest
6
+ from unittest import mock
7
+
8
+ from code_aide import commands_tools
9
+
10
+
11
+ class TestCmdList(unittest.TestCase):
12
+ """Tests for cmd_list."""
13
+
14
+ def test_lists_tools_without_runtime_probes(self):
15
+ tools = {
16
+ "x": {
17
+ "name": "Example Tool",
18
+ "command": "example",
19
+ "install_type": "npm",
20
+ "default_install": True,
21
+ }
22
+ }
23
+ args = type("Args", (), {})()
24
+ with (
25
+ mock.patch.dict(commands_tools.TOOLS, tools, clear=True),
26
+ mock.patch.object(commands_tools, "is_tool_installed", return_value=False),
27
+ mock.patch.object(commands_tools, "command_exists", return_value=False),
28
+ mock.patch.object(
29
+ commands_tools, "detect_package_manager", return_value=None
30
+ ),
31
+ ):
32
+ buf = io.StringIO()
33
+ with contextlib.redirect_stdout(buf):
34
+ commands_tools.cmd_list(args)
35
+ output = buf.getvalue()
36
+ self.assertIn("Example Tool", output)
37
+ self.assertIn("Managed by: npm (code-aide)", output)
38
+ self.assertIn("System Information:", output)
39
+
40
+
41
+ class TestCmdStatus(unittest.TestCase):
42
+ """Tests for cmd_status."""
43
+
44
+ def test_shows_upgradeable_count_for_outdated_tool(self):
45
+ tools = {
46
+ "x": {
47
+ "name": "Example Tool",
48
+ "command": "example",
49
+ "install_type": "npm",
50
+ "latest_version": "2.0.0",
51
+ }
52
+ }
53
+ status = {
54
+ "installed": True,
55
+ "version": "1.0.0",
56
+ "user": None,
57
+ "usage": None,
58
+ "errors": [],
59
+ }
60
+ args = type("Args", (), {})()
61
+ with (
62
+ mock.patch.dict(commands_tools.TOOLS, tools, clear=True),
63
+ mock.patch.object(commands_tools, "get_tool_status", return_value=status),
64
+ mock.patch.object(
65
+ commands_tools.shutil, "which", return_value="/tmp/example"
66
+ ),
67
+ mock.patch.object(
68
+ commands_tools,
69
+ "detect_install_method",
70
+ return_value={"method": "npm", "detail": "example-pkg"},
71
+ ),
72
+ mock.patch.object(
73
+ commands_tools, "format_migration_warning", return_value=None
74
+ ),
75
+ ):
76
+ buf = io.StringIO()
77
+ with contextlib.redirect_stdout(buf):
78
+ commands_tools.cmd_status(args)
79
+ output = buf.getvalue()
80
+ self.assertIn("Example Tool", output)
81
+ self.assertIn("tool(s) can be upgraded", output)
82
+
83
+ def test_status_shows_migration_warning(self):
84
+ """cmd_status shows migration warning for deprecated install."""
85
+ tools = {
86
+ "x": {
87
+ "name": "Example Tool",
88
+ "command": "example",
89
+ "install_type": "script",
90
+ "latest_version": "1.0.0",
91
+ }
92
+ }
93
+ status = {
94
+ "installed": True,
95
+ "version": "1.0.0",
96
+ "user": None,
97
+ "usage": None,
98
+ "errors": [],
99
+ }
100
+ args = type("Args", (), {})()
101
+ with (
102
+ mock.patch.dict(commands_tools.TOOLS, tools, clear=True),
103
+ mock.patch.object(commands_tools, "get_tool_status", return_value=status),
104
+ mock.patch.object(
105
+ commands_tools.shutil, "which", return_value="/tmp/example"
106
+ ),
107
+ mock.patch.object(
108
+ commands_tools,
109
+ "detect_install_method",
110
+ return_value={"method": "npm", "detail": "example-pkg"},
111
+ ),
112
+ mock.patch.object(
113
+ commands_tools,
114
+ "format_migration_warning",
115
+ return_value="Installed via npm but configured method is script. "
116
+ "Run 'code-aide upgrade x' to migrate.",
117
+ ),
118
+ ):
119
+ buf = io.StringIO()
120
+ with contextlib.redirect_stdout(buf):
121
+ commands_tools.cmd_status(args)
122
+ output = buf.getvalue()
123
+ self.assertIn("configured method is script", output)
124
+ self.assertIn("tool(s) need migration", output)
125
+
126
+ def test_list_shows_migration_warning(self):
127
+ """cmd_list shows migration warning for deprecated install."""
128
+ tools = {
129
+ "x": {
130
+ "name": "Example Tool",
131
+ "command": "example",
132
+ "install_type": "script",
133
+ "default_install": True,
134
+ }
135
+ }
136
+ args = type("Args", (), {})()
137
+ with (
138
+ mock.patch.dict(commands_tools.TOOLS, tools, clear=True),
139
+ mock.patch.object(commands_tools, "is_tool_installed", return_value=True),
140
+ mock.patch.object(
141
+ commands_tools.shutil, "which", return_value="/tmp/example"
142
+ ),
143
+ mock.patch.object(
144
+ commands_tools,
145
+ "detect_install_method",
146
+ return_value={"method": "npm", "detail": "example-pkg"},
147
+ ),
148
+ mock.patch.object(
149
+ commands_tools,
150
+ "format_migration_warning",
151
+ return_value="Installed via npm but configured method is script. "
152
+ "Run 'code-aide upgrade x' to migrate.",
153
+ ),
154
+ mock.patch.object(commands_tools, "command_exists", return_value=False),
155
+ mock.patch.object(
156
+ commands_tools, "detect_package_manager", return_value=None
157
+ ),
158
+ ):
159
+ buf = io.StringIO()
160
+ with contextlib.redirect_stdout(buf):
161
+ commands_tools.cmd_list(args)
162
+ output = buf.getvalue()
163
+ self.assertIn("configured method is script", output)