code-aide 1.0.3__tar.gz → 1.2.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 (36) hide show
  1. code_aide-1.2.0/.github/workflows/publish.yml +46 -0
  2. {code_aide-1.0.3 → code_aide-1.2.0}/AGENTS.md +3 -0
  3. {code_aide-1.0.3 → code_aide-1.2.0}/PKG-INFO +18 -10
  4. {code_aide-1.0.3 → code_aide-1.2.0}/README.md +17 -9
  5. code_aide-1.2.0/TODO.md +70 -0
  6. {code_aide-1.0.3 → code_aide-1.2.0}/pyproject.toml +4 -1
  7. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/__init__.py +1 -1
  8. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/commands_actions.py +19 -11
  9. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/config.py +26 -0
  10. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/data/tools.json +12 -7
  11. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/detection.py +3 -0
  12. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/entry.py +7 -1
  13. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/install.py +7 -2
  14. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_commands_actions.py +76 -2
  15. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_install.py +54 -0
  16. code_aide-1.0.3/.github/workflows/publish.yml +0 -20
  17. {code_aide-1.0.3 → code_aide-1.2.0}/.github/workflows/ci.yml +0 -0
  18. {code_aide-1.0.3 → code_aide-1.2.0}/.gitignore +0 -0
  19. {code_aide-1.0.3 → code_aide-1.2.0}/CLAUDE.md +0 -0
  20. {code_aide-1.0.3 → code_aide-1.2.0}/LICENSE +0 -0
  21. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/__main__.py +0 -0
  22. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/commands_tools.py +0 -0
  23. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/console.py +0 -0
  24. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/constants.py +0 -0
  25. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/operations.py +0 -0
  26. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/prereqs.py +0 -0
  27. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/status.py +0 -0
  28. {code_aide-1.0.3 → code_aide-1.2.0}/src/code_aide/versions.py +0 -0
  29. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_commands_tools.py +0 -0
  30. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_config.py +0 -0
  31. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_console.py +0 -0
  32. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_constants.py +0 -0
  33. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_detection.py +0 -0
  34. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_operations.py +0 -0
  35. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_status.py +0 -0
  36. {code_aide-1.0.3 → code_aide-1.2.0}/tests/test_versions.py +0 -0
@@ -0,0 +1,46 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ jobs:
6
+ publish:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ - uses: pypa/gh-action-pypi-publish@release/v1
18
+ - name: Build commit changelog
19
+ id: changelog
20
+ run: |
21
+ set -euo pipefail
22
+ current_tag="${GITHUB_REF_NAME}"
23
+ previous_tag="$(git describe --tags --abbrev=0 "${current_tag}^" 2>/dev/null || true)"
24
+
25
+ {
26
+ echo "body<<EOF"
27
+ echo "## Commits"
28
+ echo
29
+ if [ -n "${previous_tag}" ]; then
30
+ echo "Range: \`${previous_tag}..${current_tag}\`"
31
+ echo
32
+ git log --pretty=format:'* %s (%h)' "${previous_tag}..${current_tag}"
33
+ else
34
+ echo "First tagged release."
35
+ echo
36
+ git log --pretty=format:'* %s (%h)' "${current_tag}"
37
+ fi
38
+ echo
39
+ echo "EOF"
40
+ } >> "${GITHUB_OUTPUT}"
41
+ - name: Create GitHub release notes
42
+ uses: softprops/action-gh-release@v2
43
+ with:
44
+ tag_name: ${{ github.ref_name }}
45
+ generate_release_notes: true
46
+ body: ${{ steps.changelog.outputs.body }}
@@ -7,6 +7,9 @@
7
7
  user's local cache (~/.config/code-aide/versions.json)
8
8
  - All tests should pass before committing
9
9
  - Run `black` formatter on python before commits
10
+ - Write useful commit messages: start subjects with past-tense action verbs
11
+ (`Added`, `Changed`, `Fixed`, `Removed`), keep them user-facing, and keep
12
+ commits focused.
10
13
  - Python 3.11+ compatible
