code-aide 1.17.0__tar.gz → 1.18.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.18.1/CLAUDE.md +1 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/PKG-INFO +19 -1
- {code_aide-1.17.0 → code_aide-1.18.1}/README.md +18 -0
- code_aide-1.18.1/TODO.md +30 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/script-archive/amp-install.sh +9 -6
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/__init__.py +1 -1
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/commands_actions.py +53 -1
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/data/tools.json +1 -1
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/entry.py +24 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/operations.py +65 -1
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/status.py +5 -2
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_commands_actions.py +152 -2
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_operations.py +73 -0
- code_aide-1.18.1/tests/test_package_info.py +57 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_status.py +46 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_versions.py +51 -0
- code_aide-1.17.0/CLAUDE.md +0 -1
- code_aide-1.17.0/TODO.md +0 -77
- {code_aide-1.17.0 → code_aide-1.18.1}/.github/workflows/ci.yml +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/.github/workflows/publish.yml +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/.gitignore +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/.gitlab-ci.yml +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/.pre-commit-config.yaml +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/AGENTS.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/LICENSE +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/Makefile +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/pyproject.toml +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/script-archive/README.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/script-archive/antigravity-install.sh +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/script-archive/claude-install.sh +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/script-archive/cursor-install.sh +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/2026-05-03-qwen3.6-code-quality-analysis.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/antigravity-cli-integration.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/__main__.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/commands_tools.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/config.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/console.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/constants.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/detection.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/detection_types.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/install.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/install_types.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/operation_handlers.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/package_info.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/package_managers.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/prereqs.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/status_print.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/src/code_aide/versions.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_commands_tools.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_config.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_console.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_constants.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_detection.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_entry.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_error_paths.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_install.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_install_types.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_package_managers.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/tests/test_prereqs.py +0 -0
- {code_aide-1.17.0 → code_aide-1.18.1}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: code-aide
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.1
|
|
4
4
|
Summary: Manage AI coding CLI tools (Claude, Antigravity, 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
|
|
@@ -49,6 +49,9 @@ pipx install code-aide
|
|
|
49
49
|
## Usage
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
+
# Show compact one-line-per-tool status (default when no command is given)
|
|
53
|
+
code-aide
|
|
54
|
+
|
|
52
55
|
# List available tools and their status
|
|
53
56
|
code-aide list
|
|
54
57
|
|
|
@@ -67,6 +70,9 @@ code-aide install
|
|
|
67
70
|
# Install with automatic prerequisite installation (Node.js, npm)
|
|
68
71
|
code-aide install -p
|
|
69
72
|
|
|
73
|
+
# Verify checksums and show what would be installed, without installing
|
|
74
|
+
code-aide install -n
|
|
75
|
+
|
|
70
76
|
# Upgrade installed tools (no args = only out-of-date tools)
|
|
71
77
|
code-aide upgrade [NAMES]
|
|
72
78
|
|
|
@@ -79,6 +85,9 @@ code-aide remove -n
|
|
|
79
85
|
# Remove all managed tools without prompting
|
|
80
86
|
code-aide remove -y
|
|
81
87
|
|
|
88
|
+
# Remove stale version directories of direct-download tools
|
|
89
|
+
code-aide clean [NAMES]
|
|
90
|
+
|
|
82
91
|
# Check upstream for latest versions (dry-run)
|
|
83
92
|
code-aide update-versions -n
|
|
84
93
|
|
|
@@ -163,6 +172,15 @@ consulted when the relevant install method or platform applies.
|
|
|
163
172
|
- PATH configuration validation and warnings
|
|
164
173
|
- Supports Linux and macOS
|
|
165
174
|
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
- Tool definitions are bundled at `src/code_aide/data/tools.json`.
|
|
178
|
+
- The version cache lives at `~/.config/code-aide/versions.json` by default; set
|
|
179
|
+
`XDG_CONFIG_HOME` to relocate the `code-aide` config directory.
|
|
180
|
+
- Color output honors `NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`, `CLICOLOR=0`,
|
|
181
|
+
and `TERM=dumb`, in that priority order; color is disabled when stdout is not
|
|
182
|
+
a terminal.
|
|
183
|
+
|
|
166
184
|
## Requirements
|
|
167
185
|
|
|
168
186
|
- Python 3.11+
|
|
@@ -22,6 +22,9 @@ pipx install code-aide
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
+
# Show compact one-line-per-tool status (default when no command is given)
|
|
26
|
+
code-aide
|
|
27
|
+
|
|
25
28
|
# List available tools and their status
|
|
26
29
|
code-aide list
|
|
27
30
|
|
|
@@ -40,6 +43,9 @@ code-aide install
|
|
|
40
43
|
# Install with automatic prerequisite installation (Node.js, npm)
|
|
41
44
|
code-aide install -p
|
|
42
45
|
|
|
46
|
+
# Verify checksums and show what would be installed, without installing
|
|
47
|
+
code-aide install -n
|
|
48
|
+
|
|
43
49
|
# Upgrade installed tools (no args = only out-of-date tools)
|
|
44
50
|
code-aide upgrade [NAMES]
|
|
45
51
|
|
|
@@ -52,6 +58,9 @@ code-aide remove -n
|
|
|
52
58
|
# Remove all managed tools without prompting
|
|
53
59
|
code-aide remove -y
|
|
54
60
|
|
|
61
|
+
# Remove stale version directories of direct-download tools
|
|
62
|
+
code-aide clean [NAMES]
|
|
63
|
+
|
|
55
64
|
# Check upstream for latest versions (dry-run)
|
|
56
65
|
code-aide update-versions -n
|
|
57
66
|
|
|
@@ -136,6 +145,15 @@ consulted when the relevant install method or platform applies.
|
|
|
136
145
|
- PATH configuration validation and warnings
|
|
137
146
|
- Supports Linux and macOS
|
|
138
147
|
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
- Tool definitions are bundled at `src/code_aide/data/tools.json`.
|
|
151
|
+
- The version cache lives at `~/.config/code-aide/versions.json` by default; set
|
|
152
|
+
`XDG_CONFIG_HOME` to relocate the `code-aide` config directory.
|
|
153
|
+
- Color output honors `NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`, `CLICOLOR=0`,
|
|
154
|
+
and `TERM=dumb`, in that priority order; color is disabled when stdout is not
|
|
155
|
+
a terminal.
|
|
156
|
+
|
|
139
157
|
## Requirements
|
|
140
158
|
|
|
141
159
|
- Python 3.11+
|
code_aide-1.18.1/TODO.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
Keep the project focused on managing AI coding CLI tools (install, upgrade,
|
|
4
|
+
remove, status, and version metadata).
|
|
5
|
+
|
|
6
|
+
All items below are deferred; none are scheduled.
|
|
7
|
+
|
|
8
|
+
| # | Item | Risk | Effort | Notes |
|
|
9
|
+
|:---|:-------------------------------|:-----|:-------|:-------------------------------------------------------|
|
|
10
|
+
| 1 | `doctor` command | Low | Med | Health checks exist but are scattered |
|
|
11
|
+
| 2 | Cross-distro package detection | Med | High | Only Gentoo supported; other Linux distros get nothing |
|
|
12
|
+
| 3 | Split large modules | Low | High | Refactoring only; no behavior change |
|
|
13
|
+
| 4 | Tests for edge cases | Med | Med | PATH oddities, more version parsing corners |
|
|
14
|
+
|
|
15
|
+
## CLI and Automation UX
|
|
16
|
+
|
|
17
|
+
- [ ] Add a focused `doctor` command for environment checks (PATH,
|
|
18
|
+
prerequisites, command health).
|
|
19
|
+
|
|
20
|
+
## Platform and Package Detection
|
|
21
|
+
|
|
22
|
+
- [ ] Broaden system package metadata detection beyond Gentoo-specific tooling
|
|
23
|
+
where practical; note that non-Gentoo system installs currently report "up to
|
|
24
|
+
date" because no available version is detectable.
|
|
25
|
+
|
|
26
|
+
## Maintainability and Tests
|
|
27
|
+
|
|
28
|
+
- [ ] Split larger modules into smaller focused files where practical
|
|
29
|
+
(`install.py`, `commands_actions.py`, `versions.py`).
|
|
30
|
+
- [ ] Add tests for more prerequisite edge cases and PATH oddities.
|
|
@@ -347,6 +347,8 @@ install_amp_binary() {
|
|
|
347
347
|
fi
|
|
348
348
|
|
|
349
349
|
local binary_path="$BIN_DIR/$binary_name"
|
|
350
|
+
local staged_binary_path
|
|
351
|
+
staged_binary_path=$(mktemp "$BIN_DIR/tmp.XXXXXX")
|
|
350
352
|
local checksum_path="$AMP_HOME/amp-install-checksum.txt"
|
|
351
353
|
|
|
352
354
|
# Download checksum first
|
|
@@ -356,21 +358,22 @@ install_amp_binary() {
|
|
|
356
358
|
|
|
357
359
|
if has_gzip_support; then
|
|
358
360
|
# Download compressed binary and verify the decompressed bytes against checksum
|
|
359
|
-
download_gzipped_file "${binary_url}.gz" "$
|
|
361
|
+
download_gzipped_file "${binary_url}.gz" "$staged_binary_path" 'Amp binary'
|
|
360
362
|
else
|
|
361
363
|
warn "gzip not found; downloading uncompressed binary"
|
|
362
|
-
download_file "$binary_url" "$
|
|
364
|
+
download_file "$binary_url" "$staged_binary_path" 'Amp binary'
|
|
363
365
|
fi
|
|
364
366
|
|
|
365
367
|
# Verify checksum
|
|
366
|
-
verify_checksum "$
|
|
368
|
+
verify_checksum "$staged_binary_path" "$expected_checksum"
|
|
367
369
|
|
|
368
370
|
# Verify signature (optional, only if minisign is installed)
|
|
369
371
|
# Disabled until release signing is enabled
|
|
370
|
-
# verify_signature "$
|
|
372
|
+
# verify_signature "$staged_binary_path" "$minisign_signature_url"
|
|
371
373
|
|
|
372
|
-
# Make executable
|
|
373
|
-
chmod
|
|
374
|
+
# Make executable before atomically replacing the installed binary
|
|
375
|
+
chmod 0755 "$staged_binary_path"
|
|
376
|
+
mv "$staged_binary_path" "$binary_path"
|
|
374
377
|
|
|
375
378
|
# Clean up checksum file
|
|
376
379
|
rm -f "$checksum_path"
|
|
@@ -16,6 +16,7 @@ from code_aide.install_types import (
|
|
|
16
16
|
from code_aide.console import error, info, success, warning
|
|
17
17
|
from code_aide.operations import (
|
|
18
18
|
UpgradeResult,
|
|
19
|
+
clean_tool_versions,
|
|
19
20
|
remove_tool,
|
|
20
21
|
upgrade_tool,
|
|
21
22
|
validate_tools,
|
|
@@ -46,6 +47,7 @@ from code_aide.config import (
|
|
|
46
47
|
def cmd_install(args: argparse.Namespace) -> None:
|
|
47
48
|
"""Handle install command."""
|
|
48
49
|
dryrun = getattr(args, "dryrun", False)
|
|
50
|
+
force = getattr(args, "force", False)
|
|
49
51
|
|
|
50
52
|
if args.tools:
|
|
51
53
|
tools_to_install = args.tools
|
|
@@ -86,7 +88,7 @@ def cmd_install(args: argparse.Namespace) -> None:
|
|
|
86
88
|
else:
|
|
87
89
|
info(f"=== Installing {tool} ===")
|
|
88
90
|
|
|
89
|
-
outcome = install_tool(tool, dryrun=dryrun)
|
|
91
|
+
outcome = install_tool(tool, dryrun=dryrun, force=force)
|
|
90
92
|
if outcome:
|
|
91
93
|
installed.append(tool)
|
|
92
94
|
installed_bin_dirs.extend(outcome.bin_dirs)
|
|
@@ -294,6 +296,56 @@ def cmd_remove(args: argparse.Namespace) -> None:
|
|
|
294
296
|
info("No tools were removed (all were not installed)")
|
|
295
297
|
|
|
296
298
|
|
|
299
|
+
def cmd_clean(args: argparse.Namespace) -> None:
|
|
300
|
+
"""Handle clean command: remove stale direct-download version dirs."""
|
|
301
|
+
dryrun = getattr(args, "dryrun", False)
|
|
302
|
+
|
|
303
|
+
if args.tools:
|
|
304
|
+
tools_to_clean = resolve_tool_names(args.tools, TOOLS)
|
|
305
|
+
else:
|
|
306
|
+
tools_to_clean = [
|
|
307
|
+
name
|
|
308
|
+
for name, config in TOOLS.items()
|
|
309
|
+
if parse_install_type(config.get("install_type"))
|
|
310
|
+
== InstallType.DIRECT_DOWNLOAD
|
|
311
|
+
]
|
|
312
|
+
if not tools_to_clean:
|
|
313
|
+
info("No direct-download tools with cleanable versions")
|
|
314
|
+
return
|
|
315
|
+
info(
|
|
316
|
+
"No tools specified, cleaning direct-download tools: "
|
|
317
|
+
f"{', '.join(tools_to_clean)}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
validate_tools(tools_to_clean)
|
|
321
|
+
|
|
322
|
+
cleaned: Dict[str, List[str]] = {}
|
|
323
|
+
skipped = []
|
|
324
|
+
for tool in tools_to_clean:
|
|
325
|
+
print()
|
|
326
|
+
info(f"=== {'Checking' if dryrun else 'Cleaning'} {tool} ===")
|
|
327
|
+
removed = clean_tool_versions(tool, dryrun=dryrun)
|
|
328
|
+
if removed is None:
|
|
329
|
+
skipped.append(tool)
|
|
330
|
+
elif removed:
|
|
331
|
+
cleaned[tool] = removed
|
|
332
|
+
|
|
333
|
+
print()
|
|
334
|
+
print("=" * 42)
|
|
335
|
+
info("Cleanup Summary" + (" (dry run)" if dryrun else ""))
|
|
336
|
+
print("=" * 42)
|
|
337
|
+
|
|
338
|
+
if cleaned:
|
|
339
|
+
total = sum(len(paths) for paths in cleaned.values())
|
|
340
|
+
details = ", ".join(f"{tool} ({len(paths)})" for tool, paths in cleaned.items())
|
|
341
|
+
noun = "Would remove" if dryrun else "Removed"
|
|
342
|
+
success(f"{noun} {total} stale version directories: {details}")
|
|
343
|
+
if skipped:
|
|
344
|
+
info(f"Not cleanable (no versioned install layout): {', '.join(skipped)}")
|
|
345
|
+
if not cleaned:
|
|
346
|
+
success("Nothing to clean!")
|
|
347
|
+
|
|
348
|
+
|
|
297
349
|
def cmd_update_versions(args: argparse.Namespace) -> None:
|
|
298
350
|
"""Handle update-versions command: check upstream for latest tool versions."""
|
|
299
351
|
bundled = load_bundled_tools()
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"command": "amp",
|
|
90
90
|
"install_type": "script",
|
|
91
91
|
"install_url": "https://ampcode.com/install.sh",
|
|
92
|
-
"install_sha256": "
|
|
92
|
+
"install_sha256": "07bea1669144a5191c873a61a3244266ca046557bff7123f0be41d0723592297",
|
|
93
93
|
"install_script_path_prepend": [
|
|
94
94
|
"~/.local/bin"
|
|
95
95
|
],
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
|
|
6
6
|
from code_aide import __version__
|
|
7
7
|
from code_aide.commands_actions import (
|
|
8
|
+
cmd_clean,
|
|
8
9
|
cmd_install,
|
|
9
10
|
cmd_remove,
|
|
10
11
|
cmd_update_versions,
|
|
@@ -83,6 +84,12 @@ def main() -> None:
|
|
|
83
84
|
action="store_true",
|
|
84
85
|
help="Verify SHA256 checksums without installing (dry run mode)",
|
|
85
86
|
)
|
|
87
|
+
install_parser.add_argument(
|
|
88
|
+
"-f",
|
|
89
|
+
"--force",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Reinstall even if the command already exists in PATH",
|
|
92
|
+
)
|
|
86
93
|
install_parser.set_defaults(func=cmd_install)
|
|
87
94
|
|
|
88
95
|
upgrade_parser = subparsers.add_parser("upgrade", help="Upgrade tools")
|
|
@@ -111,6 +118,23 @@ def main() -> None:
|
|
|
111
118
|
action="store_true",
|
|
112
119
|
help="Do not prompt for confirmation",
|
|
113
120
|
)
|
|
121
|
+
clean_parser = subparsers.add_parser(
|
|
122
|
+
"clean",
|
|
123
|
+
help="Remove stale direct-download version directories",
|
|
124
|
+
)
|
|
125
|
+
clean_parser.add_argument(
|
|
126
|
+
"tools",
|
|
127
|
+
nargs="*",
|
|
128
|
+
help="Tools to clean (default: direct-download tools)",
|
|
129
|
+
)
|
|
130
|
+
clean_parser.add_argument(
|
|
131
|
+
"-n",
|
|
132
|
+
"--dryrun",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="Show what would be removed without removing",
|
|
135
|
+
)
|
|
136
|
+
clean_parser.set_defaults(func=cmd_clean)
|
|
137
|
+
|
|
114
138
|
remove_parser.set_defaults(func=cmd_remove)
|
|
115
139
|
|
|
116
140
|
update_versions_parser = subparsers.add_parser(
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Upgrade and remove operations for managed tools."""
|
|
2
2
|
|
|
3
|
+
import fnmatch
|
|
3
4
|
import os
|
|
5
|
+
import shutil
|
|
4
6
|
import subprocess
|
|
5
7
|
import sys
|
|
6
8
|
from enum import Enum
|
|
7
|
-
from typing import Dict, List, TypedDict
|
|
9
|
+
from typing import Dict, List, Optional, TypedDict
|
|
8
10
|
|
|
9
11
|
from code_aide.constants import TOOLS
|
|
10
12
|
from code_aide.detection import (
|
|
@@ -270,6 +272,68 @@ def remove_tool(tool_name: str) -> bool:
|
|
|
270
272
|
return False
|
|
271
273
|
|
|
272
274
|
|
|
275
|
+
def clean_tool_versions(tool_name: str, dryrun: bool = False) -> Optional[List[str]]:
|
|
276
|
+
"""Remove stale version directories for a direct-download tool.
|
|
277
|
+
|
|
278
|
+
Keeps the directory the tool's symlinks currently point at. Returns
|
|
279
|
+
the list of removed (or, with dryrun, removable) directory paths,
|
|
280
|
+
or None when the tool has no versioned install layout.
|
|
281
|
+
"""
|
|
282
|
+
tool_config = TOOLS[tool_name]
|
|
283
|
+
install_dir_template = tool_config.get("install_dir", "")
|
|
284
|
+
if not install_dir_template or "{version}" not in install_dir_template:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
expanded = os.path.expanduser(install_dir_template)
|
|
288
|
+
parent = os.path.dirname(expanded)
|
|
289
|
+
if not parent or not os.path.isdir(parent):
|
|
290
|
+
info(f"No install directory for {tool_config['name']}; nothing to clean")
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
# Only consider directories matching the versioned template shape.
|
|
294
|
+
pattern = os.path.basename(expanded).replace("{version}", "*")
|
|
295
|
+
|
|
296
|
+
# Never remove the version the symlinks currently point at.
|
|
297
|
+
current_dirs = set()
|
|
298
|
+
bin_dir = os.path.expanduser(tool_config.get("bin_dir", ""))
|
|
299
|
+
for link_name in tool_config.get("symlinks", {}):
|
|
300
|
+
link_path = os.path.join(bin_dir, link_name)
|
|
301
|
+
if os.path.islink(link_path):
|
|
302
|
+
current_dirs.add(os.path.dirname(os.path.realpath(link_path)))
|
|
303
|
+
|
|
304
|
+
if not current_dirs:
|
|
305
|
+
warning(
|
|
306
|
+
f"Could not determine the current version directory of "
|
|
307
|
+
f"{tool_config['name']}; not removing anything"
|
|
308
|
+
)
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
stale = []
|
|
312
|
+
for entry in sorted(os.listdir(parent)):
|
|
313
|
+
full = os.path.join(parent, entry)
|
|
314
|
+
if not os.path.isdir(full) or os.path.islink(full):
|
|
315
|
+
continue
|
|
316
|
+
if not fnmatch.fnmatch(entry, pattern):
|
|
317
|
+
continue
|
|
318
|
+
if full in current_dirs or os.path.realpath(full) in current_dirs:
|
|
319
|
+
continue
|
|
320
|
+
stale.append(full)
|
|
321
|
+
|
|
322
|
+
if not stale:
|
|
323
|
+
info(f"No stale version directories for {tool_config['name']}")
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
if dryrun:
|
|
327
|
+
for path in stale:
|
|
328
|
+
info(f"[DRYRUN] Would remove stale version directory: {path}")
|
|
329
|
+
return stale
|
|
330
|
+
|
|
331
|
+
for path in stale:
|
|
332
|
+
shutil.rmtree(path)
|
|
333
|
+
info(f"Removed stale version directory: {path}")
|
|
334
|
+
return stale
|
|
335
|
+
|
|
336
|
+
|
|
273
337
|
def validate_tools(tools: List[str]) -> None:
|
|
274
338
|
"""Validate that all tool names are valid."""
|
|
275
339
|
invalid_tools = [tool for tool in tools if tool not in TOOLS]
|
|
@@ -360,8 +360,11 @@ def get_tool_status(tool_name: str, tool_config: Dict[str, Any]) -> ToolStatus:
|
|
|
360
360
|
check=False,
|
|
361
361
|
stdin=subprocess.DEVNULL,
|
|
362
362
|
)
|
|
363
|
-
if result.returncode == 0
|
|
364
|
-
|
|
363
|
+
if result.returncode == 0:
|
|
364
|
+
# Some tools print their version to stderr; prefer stdout.
|
|
365
|
+
output = result.stdout.strip() or result.stderr.strip()
|
|
366
|
+
if output:
|
|
367
|
+
status_info["version"] = output.split("\n")[0]
|
|
365
368
|
except subprocess.TimeoutExpired:
|
|
366
369
|
status_info["errors"].append("Version check timed out after 10s")
|
|
367
370
|
except Exception:
|
|
@@ -48,9 +48,74 @@ class TestCmdInstall(unittest.TestCase):
|
|
|
48
48
|
with contextlib.redirect_stdout(buf):
|
|
49
49
|
commands_actions.cmd_install(args)
|
|
50
50
|
|
|
51
|
-
mock_install_tool.assert_called_once_with(
|
|
51
|
+
mock_install_tool.assert_called_once_with(
|
|
52
|
+
"default_tool", dryrun=True, force=False
|
|
53
|
+
)
|
|
52
54
|
mock_prereqs.assert_not_called()
|
|
53
55
|
|
|
56
|
+
def test_passes_force_flag_to_install_tool(self):
|
|
57
|
+
tools = {
|
|
58
|
+
"test": {
|
|
59
|
+
"name": "Test Tool",
|
|
60
|
+
"command": "test",
|
|
61
|
+
"install_type": "npm",
|
|
62
|
+
"next_steps": "run test",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
args = type(
|
|
66
|
+
"Args",
|
|
67
|
+
(),
|
|
68
|
+
{
|
|
69
|
+
"tools": ["test"],
|
|
70
|
+
"dryrun": False,
|
|
71
|
+
"install_prerequisites": False,
|
|
72
|
+
"force": True,
|
|
73
|
+
},
|
|
74
|
+
)()
|
|
75
|
+
|
|
76
|
+
with (
|
|
77
|
+
mock.patch.dict(constants._TOOLS_DATA, tools, clear=True),
|
|
78
|
+
mock.patch.object(
|
|
79
|
+
commands_actions,
|
|
80
|
+
"install_tool",
|
|
81
|
+
return_value=InstallOutcome(True),
|
|
82
|
+
) as mock_install,
|
|
83
|
+
):
|
|
84
|
+
commands_actions.cmd_install(args)
|
|
85
|
+
|
|
86
|
+
mock_install.assert_called_once_with("test", dryrun=False, force=True)
|
|
87
|
+
|
|
88
|
+
def test_defaults_to_force_disabled(self):
|
|
89
|
+
tools = {
|
|
90
|
+
"test": {
|
|
91
|
+
"name": "Test Tool",
|
|
92
|
+
"command": "test",
|
|
93
|
+
"install_type": "npm",
|
|
94
|
+
"next_steps": "run test",
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
args = type(
|
|
98
|
+
"Args",
|
|
99
|
+
(),
|
|
100
|
+
{
|
|
101
|
+
"tools": ["test"],
|
|
102
|
+
"dryrun": False,
|
|
103
|
+
"install_prerequisites": False,
|
|
104
|
+
},
|
|
105
|
+
)()
|
|
106
|
+
|
|
107
|
+
with (
|
|
108
|
+
mock.patch.dict(constants._TOOLS_DATA, tools, clear=True),
|
|
109
|
+
mock.patch.object(
|
|
110
|
+
commands_actions,
|
|
111
|
+
"install_tool",
|
|
112
|
+
return_value=InstallOutcome(True),
|
|
113
|
+
) as mock_install,
|
|
114
|
+
):
|
|
115
|
+
commands_actions.cmd_install(args)
|
|
116
|
+
|
|
117
|
+
mock_install.assert_called_once_with("test", dryrun=False, force=False)
|
|
118
|
+
|
|
54
119
|
def test_checks_bin_directories_reported_by_installers(self):
|
|
55
120
|
tools = {
|
|
56
121
|
"test": {
|
|
@@ -111,7 +176,7 @@ class TestCmdInstall(unittest.TestCase):
|
|
|
111
176
|
):
|
|
112
177
|
commands_actions.cmd_install(args)
|
|
113
178
|
|
|
114
|
-
mock_install.assert_called_once_with("antigravity", dryrun=False)
|
|
179
|
+
mock_install.assert_called_once_with("antigravity", dryrun=False, force=False)
|
|
115
180
|
|
|
116
181
|
|
|
117
182
|
class TestCmdUpdateVersions(unittest.TestCase):
|
|
@@ -580,3 +645,88 @@ class TestCmdRemove(unittest.TestCase):
|
|
|
580
645
|
|
|
581
646
|
mock_input.assert_not_called()
|
|
582
647
|
mock_remove.assert_called_once_with("a")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class TestCmdClean(unittest.TestCase):
|
|
651
|
+
"""Tests for cmd_clean selection and dry-run behavior."""
|
|
652
|
+
|
|
653
|
+
def _tools(self):
|
|
654
|
+
return {
|
|
655
|
+
"dl": {
|
|
656
|
+
"name": "DL Tool",
|
|
657
|
+
"command": "dl",
|
|
658
|
+
"install_type": "direct_download",
|
|
659
|
+
"install_dir": "~/.local/share/dl/versions/{version}",
|
|
660
|
+
},
|
|
661
|
+
"npmtool": {
|
|
662
|
+
"name": "NPM Tool",
|
|
663
|
+
"command": "npmtool",
|
|
664
|
+
"install_type": "npm",
|
|
665
|
+
"npm_package": "npmtool",
|
|
666
|
+
},
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
def test_default_clean_targets_only_direct_download_tools(self):
|
|
670
|
+
args = type("Args", (), {"tools": [], "dryrun": True})()
|
|
671
|
+
with (
|
|
672
|
+
mock.patch.dict(constants._TOOLS_DATA, self._tools(), clear=True),
|
|
673
|
+
mock.patch.object(
|
|
674
|
+
commands_actions, "clean_tool_versions", return_value=[]
|
|
675
|
+
) as mock_clean,
|
|
676
|
+
contextlib.redirect_stdout(io.StringIO()) as buf,
|
|
677
|
+
):
|
|
678
|
+
commands_actions.cmd_clean(args)
|
|
679
|
+
|
|
680
|
+
mock_clean.assert_called_once_with("dl", dryrun=True)
|
|
681
|
+
self.assertIn("cleaning direct-download tools: dl", buf.getvalue())
|
|
682
|
+
|
|
683
|
+
def test_named_non_cleanable_tool_is_reported(self):
|
|
684
|
+
args = type("Args", (), {"tools": ["npmtool"], "dryrun": False})()
|
|
685
|
+
with (
|
|
686
|
+
mock.patch.dict(constants._TOOLS_DATA, self._tools(), clear=True),
|
|
687
|
+
mock.patch.object(
|
|
688
|
+
commands_actions, "clean_tool_versions", return_value=None
|
|
689
|
+
) as mock_clean,
|
|
690
|
+
contextlib.redirect_stdout(io.StringIO()) as buf,
|
|
691
|
+
):
|
|
692
|
+
commands_actions.cmd_clean(args)
|
|
693
|
+
|
|
694
|
+
mock_clean.assert_called_once_with("npmtool", dryrun=False)
|
|
695
|
+
self.assertIn(
|
|
696
|
+
"Not cleanable (no versioned install layout): npmtool", buf.getvalue()
|
|
697
|
+
)
|
|
698
|
+
self.assertIn("Nothing to clean!", buf.getvalue())
|
|
699
|
+
|
|
700
|
+
def test_dryrun_summary_is_labeled(self):
|
|
701
|
+
args = type("Args", (), {"tools": ["dl"], "dryrun": True})()
|
|
702
|
+
with (
|
|
703
|
+
mock.patch.dict(constants._TOOLS_DATA, self._tools(), clear=True),
|
|
704
|
+
mock.patch.object(
|
|
705
|
+
commands_actions,
|
|
706
|
+
"clean_tool_versions",
|
|
707
|
+
return_value=["/tmp/stale"],
|
|
708
|
+
),
|
|
709
|
+
contextlib.redirect_stdout(io.StringIO()) as buf,
|
|
710
|
+
):
|
|
711
|
+
commands_actions.cmd_clean(args)
|
|
712
|
+
|
|
713
|
+
self.assertIn("Cleanup Summary (dry run)", buf.getvalue())
|
|
714
|
+
self.assertIn(
|
|
715
|
+
"Would remove 1 stale version directories: dl (1)", buf.getvalue()
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def test_removal_summary_is_not_labeled_dry_run(self):
|
|
719
|
+
args = type("Args", (), {"tools": ["dl"], "dryrun": False})()
|
|
720
|
+
with (
|
|
721
|
+
mock.patch.dict(constants._TOOLS_DATA, self._tools(), clear=True),
|
|
722
|
+
mock.patch.object(
|
|
723
|
+
commands_actions,
|
|
724
|
+
"clean_tool_versions",
|
|
725
|
+
return_value=["/tmp/stale"],
|
|
726
|
+
),
|
|
727
|
+
contextlib.redirect_stdout(io.StringIO()) as buf,
|
|
728
|
+
):
|
|
729
|
+
commands_actions.cmd_clean(args)
|
|
730
|
+
|
|
731
|
+
self.assertIn("[INFO] Cleanup Summary", buf.getvalue())
|
|
732
|
+
self.assertNotIn("(dry run)", buf.getvalue())
|
|
@@ -512,3 +512,76 @@ class TestCmdUpgradeDefaultSelection(unittest.TestCase):
|
|
|
512
512
|
cli_commands_actions.cmd_upgrade(args)
|
|
513
513
|
|
|
514
514
|
mock_upgrade.assert_called_once_with("current")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class TestCleanToolVersions(unittest.TestCase):
|
|
518
|
+
"""Tests for clean_tool_versions."""
|
|
519
|
+
|
|
520
|
+
def _setup(self, td, template_name="versions/{version}"):
|
|
521
|
+
parent = os.path.join(td, os.path.dirname(template_name))
|
|
522
|
+
os.makedirs(parent, exist_ok=True)
|
|
523
|
+
version_dirs = []
|
|
524
|
+
for v in ("1.0.0", "2.0.0"):
|
|
525
|
+
version_dir = os.path.join(
|
|
526
|
+
parent, template_name.replace("{version}", v).split("/")[-1]
|
|
527
|
+
)
|
|
528
|
+
os.makedirs(version_dir)
|
|
529
|
+
with open(os.path.join(version_dir, "tool-bin"), "w") as f:
|
|
530
|
+
f.write("#!/bin/sh\n")
|
|
531
|
+
version_dirs.append(version_dir)
|
|
532
|
+
bin_dir = os.path.join(td, "bin")
|
|
533
|
+
os.makedirs(bin_dir)
|
|
534
|
+
os.symlink(
|
|
535
|
+
os.path.join(version_dirs[1], "tool-bin"),
|
|
536
|
+
os.path.join(bin_dir, "tool"),
|
|
537
|
+
)
|
|
538
|
+
return {
|
|
539
|
+
"name": "Test Tool",
|
|
540
|
+
"command": "tool",
|
|
541
|
+
"install_type": "direct_download",
|
|
542
|
+
"install_dir": os.path.join(td, template_name),
|
|
543
|
+
"bin_dir": bin_dir,
|
|
544
|
+
"symlinks": {"tool": "tool-bin"},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
def _clean(self, cfg, dryrun=False):
|
|
548
|
+
with mock.patch.dict(constants._TOOLS_DATA, {"test": cfg}, clear=True):
|
|
549
|
+
return cli_operations.clean_tool_versions("test", dryrun=dryrun)
|
|
550
|
+
|
|
551
|
+
def test_dryrun_lists_stale_without_removing(self):
|
|
552
|
+
with tempfile.TemporaryDirectory() as td:
|
|
553
|
+
cfg = self._setup(td)
|
|
554
|
+
stale = self._clean(cfg, dryrun=True)
|
|
555
|
+
self.assertEqual(stale, [os.path.join(td, "versions", "1.0.0")])
|
|
556
|
+
self.assertTrue(os.path.isdir(os.path.join(td, "versions", "1.0.0")))
|
|
557
|
+
|
|
558
|
+
def test_removes_stale_and_keeps_current(self):
|
|
559
|
+
with tempfile.TemporaryDirectory() as td:
|
|
560
|
+
cfg = self._setup(td)
|
|
561
|
+
stale = self._clean(cfg)
|
|
562
|
+
self.assertEqual(stale, [os.path.join(td, "versions", "1.0.0")])
|
|
563
|
+
self.assertFalse(os.path.exists(os.path.join(td, "versions", "1.0.0")))
|
|
564
|
+
self.assertTrue(os.path.isdir(os.path.join(td, "versions", "2.0.0")))
|
|
565
|
+
|
|
566
|
+
def test_prefix_pattern_leaves_unrelated_dirs(self):
|
|
567
|
+
with tempfile.TemporaryDirectory() as td:
|
|
568
|
+
cfg = self._setup(td, template_name="store/pkg-{version}")
|
|
569
|
+
os.makedirs(os.path.join(td, "store", "data"))
|
|
570
|
+
stale = self._clean(cfg)
|
|
571
|
+
self.assertEqual(stale, [os.path.join(td, "store", "pkg-1.0.0")])
|
|
572
|
+
self.assertTrue(os.path.isdir(os.path.join(td, "store", "data")))
|
|
573
|
+
|
|
574
|
+
def test_no_current_version_removes_nothing(self):
|
|
575
|
+
with tempfile.TemporaryDirectory() as td:
|
|
576
|
+
cfg = self._setup(td)
|
|
577
|
+
os.remove(os.path.join(td, "bin", "tool"))
|
|
578
|
+
stale = self._clean(cfg)
|
|
579
|
+
self.assertEqual(stale, [])
|
|
580
|
+
self.assertTrue(os.path.isdir(os.path.join(td, "versions", "1.0.0")))
|
|
581
|
+
self.assertTrue(os.path.isdir(os.path.join(td, "versions", "2.0.0")))
|
|
582
|
+
|
|
583
|
+
def test_tool_without_versioned_layout_returns_none(self):
|
|
584
|
+
with tempfile.TemporaryDirectory() as td:
|
|
585
|
+
cfg = self._setup(td)
|
|
586
|
+
cfg["install_dir"] = os.path.join(td, "flat")
|
|
587
|
+
self.assertIsNone(self._clean(cfg))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Unit tests for package version info lookups."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest import mock
|
|
6
|
+
|
|
7
|
+
from code_aide import package_info as cli_package_info
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestGetSystemPackageInfo(unittest.TestCase):
|
|
11
|
+
"""Tests for get_system_package_info."""
|
|
12
|
+
|
|
13
|
+
def test_returns_empty_info_when_qfile_missing(self):
|
|
14
|
+
with mock.patch.object(cli_package_info, "command_exists", return_value=False):
|
|
15
|
+
info = cli_package_info.get_system_package_info("/usr/bin/tool")
|
|
16
|
+
self.assertEqual(
|
|
17
|
+
info,
|
|
18
|
+
{
|
|
19
|
+
"package": None,
|
|
20
|
+
"installed_version": None,
|
|
21
|
+
"available_version": None,
|
|
22
|
+
"available_date": None,
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def test_returns_empty_info_when_qfile_finds_nothing(self):
|
|
27
|
+
no_match = subprocess.CompletedProcess(["qfile"], 1, stdout="", stderr="")
|
|
28
|
+
with (
|
|
29
|
+
mock.patch.object(cli_package_info, "command_exists", return_value=True),
|
|
30
|
+
mock.patch.object(
|
|
31
|
+
cli_package_info.subprocess, "run", return_value=no_match
|
|
32
|
+
),
|
|
33
|
+
):
|
|
34
|
+
info = cli_package_info.get_system_package_info("/usr/bin/tool")
|
|
35
|
+
self.assertIsNone(info["package"])
|
|
36
|
+
self.assertIsNone(info["installed_version"])
|
|
37
|
+
|
|
38
|
+
def test_resolves_installed_version_through_qlist_and_qatom(self):
|
|
39
|
+
responses = [
|
|
40
|
+
subprocess.CompletedProcess(
|
|
41
|
+
["qfile"], 0, stdout="dev-util/tool\n", stderr=""
|
|
42
|
+
),
|
|
43
|
+
subprocess.CompletedProcess(
|
|
44
|
+
["qlist"], 0, stdout="dev-util/tool-1.4.2\n", stderr=""
|
|
45
|
+
),
|
|
46
|
+
subprocess.CompletedProcess(["qatom"], 0, stdout="1.4.2\n", stderr=""),
|
|
47
|
+
]
|
|
48
|
+
with (
|
|
49
|
+
mock.patch.object(cli_package_info, "command_exists", return_value=True),
|
|
50
|
+
mock.patch.object(
|
|
51
|
+
cli_package_info.subprocess, "run", side_effect=responses
|
|
52
|
+
),
|
|
53
|
+
mock.patch.object(cli_package_info.globmod, "glob", return_value=[]),
|
|
54
|
+
):
|
|
55
|
+
info = cli_package_info.get_system_package_info("/usr/bin/tool")
|
|
56
|
+
self.assertEqual(info["package"], "dev-util/tool")
|
|
57
|
+
self.assertEqual(info["installed_version"], "1.4.2")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Unit tests for status-rendering helpers."""
|
|
2
2
|
|
|
3
|
+
import subprocess
|
|
3
4
|
import unittest
|
|
4
5
|
from unittest import mock
|
|
5
6
|
|
|
@@ -371,3 +372,48 @@ class TestToolUpgradeEvaluatorDiscovery(unittest.TestCase):
|
|
|
371
372
|
assessment.decision, cli_status.UpgradeDecision.PACKAGE_MANAGED
|
|
372
373
|
)
|
|
373
374
|
mock_pkg.assert_called_once_with("/usr/bin/sys-tool")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class TestGetToolStatus(unittest.TestCase):
|
|
378
|
+
"""Tests for get_tool_status version capture."""
|
|
379
|
+
|
|
380
|
+
_TOOL_CONFIG = {"name": "Test Tool", "command": "test-tool"}
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def _completed(returncode=0, stdout="", stderr=""):
|
|
384
|
+
return subprocess.CompletedProcess(
|
|
385
|
+
["test-tool", "--version"], returncode, stdout=stdout, stderr=stderr
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def _run(self, **kwargs):
|
|
389
|
+
with (
|
|
390
|
+
mock.patch.object(cli_status, "is_tool_installed", return_value=True),
|
|
391
|
+
mock.patch.object(
|
|
392
|
+
cli_status.subprocess, "run", return_value=self._completed(**kwargs)
|
|
393
|
+
),
|
|
394
|
+
):
|
|
395
|
+
return cli_status.get_tool_status("test", self._TOOL_CONFIG)
|
|
396
|
+
|
|
397
|
+
def test_captures_version_from_stdout(self):
|
|
398
|
+
status = self._run(stdout="1.2.3\n")
|
|
399
|
+
self.assertEqual(status["version"], "1.2.3")
|
|
400
|
+
|
|
401
|
+
def test_captures_version_from_stderr_when_stdout_empty(self):
|
|
402
|
+
status = self._run(stdout="", stderr="2.0.0\n")
|
|
403
|
+
self.assertEqual(status["version"], "2.0.0")
|
|
404
|
+
|
|
405
|
+
def test_stdout_wins_over_stderr(self):
|
|
406
|
+
status = self._run(stdout="1.2.3\n", stderr="noise\n")
|
|
407
|
+
self.assertEqual(status["version"], "1.2.3")
|
|
408
|
+
|
|
409
|
+
def test_nonzero_exit_leaves_version_missing(self):
|
|
410
|
+
status = self._run(returncode=1, stdout="1.2.3\n")
|
|
411
|
+
self.assertIsNone(status["version"])
|
|
412
|
+
|
|
413
|
+
def test_not_installed_short_circuits(self):
|
|
414
|
+
with mock.patch.object(
|
|
415
|
+
cli_status, "is_tool_installed", return_value=False
|
|
416
|
+
) as mock_installed:
|
|
417
|
+
status = cli_status.get_tool_status("test", self._TOOL_CONFIG)
|
|
418
|
+
mock_installed.assert_called_once_with("test")
|
|
419
|
+
self.assertIsNone(status["version"])
|
|
@@ -420,3 +420,54 @@ class TestExtractScriptVersion(unittest.TestCase):
|
|
|
420
420
|
)
|
|
421
421
|
|
|
422
422
|
self.assertEqual(result, "1.0.0")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class TestFetchUrl(unittest.TestCase):
|
|
426
|
+
"""Tests for fetch_url network fetch behavior."""
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _response(content=b"payload", last_modified="Tue, 25 Aug 2026 10:00:00 GMT"):
|
|
430
|
+
response = mock.Mock()
|
|
431
|
+
response.read.return_value = content
|
|
432
|
+
response.headers = {"Last-Modified": last_modified}
|
|
433
|
+
response.__enter__ = mock.Mock(return_value=response)
|
|
434
|
+
response.__exit__ = mock.Mock(return_value=False)
|
|
435
|
+
return response
|
|
436
|
+
|
|
437
|
+
def test_returns_content_and_last_modified(self):
|
|
438
|
+
with mock.patch.object(
|
|
439
|
+
cli_versions.urllib.request,
|
|
440
|
+
"urlopen",
|
|
441
|
+
return_value=self._response(content=b"abc"),
|
|
442
|
+
) as mock_urlopen:
|
|
443
|
+
content, last_modified = cli_versions.fetch_url("https://example.com/x")
|
|
444
|
+
self.assertEqual(content, b"abc")
|
|
445
|
+
self.assertEqual(last_modified, "Tue, 25 Aug 2026 10:00:00 GMT")
|
|
446
|
+
request = mock_urlopen.call_args[0][0]
|
|
447
|
+
self.assertEqual(request.full_url, "https://example.com/x")
|
|
448
|
+
self.assertEqual(mock_urlopen.call_args[1]["timeout"], 30)
|
|
449
|
+
|
|
450
|
+
def test_sends_code_aide_user_agent(self):
|
|
451
|
+
with mock.patch.object(
|
|
452
|
+
cli_versions.urllib.request, "urlopen", return_value=self._response()
|
|
453
|
+
) as mock_urlopen:
|
|
454
|
+
cli_versions.fetch_url("https://example.com/x")
|
|
455
|
+
request = mock_urlopen.call_args[0][0]
|
|
456
|
+
self.assertIn("code-aide/", request.headers.get("User-agent", ""))
|
|
457
|
+
|
|
458
|
+
def test_last_modified_is_none_when_header_absent(self):
|
|
459
|
+
with mock.patch.object(
|
|
460
|
+
cli_versions.urllib.request,
|
|
461
|
+
"urlopen",
|
|
462
|
+
return_value=self._response(last_modified=None),
|
|
463
|
+
):
|
|
464
|
+
content, last_modified = cli_versions.fetch_url("https://example.com/x")
|
|
465
|
+
self.assertEqual(content, b"payload")
|
|
466
|
+
self.assertIsNone(last_modified)
|
|
467
|
+
|
|
468
|
+
def test_custom_timeout_is_passed(self):
|
|
469
|
+
with mock.patch.object(
|
|
470
|
+
cli_versions.urllib.request, "urlopen", return_value=self._response()
|
|
471
|
+
) as mock_urlopen:
|
|
472
|
+
cli_versions.fetch_url("https://example.com/x", timeout=5)
|
|
473
|
+
self.assertEqual(mock_urlopen.call_args[1]["timeout"], 5)
|
code_aide-1.17.0/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
See AGENTS.md for project instructions.
|
code_aide-1.17.0/TODO.md
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# TODO
|
|
2
|
-
|
|
3
|
-
Keep the project focused on managing AI coding CLI tools (install, upgrade,
|
|
4
|
-
remove, status, and version metadata).
|
|
5
|
-
|
|
6
|
-
## Summary
|
|
7
|
-
|
|
8
|
-
| # | Item | Risk | Effort | Notes |
|
|
9
|
-
|:---|:--------------------------------------------|:---------|:--------|:-------------------------------------------------------------------|
|
|
10
|
-
| 1 | ~~Handle missing `node` cleanly~~ (done) | - | - | Fixed in `prereqs.py` `check_prerequisites()` |
|
|
11
|
-
| 2 | Read version from stdout+stderr | Med | Low | Some tools write version to stderr; shown as not installed |
|
|
12
|
-
| 3 | ~~Atomic version cache writes~~ (done) | - | - | Fixed in `config.py` `save_versions_cache()` |
|
|
13
|
-
| 4 | ~~Warn on invalid cache JSON~~ (done) | - | - | `config.py` `load_versions_cache()` logs a warning |
|
|
14
|
-
| 5 | ~~Use `os.pathsep` not `":"`~~ (done) | - | - | Fixed in `prereqs.py` `check_path_directories()` |
|
|
15
|
-
| 6 | ~~Verify direct-download tarballs~~ (done) | - | - | `download_sha256` recorded by update-versions, verified on install |
|
|
16
|
-
| 7 | ~~Tarball checksum metadata fields~~ (done) | - | - | `download_sha256` field in tools schema and versions cache |
|
|
17
|
-
| 8 | `--json` output mode | Low | Med | Needed for CI/automation consumers |
|
|
18
|
-
| 9 | `doctor` command | Low | Med | Health checks exist but are scattered |
|
|
19
|
-
| 10 | `install --force` | Low | Trivial | Internal `force` param exists; not exposed in CLI |
|
|
20
|
-
| 11 | Cleanup stale direct-download versions | Low | Med | Old version dirs accumulate on disk |
|
|
21
|
-
| 12 | Document default subcommand | Very Low | Trivial | One line in README |
|
|
22
|
-
| 13 | Document `install --dryrun` | Very Low | Trivial | One line in README |
|
|
23
|
-
| 14 | Document env vars and config paths | Low | Low | `XDG_CONFIG_HOME` undocumented |
|
|
24
|
-
| 15 | Cross-distro package detection | Med | High | Only Gentoo supported; other Linux distros get nothing |
|
|
25
|
-
| 16 | Split large modules | Low | High | Refactoring only; no behavior change |
|
|
26
|
-
| 17 | Tests for untested functions | Med | High | `fetch_url`, `get_system_package_info` |
|
|
27
|
-
| 18 | Tests for edge cases | Med | Med | PATH oddities, more version parsing corners |
|
|
28
|
-
|
|
29
|
-
## Reliability and Correctness
|
|
30
|
-
|
|
31
|
-
- [x] Handle missing `node` cleanly during prerequisite checks (`OSError` path
|
|
32
|
-
in Node version probing).
|
|
33
|
-
- [ ] Read tool version output from both stdout and stderr so status does not
|
|
34
|
-
miss installed versions.
|
|
35
|
-
- [x] Make version cache writes atomic (temp file + rename) to avoid
|
|
36
|
-
partial/corrupted `versions.json`.
|
|
37
|
-
- [x] Warn when `versions.json` cache contains invalid JSON instead of silently
|
|
38
|
-
returning empty data.
|
|
39
|
-
- [x] Use `os.pathsep` instead of hardcoded `":"` in `prereqs.py`
|
|
40
|
-
`check_path_directories()`.
|
|
41
|
-
|
|
42
|
-
## Security and Integrity
|
|
43
|
-
|
|
44
|
-
- [x] Verify direct-download tarballs against a checksum recorded by
|
|
45
|
-
`update-versions` (warn when no checksum has been recorded yet).
|
|
46
|
-
- [x] Extend tool metadata with a `download_sha256` field for tarball checksums.
|
|
47
|
-
- [ ] Consider signature verification (beyond SHA256) for downloads where
|
|
48
|
-
upstream publishes signatures.
|
|
49
|
-
|
|
50
|
-
## CLI and Automation UX
|
|
51
|
-
|
|
52
|
-
- [ ] Add `--json` output mode for `list`, `status`, and `update-versions`.
|
|
53
|
-
- [ ] Add a focused `doctor` command for environment checks (PATH,
|
|
54
|
-
prerequisites, command health).
|
|
55
|
-
- [ ] Consider `install --force` for reinstall/repair flows.
|
|
56
|
-
- [ ] Add cleanup support for stale direct-download versions no longer in use.
|
|
57
|
-
|
|
58
|
-
## Documentation
|
|
59
|
-
|
|
60
|
-
- [ ] Document that running `code-aide` with no subcommand defaults to `status`.
|
|
61
|
-
- [ ] Add `install --dryrun` to the README usage examples.
|
|
62
|
-
- [ ] Document environment variables and config file paths
|
|
63
|
-
(`~/.config/code-aide/versions.json`).
|
|
64
|
-
|
|
65
|
-
## Platform and Package Detection
|
|
66
|
-
|
|
67
|
-
- [ ] Broaden system package metadata detection beyond Gentoo-specific tooling
|
|
68
|
-
where practical.
|
|
69
|
-
|
|
70
|
-
## Maintainability and Tests
|
|
71
|
-
|
|
72
|
-
- [ ] Split larger modules into smaller focused files where practical
|
|
73
|
-
(`commands_actions.py`, `versions.py`, `install.py`).
|
|
74
|
-
- [ ] Add tests for remaining untested functions:
|
|
75
|
-
- `fetch_url()` (network; needs mocking)
|
|
76
|
-
- `get_system_package_info()` (Gentoo tooling)
|
|
77
|
-
- [ ] Add tests for more prerequisite edge cases and PATH oddities.
|
|
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
|
{code_aide-1.17.0 → code_aide-1.18.1}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_aide-1.17.0 → code_aide-1.18.1}/specs/unify-upgrade-eligibility-with-shared-evaluator.md
RENAMED
|
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
|