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 +3 -0
- code_aide/__main__.py +6 -0
- code_aide/commands_actions.py +344 -0
- code_aide/commands_tools.py +183 -0
- code_aide/config.py +105 -0
- code_aide/console.py +58 -0
- code_aide/constants.py +63 -0
- code_aide/data/tools.json +100 -0
- code_aide/detection.py +184 -0
- code_aide/entry.py +112 -0
- code_aide/install.py +305 -0
- code_aide/operations.py +264 -0
- code_aide/prereqs.py +179 -0
- code_aide/status.py +86 -0
- code_aide/versions.py +356 -0
- code_aide-1.0.0.dist-info/METADATA +133 -0
- code_aide-1.0.0.dist-info/RECORD +20 -0
- code_aide-1.0.0.dist-info/WHEEL +4 -0
- code_aide-1.0.0.dist-info/entry_points.txt +2 -0
- code_aide-1.0.0.dist-info/licenses/LICENSE +191 -0
code_aide/__init__.py
ADDED
code_aide/__main__.py
ADDED
|
@@ -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
|