11
14
  - Keep files under 300 lines where practical
12
15
  - Run `format-markdown` on markdown files before commits (if it is available)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-aide
3
- Version: 1.0.3
3
+ Version: 1.2.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
@@ -73,6 +73,9 @@ code-aide update-versions -n
73
73
 
74
74
  # Update version cache
75
75
  code-aide update-versions -y
76
+
77
+ # Update bundled version baseline (developer use, before releases)
78
+ code-aide update-versions -b -y
76
79
  ```
77
80
 
78
81
  ## Supported Tools
@@ -137,27 +140,32 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
137
140
 
138
141
  `publish.yml` publishes to PyPI when a Git tag matching `v*` is pushed.
139
142
 
140
- 1. Update version strings:
141
- - `pyproject.toml` (`[project].version`)
142
- - `src/code_aide/__init__.py` (`__version__`)
143
- 2. Run checks:
143
+ 1. Update the bundled version baseline:
144
+ - `code-aide update-versions -b -y`
145
+ - `git add src/code_aide/data/tools.json`
146
+ - `git commit -m "Updated bundled version data"`
147
+ 2. Update the version string in `src/code_aide/__init__.py` (`__version__`).
148
+ `pyproject.toml` reads it automatically via Hatchling.
149
+ 3. Run checks:
144
150
  - `uv run pytest tests/ -v`
145
151
  - `uv build`
146
- 3. Commit the release version bump:
147
- - `git add pyproject.toml src/code_aide/__init__.py`
152
+ 4. Commit the release version bump:
153
+ - `git add src/code_aide/__init__.py`
148
154
  - `git commit -m "Bumped version to X.Y.Z"`
149
- 4. Write useful commit messages before tagging:
155
+ 5. Write useful commit messages before tagging:
150
156
  - Start subject lines with an action verb in past tense (`Added`, `Changed`,
151
157
  `Fixed`, `Removed`).
152
158
  - Keep subjects user-facing so auto-generated release notes are meaningful.
153
159
  - Group related changes into focused commits instead of one broad commit.
154
160
  - Example: `Fixed timeout handling in status command`
155
- 5. Tag and push:
161
+ 6. Tag and push:
156
162
  - `git tag vX.Y.Z`
157
163
  - `git push origin main --follow-tags`
158
- 6. Confirm GitHub Actions:
164
+ 7. Confirm GitHub Actions:
159
165
  - CI should pass.
160
166
  - Publish workflow should upload to PyPI and create GitHub Release notes.
167
+ - Release notes should include generated notes plus a commit summary from the
168
+ previous tag to the current tag.
161
169
 
162
170
  ## License
163
171
 
@@ -47,6 +47,9 @@ code-aide update-versions -n
47
47
 
48
48
  # Update version cache
49
49
  code-aide update-versions -y
50
+
51
+ # Update bundled version baseline (developer use, before releases)
52
+ code-aide update-versions -b -y
50
53
  ```
51
54
 
52
55
  ## Supported Tools
@@ -111,27 +114,32 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
111
114
 
112
115
  `publish.yml` publishes to PyPI when a Git tag matching `v*` is pushed.
113
116
 
114
- 1. Update version strings:
115
- - `pyproject.toml` (`[project].version`)
116
- - `src/code_aide/__init__.py` (`__version__`)
117
- 2. Run checks:
117
+ 1. Update the bundled version baseline:
118
+ - `code-aide update-versions -b -y`
119
+ - `git add src/code_aide/data/tools.json`
120
+ - `git commit -m "Updated bundled version data"`
121
+ 2. Update the version string in `src/code_aide/__init__.py` (`__version__`).
122
+ `pyproject.toml` reads it automatically via Hatchling.
123
+ 3. Run checks:
118
124
  - `uv run pytest tests/ -v`
119
125
  - `uv build`
120
- 3. Commit the release version bump:
121
- - `git add pyproject.toml src/code_aide/__init__.py`
126
+ 4. Commit the release version bump:
127
+ - `git add src/code_aide/__init__.py`
122
128
  - `git commit -m "Bumped version to X.Y.Z"`
