code-aide 1.0.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """code-aide - Manage AI coding CLI tools."""
2
+
3
+ __version__ = "1.0.0"
code_aide/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m code_aide."""
2
+
3
+ from code_aide.entry import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,344 @@
1
+ """Mutating CLI commands: install, upgrade, remove, update-versions."""
2
+
3
+ import argparse
4
+ import sys
5
+ from typing import Any, Dict, List
6
+
7
+ from code_aide.constants import TOOLS
8
+ from code_aide.install import install_tool
9
+ from code_aide.console import error, info, success, warning
10
+ from code_aide.operations import remove_tool, upgrade_tool, validate_tools
11
+ from code_aide.prereqs import (
12
+ check_path_directories,
13
+ check_prerequisites,
14
+ is_tool_installed,
15
+ )
16
+ from code_aide.status import get_tool_status
17
+ from code_aide.versions import (
18
+ apply_sha256_updates,
19
+ check_npm_tool,
20
+ check_script_tool,
21
+ normalize_version,
22
+ print_check_results_table,
23
+ status_version_matches_latest,
24
+ )
25
+ from code_aide.config import (
26
+ load_bundled_tools,
27
+ load_versions_cache,
28
+ merge_cached_versions,
29
+ save_updated_versions,
30
+ )
31
+
32
+
33
+ def cmd_install(args: argparse.Namespace) -> None:
34
+ """Handle install command."""
35
+ dryrun = getattr(args, "dryrun", False)
36
+
37
+ if args.tools:
38
+ tools_to_install = args.tools
39
+ if dryrun:
40
+ info(f"[DRYRUN] Checking specified tools: {', '.join(tools_to_install)}")
41
+ else:
42
+ info(f"Installing specified tools: {', '.join(tools_to_install)}")
43
+ else:
44
+ tools_to_install = [
45
+ name
46
+ for name, config in TOOLS.items()
47
+ if config.get("default_install", True)
48
+ ]
49
+ if dryrun:
50
+ info(f"[DRYRUN] Checking default tools: {', '.join(tools_to_install)}")
51
+ else:
52
+ info(
53
+ "No tools specified, installing default tools: "
54
+ f"{', '.join(tools_to_install)}"
55
+ )
56
+
57
+ validate_tools(tools_to_install)
58
+
59
+ if not dryrun:
60
+ check_prerequisites(
61
+ tools_to_install, install_prereqs=args.install_prerequisites
62
+ )
63
+
64
+ installed = []
65
+ failed = []
66
+
67
+ for tool in tools_to_install:
68
+ print()
69
+ if dryrun:
70
+ info(f"=== Checking {tool} ===")
71
+ else:
72
+ info(f"=== Installing {tool} ===")
73
+
74
+ if install_tool(tool, dryrun=dryrun):
75
+ installed.append(tool)
76
+ else:
77
+ failed.append(tool)
78
+
79
+ print()
80
+ print("=" * 42)
81
+ if dryrun:
82
+ info("Verification Summary")
83
+ else:
84
+ info("Installation Summary")
85
+ print("=" * 42)
86
+
87
+ if installed:
88
+ if dryrun:
89
+ success(f"Verification passed: {', '.join(installed)}")
90
+ else:
91
+ success(f"Successfully installed: {', '.join(installed)}")
92
+
93
+ if failed:
94
+ if dryrun:
95
+ error(f"Verification failed: {', '.join(failed)}")
96
+ else:
97
+ error(f"Failed to install: {', '.join(failed)}")
98
+ sys.exit(1)
99
+
100
+ if dryrun:
101
+ print()
102
+ success("All verifications completed successfully!")
103
+ else:
104
+ print()
105
+ info("Next steps:")
106
+ for tool in installed:
107
+ tool_config = TOOLS.get(tool)
108
+ if tool_config:
109
+ print(f" {tool_config['next_steps']}")
110
+
111
+ print()
112
+ check_path_directories()
113
+ success("All installations completed successfully!")
114
+
115
+
116
+ def cmd_upgrade(args: argparse.Namespace) -> None:
117
+ """Handle upgrade command."""
118
+ if args.tools:
119
+ tools_to_upgrade = args.tools
120
+ info(f"Upgrading specified tools: {', '.join(tools_to_upgrade)}")
121
+ else:
122
+ tools_to_upgrade = []
123
+ for name, config in TOOLS.items():
124
+ if not is_tool_installed(name):
125
+ continue
126
+ latest = config.get("latest_version")
127
+ if not latest:
128
+ continue
129
+ status = get_tool_status(name, config)
130
+ if status["version"] and status_version_matches_latest(
131
+ status["version"], latest
132
+ ):
133
+ continue
134
+ tools_to_upgrade.append(name)
135
+ if tools_to_upgrade:
136
+ info(f"Upgrading out-of-date tools: {', '.join(tools_to_upgrade)}")
137
+ else:
138
+ info("All installed tools are up to date")
139
+ return
140
+
141
+ validate_tools(tools_to_upgrade)
142
+
143
+ upgraded = []
144
+ failed = []
145
+ skipped = []
146
+
147
+ for tool in tools_to_upgrade:
148
+ print()
149
+ info(f"=== Upgrading {tool} ===")
150
+
151
+ if not is_tool_installed(tool):
152
+ skipped.append(tool)
153
+ continue
154
+
155
+ if upgrade_tool(tool):
156
+ upgraded.append(tool)
157
+ else:
158
+ failed.append(tool)
159
+
160
+ print()
161
+ print("=" * 42)
162
+ info("Upgrade Summary")
163
+ print("=" * 42)
164
+
165
+ if upgraded:
166
+ success(f"Successfully upgraded: {', '.join(upgraded)}")
167
+
168
+ if skipped:
169
+ warning(f"Skipped (not installed): {', '.join(skipped)}")
170
+
171
+ if failed:
172
+ error(f"Failed to upgrade: {', '.join(failed)}")
173
+ sys.exit(1)
174
+
175
+ if upgraded:
176
+ success("All upgrades completed successfully!")
177
+ elif skipped and not upgraded:
178
+ info(
179
+ "No tools were upgraded (all were either not installed or already up to date)"
180
+ )
181
+
182
+
183
+ def cmd_remove(args: argparse.Namespace) -> None:
184
+ """Handle remove command."""
185
+ if args.tools:
186
+ tools_to_remove = args.tools
187
+ info(f"Removing specified tools: {', '.join(tools_to_remove)}")
188
+ else:
189
+ tools_to_remove = list(TOOLS.keys())
190
+ info(f"No tools specified, removing all: {', '.join(tools_to_remove)}")
191
+
192
+ validate_tools(tools_to_remove)
193
+
194
+ removed = []
195
+ failed = []
196
+ skipped = []
197
+
198
+ for tool in tools_to_remove:
199
+ print()
200
+ info(f"=== Removing {tool} ===")
201
+
202
+ if not is_tool_installed(tool):
203
+ skipped.append(tool)
204
+ continue
205
+
206
+ if remove_tool(tool):
207
+ removed.append(tool)
208
+ else:
209
+ failed.append(tool)
210
+
211
+ print()
212
+ print("=" * 42)
213
+ info("Removal Summary")
214
+ print("=" * 42)
215
+
216
+ if removed:
217
+ success(f"Successfully removed: {', '.join(removed)}")
218
+
219
+ if skipped:
220
+ warning(f"Skipped (not installed): {', '.join(skipped)}")
221
+
222
+ if failed:
223
+ error(f"Failed to remove: {', '.join(failed)}")
224
+ sys.exit(1)
225
+
226
+ if removed:
227
+ success("All removals completed successfully!")
228
+ elif skipped and not removed:
229
+ info("No tools were removed (all were not installed)")
230
+
231
+
232
+ def cmd_update_versions(args: argparse.Namespace) -> None:
233
+ """Handle update-versions command: check upstream for latest tool versions."""
234
+ bundled = load_bundled_tools()
235
+ tools = bundled.get("tools", {})
236
+ merge_cached_versions(tools, load_versions_cache())
237
+
238
+ config: Dict[str, Any] = {"tools": tools}
239
+
240
+ if args.tools:
241
+ invalid = [tool for tool in args.tools if tool not in tools]
242
+ if invalid:
243
+ error(f"Unknown tool(s): {', '.join(invalid)}")
244
+ print(f"Available: {', '.join(tools.keys())}", file=sys.stderr)
245
+ sys.exit(1)
246
+ tool_names = args.tools
247
+ else:
248
+ tool_names = list(tools.keys())
249
+
250
+ print(f"Checking {len(tool_names)} tool(s) for updates...")
251
+
252
+ results: List[Dict[str, Any]] = []
253
+ for name in tool_names:
254
+ tool_config = tools[name]
255
+ install_type = tool_config["install_type"]
256
+
257
+ if install_type in ("npm", "self_managed"):
258
+ results.append(check_npm_tool(name, tool_config, args.verbose))
259
+ elif install_type in ("script", "direct_download"):
260
+ results.append(check_script_tool(name, tool_config, args.verbose))
261
+ else:
262
+ results.append(
263
+ {
264
+ "tool": name,
265
+ "type": install_type,
266
+ "version": "-",
267
+ "date": "-",
268
+ "status": f"unknown type: {install_type}",
269
+ "update": None,
270
+ }
271
+ )
272
+
273
+ print_check_results_table(results, verbose=args.verbose)
274
+
275
+ version_info_changed = False
276
+ for result in results:
277
+ if result["status"] == "error":
278
+ continue
279
+ tool_name = result["tool"]
280
+ version = result.get("version", "-")
281
+ date = result.get("date", "-")
282
+ tool_entry = config["tools"][tool_name]
283
+ if version and version != "-":
284
+ normalized = normalize_version(version)
285
+ if tool_entry.get("latest_version") != normalized:
286
+ tool_entry["latest_version"] = normalized
287
+ version_info_changed = True
288
+ if date and date != "-":
289
+ if tool_entry.get("latest_date") != date:
290
+ tool_entry["latest_date"] = date
291
+ version_info_changed = True
292
+
293
+ updates = [result for result in results if result["update"]]
294
+ 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.")
298
+ if version_info_changed:
299
+ print(
300
+ "No installer checksum updates required "
301
+ "(latest version metadata was refreshed)."
302
+ )
303
+ else:
304
+ print("No upstream config changes detected.")
305
+ print(
306
+ "Note: 'update-versions' checks upstream metadata, not your installed "
307
+ "binary versions. Use 'code-aide status' and 'code-aide upgrade' "
308
+ "for local installs."
309
+ )
310
+ return
311
+
312
+ print(f"{len(updates)} update(s) available:")
313
+ for result in updates:
314
+ print(f" {result['tool']}: SHA256 changed")
315
+
316
+ if args.dry_run:
317
+ print("\nDry run mode - no changes written.")
318
+ return
319
+
320
+ if not args.yes:
321
+ try:
322
+ answer = input("\nApply these updates? [y/N] ").strip().lower()
323
+ except (EOFError, KeyboardInterrupt):
324
+ print("\nAborted.")
325
+ return
326
+ if answer not in ("y", "yes"):
327
+ if version_info_changed:
328
+ save_updated_versions(config["tools"])
329
+ print(
330
+ "Updated latest version info in ~/.config/code-aide/versions.json."
331
+ )
332
+ else:
333
+ print("No changes made.")
334
+ return
335
+
336
+ updated = apply_sha256_updates(config, results)
337
+ save_updated_versions(config["tools"])
338
+
339
+ print(f"\nUpdated {len(updated)} tool(s) in ~/.config/code-aide/versions.json:")
340
+ for name in updated:
341
+ print(f" {name}")
342
+ if version_info_changed:
343
+ print("Also updated latest version info for checked tools.")
344
+ print("\nRun 'code-aide status' to see current state.")
@@ -0,0 +1,183 @@
1
+ """Read-only CLI commands: list and status."""
2
+
3
+ import argparse
4
+ import platform
5
+ import shutil
6
+ from typing import List
7
+
8
+ from code_aide.constants import Colors, PACKAGE_MANAGERS, TOOLS
9
+ from code_aide.detection import (
10
+ format_install_method,
11
+ get_system_package_info,
12
+ detect_install_method,
13
+ )
14
+ from code_aide.console import command_exists, info, warning
15
+ from code_aide.prereqs import detect_package_manager, is_tool_installed
16
+ from code_aide.status import print_system_version_status, get_tool_status
17
+ from code_aide.versions import (
18
+ extract_version_from_string,
19
+ status_version_matches_latest,
20
+ version_is_newer,
21
+ )
22
+
23
+
24
+ def cmd_list(args: argparse.Namespace) -> None:
25
+ """Handle list command."""
26
+ print("Available AI Coding CLI Tools:")
27
+ print("=" * 70)
28
+ print()
29
+
30
+ for tool_name, tool_config in TOOLS.items():
31
+ installed = is_tool_installed(tool_name)
32
+ status = (
33
+ f"{Colors.GREEN}✓ Installed{Colors.NC}"
34
+ if installed
35
+ else f"{Colors.RED}✗ Not installed{Colors.NC}"
36
+ )
37
+
38
+ print(f"{Colors.BLUE}{tool_config['name']}{Colors.NC}")
39
+ print(f" Command: {tool_config['command']}")
40
+ print(f" Status: {status}")
41
+ print(f" Managed by: {tool_config['install_type']} (code-aide)")
42
+
43
+ if installed:
44
+ tool_path = shutil.which(tool_config["command"])
45
+ print(f" Location: {tool_path}")
46
+ install_info = detect_install_method(tool_name)
47
+ print(
48
+ " Installed via: "
49
+ f"{format_install_method(install_info['method'], install_info['detail'])}"
50
+ )
51
+
52
+ if tool_config.get("min_node_version"):
53
+ print(f" Requires: Node.js v{tool_config['min_node_version']}+")
54
+
55
+ if not tool_config.get("default_install", True):
56
+ print(
57
+ " Note: Opt-in only "
58
+ f"(specify 'code-aide install {tool_name}')"
59
+ )
60
+
61
+ if tool_config.get("docs_url"):
62
+ print(f" Docs: {tool_config['docs_url']}")
63
+
64
+ print()
65
+
66
+ print("=" * 70)
67
+ info("System Information:")
68
+ print(f" Platform: {platform.system()}")
69
+
70
+ if command_exists("npm"):
71
+ try:
72
+ from code_aide.console import run_command
73
+
74
+ npm_version = run_command(["npm", "--version"]).stdout.strip()
75
+ print(f" npm: {npm_version}")
76
+ except Exception:
77
+ pass
78
+
79
+ if command_exists("node"):
80
+ try:
81
+ from code_aide.console import run_command
82
+
83
+ node_version = run_command(["node", "--version"]).stdout.strip()
84
+ print(f" Node.js: {node_version}")
85
+ except Exception:
86
+ pass
87
+
88
+ pkg_mgr = detect_package_manager()
89
+ if pkg_mgr:
90
+ print(
91
+ f" Package manager: {PACKAGE_MANAGERS[pkg_mgr]['description']} ({pkg_mgr})"
92
+ )
93
+
94
+
95
+ def cmd_status(args: argparse.Namespace) -> None:
96
+ """Handle status command."""
97
+ print("AI Coding CLI Tools Status:")
98
+ print("=" * 70)
99
+ print()
100
+
101
+ outdated_count = 0
102
+ config_outdated: List[str] = []
103
+
104
+ for tool_name, tool_config in TOOLS.items():
105
+ print(f"{Colors.BLUE}{tool_config['name']}{Colors.NC}")
106
+
107
+ status = get_tool_status(tool_name, tool_config)
108
+
109
+ if not status["installed"]:
110
+ print(f" Status: {Colors.RED}✗ Not installed{Colors.NC}")
111
+ else:
112
+ print(f" Status: {Colors.GREEN}✓ Installed{Colors.NC}")
113
+
114
+ tool_path = shutil.which(tool_config["command"])
115
+ install_info = detect_install_method(tool_name)
116
+ is_system = install_info["method"] == "system"
117
+
118
+ if status["version"]:
119
+ latest_version = tool_config.get("latest_version")
120
+
121
+ if is_system and tool_path:
122
+ pkg_info = get_system_package_info(tool_path)
123
+ print_system_version_status(
124
+ status["version"], latest_version, pkg_info
125
+ )
126
+ elif latest_version:
127
+ if status_version_matches_latest(status["version"], latest_version):
128
+ version_annotation = (
129
+ f" Version: {status['version']} "
130
+ f"{Colors.GREEN}(up to date){Colors.NC}"
131
+ )
132
+ else:
133
+ installed_ver = extract_version_from_string(status["version"])
134
+ if installed_ver and version_is_newer(
135
+ installed_ver, latest_version
136
+ ):
137
+ version_annotation = (
138
+ f" Version: {status['version']} "
139
+ f"{Colors.YELLOW}(newer than configured "
140
+ f"{latest_version}){Colors.NC}"
141
+ )
142
+ config_outdated.append(tool_name)
143
+ else:
144
+ version_annotation = (
145
+ f" Version: {status['version']} "
146
+ f"{Colors.YELLOW}(latest: {latest_version}){Colors.NC}"
147
+ )
148
+ outdated_count += 1
149
+ print(version_annotation)
150
+ else:
151
+ print(f" Version: {status['version']}")
152
+
153
+ if tool_path:
154
+ print(f" Location: {tool_path}")
155
+ print(
156
+ " Installed via: "
157
+ f"{format_install_method(install_info['method'], install_info['detail'])}"
158
+ )
159
+
160
+ if status["user"]:
161
+ print(f" User: {status['user']}")
162
+
163
+ if status["usage"]:
164
+ print(f" Usage: {status['usage']}")
165
+
166
+ if status["errors"]:
167
+ for err_msg in status["errors"]:
168
+ warning(f" {err_msg}")
169
+
170
+ print()
171
+
172
+ if config_outdated:
173
+ tools_str = " ".join(config_outdated)
174
+ print(
175
+ f"{Colors.YELLOW}Configured version outdated for: "
176
+ f"{', '.join(config_outdated)}. Run 'code-aide update-versions "
177
+ f"{tools_str}' to update.{Colors.NC}"
178
+ )
179
+ if outdated_count > 0:
180
+ print(
181
+ f"{Colors.YELLOW}{outdated_count} tool(s) can be upgraded with "
182
+ f"'code-aide upgrade'.{Colors.NC}"
183
+ )
code_aide/config.py ADDED
@@ -0,0 +1,105 @@
1
+ """Configuration management for code-aide.
2
+
3
+ Handles XDG base directory paths, loading bundled tool definitions from
4
+ package data, loading/saving the user's version cache, and merging
5
+ bundled defaults with cached data.
6
+ """
7
+
8
+ import importlib.resources
9
+ import json
10
+ import os
11
+
12
+
13
+ def get_config_dir() -> str:
14
+ """Return XDG config directory for code-aide.
15
+
16
+ Uses $XDG_CONFIG_HOME/code-aide if set, else ~/.config/code-aide.
17
+ Creates the directory if it doesn't exist.
18
+ """
19
+ xdg = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
20
+ config_dir = os.path.join(xdg, "code-aide")
21
+ os.makedirs(config_dir, exist_ok=True)
22
+ return config_dir
23
+
24
+
25
+ def get_versions_cache_path() -> str:
26
+ """Return path to the user's version cache file."""
27
+ return os.path.join(get_config_dir(), "versions.json")
28
+
29
+
30
+ def load_bundled_tools() -> dict:
31
+ """Load static tool definitions bundled with the package.
32
+
33
+ Uses importlib.resources to read src/code_aide/data/tools.json.
34
+ """
35
+ ref = importlib.resources.files("code_aide").joinpath("data/tools.json")
36
+ with importlib.resources.as_file(ref) as path:
37
+ with open(path) as f:
38
+ return json.load(f)
39
+
40
+
41
+ def load_versions_cache() -> dict:
42
+ """Load the user's cached version data, or empty dict if none."""
43
+ cache_path = get_versions_cache_path()
44
+ if os.path.exists(cache_path):
45
+ try:
46
+ with open(cache_path, encoding="utf-8") as f:
47
+ data = json.load(f)
48
+ if isinstance(data, dict):
49
+ return data
50
+ except (OSError, json.JSONDecodeError, ValueError):
51
+ pass
52
+ return {}
53
+
54
+
55
+ def save_versions_cache(data: dict) -> None:
56
+ """Write version data to the user's cache file."""
57
+ cache_path = get_versions_cache_path()
58
+ with open(cache_path, "w") as f:
59
+ json.dump(data, f, indent=2)
60
+ f.write("\n")
61
+
62
+
63
+ DYNAMIC_FIELDS = ["latest_version", "latest_date", "install_sha256"]
64
+
65
+
66
+ def merge_cached_versions(tools: dict, cache: dict) -> None:
67
+ """Merge cached dynamic fields into tool definitions in-place."""
68
+ cached_tools = cache.get("tools", {})
69
+ for tool_key, tool_data in tools.items():
70
+ if tool_key in cached_tools:
71
+ for field in DYNAMIC_FIELDS:
72
+ if field in cached_tools[tool_key]:
73
+ tool_data[field] = cached_tools[tool_key][field]
74
+
75
+
76
+ def load_tools_config() -> dict:
77
+ """Load tool config: bundled definitions merged with cached versions.
78
+
79
+ The bundled tools.json provides all tool definitions plus a baseline
80
+ for latest_version, latest_date, and install_sha256. The user's
81
+ version cache (from update-versions) overrides these dynamic fields
82
+ when present.
83
+ """
84
+ bundled = load_bundled_tools()
85
+ cache = load_versions_cache()
86
+ tools = bundled.get("tools", {})
87
+ merge_cached_versions(tools, cache)
88
+ return tools
89
+
90
+
91
+ def save_updated_versions(tools: dict) -> None:
92
+ """Save only dynamic version fields to the user's cache.
93
+
94
+ Called by update-versions command. Only stores latest_version,
95
+ latest_date, and install_sha256 per tool.
96
+ """
97
+ cache_data = {"tools": {}}
98
+ for tool_key, tool_data in tools.items():
99
+ entry = {}
100
+ for field in DYNAMIC_FIELDS:
101
+ if field in tool_data:
102
+ entry[field] = tool_data[field]
103
+ if entry:
104
+ cache_data["tools"][tool_key] = entry
105
+ save_versions_cache(cache_data)
code_aide/console.py ADDED
@@ -0,0 +1,58 @@
1
+ """Console output and subprocess helpers for CLI modules."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from typing import List, Union
6
+
7
+ from code_aide.constants import Colors
8
+
9
+
10
+ def info(message: str) -> None:
11
+ """Print info message."""
12
+ print(f"{Colors.BLUE}[INFO]{Colors.NC} {message}")
13
+
14
+
15
+ def success(message: str) -> None:
16
+ """Print success message."""
17
+ print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {message}")
18
+
19
+
20
+ def warning(message: str) -> None:
21
+ """Print warning message."""
22
+ print(f"{Colors.YELLOW}[WARNING]{Colors.NC} {message}")
23
+
24
+
25
+ def error(message: str) -> None:
26
+ """Print error message."""
27
+ print(f"{Colors.RED}[ERROR]{Colors.NC} {message}")
28
+
29
+
30
+ def command_exists(command: str) -> bool:
31
+ """Check if a command exists in PATH."""
32
+ return shutil.which(command) is not None
33
+
34
+
35
+ def run_command(
36
+ cmd: List[str], check: bool = True, capture: bool = True
37
+ ) -> Union[subprocess.CompletedProcess, subprocess.CalledProcessError]:
38
+ """Run a command and return the result."""
39
+ try:
40
+ if capture:
41
+ result = subprocess.run(
42
+ cmd,
43
+ check=check,
44
+ capture_output=True,
45
+ text=True,
46
+ stdin=subprocess.DEVNULL,
47
+ )
48
+ else:
49
+ result = subprocess.run(
50
+ cmd,
51
+ check=check,
52
+ stdin=subprocess.DEVNULL,
53
+ )
54
+ return result
55
+ except subprocess.CalledProcessError as exc:
56
+ if check:
57
+ raise
58
+ return exc