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/prereqs.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Prerequisite and environment checks for tool installation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from code_aide.constants import PACKAGE_MANAGERS, TOOLS
|
|
10
|
+
from code_aide.console import (
|
|
11
|
+
command_exists,
|
|
12
|
+
error,
|
|
13
|
+
info,
|
|
14
|
+
run_command,
|
|
15
|
+
success,
|
|
16
|
+
warning,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_package_manager() -> Optional[str]:
|
|
21
|
+
"""Detect the Linux distribution and return package manager name."""
|
|
22
|
+
if platform.system() != "Linux":
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
for pkg_mgr_name, config in PACKAGE_MANAGERS.items():
|
|
26
|
+
if command_exists(config["detect_command"]):
|
|
27
|
+
return pkg_mgr_name
|
|
28
|
+
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def install_nodejs_npm() -> bool:
|
|
33
|
+
"""Install Node.js and npm using the system package manager."""
|
|
34
|
+
pkg_mgr_name = detect_package_manager()
|
|
35
|
+
|
|
36
|
+
if not pkg_mgr_name:
|
|
37
|
+
error("Could not detect package manager. Please install Node.js manually:")
|
|
38
|
+
for manager_name, config in PACKAGE_MANAGERS.items():
|
|
39
|
+
install_cmd = " ".join(config["install_command"] + config["packages"])
|
|
40
|
+
print(f" {config['description']}: {install_cmd}")
|
|
41
|
+
print(" Or visit: https://nodejs.org/")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
config = PACKAGE_MANAGERS[pkg_mgr_name]
|
|
45
|
+
info(f"Detected package manager: {pkg_mgr_name} ({config['description']})")
|
|
46
|
+
info("Installing Node.js and npm...")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
for pre_cmd in config["pre_install"]:
|
|
50
|
+
info(f"Running: {' '.join(pre_cmd)}")
|
|
51
|
+
run_command(pre_cmd, check=True, capture=False)
|
|
52
|
+
|
|
53
|
+
install_cmd = config["install_command"] + config["packages"]
|
|
54
|
+
run_command(install_cmd, check=True, capture=False)
|
|
55
|
+
|
|
56
|
+
if not command_exists("npm"):
|
|
57
|
+
error("npm installation completed but npm command not found in PATH")
|
|
58
|
+
error("You may need to restart your shell or add npm to your PATH")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
success("Node.js and npm installed successfully")
|
|
62
|
+
return True
|
|
63
|
+
except subprocess.CalledProcessError as exc:
|
|
64
|
+
stderr_msg = (
|
|
65
|
+
getattr(exc, "stderr", None) or getattr(exc, "stdout", None) or str(exc)
|
|
66
|
+
)
|
|
67
|
+
error(f"Failed to install Node.js and npm: {stderr_msg}")
|
|
68
|
+
return False
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
error(f"Failed to install Node.js and npm: {exc}")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_prerequisites(
|
|
75
|
+
tools_to_install: List[str], install_prereqs: bool = False
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Check if prerequisites are met, optionally installing them."""
|
|
78
|
+
needed_prereqs = set()
|
|
79
|
+
tools_needing_node = []
|
|
80
|
+
|
|
81
|
+
for tool_name in tools_to_install:
|
|
82
|
+
tool_config = TOOLS.get(tool_name)
|
|
83
|
+
if not tool_config:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
needed_prereqs.update(tool_config.get("prerequisites", []))
|
|
87
|
+
if tool_config.get("min_node_version"):
|
|
88
|
+
tools_needing_node.append((tool_name, tool_config["min_node_version"]))
|
|
89
|
+
|
|
90
|
+
if "npm" in needed_prereqs:
|
|
91
|
+
if not command_exists("npm"):
|
|
92
|
+
if install_prereqs:
|
|
93
|
+
info("npm not found, attempting to install prerequisites...")
|
|
94
|
+
if not install_nodejs_npm():
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
else:
|
|
97
|
+
error("npm is required but not installed.")
|
|
98
|
+
error("Please install Node.js and npm first:")
|
|
99
|
+
for manager_name, config in PACKAGE_MANAGERS.items():
|
|
100
|
+
install_cmd = " ".join(
|
|
101
|
+
config["install_command"] + config["packages"]
|
|
102
|
+
)
|
|
103
|
+
print(f" {config['description']}: {install_cmd}")
|
|
104
|
+
print(" Or visit: https://nodejs.org/")
|
|
105
|
+
print("\nOr use -p/--install-prerequisites to install automatically")
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
npm_version = run_command(["npm", "--version"]).stdout.strip()
|
|
110
|
+
info(f"Prerequisites check passed (npm found: {npm_version})")
|
|
111
|
+
except subprocess.CalledProcessError:
|
|
112
|
+
error("Failed to check npm version")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
for tool_name, min_version in tools_needing_node:
|
|
116
|
+
try:
|
|
117
|
+
node_version_output = run_command(["node", "--version"]).stdout.strip()
|
|
118
|
+
version_str = node_version_output.lstrip("v")
|
|
119
|
+
version_parts = version_str.replace("-", ".").split(".")
|
|
120
|
+
if not version_parts or not version_parts[0].isdigit():
|
|
121
|
+
raise ValueError(f"Invalid version format: {node_version_output}")
|
|
122
|
+
node_major_version = int(version_parts[0])
|
|
123
|
+
if node_major_version < min_version:
|
|
124
|
+
if install_prereqs:
|
|
125
|
+
warning(
|
|
126
|
+
f"Node.js version {node_version_output} is below v{min_version} "
|
|
127
|
+
f"required for {TOOLS[tool_name]['name']}"
|
|
128
|
+
)
|
|
129
|
+
warning(
|
|
130
|
+
"You may need to upgrade Node.js manually or use a Node "
|
|
131
|
+
"version manager"
|
|
132
|
+
)
|
|
133
|
+
warning("See: https://nodejs.org/ or https://github.com/nvm-sh/nvm")
|
|
134
|
+
else:
|
|
135
|
+
error(
|
|
136
|
+
f"{TOOLS[tool_name]['name']} requires Node.js version "
|
|
137
|
+
f"{min_version} or higher."
|
|
138
|
+
)
|
|
139
|
+
error(f"Current version: {node_version_output}")
|
|
140
|
+
error("Please upgrade Node.js: https://nodejs.org/")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
except (subprocess.CalledProcessError, ValueError, IndexError) as exc:
|
|
143
|
+
error(f"Failed to check Node.js version: {exc}")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_tool_installed(tool_name: str) -> bool:
|
|
148
|
+
"""Check if a tool is installed."""
|
|
149
|
+
tool_config = TOOLS.get(tool_name)
|
|
150
|
+
if not tool_config:
|
|
151
|
+
return False
|
|
152
|
+
return command_exists(tool_config["command"])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_path_directories() -> None:
|
|
156
|
+
"""Check if common binary installation directories are in PATH and warn if not."""
|
|
157
|
+
current_path = os.environ.get("PATH", "")
|
|
158
|
+
path_entries = current_path.split(":")
|
|
159
|
+
|
|
160
|
+
common_dirs = [
|
|
161
|
+
os.path.expanduser("~/.local/bin"),
|
|
162
|
+
os.path.expanduser("~/.npm-packages/bin"),
|
|
163
|
+
os.path.expanduser("~/.amp/bin"),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
missing_dirs = []
|
|
167
|
+
for dir_path in common_dirs:
|
|
168
|
+
if dir_path not in path_entries and os.path.isdir(dir_path):
|
|
169
|
+
missing_dirs.append(dir_path)
|
|
170
|
+
|
|
171
|
+
if missing_dirs:
|
|
172
|
+
warning("The following directories exist but are not in your PATH:")
|
|
173
|
+
for dir_path in missing_dirs:
|
|
174
|
+
print(f" {dir_path}")
|
|
175
|
+
print()
|
|
176
|
+
print("To use installed tools, add to ~/.bashrc or ~/.zshrc:")
|
|
177
|
+
for dir_path in missing_dirs:
|
|
178
|
+
print(f' export PATH="{dir_path}:$PATH"')
|
|
179
|
+
print()
|
code_aide/status.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Status helpers for installed tools and version reporting."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from code_aide.constants import Colors
|
|
7
|
+
from code_aide.prereqs import is_tool_installed
|
|
8
|
+
from code_aide.versions import (
|
|
9
|
+
extract_version_from_string,
|
|
10
|
+
normalize_version,
|
|
11
|
+
status_version_matches_latest,
|
|
12
|
+
version_is_newer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_system_version_status(
|
|
17
|
+
cli_version: str,
|
|
18
|
+
latest_version: Optional[str],
|
|
19
|
+
pkg_info: Dict[str, Optional[str]],
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Print version status for a system-package-managed tool."""
|
|
22
|
+
installed_ver = extract_version_from_string(cli_version)
|
|
23
|
+
avail_ver = pkg_info.get("available_version")
|
|
24
|
+
avail_date = pkg_info.get("available_date")
|
|
25
|
+
|
|
26
|
+
pkg_up_to_date = True
|
|
27
|
+
if installed_ver and avail_ver:
|
|
28
|
+
pkg_up_to_date = installed_ver == normalize_version(
|
|
29
|
+
avail_ver
|
|
30
|
+
) or version_is_newer(installed_ver, normalize_version(avail_ver))
|
|
31
|
+
|
|
32
|
+
if pkg_up_to_date:
|
|
33
|
+
print(f" Version: {cli_version} {Colors.GREEN}(up to date){Colors.NC}")
|
|
34
|
+
else:
|
|
35
|
+
print(
|
|
36
|
+
f" Version: {cli_version} {Colors.YELLOW}(package has {avail_ver})"
|
|
37
|
+
f"{Colors.NC}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if avail_ver:
|
|
41
|
+
date_suffix = f", {avail_date}" if avail_date else ""
|
|
42
|
+
pkg_name = pkg_info.get("package") or "system"
|
|
43
|
+
if latest_version and not status_version_matches_latest(
|
|
44
|
+
avail_ver, latest_version
|
|
45
|
+
):
|
|
46
|
+
print(
|
|
47
|
+
f" Packaged: {avail_ver} ({pkg_name}{date_suffix}) "
|
|
48
|
+
f"{Colors.YELLOW}(upstream: {latest_version}){Colors.NC}"
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
print(f" Packaged: {avail_ver} ({pkg_name}{date_suffix})")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_tool_status(tool_name: str, tool_config: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
|
+
"""Get status information for a specific tool."""
|
|
56
|
+
status_info = {
|
|
57
|
+
"installed": is_tool_installed(tool_name),
|
|
58
|
+
"version": None,
|
|
59
|
+
"user": None,
|
|
60
|
+
"usage": None,
|
|
61
|
+
"errors": [],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if not status_info["installed"]:
|
|
65
|
+
return status_info
|
|
66
|
+
|
|
67
|
+
command = tool_config["command"]
|
|
68
|
+
version_args = tool_config.get("version_args", ["--version"])
|
|
69
|
+
cmd = [command] + version_args
|
|
70
|
+
try:
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
cmd,
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
timeout=10,
|
|
76
|
+
check=False,
|
|
77
|
+
stdin=subprocess.DEVNULL,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
80
|
+
status_info["version"] = result.stdout.strip().split("\n")[0]
|
|
81
|
+
except subprocess.TimeoutExpired:
|
|
82
|
+
status_info["errors"].append("Version check timed out after 10s")
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
return status_info
|
code_aide/versions.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Version parsing and upstream update-check helpers."""
|
|
2
|
+
|
|
3
|
+
import email.utils
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import urllib.request
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from code_aide import __version__
|
|
12
|
+
from code_aide.constants import Colors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fetch_url(url: str, timeout: int = 30) -> tuple:
|
|
16
|
+
"""Fetch content from a URL. Returns (bytes, last_modified_str)."""
|
|
17
|
+
req = urllib.request.Request(
|
|
18
|
+
url, headers={"User-Agent": f"code-aide/{__version__}"}
|
|
19
|
+
)
|
|
20
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
21
|
+
content = response.read()
|
|
22
|
+
last_modified = response.headers.get("Last-Modified")
|
|
23
|
+
return content, last_modified
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_http_date(date_str: Optional[str]) -> Optional[str]:
|
|
27
|
+
"""Parse an HTTP date header into YYYY-MM-DD format."""
|
|
28
|
+
if not date_str:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
parsed = email.utils.parsedate_to_datetime(date_str)
|
|
32
|
+
return parsed.strftime("%Y-%m-%d")
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_iso_date(date_str: Optional[str]) -> Optional[str]:
|
|
38
|
+
"""Parse an ISO 8601 date string into YYYY-MM-DD format."""
|
|
39
|
+
if not date_str:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
|
43
|
+
return dt.strftime("%Y-%m-%d")
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_version(version: str) -> str:
|
|
49
|
+
"""Normalize a version string for storage and comparison."""
|
|
50
|
+
return version.lstrip("v")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def status_version_matches_latest(status_version: str, latest_version: str) -> bool:
|
|
54
|
+
"""Return True when a tool-reported version string matches latest_version."""
|
|
55
|
+
if not status_version or not latest_version:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
latest_norm = normalize_version(latest_version.strip())
|
|
59
|
+
status_text = status_version.strip()
|
|
60
|
+
|
|
61
|
+
if normalize_version(status_text) == latest_norm:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
patterns = [
|
|
65
|
+
r"\d{4}\.\d{2}\.\d{2}-[0-9a-f]+",
|
|
66
|
+
r"[vV]?\d+(?:\.\d+)+(?:[-+][0-9A-Za-z._-]+)?",
|
|
67
|
+
]
|
|
68
|
+
for pattern in patterns:
|
|
69
|
+
for match in re.finditer(pattern, status_text):
|
|
70
|
+
if normalize_version(match.group(0)) == latest_norm:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_version_from_string(version_string: str) -> Optional[str]:
|
|
77
|
+
"""Extract a normalized version number from a tool version output string."""
|
|
78
|
+
if not version_string:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
text = version_string.strip()
|
|
82
|
+
|
|
83
|
+
normalized = normalize_version(text)
|
|
84
|
+
if re.match(r"^\d+(?:\.\d+)+$", normalized):
|
|
85
|
+
return normalized
|
|
86
|
+
|
|
87
|
+
patterns = [
|
|
88
|
+
r"\d{4}\.\d{2}\.\d{2}-[0-9a-f]+",
|
|
89
|
+
r"[vV]?\d+(?:\.\d+)+(?:[-+][0-9A-Za-z._-]+)?",
|
|
90
|
+
]
|
|
91
|
+
for pattern in patterns:
|
|
92
|
+
match = re.search(pattern, text)
|
|
93
|
+
if match:
|
|
94
|
+
return normalize_version(match.group(0))
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def version_is_newer(version_a: str, version_b: str) -> bool:
|
|
100
|
+
"""Return True if version_a is strictly newer than version_b."""
|
|
101
|
+
|
|
102
|
+
def parse_components(version: str) -> list:
|
|
103
|
+
parts = re.split(r"[.\-]", version)
|
|
104
|
+
result = []
|
|
105
|
+
for part in parts:
|
|
106
|
+
try:
|
|
107
|
+
result.append((0, int(part)))
|
|
108
|
+
except ValueError:
|
|
109
|
+
result.append((1, part))
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
a_parts = parse_components(version_a)
|
|
113
|
+
b_parts = parse_components(version_b)
|
|
114
|
+
|
|
115
|
+
return a_parts > b_parts
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def check_npm_tool(
|
|
119
|
+
tool_name: str, tool_config: Dict[str, Any], verbose: bool = False
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""Check an npm tool for the latest version and publish date."""
|
|
122
|
+
package = tool_config["npm_package"]
|
|
123
|
+
result: Dict[str, Any] = {
|
|
124
|
+
"tool": tool_name,
|
|
125
|
+
"type": "npm",
|
|
126
|
+
"version": "-",
|
|
127
|
+
"date": "-",
|
|
128
|
+
"status": "unknown",
|
|
129
|
+
"update": None,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
url = f"https://registry.npmjs.org/{package}"
|
|
134
|
+
raw, _ = fetch_url(url)
|
|
135
|
+
data = json.loads(raw)
|
|
136
|
+
|
|
137
|
+
latest_version = data.get("dist-tags", {}).get("latest", "?")
|
|
138
|
+
result["version"] = latest_version
|
|
139
|
+
|
|
140
|
+
time_info = data.get("time", {})
|
|
141
|
+
publish_date = time_info.get(latest_version)
|
|
142
|
+
if publish_date:
|
|
143
|
+
result["date"] = parse_iso_date(publish_date) or "-"
|
|
144
|
+
|
|
145
|
+
result["status"] = "ok"
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
result["status"] = "error"
|
|
148
|
+
if verbose:
|
|
149
|
+
result["version"] = f"error: {exc}"
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_script_date(
|
|
155
|
+
version_info: Optional[str], last_modified: Optional[str]
|
|
156
|
+
) -> Optional[str]:
|
|
157
|
+
"""Extract a date from a script tool's version string or HTTP header."""
|
|
158
|
+
if version_info:
|
|
159
|
+
epoch_match = re.search(r"\.(\d{10,})", version_info)
|
|
160
|
+
if epoch_match:
|
|
161
|
+
try:
|
|
162
|
+
dt = datetime.fromtimestamp(int(epoch_match.group(1)), tz=timezone.utc)
|
|
163
|
+
return dt.strftime("%Y-%m-%d")
|
|
164
|
+
except (ValueError, OSError):
|
|
165
|
+
pass
|
|
166
|
+
match = re.match(r"(\d{4})\.(\d{2})\.(\d{2})", version_info)
|
|
167
|
+
if match:
|
|
168
|
+
return f"{match.group(1)}-{match.group(2)}-{match.group(3)}"
|
|
169
|
+
return parse_http_date(last_modified)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def extract_script_version(
|
|
173
|
+
tool_name: str,
|
|
174
|
+
tool_config: Dict[str, Any],
|
|
175
|
+
script_content: bytes,
|
|
176
|
+
) -> Optional[str]:
|
|
177
|
+
"""Try to extract a version string from script content or version URL."""
|
|
178
|
+
version_url = tool_config.get("version_url")
|
|
179
|
+
if version_url:
|
|
180
|
+
try:
|
|
181
|
+
version_data, _ = fetch_url(version_url)
|
|
182
|
+
version_str = version_data.decode("utf-8").strip()
|
|
183
|
+
if "<" not in version_str and len(version_str) < 50:
|
|
184
|
+
if not version_str.startswith("v"):
|
|
185
|
+
version_str = f"v{version_str}"
|
|
186
|
+
return version_str
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
text = script_content.decode("utf-8", errors="replace")
|
|
191
|
+
|
|
192
|
+
if tool_name == "cursor":
|
|
193
|
+
match = re.search(r"(\d{4}\.\d{2}\.\d{2}-[0-9a-f]+)", text)
|
|
194
|
+
if match:
|
|
195
|
+
return match.group(1)
|
|
196
|
+
|
|
197
|
+
for pattern in [
|
|
198
|
+
r'VERSION="([^"]+)"',
|
|
199
|
+
r"VERSION='([^']+)'",
|
|
200
|
+
r"VERSION=(\S+)",
|
|
201
|
+
]:
|
|
202
|
+
match = re.search(pattern, text)
|
|
203
|
+
if match:
|
|
204
|
+
return match.group(1)
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def check_script_tool(
|
|
210
|
+
tool_name: str, tool_config: Dict[str, Any], verbose: bool = False
|
|
211
|
+
) -> Dict[str, Any]:
|
|
212
|
+
"""Check a script/direct_download tool for SHA256 changes, version, and date."""
|
|
213
|
+
install_url = tool_config["install_url"]
|
|
214
|
+
current_sha256 = tool_config.get("install_sha256", "")
|
|
215
|
+
|
|
216
|
+
result: Dict[str, Any] = {
|
|
217
|
+
"tool": tool_name,
|
|
218
|
+
"type": tool_config.get("install_type", "script"),
|
|
219
|
+
"version": "-",
|
|
220
|
+
"date": "-",
|
|
221
|
+
"sha256_current": current_sha256[:12] + "..." if current_sha256 else "none",
|
|
222
|
+
"sha256_latest": "-",
|
|
223
|
+
"status": "unknown",
|
|
224
|
+
"update": None,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
script_content, last_modified = fetch_url(install_url)
|
|
229
|
+
actual_sha256 = hashlib.sha256(script_content).hexdigest()
|
|
230
|
+
|
|
231
|
+
if verbose:
|
|
232
|
+
result["sha256_current"] = current_sha256 or "none"
|
|
233
|
+
result["sha256_latest"] = actual_sha256
|
|
234
|
+
else:
|
|
235
|
+
result["sha256_latest"] = actual_sha256[:12] + "..."
|
|
236
|
+
|
|
237
|
+
version_info = extract_script_version(tool_name, tool_config, script_content)
|
|
238
|
+
if version_info:
|
|
239
|
+
result["version"] = version_info
|
|
240
|
+
|
|
241
|
+
date_str = extract_script_date(version_info, last_modified)
|
|
242
|
+
if date_str:
|
|
243
|
+
result["date"] = date_str
|
|
244
|
+
|
|
245
|
+
if actual_sha256 == current_sha256:
|
|
246
|
+
result["status"] = "ok"
|
|
247
|
+
else:
|
|
248
|
+
result["status"] = "changed"
|
|
249
|
+
result["update"] = {"install_sha256": actual_sha256}
|
|
250
|
+
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
result["status"] = "error"
|
|
253
|
+
if verbose:
|
|
254
|
+
result["version"] = f"error: {exc}"
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def format_check_status(status: str) -> str:
|
|
260
|
+
"""Format an update-check status string with color."""
|
|
261
|
+
if status == "ok":
|
|
262
|
+
return f"{Colors.GREEN}ok{Colors.NC}"
|
|
263
|
+
if status == "changed":
|
|
264
|
+
return f"{Colors.YELLOW}changed{Colors.NC}"
|
|
265
|
+
if status == "error":
|
|
266
|
+
return f"{Colors.RED}error{Colors.NC}"
|
|
267
|
+
return status
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def format_check_backend(check_type: str) -> str:
|
|
271
|
+
"""Format update-check backend labels for display."""
|
|
272
|
+
if check_type == "npm":
|
|
273
|
+
return "npm-registry"
|
|
274
|
+
if check_type in ("script", "direct_download"):
|
|
275
|
+
return "script-url"
|
|
276
|
+
if check_type == "self_managed":
|
|
277
|
+
return "npm-registry"
|
|
278
|
+
return check_type
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def print_check_results_table(
|
|
282
|
+
results: List[Dict[str, Any]], verbose: bool = False
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Print update-check results as a formatted table."""
|
|
285
|
+
if verbose:
|
|
286
|
+
headers = [
|
|
287
|
+
"Tool",
|
|
288
|
+
"Check",
|
|
289
|
+
"Version",
|
|
290
|
+
"Date",
|
|
291
|
+
"Current SHA256",
|
|
292
|
+
"Latest SHA256",
|
|
293
|
+
"Status",
|
|
294
|
+
]
|
|
295
|
+
else:
|
|
296
|
+
headers = ["Tool", "Check", "Version", "Date", "Status"]
|
|
297
|
+
|
|
298
|
+
rows = []
|
|
299
|
+
for result in results:
|
|
300
|
+
if verbose:
|
|
301
|
+
rows.append(
|
|
302
|
+
[
|
|
303
|
+
result["tool"],
|
|
304
|
+
format_check_backend(result["type"]),
|
|
305
|
+
result.get("version", "-"),
|
|
306
|
+
result.get("date", "-"),
|
|
307
|
+
result.get("sha256_current", "-"),
|
|
308
|
+
result.get("sha256_latest", "-"),
|
|
309
|
+
result["status"],
|
|
310
|
+
]
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
rows.append(
|
|
314
|
+
[
|
|
315
|
+
result["tool"],
|
|
316
|
+
format_check_backend(result["type"]),
|
|
317
|
+
result.get("version", "-"),
|
|
318
|
+
result.get("date", "-"),
|
|
319
|
+
result["status"],
|
|
320
|
+
]
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
widths = [len(header) for header in headers]
|
|
324
|
+
for row in rows:
|
|
325
|
+
for i, cell in enumerate(row):
|
|
326
|
+
plain = re.sub(r"\033\[[^m]*m", "", str(cell))
|
|
327
|
+
widths[i] = max(widths[i], len(plain))
|
|
328
|
+
|
|
329
|
+
header_line = " ".join(header.ljust(widths[i]) for i, header in enumerate(headers))
|
|
330
|
+
print(f"\n{Colors.BOLD}{header_line}{Colors.NC}")
|
|
331
|
+
print(" ".join("-" * width for width in widths))
|
|
332
|
+
|
|
333
|
+
for row in rows:
|
|
334
|
+
cells = []
|
|
335
|
+
for i, cell in enumerate(row):
|
|
336
|
+
if i == len(row) - 1:
|
|
337
|
+
cells.append(format_check_status(cell))
|
|
338
|
+
else:
|
|
339
|
+
cells.append(str(cell).ljust(widths[i]))
|
|
340
|
+
print(" ".join(cells))
|
|
341
|
+
|
|
342
|
+
print()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def apply_sha256_updates(
|
|
346
|
+
config: Dict[str, Any], results: List[Dict[str, Any]]
|
|
347
|
+
) -> List[str]:
|
|
348
|
+
"""Apply pending SHA256 updates to the config dict."""
|
|
349
|
+
updated = []
|
|
350
|
+
for result in results:
|
|
351
|
+
if result["update"]:
|
|
352
|
+
tool_name = result["tool"]
|
|
353
|
+
for key, value in result["update"].items():
|
|
354
|
+
config["tools"][tool_name][key] = value
|
|
355
|
+
updated.append(tool_name)
|
|
356
|
+
return updated
|