code-aide 1.10.1__tar.gz → 1.11.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.10.1 → code_aide-1.11.1}/PKG-INFO +2 -1
- {code_aide-1.10.1 → code_aide-1.11.1}/pyproject.toml +1 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/__init__.py +1 -1
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/commands_tools.py +30 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/constants.py +7 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/data/tools.json +12 -4
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/detection.py +83 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/install.py +32 -1
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/install_types.py +3 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/operations.py +25 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/prereqs.py +7 -1
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/status.py +49 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_detection.py +150 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_install.py +70 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_install_types.py +9 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/.github/workflows/ci.yml +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/.github/workflows/publish.yml +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/.gitignore +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/.gitlab-ci.yml +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/.pre-commit-config.yaml +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/AGENTS.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/CLAUDE.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/LICENSE +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/README.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/TODO.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/script-archive/README.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/script-archive/amp-install.sh +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/script-archive/claude-install.sh +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/script-archive/cursor-install.sh +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/__main__.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/commands_actions.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/config.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/console.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/entry.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/src/code_aide/versions.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_commands_actions.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_commands_tools.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_config.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_console.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_constants.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_operations.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_status.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/tests/test_versions.py +0 -0
- {code_aide-1.10.1 → code_aide-1.11.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-aide
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.1
|
|
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
|
|
@@ -14,6 +14,7 @@ Classifier: Environment :: Console
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: BSD :: FreeBSD
|
|
17
18
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
19
|
Classifier: Programming Language :: Python :: 3
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"License :: OSI Approved :: Apache Software License",
|
|
17
17
|
"Operating System :: MacOS",
|
|
18
18
|
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Operating System :: POSIX :: BSD :: FreeBSD",
|
|
19
20
|
"Programming Language :: Python :: 3",
|
|
20
21
|
"Programming Language :: Python :: 3.11",
|
|
21
22
|
"Programming Language :: Python :: 3.12",
|
|
@@ -20,8 +20,10 @@ from code_aide.install_types import (
|
|
|
20
20
|
parse_install_type,
|
|
21
21
|
)
|
|
22
22
|
from code_aide.prereqs import detect_package_manager, is_tool_installed
|
|
23
|
+
from code_aide.detection import is_freebsd
|
|
23
24
|
from code_aide.status import (
|
|
24
25
|
print_brew_version_status,
|
|
26
|
+
print_pkg_version_status,
|
|
25
27
|
print_system_version_status,
|
|
26
28
|
ToolUpgradeEvaluator,
|
|
27
29
|
UpgradeDecision,
|
|
@@ -68,6 +70,9 @@ def cmd_list(args: argparse.Namespace) -> None:
|
|
|
68
70
|
if tool_config.get("min_node_version"):
|
|
69
71
|
print(f" Requires: Node.js v{tool_config['min_node_version']}+")
|
|
70
72
|
|
|
73
|
+
if is_freebsd() and not tool_config.get("freebsd_port"):
|
|
74
|
+
print(" Note: Not available on FreeBSD")
|
|
75
|
+
|
|
71
76
|
if not tool_config.get("default_install", True):
|
|
72
77
|
print(
|
|
73
78
|
" Note: Opt-in only "
|
|
@@ -116,6 +121,7 @@ def _short_install_method(method: str | None) -> str:
|
|
|
116
121
|
InstallMethod.NPM: "npm",
|
|
117
122
|
InstallMethod.BREW_NPM: "brew-npm",
|
|
118
123
|
InstallMethod.SYSTEM: "system",
|
|
124
|
+
InstallMethod.PKG: "pkg",
|
|
119
125
|
InstallMethod.SCRIPT: "script",
|
|
120
126
|
InstallMethod.DIRECT_DOWNLOAD: "download",
|
|
121
127
|
}
|
|
@@ -243,6 +249,30 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
243
249
|
assessment.latest_version,
|
|
244
250
|
assessment.package_info,
|
|
245
251
|
)
|
|
252
|
+
elif assessment.install_method == InstallMethod.PKG:
|
|
253
|
+
if assessment.package_info and assessment.package_info.get(
|
|
254
|
+
"available_version"
|
|
255
|
+
):
|
|
256
|
+
print_pkg_version_status(
|
|
257
|
+
status["version"],
|
|
258
|
+
assessment.latest_version,
|
|
259
|
+
assessment.package_info,
|
|
260
|
+
repo=tool_config.get("freebsd_pkg_repo"),
|
|
261
|
+
)
|
|
262
|
+
elif assessment.latest_version:
|
|
263
|
+
if assessment.version_state == VersionDisplayState.UP_TO_DATE:
|
|
264
|
+
print(
|
|
265
|
+
f" Version: {status['version']} "
|
|
266
|
+
f"{Colors.GREEN}(up to date){Colors.NC}"
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
print(
|
|
270
|
+
_generic_version_annotation(
|
|
271
|
+
status["version"], assessment.latest_version
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
print(f" Version: {status['version']}")
|
|
246
276
|
elif assessment.install_method in (
|
|
247
277
|
InstallMethod.BREW_FORMULA,
|
|
248
278
|
InstallMethod.BREW_CASK,
|
|
@@ -57,6 +57,13 @@ PACKAGE_MANAGERS: Dict[str, Dict[str, Any]] = {
|
|
|
57
57
|
"install_command": ["sudo", "emerge", "--quiet-build"],
|
|
58
58
|
"description": "Gentoo",
|
|
59
59
|
},
|
|
60
|
+
"pkg": {
|
|
61
|
+
"detect_command": "pkg",
|
|
62
|
+
"packages": ["node22", "npm-node22"],
|
|
63
|
+
"pre_install": [],
|
|
64
|
+
"install_command": ["sudo", "pkg", "install", "-y"],
|
|
65
|
+
"description": "FreeBSD",
|
|
66
|
+
},
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"--version"
|
|
37
37
|
],
|
|
38
38
|
"docs_url": "https://code.claude.com/docs/en/setup",
|
|
39
|
-
"default_install": true
|
|
39
|
+
"default_install": true,
|
|
40
|
+
"freebsd_port": "claude-code",
|
|
41
|
+
"freebsd_pkg_repo": "FreeBSD-latest"
|
|
40
42
|
},
|
|
41
43
|
"gemini": {
|
|
42
44
|
"name": "Gemini CLI",
|
|
@@ -52,7 +54,9 @@
|
|
|
52
54
|
"--version"
|
|
53
55
|
],
|
|
54
56
|
"docs_url": "https://github.com/google-gemini/gemini-cli",
|
|
55
|
-
"default_install": true
|
|
57
|
+
"default_install": true,
|
|
58
|
+
"freebsd_port": "gemini-cli",
|
|
59
|
+
"freebsd_pkg_repo": "FreeBSD-latest"
|
|
56
60
|
},
|
|
57
61
|
"amp": {
|
|
58
62
|
"name": "Amp (Sourcegraph)",
|
|
@@ -84,7 +88,9 @@
|
|
|
84
88
|
"--version"
|
|
85
89
|
],
|
|
86
90
|
"docs_url": "https://github.com/openai/codex",
|
|
87
|
-
"default_install": false
|
|
91
|
+
"default_install": false,
|
|
92
|
+
"freebsd_port": "codex",
|
|
93
|
+
"freebsd_pkg_repo": "FreeBSD-latest"
|
|
88
94
|
},
|
|
89
95
|
"copilot": {
|
|
90
96
|
"name": "Copilot CLI",
|
|
@@ -100,7 +106,9 @@
|
|
|
100
106
|
"--version"
|
|
101
107
|
],
|
|
102
108
|
"docs_url": "https://github.com/github/copilot-cli",
|
|
103
|
-
"default_install": false
|
|
109
|
+
"default_install": false,
|
|
110
|
+
"freebsd_port": "github-copilot-cli",
|
|
111
|
+
"freebsd_pkg_repo": "FreeBSD-latest"
|
|
104
112
|
},
|
|
105
113
|
"opencode": {
|
|
106
114
|
"name": "OpenCode",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import glob as globmod
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import platform
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import subprocess
|
|
@@ -40,6 +41,29 @@ class PackageVersionInfo(TypedDict, total=False):
|
|
|
40
41
|
outdated: bool | None
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def is_freebsd() -> bool:
|
|
45
|
+
"""Return True when running on FreeBSD."""
|
|
46
|
+
return platform.system() == "FreeBSD"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pkg_owns_file(path: str) -> bool:
|
|
50
|
+
"""Return True when FreeBSD pkg owns the given file path."""
|
|
51
|
+
if not command_exists("pkg"):
|
|
52
|
+
return False
|
|
53
|
+
try:
|
|
54
|
+
proc = subprocess.run(
|
|
55
|
+
["pkg", "which", "-q", path],
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
timeout=5,
|
|
59
|
+
check=False,
|
|
60
|
+
stdin=subprocess.DEVNULL,
|
|
61
|
+
)
|
|
62
|
+
return proc.returncode == 0 and bool(proc.stdout.strip())
|
|
63
|
+
except Exception:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
43
67
|
def _empty_install_info() -> DetectedInstallInfo:
|
|
44
68
|
"""Return the default empty install-info shape."""
|
|
45
69
|
return {"method": None, "detail": None}
|
|
@@ -78,6 +102,9 @@ def detect_install_method(tool_name: str) -> DetectedInstallInfo:
|
|
|
78
102
|
|
|
79
103
|
system_prefixes = ("/opt/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/")
|
|
80
104
|
if any(real_path.startswith(prefix) for prefix in system_prefixes):
|
|
105
|
+
freebsd_port = tool_config.get("freebsd_port")
|
|
106
|
+
if is_freebsd() and freebsd_port and _pkg_owns_file(real_path):
|
|
107
|
+
return {"method": InstallMethod.PKG, "detail": freebsd_port}
|
|
81
108
|
return {"method": InstallMethod.SYSTEM, "detail": real_path}
|
|
82
109
|
|
|
83
110
|
return {
|
|
@@ -263,6 +290,59 @@ def get_brew_package_info(
|
|
|
263
290
|
return result
|
|
264
291
|
|
|
265
292
|
|
|
293
|
+
def get_pkg_package_info(
|
|
294
|
+
package_name: str, repo: Optional[str] = None
|
|
295
|
+
) -> PackageVersionInfo:
|
|
296
|
+
"""Get package version info for a FreeBSD pkg-installed tool."""
|
|
297
|
+
result: PackageVersionInfo = {
|
|
298
|
+
"package": package_name,
|
|
299
|
+
"installed_version": None,
|
|
300
|
+
"available_version": None,
|
|
301
|
+
"available_date": None,
|
|
302
|
+
"outdated": None,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if not command_exists("pkg"):
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
proc = subprocess.run(
|
|
310
|
+
["pkg", "query", "%v", package_name],
|
|
311
|
+
capture_output=True,
|
|
312
|
+
text=True,
|
|
313
|
+
timeout=5,
|
|
314
|
+
check=False,
|
|
315
|
+
stdin=subprocess.DEVNULL,
|
|
316
|
+
)
|
|
317
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
318
|
+
result["installed_version"] = proc.stdout.strip().split("\n")[0]
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
rquery_cmd = ["pkg", "rquery"]
|
|
324
|
+
if repo:
|
|
325
|
+
rquery_cmd.extend(["-r", repo])
|
|
326
|
+
rquery_cmd.extend(["%v", package_name])
|
|
327
|
+
proc = subprocess.run(
|
|
328
|
+
rquery_cmd,
|
|
329
|
+
capture_output=True,
|
|
330
|
+
text=True,
|
|
331
|
+
timeout=10,
|
|
332
|
+
check=False,
|
|
333
|
+
stdin=subprocess.DEVNULL,
|
|
334
|
+
)
|
|
335
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
336
|
+
result["available_version"] = proc.stdout.strip().split("\n")[0]
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
if result["installed_version"] and result["available_version"]:
|
|
341
|
+
result["outdated"] = result["installed_version"] != result["available_version"]
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
266
346
|
def format_install_method(method: InstallMethodInput, detail: Optional[str]) -> str:
|
|
267
347
|
"""Format detected local install method for display."""
|
|
268
348
|
install_method = parse_install_method(method)
|
|
@@ -279,6 +359,8 @@ def format_install_method(method: InstallMethodInput, detail: Optional[str]) ->
|
|
|
279
359
|
if detail
|
|
280
360
|
else "Homebrew prefix npm-global"
|
|
281
361
|
)
|
|
362
|
+
if install_method == InstallMethod.PKG:
|
|
363
|
+
return f"FreeBSD pkg ({detail})" if detail else "FreeBSD pkg"
|
|
282
364
|
if install_method == InstallMethod.SYSTEM:
|
|
283
365
|
return f"system package ({detail})" if detail else "system package"
|
|
284
366
|
if install_method == InstallMethod.SCRIPT or install_type == InstallType.SCRIPT:
|
|
@@ -299,6 +381,7 @@ _USER_MANAGED_METHODS = frozenset(
|
|
|
299
381
|
InstallMethod.BREW_FORMULA,
|
|
300
382
|
InstallMethod.BREW_CASK,
|
|
301
383
|
InstallMethod.SYSTEM,
|
|
384
|
+
InstallMethod.PKG,
|
|
302
385
|
}
|
|
303
386
|
)
|
|
304
387
|
|
|
@@ -88,7 +88,7 @@ ARCH_MAP = {
|
|
|
88
88
|
def detect_os_arch() -> tuple:
|
|
89
89
|
"""Detect OS and architecture for direct download URLs."""
|
|
90
90
|
os_name = platform.system().lower()
|
|
91
|
-
if os_name not in ("linux", "darwin"):
|
|
91
|
+
if os_name not in ("linux", "darwin", "freebsd"):
|
|
92
92
|
raise RuntimeError(f"Unsupported OS: {os_name}")
|
|
93
93
|
|
|
94
94
|
machine = platform.machine()
|
|
@@ -255,6 +255,37 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
255
255
|
f"{tool_path}"
|
|
256
256
|
)
|
|
257
257
|
|
|
258
|
+
if platform.system() == "FreeBSD":
|
|
259
|
+
freebsd_port = tool_config.get("freebsd_port")
|
|
260
|
+
if not freebsd_port:
|
|
261
|
+
error(f"{tool_config['name']} is not available on FreeBSD (no port exists)")
|
|
262
|
+
return False
|
|
263
|
+
pkg_repo = tool_config.get("freebsd_pkg_repo")
|
|
264
|
+
if dryrun:
|
|
265
|
+
repo_flag = f" -r {pkg_repo}" if pkg_repo else ""
|
|
266
|
+
info(
|
|
267
|
+
f"[DRYRUN] Would install FreeBSD port: "
|
|
268
|
+
f"pkg install{repo_flag} {freebsd_port}"
|
|
269
|
+
)
|
|
270
|
+
return True
|
|
271
|
+
try:
|
|
272
|
+
cmd = ["sudo", "pkg", "install", "-y"]
|
|
273
|
+
if pkg_repo:
|
|
274
|
+
cmd.extend(["-r", pkg_repo])
|
|
275
|
+
cmd.append(freebsd_port)
|
|
276
|
+
run_command(cmd, check=True)
|
|
277
|
+
success(f"{tool_config['name']} installed successfully via FreeBSD pkg")
|
|
278
|
+
info(tool_config["next_steps"])
|
|
279
|
+
if "docs_url" in tool_config:
|
|
280
|
+
info(f"Documentation: {tool_config['docs_url']}")
|
|
281
|
+
return True
|
|
282
|
+
except subprocess.CalledProcessError as exc:
|
|
283
|
+
error(f"Failed to install {tool_config['name']}: {exc.stderr}")
|
|
284
|
+
return False
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
error(f"Failed to install {tool_config['name']}: {exc}")
|
|
287
|
+
return False
|
|
288
|
+
|
|
258
289
|
try:
|
|
259
290
|
install_type = get_tool_install_type(tool_config)
|
|
260
291
|
|
|
@@ -17,6 +17,7 @@ class InstallType(_ValueEnum):
|
|
|
17
17
|
NPM = "npm"
|
|
18
18
|
SCRIPT = "script"
|
|
19
19
|
DIRECT_DOWNLOAD = "direct_download"
|
|
20
|
+
PKG = "pkg"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class InstallMethod(_ValueEnum):
|
|
@@ -29,6 +30,7 @@ class InstallMethod(_ValueEnum):
|
|
|
29
30
|
SYSTEM = "system"
|
|
30
31
|
SCRIPT = "script"
|
|
31
32
|
DIRECT_DOWNLOAD = "direct_download"
|
|
33
|
+
PKG = "pkg"
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
InstallTypeInput: TypeAlias = InstallType | str
|
|
@@ -40,6 +42,7 @@ _INSTALL_METHOD_BY_TYPE = {
|
|
|
40
42
|
InstallType.NPM: InstallMethod.NPM,
|
|
41
43
|
InstallType.SCRIPT: InstallMethod.SCRIPT,
|
|
42
44
|
InstallType.DIRECT_DOWNLOAD: InstallMethod.DIRECT_DOWNLOAD,
|
|
45
|
+
InstallType.PKG: InstallMethod.PKG,
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
|
|
@@ -190,6 +190,18 @@ def upgrade_tool(tool_name: str) -> UpgradeResult:
|
|
|
190
190
|
if not install_direct_download(tool_name, tool_config):
|
|
191
191
|
return UpgradeResult.FAILED
|
|
192
192
|
|
|
193
|
+
elif method == InstallMethod.PKG:
|
|
194
|
+
pkg_name = detail or tool_config.get("freebsd_port")
|
|
195
|
+
if not pkg_name:
|
|
196
|
+
error(f"No FreeBSD port configured for {tool_config['name']}")
|
|
197
|
+
return UpgradeResult.FAILED
|
|
198
|
+
cmd = ["sudo", "pkg", "upgrade", "-y"]
|
|
199
|
+
pkg_repo = tool_config.get("freebsd_pkg_repo")
|
|
200
|
+
if pkg_repo:
|
|
201
|
+
cmd.extend(["-r", pkg_repo])
|
|
202
|
+
cmd.append(pkg_name)
|
|
203
|
+
run_command(cmd, check=True, capture=False)
|
|
204
|
+
|
|
193
205
|
elif method == InstallMethod.SYSTEM:
|
|
194
206
|
error(
|
|
195
207
|
f"{tool_config['name']} is managed by the system package manager. "
|
|
@@ -327,6 +339,19 @@ def remove_tool(tool_name: str) -> bool:
|
|
|
327
339
|
|
|
328
340
|
success(f"{tool_config['name']} removed successfully")
|
|
329
341
|
|
|
342
|
+
elif method == InstallMethod.PKG:
|
|
343
|
+
pkg_name = detail or tool_config.get("freebsd_port")
|
|
344
|
+
if not pkg_name:
|
|
345
|
+
error(f"No FreeBSD port configured for {tool_config['name']}")
|
|
346
|
+
return False
|
|
347
|
+
cmd = ["sudo", "pkg", "delete", "-y"]
|
|
348
|
+
pkg_repo = tool_config.get("freebsd_pkg_repo")
|
|
349
|
+
if pkg_repo:
|
|
350
|
+
cmd.extend(["-r", pkg_repo])
|
|
351
|
+
cmd.append(pkg_name)
|
|
352
|
+
run_command(cmd, check=True, capture=False)
|
|
353
|
+
success(f"{tool_config['name']} removed successfully")
|
|
354
|
+
|
|
330
355
|
elif method == InstallMethod.SYSTEM:
|
|
331
356
|
error(
|
|
332
357
|
f"{tool_config['name']} is managed by the system package manager. "
|
|
@@ -19,7 +19,7 @@ from code_aide.console import (
|
|
|
19
19
|
|
|
20
20
|
def detect_package_manager() -> Optional[str]:
|
|
21
21
|
"""Detect the Linux distribution and return package manager name."""
|
|
22
|
-
if platform.system()
|
|
22
|
+
if platform.system() not in ("Linux", "FreeBSD"):
|
|
23
23
|
return None
|
|
24
24
|
|
|
25
25
|
for pkg_mgr_name, config in PACKAGE_MANAGERS.items():
|
|
@@ -78,11 +78,17 @@ def check_prerequisites(
|
|
|
78
78
|
needed_prereqs = set()
|
|
79
79
|
tools_needing_node = []
|
|
80
80
|
|
|
81
|
+
on_freebsd = platform.system() == "FreeBSD"
|
|
82
|
+
|
|
81
83
|
for tool_name in tools_to_install:
|
|
82
84
|
tool_config = TOOLS.get(tool_name)
|
|
83
85
|
if not tool_config:
|
|
84
86
|
continue
|
|
85
87
|
|
|
88
|
+
# FreeBSD pkg handles all dependencies for ported tools
|
|
89
|
+
if on_freebsd and tool_config.get("freebsd_port"):
|
|
90
|
+
continue
|
|
91
|
+
|
|
86
92
|
needed_prereqs.update(tool_config.get("prerequisites", []))
|
|
87
93
|
if tool_config.get("min_node_version"):
|
|
88
94
|
tools_needing_node.append((tool_name, tool_config["min_node_version"]))
|
|
@@ -12,6 +12,7 @@ from code_aide.detection import (
|
|
|
12
12
|
PackageVersionInfo,
|
|
13
13
|
detect_install_method,
|
|
14
14
|
get_brew_package_info,
|
|
15
|
+
get_pkg_package_info,
|
|
15
16
|
get_system_package_info,
|
|
16
17
|
is_install_method_deprecated,
|
|
17
18
|
)
|
|
@@ -125,6 +126,9 @@ class ToolUpgradeEvaluator:
|
|
|
125
126
|
if method in (InstallMethod.BREW_FORMULA, InstallMethod.BREW_CASK):
|
|
126
127
|
return self._evaluate_brew(status, install_info, package_info)
|
|
127
128
|
|
|
129
|
+
if method == InstallMethod.PKG:
|
|
130
|
+
return self._evaluate_brew(status, install_info, package_info)
|
|
131
|
+
|
|
128
132
|
if method == InstallMethod.SYSTEM:
|
|
129
133
|
return self._result(
|
|
130
134
|
decision=UpgradeDecision.PACKAGE_MANAGED,
|
|
@@ -243,6 +247,14 @@ class ToolUpgradeEvaluator:
|
|
|
243
247
|
self._package_info = get_brew_package_info(
|
|
244
248
|
method, install_info.get("detail")
|
|
245
249
|
)
|
|
250
|
+
elif method == InstallMethod.PKG:
|
|
251
|
+
pkg_name = install_info.get("detail") or self.tool_config.get(
|
|
252
|
+
"freebsd_port"
|
|
253
|
+
)
|
|
254
|
+
pkg_repo = self.tool_config.get("freebsd_pkg_repo")
|
|
255
|
+
self._package_info = (
|
|
256
|
+
get_pkg_package_info(pkg_name, repo=pkg_repo) if pkg_name else None
|
|
257
|
+
)
|
|
246
258
|
elif method == InstallMethod.SYSTEM:
|
|
247
259
|
tool_path = self._tool_path or shutil.which(self.tool_config["command"])
|
|
248
260
|
self._package_info = (
|
|
@@ -383,6 +395,43 @@ def print_brew_version_status(
|
|
|
383
395
|
print(f" Packaged: {avail_ver} ({pkg_name})")
|
|
384
396
|
|
|
385
397
|
|
|
398
|
+
def print_pkg_version_status(
|
|
399
|
+
cli_version: str,
|
|
400
|
+
latest_version: Optional[str],
|
|
401
|
+
pkg_info: PackageInfo,
|
|
402
|
+
repo: Optional[str] = None,
|
|
403
|
+
) -> None:
|
|
404
|
+
"""Print version status for a FreeBSD pkg-managed tool."""
|
|
405
|
+
avail_ver = pkg_info.get("available_version")
|
|
406
|
+
outdated = pkg_info.get("outdated")
|
|
407
|
+
|
|
408
|
+
if outdated:
|
|
409
|
+
print(
|
|
410
|
+
f" Version: {cli_version} {Colors.YELLOW}(pkg has {avail_ver})"
|
|
411
|
+
f"{Colors.NC}"
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
print(f" Version: {cli_version} {Colors.GREEN}(up to date){Colors.NC}")
|
|
415
|
+
|
|
416
|
+
if avail_ver:
|
|
417
|
+
pkg_name = pkg_info.get("package") or "FreeBSD pkg"
|
|
418
|
+
repo_suffix = f", {repo}" if repo else ""
|
|
419
|
+
show_upstream = (
|
|
420
|
+
latest_version
|
|
421
|
+
and not status_version_matches_latest(avail_ver, latest_version)
|
|
422
|
+
and version_is_newer(
|
|
423
|
+
normalize_version(latest_version), normalize_version(avail_ver)
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
if show_upstream:
|
|
427
|
+
print(
|
|
428
|
+
f" Packaged: {avail_ver} ({pkg_name}{repo_suffix}) "
|
|
429
|
+
f"{Colors.YELLOW}(upstream: {latest_version}){Colors.NC}"
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
print(f" Packaged: {avail_ver} ({pkg_name}{repo_suffix})")
|
|
433
|
+
|
|
434
|
+
|
|
386
435
|
def get_tool_status(tool_name: str, tool_config: Dict[str, Any]) -> ToolStatus:
|
|
387
436
|
"""Get status information for a specific tool."""
|
|
388
437
|
status_info = {
|
|
@@ -92,6 +92,156 @@ class TestDetectInstallMethod(unittest.TestCase):
|
|
|
92
92
|
{"method": "system", "detail": "/usr/bin/claude"},
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
+
@mock.patch.object(cli_detection, "is_freebsd", return_value=True)
|
|
96
|
+
@mock.patch.object(cli_detection, "_pkg_owns_file", return_value=True)
|
|
97
|
+
@mock.patch.object(cli_detection.os.path, "realpath")
|
|
98
|
+
@mock.patch.object(cli_detection.shutil, "which")
|
|
99
|
+
def test_detects_freebsd_pkg(self, mock_which, mock_realpath, mock_pkg, mock_fbsd):
|
|
100
|
+
mock_which.return_value = "/usr/local/bin/claude"
|
|
101
|
+
mock_realpath.return_value = "/usr/local/bin/claude"
|
|
102
|
+
|
|
103
|
+
self.assertEqual(
|
|
104
|
+
cli_detection.detect_install_method("claude"),
|
|
105
|
+
{"method": "pkg", "detail": "claude-code"},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@mock.patch.object(cli_detection, "is_freebsd", return_value=True)
|
|
109
|
+
@mock.patch.object(cli_detection, "_pkg_owns_file", return_value=False)
|
|
110
|
+
@mock.patch.object(cli_detection.os.path, "realpath")
|
|
111
|
+
@mock.patch.object(cli_detection.shutil, "which")
|
|
112
|
+
def test_freebsd_not_pkg_owned_falls_back_to_system(
|
|
113
|
+
self, mock_which, mock_realpath, mock_pkg, mock_fbsd
|
|
114
|
+
):
|
|
115
|
+
mock_which.return_value = "/usr/local/bin/claude"
|
|
116
|
+
mock_realpath.return_value = "/usr/local/bin/claude"
|
|
117
|
+
|
|
118
|
+
self.assertEqual(
|
|
119
|
+
cli_detection.detect_install_method("claude"),
|
|
120
|
+
{"method": "system", "detail": "/usr/local/bin/claude"},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@mock.patch.object(cli_detection, "is_freebsd", return_value=False)
|
|
124
|
+
@mock.patch.object(cli_detection.os.path, "realpath")
|
|
125
|
+
@mock.patch.object(cli_detection.shutil, "which")
|
|
126
|
+
def test_not_freebsd_ignores_port(self, mock_which, mock_realpath, mock_fbsd):
|
|
127
|
+
mock_which.return_value = "/usr/local/bin/claude"
|
|
128
|
+
mock_realpath.return_value = "/usr/local/bin/claude"
|
|
129
|
+
|
|
130
|
+
self.assertEqual(
|
|
131
|
+
cli_detection.detect_install_method("claude"),
|
|
132
|
+
{"method": "system", "detail": "/usr/local/bin/claude"},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestFormatInstallMethodPkg(unittest.TestCase):
|
|
137
|
+
"""Tests for format_install_method with pkg method."""
|
|
138
|
+
|
|
139
|
+
def test_pkg_with_detail(self):
|
|
140
|
+
self.assertEqual(
|
|
141
|
+
cli_detection.format_install_method("pkg", "claude-code"),
|
|
142
|
+
"FreeBSD pkg (claude-code)",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_pkg_without_detail(self):
|
|
146
|
+
self.assertEqual(
|
|
147
|
+
cli_detection.format_install_method("pkg", None),
|
|
148
|
+
"FreeBSD pkg",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestPkgNeverDeprecated(unittest.TestCase):
|
|
153
|
+
"""Tests that pkg install method is never deprecated."""
|
|
154
|
+
|
|
155
|
+
def test_pkg_never_deprecated(self):
|
|
156
|
+
tool_config = {
|
|
157
|
+
"name": "Test Tool",
|
|
158
|
+
"command": "test-tool",
|
|
159
|
+
"install_type": "script",
|
|
160
|
+
}
|
|
161
|
+
with (
|
|
162
|
+
mock.patch.dict(cli_detection.TOOLS, {"test": tool_config}, clear=True),
|
|
163
|
+
mock.patch.object(
|
|
164
|
+
cli_detection,
|
|
165
|
+
"detect_install_method",
|
|
166
|
+
return_value={"method": "pkg", "detail": "test-tool"},
|
|
167
|
+
),
|
|
168
|
+
):
|
|
169
|
+
self.assertFalse(cli_detection.is_deprecated_install("test"))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestGetPkgPackageInfo(unittest.TestCase):
|
|
173
|
+
"""Tests for get_pkg_package_info."""
|
|
174
|
+
|
|
175
|
+
@mock.patch.object(cli_detection, "command_exists", return_value=False)
|
|
176
|
+
def test_no_pkg_command(self, mock_cmd):
|
|
177
|
+
result = cli_detection.get_pkg_package_info("claude-code")
|
|
178
|
+
self.assertEqual(result["package"], "claude-code")
|
|
179
|
+
self.assertIsNone(result["installed_version"])
|
|
180
|
+
self.assertIsNone(result["available_version"])
|
|
181
|
+
|
|
182
|
+
@mock.patch.object(cli_detection, "command_exists", return_value=True)
|
|
183
|
+
@mock.patch.object(cli_detection.subprocess, "run")
|
|
184
|
+
def test_parses_pkg_output(self, mock_run, mock_cmd):
|
|
185
|
+
def side_effect(cmd, **kwargs):
|
|
186
|
+
result = mock.Mock()
|
|
187
|
+
if cmd[1] == "query":
|
|
188
|
+
result.returncode = 0
|
|
189
|
+
result.stdout = "2.1.62\n"
|
|
190
|
+
elif cmd[1] == "rquery":
|
|
191
|
+
result.returncode = 0
|
|
192
|
+
result.stdout = "2.1.63\n"
|
|
193
|
+
else:
|
|
194
|
+
result.returncode = 1
|
|
195
|
+
result.stdout = ""
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
mock_run.side_effect = side_effect
|
|
199
|
+
|
|
200
|
+
result = cli_detection.get_pkg_package_info("claude-code")
|
|
201
|
+
self.assertEqual(result["installed_version"], "2.1.62")
|
|
202
|
+
self.assertEqual(result["available_version"], "2.1.63")
|
|
203
|
+
self.assertTrue(result["outdated"])
|
|
204
|
+
|
|
205
|
+
@mock.patch.object(cli_detection, "command_exists", return_value=True)
|
|
206
|
+
@mock.patch.object(cli_detection.subprocess, "run")
|
|
207
|
+
def test_up_to_date(self, mock_run, mock_cmd):
|
|
208
|
+
def side_effect(cmd, **kwargs):
|
|
209
|
+
result = mock.Mock()
|
|
210
|
+
result.returncode = 0
|
|
211
|
+
result.stdout = "2.1.63\n"
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
mock_run.side_effect = side_effect
|
|
215
|
+
|
|
216
|
+
result = cli_detection.get_pkg_package_info("claude-code")
|
|
217
|
+
self.assertEqual(result["installed_version"], "2.1.63")
|
|
218
|
+
self.assertEqual(result["available_version"], "2.1.63")
|
|
219
|
+
self.assertFalse(result["outdated"])
|
|
220
|
+
|
|
221
|
+
@mock.patch.object(cli_detection, "command_exists", return_value=True)
|
|
222
|
+
@mock.patch.object(cli_detection.subprocess, "run")
|
|
223
|
+
def test_repo_passes_r_flag_to_rquery(self, mock_run, mock_cmd):
|
|
224
|
+
calls = []
|
|
225
|
+
|
|
226
|
+
def side_effect(cmd, **kwargs):
|
|
227
|
+
calls.append(list(cmd))
|
|
228
|
+
result = mock.Mock()
|
|
229
|
+
result.returncode = 0
|
|
230
|
+
result.stdout = "2.1.63\n"
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
mock_run.side_effect = side_effect
|
|
234
|
+
|
|
235
|
+
cli_detection.get_pkg_package_info("claude-code", repo="FreeBSD-latest")
|
|
236
|
+
|
|
237
|
+
# pkg query (local) should NOT have -r
|
|
238
|
+
self.assertEqual(calls[0], ["pkg", "query", "%v", "claude-code"])
|
|
239
|
+
# pkg rquery (remote) should have -r
|
|
240
|
+
self.assertEqual(
|
|
241
|
+
calls[1],
|
|
242
|
+
["pkg", "rquery", "-r", "FreeBSD-latest", "%v", "claude-code"],
|
|
243
|
+
)
|
|
244
|
+
|
|
95
245
|
|
|
96
246
|
class TestFormatInstallMethodSystem(unittest.TestCase):
|
|
97
247
|
"""Tests for format_install_method with system method."""
|
|
@@ -45,6 +45,76 @@ class TestDetectOsArch(unittest.TestCase):
|
|
|
45
45
|
with self.assertRaises(RuntimeError):
|
|
46
46
|
cli_install.detect_os_arch()
|
|
47
47
|
|
|
48
|
+
@mock.patch.object(cli_install.platform, "machine", return_value="amd64")
|
|
49
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
50
|
+
def test_freebsd_amd64(self, mock_sys, mock_mach):
|
|
51
|
+
self.assertEqual(cli_install.detect_os_arch(), ("freebsd", "x64"))
|
|
52
|
+
|
|
53
|
+
@mock.patch.object(cli_install.platform, "machine", return_value="arm64")
|
|
54
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
55
|
+
def test_freebsd_arm64(self, mock_sys, mock_mach):
|
|
56
|
+
self.assertEqual(cli_install.detect_os_arch(), ("freebsd", "arm64"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestInstallToolFreeBSD(unittest.TestCase):
|
|
60
|
+
"""Tests for install_tool on FreeBSD."""
|
|
61
|
+
|
|
62
|
+
def _make_tool_config(self, **overrides):
|
|
63
|
+
base = {
|
|
64
|
+
"name": "Test Tool",
|
|
65
|
+
"command": "test-tool",
|
|
66
|
+
"install_type": "npm",
|
|
67
|
+
"npm_package": "test-tool",
|
|
68
|
+
"next_steps": "Run test-tool",
|
|
69
|
+
"docs_url": "https://example.com",
|
|
70
|
+
}
|
|
71
|
+
base.update(overrides)
|
|
72
|
+
return base
|
|
73
|
+
|
|
74
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
75
|
+
@mock.patch.object(cli_install, "command_exists", return_value=False)
|
|
76
|
+
def test_freebsd_no_port_returns_false(self, mock_cmd, mock_sys):
|
|
77
|
+
tool_config = self._make_tool_config()
|
|
78
|
+
with mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True):
|
|
79
|
+
result = cli_install.install_tool("test")
|
|
80
|
+
self.assertFalse(result)
|
|
81
|
+
|
|
82
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
83
|
+
@mock.patch.object(cli_install, "command_exists", return_value=False)
|
|
84
|
+
def test_freebsd_with_port_dryrun(self, mock_cmd, mock_sys):
|
|
85
|
+
tool_config = self._make_tool_config(freebsd_port="test-tool-port")
|
|
86
|
+
with mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True):
|
|
87
|
+
result = cli_install.install_tool("test", dryrun=True)
|
|
88
|
+
self.assertTrue(result)
|
|
89
|
+
|
|
90
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
91
|
+
@mock.patch.object(cli_install, "command_exists", return_value=False)
|
|
92
|
+
@mock.patch.object(cli_install, "run_command")
|
|
93
|
+
def test_freebsd_with_port_installs_via_pkg(self, mock_run, mock_cmd, mock_sys):
|
|
94
|
+
tool_config = self._make_tool_config(freebsd_port="test-tool-port")
|
|
95
|
+
with mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True):
|
|
96
|
+
result = cli_install.install_tool("test")
|
|
97
|
+
self.assertTrue(result)
|
|
98
|
+
mock_run.assert_called_once_with(
|
|
99
|
+
["sudo", "pkg", "install", "-y", "test-tool-port"], check=True
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@mock.patch.object(cli_install.platform, "system", return_value="FreeBSD")
|
|
103
|
+
@mock.patch.object(cli_install, "command_exists", return_value=False)
|
|
104
|
+
@mock.patch.object(cli_install, "run_command")
|
|
105
|
+
def test_freebsd_with_repo_passes_r_flag(self, mock_run, mock_cmd, mock_sys):
|
|
106
|
+
tool_config = self._make_tool_config(
|
|
107
|
+
freebsd_port="test-tool-port",
|
|
108
|
+
freebsd_pkg_repo="FreeBSD-latest",
|
|
109
|
+
)
|
|
110
|
+
with mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True):
|
|
111
|
+
result = cli_install.install_tool("test")
|
|
112
|
+
self.assertTrue(result)
|
|
113
|
+
mock_run.assert_called_once_with(
|
|
114
|
+
["sudo", "pkg", "install", "-y", "-r", "FreeBSD-latest", "test-tool-port"],
|
|
115
|
+
check=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
48
118
|
|
|
49
119
|
class TestExtractTarMember(unittest.TestCase):
|
|
50
120
|
"""Tests for extract_tar_member."""
|
|
@@ -44,6 +44,15 @@ class TestInstallMethodParsing(unittest.TestCase):
|
|
|
44
44
|
def test_install_method_from_type_returns_matching_method(self):
|
|
45
45
|
self.assertEqual(install_method_from_type(InstallType.NPM), InstallMethod.NPM)
|
|
46
46
|
|
|
47
|
+
def test_parse_pkg_install_type(self):
|
|
48
|
+
self.assertEqual(parse_install_type("pkg"), InstallType.PKG)
|
|
49
|
+
|
|
50
|
+
def test_parse_pkg_install_method(self):
|
|
51
|
+
self.assertEqual(parse_install_method("pkg"), InstallMethod.PKG)
|
|
52
|
+
|
|
53
|
+
def test_install_method_from_pkg_type(self):
|
|
54
|
+
self.assertEqual(install_method_from_type(InstallType.PKG), InstallMethod.PKG)
|
|
55
|
+
|
|
47
56
|
|
|
48
57
|
class TestDetectionTypedMethods(unittest.TestCase):
|
|
49
58
|
"""Tests for enum-returning detect_install_method behavior."""
|
|
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
|
{code_aide-1.10.1 → code_aide-1.11.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
|