123
- 4. Write useful commit messages before tagging:
129
+ 5. Write useful commit messages before tagging:
124
130
  - Start subject lines with an action verb in past tense (`Added`, `Changed`,
125
131
  `Fixed`, `Removed`).
126
132
  - Keep subjects user-facing so auto-generated release notes are meaningful.
127
133
  - Group related changes into focused commits instead of one broad commit.
128
134
  - Example: `Fixed timeout handling in status command`
129
- 5. Tag and push:
135
+ 6. Tag and push:
130
136
  - `git tag vX.Y.Z`
131
137
  - `git push origin main --follow-tags`
132
- 6. Confirm GitHub Actions:
138
+ 7. Confirm GitHub Actions:
133
139
  - CI should pass.
134
140
  - Publish workflow should upload to PyPI and create GitHub Release notes.
141
+ - Release notes should include generated notes plus a commit summary from the
142
+ previous tag to the current tag.
135
143
 
136
144
  ## License
137
145
 
@@ -0,0 +1,70 @@
1
+ # TODO
2
+
3
+ Keep the project focused on managing AI coding CLI tools (install, upgrade,
4
+ remove, status, and version metadata).
5
+
6
+ ## Bugs
7
+
8
+ - [x] Standardize `--dryrun` flag: `update-versions` uses `--dry-run` but
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()`).
12
+ - [x] Replace PID-based temp directory naming with `tempfile.mkdtemp()` in
13
+ `install.py` `install_direct_download()`.
14
+ - [x] Guard `package.split("/", 1)` in `detection.py`
15
+ `get_system_package_info()` against missing `/` in subprocess output.
16
+
17
+ ## Reliability and Correctness
18
+
19
+ - [ ] Handle missing `node` cleanly during prerequisite checks
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.
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.
27
+ - [ ] Use `os.pathsep` instead of hardcoded `":"` in `prereqs.py`
28
+ `check_path_directories()`.
29
+
30
+ ## Security and Integrity
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.
36
+
37
+ ## CLI and Automation UX
38
+
39
+ - [ ] Add `--json` output mode for `list`, `status`, and `update-versions`.
40
+ - [ ] Add a focused `doctor` command for environment checks (PATH,
41
+ prerequisites, command health).
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.
46
+
47
+ ## Documentation
48
+
49
+ - [ ] Document that running `code-aide` with no subcommand defaults to `status`.
50
+ - [ ] Add `install --dryrun` to the README usage examples.
51
+ - [ ] Document environment variables and config file paths
52
+ (`~/.config/code-aide/versions.json`).
53
+
54
+ ## Platform and Package Detection
55
+
56
+ - [ ] Broaden system package metadata detection beyond Gentoo-specific tooling
57
+ where practical.
58
+
59
+ ## Maintainability and Tests
60
+
61
+ - [ ] Split larger modules into smaller focused files where practical
62
+ (`commands_actions.py`, `versions.py`, `install.py`).
63
+ - [ ] Add tests for major untested functions:
64
+ - `upgrade_tool()` (completely untested)
65
+ - `cmd_remove()` (completely untested)
66
+ - `fetch_url()` (completely untested)
67
+ - `check_prerequisites()` / `install_nodejs_npm()` (completely untested)
68
+ - `save_bundled_versions()` (untested)
69
+ - `get_system_package_info()` (completely untested)
70
+ - [ ] Add tests for prerequisite edge cases and cache write behavior.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "code-aide"
3
- version = "1.0.3"
3
+ dynamic = ["version"]
4
4
  description = "Manage AI coding CLI tools (Claude, Copilot, Cursor, Gemini, Amp, Codex)"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -37,6 +37,9 @@ code-aide = "code_aide.__main__:main"
37
37
  requires = ["hatchling"]
38
38
  build-backend = "hatchling.build"
39
39
 
40
+ [tool.hatch.version]
41
+ path = "src/code_aide/__init__.py"
42
+
40
43
  [tool.pytest.ini_options]
41
44
  testpaths = ["tests"]
42
45
 
@@ -1,3 +1,3 @@
1
1
  """code-aide - Manage AI coding CLI tools."""
2
2
 
3
- __version__ = "1.0.3"
3
+ __version__ = "1.2.0"
@@ -26,6 +26,7 @@ from code_aide.config import (
26
26
  load_bundled_tools,
27
27
  load_versions_cache,
28
28
  merge_cached_versions,
29
+ save_bundled_versions,
29
30
  save_updated_versions,
30
31
  )
31
32
 
@@ -233,7 +234,8 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
233
234
  """Handle update-versions command: check upstream for latest tool versions."""
234
235
  bundled = load_bundled_tools()
235
236
  tools = bundled.get("tools", {})
236
- merge_cached_versions(tools, load_versions_cache())
237
+ if not args.bundled:
238
+ merge_cached_versions(tools, load_versions_cache())
237
239
 
238
240
  config: Dict[str, Any] = {"tools": tools}
239
241
 
@@ -290,11 +292,19 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
290
292
  tool_entry["latest_date"] = date
291
293
  version_info_changed = True
292
294
 
295
+ def _save(tools: dict) -> str:
296
+ """Save versions to bundled or user cache. Returns description."""
297
+ if args.bundled:
298
+ path = save_bundled_versions(tools)
299
+ return path
300
+ save_updated_versions(tools)
301
+ return "~/.config/code-aide/versions.json"
302
+
293
303
  updates = [result for result in results if result["update"]]
294
304
  if not updates:
295
- if version_info_changed and not args.dry_run:
296
- save_updated_versions(config["tools"])
297
- print("Updated latest version info in ~/.config/code-aide/versions.json.")
305
+ if version_info_changed and not args.dryrun:
306
+ dest = _save(config["tools"])
307
+ print(f"Updated latest version info in {dest}.")
298
308
  if version_info_changed:
299
309
  print(
300
310
  "No installer checksum updates required "
@@ -313,7 +323,7 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
313
323
  for result in updates:
314
324
  print(f" {result['tool']}: SHA256 changed")
315
325
 
316
- if args.dry_run:
326
+ if args.dryrun:
317
327
  print("\nDry run mode - no changes written.")
318
328
  return
319
329
 
@@ -325,18 +335,16 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
325
335
  return
326
336
  if answer not in ("y", "yes"):
327
337
  if version_info_changed:
328
- save_updated_versions(config["tools"])
329
- print(
330
- "Updated latest version info in ~/.config/code-aide/versions.json."
331
- )
338
+ dest = _save(config["tools"])
339
+ print(f"Updated latest version info in {dest}.")
332
340
  else:
333
341
  print("No changes made.")
334
342
  return
335
343
 
336
344
  updated = apply_sha256_updates(config, results)
337
- save_updated_versions(config["tools"])
345
+ dest = _save(config["tools"])
338
346
 
339
- print(f"\nUpdated {len(updated)} tool(s) in ~/.config/code-aide/versions.json:")
347
+ print(f"\nUpdated {len(updated)} tool(s) in {dest}:")
340
348
  for name in updated:
341
349
  print(f" {name}")
342
350
  if version_info_changed:
@@ -103,3 +103,29 @@ def save_updated_versions(tools: dict) -> None:
103
103
  if entry:
104
104
  cache_data["tools"][tool_key] = entry
105
105
  save_versions_cache(cache_data)
106
+
107
+
108
+ def save_bundled_versions(tools: dict) -> str:
109
+ """Update dynamic fields in the bundled data/tools.json.
110
+
111
+ Loads the existing bundled file to preserve static fields, then
112
+ overwrites only the dynamic version fields from the provided tools
113
+ dict. Returns the file path written.
114
+ """
115
+ ref = importlib.resources.files("code_aide").joinpath("data/tools.json")
116
+ path = str(ref)
117
+
118
+ with open(path, encoding="utf-8") as f:
119
+ bundled = json.load(f)
120
+
121
+ for tool_key, tool_data in tools.items():
122
+ if tool_key in bundled.get("tools", {}):
123
+ for field in DYNAMIC_FIELDS:
124
+ if field in tool_data:
125
+ bundled["tools"][tool_key][field] = tool_data[field]
126
+
127
+ with open(path, "w", encoding="utf-8") as f:
128
+ json.dump(bundled, f, indent=2)
129
+ f.write("\n")
130
+
131
+ return path
@@ -30,8 +30,13 @@
30
30
  "command": "claude",
31
31
  "install_type": "self_managed",
32
32
  "npm_package": "@anthropic-ai/claude-code",
33
- "upgrade_command": ["claude", "upgrade"],
34
- "prerequisites": ["npm"],
33
+ "upgrade_command": [
34
+ "claude",
35
+ "upgrade"
36
+ ],
37
+ "prerequisites": [
38
+ "npm"
39
+ ],
35
40
  "min_node_version": null,
36
41
  "next_steps": "Run 'claude' and then use '/login' to authenticate",
37
42
  "version_args": [
@@ -39,8 +44,8 @@
39
44
  ],
40
45
  "docs_url": "https://docs.anthropic.com/en/docs/build-with-claude/claude-code",
41
46
  "default_install": true,
42
- "latest_version": "2.1.62",
43
- "latest_date": "2026-02-27"
47
+ "latest_version": "2.1.63",
48
+ "latest_date": "2026-02-28"
44
49
  },
45
50
  "gemini": {
46
51
  "name": "Gemini CLI",
@@ -57,7 +62,7 @@
57
62
  ],
58
63
  "docs_url": "https://github.com/google-gemini/gemini-cli",
59
64
  "default_install": true,
60
- "latest_version": "0.30.1",
65
+ "latest_version": "0.31.0",
61
66
  "latest_date": "2026-02-27"
62
67
  },
63
68
  "amp": {
@@ -75,8 +80,8 @@
75
80
  ],
76
81
  "docs_url": "https://ampcode.com/manual",
77
82
  "default_install": false,
78
- "latest_version": "0.0.1772222620-g1de875",
79
- "latest_date": "2026-02-27"
83
+ "latest_version": "0.0.1772308894-g06c525",
84
+ "latest_date": "2026-02-28"
80
85
  },
81
86
  "codex": {
82
87
  "name": "Codex CLI",
@@ -105,6 +105,9 @@ def get_system_package_info(binary_path: str) -> Dict[str, Optional[str]]:
105
105
  except Exception:
106
106
  pass
107
107
 
108
+ if "/" not in package:
109
+ return result
110
+
108
111
  category, package_name = package.split("/", 1)
109
112
  ebuild_dirs = globmod.glob(f"/var/db/repos/*/{category}/{package_name}/")
110
113
  ebuilds = []
@@ -84,7 +84,7 @@ def main() -> None:
84
84
  )
85
85
  update_versions_parser.add_argument(
86
86
  "-n",
87
- "--dry-run",
87
+ "--dryrun",
88
88
  action="store_true",
89
89
  help="Show changes only, do not write updates",
90
90
  )
@@ -100,6 +100,12 @@ def main() -> None:
100
100
  action="store_true",
101
101
  help="Show full SHA256 hashes",
102
102
  )
103
+ update_versions_parser.add_argument(
104
+ "-b",
105
+ "--bundled",
106
+ action="store_true",
107
+ help="Update bundled data/tools.json instead of user cache (developer use)",
108
+ )
103
109
  update_versions_parser.set_defaults(func=cmd_update_versions)
104
110
 
105
111
  args = parser.parse_args()
@@ -7,6 +7,7 @@ import platform
7
7
  import shutil
8
8
  import subprocess
9
9
  import tarfile
10
+ import tempfile
10
11
  from typing import Any, Dict, Optional
11
12
 
12
13
  from code_aide.constants import TOOLS
@@ -162,8 +163,11 @@ def install_direct_download(
162
163
  tarball_data, _ = fetch_url(download_url, timeout=120)
163
164
  success(f"Downloaded {len(tarball_data)} bytes")
164
165
 
165
- temp_dir = install_dir + f".tmp-{os.getpid()}"
166
- os.makedirs(temp_dir, exist_ok=True)
166
+ install_parent = os.path.dirname(install_dir) or "."
167
+ os.makedirs(install_parent, exist_ok=True)
168
+ temp_dir = tempfile.mkdtemp(
169
+ prefix=os.path.basename(install_dir) + ".tmp-", dir=install_parent
170
+ )
167
171
 
168
172
  try:
169
173
  with tarfile.open(fileobj=io.BytesIO(tarball_data), mode="r:gz") as tf:
@@ -275,6 +279,7 @@ def install_tool(tool_name: str, dryrun: bool = False) -> bool:
275
279
  if dryrun:
276
280
  success(f"{tool_config['name']} verification passed")
277
281
  else:
282
+ success(f"{tool_config['name']} installed successfully")
278
283
  info(tool_config["next_steps"])
279
284
  if "docs_url" in tool_config:
280
285
  info(f"Documentation: {tool_config['docs_url']}")
@@ -57,7 +57,13 @@ class TestCmdUpdateVersions(unittest.TestCase):
57
57
  args = type(
58
58
  "Args",
59
59
  (),
60
- {"tools": ["missing"], "dry_run": False, "yes": False, "verbose": False},
60
+ {
61
+ "tools": ["missing"],
62
+ "dryrun": False,
63
+ "yes": False,
64
+ "verbose": False,
65
+ "bundled": False,
66
+ },
61
67
  )()
62
68
  with mock.patch.object(
63
69
  commands_actions,
@@ -73,7 +79,13 @@ class TestCmdUpdateVersions(unittest.TestCase):
73
79
  args = type(
74
80
  "Args",
75
81
  (),
76
- {"tools": [], "dry_run": True, "yes": False, "verbose": False},
82
+ {
83
+ "tools": [],
84
+ "dryrun": True,
85
+ "yes": False,
86
+ "verbose": False,
87
+ "bundled": False,
88
+ },
77
89
  )()
78
90
  with (
79
91
  mock.patch.object(
@@ -113,6 +125,68 @@ class TestCmdUpdateVersions(unittest.TestCase):
113
125
  mock_save.assert_not_called()
114
126
 
115
127
 
128
+ def test_bundled_flag_skips_cache_and_saves_to_bundled(self):
129
+ args = type(
130
+ "Args",
131
+ (),
132
+ {
133
+ "tools": [],
134
+ "dryrun": False,
135
+ "yes": True,
136
+ "verbose": False,
137
+ "bundled": True,
138
+ },
139
+ )()
140
+ bundled_tools = {
141
+ "tools": {
142
+ "ok": {
143
+ "install_type": "npm",
144
+ "npm_package": "pkg",
145
+ "latest_version": "1.0.0",
146
+ "latest_date": "2026-01-01",
147
+ }
148
+ }
149
+ }
150
+ with (
151
+ mock.patch.object(
152
+ commands_actions,
153
+ "load_bundled_tools",
154
+ return_value=bundled_tools,
155
+ ),
156
+ mock.patch.object(
157
+ commands_actions, "load_versions_cache", return_value={}
158
+ ) as mock_cache,
159
+ mock.patch.object(
160
+ commands_actions, "merge_cached_versions"
161
+ ) as mock_merge,
162
+ mock.patch.object(
163
+ commands_actions,
164
+ "check_npm_tool",
165
+ return_value={
166
+ "tool": "ok",
167
+ "type": "npm",
168
+ "version": "2.0.0",
169
+ "date": "2026-02-01",
170
+ "status": "ok",
171
+ "update": None,
172
+ },
173
+ ),
174
+ mock.patch.object(commands_actions, "print_check_results_table"),
175
+ mock.patch.object(commands_actions, "save_updated_versions") as mock_save,
176
+ mock.patch.object(
177
+ commands_actions, "save_bundled_versions", return_value="/fake/path"
178
+ ) as mock_bundled,
179
+ ):
180
+ buf = io.StringIO()
181
+ with contextlib.redirect_stdout(buf):
182
+ commands_actions.cmd_update_versions(args)
183
+
184
+ mock_cache.assert_not_called()
185
+ mock_merge.assert_not_called()
186
+ mock_save.assert_not_called()
187
+ mock_bundled.assert_called_once()
188
+
189
+
116
190
  class TestUpgradeNoArgsParsing(unittest.TestCase):
117
191
  """Test that 'code-aide upgrade' with no arguments parses successfully."""
118
192
 
@@ -122,3 +122,57 @@ class TestInstallDirectDownloadDryrun(unittest.TestCase):
122
122
 
123
123
  result = cli_install.install_direct_download("test", tool_config, dryrun=True)
124
124
  self.assertFalse(result)
125
+
126
+
127
+ class TestInstallDirectDownload(unittest.TestCase):
128
+ """Tests for install_direct_download in non-dryrun mode."""
129
+
130
+ @mock.patch.object(cli_install, "detect_os_arch", return_value=("linux", "x64"))
131
+ @mock.patch.object(cli_install, "fetch_url")
132
+ def test_creates_missing_install_parent_directory(self, mock_fetch, mock_os_arch):
133
+ script_content = b"echo install"
134
+ expected_sha256 = cli_install.hashlib.sha256(script_content).hexdigest()
135
+
136
+ tarball_data = io.BytesIO()
137
+ with tarfile.open(fileobj=tarball_data, mode="w:gz") as tf:
138
+ payload = b"#!/bin/sh\necho ok\n"
139
+ info = tarfile.TarInfo("package/test-bin")
140
+ info.mode = 0o755
141
+ info.size = len(payload)
142
+ tf.addfile(info, io.BytesIO(payload))
143
+ tarball_bytes = tarball_data.getvalue()
144
+
145
+ def _fake_fetch(url, timeout=30):
146
+ if url.endswith("/install"):
147
+ return script_content, None
148
+ return tarball_bytes, None
149
+
150
+ mock_fetch.side_effect = _fake_fetch
151
+
152
+ tool_config = {
153
+ "name": "Test Tool",
154
+ "install_url": "https://example.com/install",
155
+ "install_sha256": expected_sha256,
156
+ "download_url_template": "https://example.com/{version}/{os}/{arch}/pkg.tar.gz",
157
+ "install_dir": "/tmp/test-{version}",
158
+ "bin_dir": "/tmp/test-bin",
159
+ "symlinks": {"test": "test-bin"},
160
+ "latest_version": "1.0.0",
161
+ }
162
+
163
+ with tempfile.TemporaryDirectory() as td:
164
+ install_dir = os.path.join(td, "missing", "versions", "1.0.0")
165
+ bin_dir = os.path.join(td, "bin")
166
+
167
+ result = cli_install.install_direct_download(
168
+ "test",
169
+ tool_config,
170
+ dryrun=False,
171
+ install_dir_override=install_dir,
172
+ bin_dir_override=bin_dir,
173
+ )
174
+
175
+ self.assertTrue(result)
176
+ self.assertTrue(os.path.isdir(os.path.dirname(install_dir)))
177
+ self.assertTrue(os.path.exists(os.path.join(install_dir, "test-bin")))
178
+ self.assertTrue(os.path.islink(os.path.join(bin_dir, "test")))
@@ -1,20 +0,0 @@
1
- name: Publish to PyPI
2
- on:
3
- push:
4
- tags: ["v*"]
5
- jobs:
6
- publish:
7
- runs-on: ubuntu-latest
8
- permissions:
9
- contents: write
10
- id-token: write
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: astral-sh/setup-uv@v5
14
- - run: uv build
15
- - uses: pypa/gh-action-pypi-publish@release/v1
16
- - name: Create GitHub release notes
17
- uses: softprops/action-gh-release@v2
18
- with:
19
- tag_name: ${{ github.ref_name }}
20
- generate_release_notes: true
File without changes
File without changes
File without changes