code-aide 1.12.3__tar.gz → 1.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {code_aide-1.12.3 → code_aide-1.14.0}/PKG-INFO +1 -1
  2. {code_aide-1.12.3 → code_aide-1.14.0}/script-archive/amp-install.sh +2 -9
  3. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/__init__.py +1 -1
  4. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/commands_tools.py +7 -6
  5. code_aide-1.14.0/src/code_aide/constants.py +51 -0
  6. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/data/tools.json +4 -1
  7. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/install.py +23 -1
  8. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/operations.py +45 -1
  9. code_aide-1.14.0/src/code_aide/package_managers.py +188 -0
  10. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/prereqs.py +19 -28
  11. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_commands_tools.py +2 -2
  12. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_install.py +96 -0
  13. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_operations.py +59 -0
  14. code_aide-1.14.0/tests/test_package_managers.py +277 -0
  15. code_aide-1.12.3/src/code_aide/constants.py +0 -104
  16. {code_aide-1.12.3 → code_aide-1.14.0}/.github/workflows/ci.yml +0 -0
  17. {code_aide-1.12.3 → code_aide-1.14.0}/.github/workflows/publish.yml +0 -0
  18. {code_aide-1.12.3 → code_aide-1.14.0}/.gitignore +0 -0
  19. {code_aide-1.12.3 → code_aide-1.14.0}/.gitlab-ci.yml +0 -0
  20. {code_aide-1.12.3 → code_aide-1.14.0}/.pre-commit-config.yaml +0 -0
  21. {code_aide-1.12.3 → code_aide-1.14.0}/AGENTS.md +0 -0
  22. {code_aide-1.12.3 → code_aide-1.14.0}/CLAUDE.md +0 -0
  23. {code_aide-1.12.3 → code_aide-1.14.0}/LICENSE +0 -0
  24. {code_aide-1.12.3 → code_aide-1.14.0}/README.md +0 -0
  25. {code_aide-1.12.3 → code_aide-1.14.0}/TODO.md +0 -0
  26. {code_aide-1.12.3 → code_aide-1.14.0}/pyproject.toml +0 -0
  27. {code_aide-1.12.3 → code_aide-1.14.0}/script-archive/README.md +0 -0
  28. {code_aide-1.12.3 → code_aide-1.14.0}/script-archive/claude-install.sh +0 -0
  29. {code_aide-1.12.3 → code_aide-1.14.0}/script-archive/cursor-install.sh +0 -0
  30. {code_aide-1.12.3 → code_aide-1.14.0}/specs/auto-migrate-deprecated-installs.md +0 -0
  31. {code_aide-1.12.3 → code_aide-1.14.0}/specs/claude-native-installer-migration.md +0 -0
  32. {code_aide-1.12.3 → code_aide-1.14.0}/specs/missing-coding-llm-cli-tools.md +0 -0
  33. {code_aide-1.12.3 → code_aide-1.14.0}/specs/pre-commit-uv-setup.md +0 -0
  34. {code_aide-1.12.3 → code_aide-1.14.0}/specs/remove-bundled-version-baseline.md +0 -0
  35. {code_aide-1.12.3 → code_aide-1.14.0}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
  36. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/__main__.py +0 -0
  37. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/commands_actions.py +0 -0
  38. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/config.py +0 -0
  39. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/console.py +0 -0
  40. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/detection.py +0 -0
  41. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/entry.py +0 -0
  42. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/install_types.py +0 -0
  43. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/status.py +0 -0
  44. {code_aide-1.12.3 → code_aide-1.14.0}/src/code_aide/versions.py +0 -0
  45. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_commands_actions.py +0 -0
  46. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_config.py +0 -0
  47. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_console.py +0 -0
  48. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_constants.py +0 -0
  49. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_detection.py +0 -0
  50. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_install_types.py +0 -0
  51. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_status.py +0 -0
  52. {code_aide-1.12.3 → code_aide-1.14.0}/tests/test_versions.py +0 -0
  53. {code_aide-1.12.3 → code_aide-1.14.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-aide
3
- Version: 1.12.3
3
+ Version: 1.14.0
4
4
  Summary: Manage AI coding CLI tools (Claude, Copilot, Cursor, Gemini, Amp, Codex)
5
5
  Project-URL: Homepage, https://github.com/dajobe/code-aide
6
6
  Project-URL: Repository, https://github.com/dajobe/code-aide
@@ -1,10 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- # Amp CLI Binary Installation Script (EXPERIMENTAL - NOT DOCUMENTED)
5
- # This is a secondary install script for testing binary distribution
6
- # Downloads pre-compiled Amp CLI binary instead of using npm
7
-
8
4
  # Configuration
9
5
  AMP_HOME="${AMP_HOME:-$HOME/.amp}"
10
6
  BIN_DIR="$AMP_HOME/bin"
@@ -437,11 +433,8 @@ update_shell_profile() {
437
433
  return
438
434
  fi
439
435
  else
440
- # Non-interactive: show instructions instead
441
- log "Non-interactive mode: skipping shell config modification."
442
- log "To use amp, add ~/.local/bin to your PATH:"
443
- echo " $path_export"
444
- return
436
+ # Non-interactive: add automatically
437
+ log "Adding ~/.local/bin to PATH in $tilde_profile..."
445
438
  fi
446
439
 
447
440
  # Create config file if it doesn't exist
@@ -1,3 +1,3 @@
1
1
  """code-aide - Manage AI coding CLI tools."""
2
2
 
3
- __version__ = "1.12.3"
3
+ __version__ = "1.14.0"
@@ -8,7 +8,7 @@ import shutil
8
8
  from typing import List
9
9
 
10
10
  from code_aide.config import ensure_versions_cache
11
- from code_aide.constants import Colors, PACKAGE_MANAGERS, TOOLS
11
+ from code_aide.constants import Colors, TOOLS
12
12
  from code_aide.detection import (
13
13
  format_install_method,
14
14
  format_migration_warning,
@@ -20,7 +20,10 @@ from code_aide.install_types import (
20
20
  parse_install_method,
21
21
  parse_install_type,
22
22
  )
23
- from code_aide.prereqs import detect_package_manager, is_tool_installed
23
+ from code_aide.package_managers import (
24
+ detect_package_manager as _detect_package_manager,
25
+ )
26
+ from code_aide.prereqs import is_tool_installed
24
27
  from code_aide.detection import is_freebsd
25
28
  from code_aide.status import (
26
29
  print_brew_version_status,
@@ -107,11 +110,9 @@ def cmd_list(args: argparse.Namespace) -> None:
107
110
  except Exception:
108
111
  pass
109
112
 
110
- pkg_mgr = detect_package_manager()
113
+ pkg_mgr = _detect_package_manager()
111
114
  if pkg_mgr:
112
- print(
113
- f" Package manager: {PACKAGE_MANAGERS[pkg_mgr]['description']} ({pkg_mgr})"
114
- )
115
+ print(f" Package manager: {pkg_mgr.description} ({pkg_mgr.detect_command})")
115
116
 
116
117
 
117
118
  def _short_install_method(method: str | None) -> str:
@@ -0,0 +1,51 @@
1
+ """Shared constants and mutable tool configuration for CLI modules."""
2
+
3
+ import os
4
+ import sys
5
+ from typing import Any, Dict
6
+
7
+ from code_aide.config import load_tools_config
8
+
9
+
10
+ def _use_color() -> bool:
11
+ """Determine whether to emit ANSI color codes.
12
+
13
+ Checks (in priority order):
14
+ NO_COLOR — if set (any value), no color (https://no-color.org/)
15
+ FORCE_COLOR — if set (any value), force color
16
+ CLICOLOR_FORCE — if non-empty, force color
17
+ TERM=dumb — no color
18
+ CLICOLOR=0 — no color
19
+ stdout is not a TTY — no color
20
+ """
21
+ if os.environ.get("NO_COLOR") is not None:
22
+ return False
23
+ if os.environ.get("FORCE_COLOR") is not None:
24
+ return True
25
+ if os.environ.get("CLICOLOR_FORCE", ""):
26
+ return True
27
+ if os.environ.get("TERM") == "dumb":
28
+ return False
29
+ if os.environ.get("CLICOLOR") == "0":
30
+ return False
31
+ return sys.stdout.isatty()
32
+
33
+
34
+ class Colors:
35
+ if _use_color():
36
+ RED = "\033[0;31m"
37
+ GREEN = "\033[0;32m"
38
+ YELLOW = "\033[1;33m"
39
+ BLUE = "\033[0;34m"
40
+ BOLD = "\033[1m"
41
+ NC = "\033[0m"
42
+ else:
43
+ RED = ""
44
+ GREEN = ""
45
+ YELLOW = ""
46
+ BLUE = ""
47
+ BOLD = ""
48
+ NC = ""
49
+
50
+
51
+ TOOLS: Dict[str, Dict[str, Any]] = load_tools_config()
@@ -64,7 +64,10 @@
64
64
  "command": "amp",
65
65
  "install_type": "script",
66
66
  "install_url": "https://ampcode.com/install.sh",
67
- "install_sha256": "a529b01e1e3653ad3e77cf8e5ec1f2cede5d65c880a62e783a3365bbd825be6d",
67
+ "install_sha256": "2442f20759a2be3beaef6691c7e5bf349f469eb0ff7ea2885746500243446e24",
68
+ "install_script_path_prepend": [
69
+ "~/.local/bin"
70
+ ],
68
71
  "version_url": "https://storage.googleapis.com/amp-public-assets-prod-0/cli/cli-version.txt",
69
72
  "prerequisites": [],
70
73
  "min_node_version": null,
@@ -43,6 +43,7 @@ def run_install_script(
43
43
  tool_name: str,
44
44
  expected_sha256: Optional[str] = None,
45
45
  dryrun: bool = False,
46
+ env: Optional[Dict[str, str]] = None,
46
47
  ) -> bool:
47
48
  """Download and run an installation script with SHA256 verification."""
48
49
  try:
@@ -82,6 +83,7 @@ def run_install_script(
82
83
  ["bash"],
83
84
  stdin=subprocess.PIPE,
84
85
  stderr=subprocess.PIPE,
86
+ env=env,
85
87
  )
86
88
  _, stderr = bash_process.communicate(input=script_content)
87
89
 
@@ -99,6 +101,22 @@ def run_install_script(
99
101
  return False
100
102
 
101
103
 
104
+ def get_install_script_env(tool_config: Dict[str, Any]) -> Optional[Dict[str, str]]:
105
+ """Return environment overrides for running a tool's install script."""
106
+ path_prepend = tool_config.get("install_script_path_prepend", [])
107
+ if not path_prepend:
108
+ return None
109
+
110
+ env = os.environ.copy()
111
+ expanded_paths = [os.path.expanduser(path) for path in path_prepend]
112
+ current_path = env.get("PATH", "")
113
+ path_parts = [*expanded_paths]
114
+ if current_path:
115
+ path_parts.append(current_path)
116
+ env["PATH"] = os.pathsep.join(path_parts)
117
+ return env
118
+
119
+
102
120
  ARCH_MAP = {
103
121
  "x86_64": "x64",
104
122
  "amd64": "x64",
@@ -327,7 +345,11 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
327
345
  install_url = tool_config["install_url"]
328
346
  expected_sha256 = tool_config.get("install_sha256")
329
347
  if run_install_script(
330
- install_url, tool_config["name"], expected_sha256, dryrun
348
+ install_url,
349
+ tool_config["name"],
350
+ expected_sha256,
351
+ dryrun,
352
+ env=get_install_script_env(tool_config),
331
353
  ):
332
354
  if dryrun:
333
355
  success(f"{tool_config['name']} verification passed")
@@ -14,7 +14,9 @@ from code_aide.detection import (
14
14
  format_install_method,
15
15
  is_deprecated_install,
16
16
  )
17
+ from code_aide.package_managers import query_package_owner
17
18
  from code_aide.install import (
19
+ get_install_script_env,
18
20
  install_direct_download,
19
21
  install_tool,
20
22
  run_install_script,
@@ -83,6 +85,44 @@ def _upgrade_result_from_snapshots(
83
85
  return UpgradeResult.CHANGED
84
86
 
85
87
 
88
+ def _warn_duplicate_system_install(tool_name: str) -> None:
89
+ """Warn if a duplicate system-packaged binary shadows or coexists."""
90
+ tool_config = TOOLS[tool_name]
91
+ command = tool_config["command"]
92
+
93
+ # Find all instances of the command in PATH
94
+ seen = set()
95
+ paths = []
96
+ for directory in os.environ.get("PATH", "").split(os.pathsep):
97
+ candidate = os.path.join(directory, command)
98
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
99
+ real = os.path.realpath(candidate)
100
+ if real not in seen:
101
+ seen.add(real)
102
+ paths.append(real)
103
+
104
+ if len(paths) < 2:
105
+ return
106
+
107
+ system_prefixes = ("/opt/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/")
108
+ for path in paths:
109
+ if not any(path.startswith(p) for p in system_prefixes):
110
+ continue
111
+ package, remove_cmd = query_package_owner(path)
112
+ if package and remove_cmd:
113
+ warning(
114
+ f"A system-packaged {command} is also installed at {path} "
115
+ f"(package: {package})."
116
+ )
117
+ info(f"To remove it, run: {remove_cmd}")
118
+ else:
119
+ warning(
120
+ f"A system-packaged {command} is also installed at {path}. "
121
+ "You may want to remove it with your package manager."
122
+ )
123
+ return
124
+
125
+
86
126
  def _migrate_install_method(tool_name: str) -> UpgradeResult:
87
127
  """Migrate a tool from a deprecated install method to the configured one.
88
128
 
@@ -129,6 +169,7 @@ def _migrate_install_method(tool_name: str) -> UpgradeResult:
129
169
  return UpgradeResult.FAILED
130
170
 
131
171
  success(f"{tool_config['name']} migrated from {old_label} to {new_label}")
172
+ _warn_duplicate_system_install(tool_name)
132
173
  return UpgradeResult.CHANGED
133
174
 
134
175
 
@@ -185,7 +226,10 @@ def upgrade_tool(tool_name: str) -> UpgradeResult:
185
226
  install_url = tool_config["install_url"]
186
227
  expected_sha256 = tool_config.get("install_sha256")
187
228
  if run_install_script(
188
- install_url, tool_config["name"], expected_sha256
229
+ install_url,
230
+ tool_config["name"],
231
+ expected_sha256,
232
+ env=get_install_script_env(tool_config),
189
233
  ):
190
234
  pass
191
235
  else:
@@ -0,0 +1,188 @@
1
+ """System package manager definitions and helpers."""
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import List, Optional
10
+
11
+
12
+ class PackageManager(Enum):
13
+ """Known system package managers."""
14
+
15
+ APT = "apt-get"
16
+ DNF = "dnf"
17
+ YUM = "yum"
18
+ PACMAN = "pacman"
19
+ ZYPPER = "zypper"
20
+ EMERGE = "emerge"
21
+ PKG = "pkg"
22
+ HOMEBREW = "brew"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PackageManagerInfo:
27
+ """Configuration for a system package manager."""
28
+
29
+ manager: PackageManager
30
+ description: str
31
+ detect_command: str
32
+ packages: List[str]
33
+ install_command: List[str]
34
+ pre_install: List[List[str]] = field(default_factory=list)
35
+ query_owner_command: List[str] = field(default_factory=list)
36
+ remove_command: List[str] = field(default_factory=list)
37
+
38
+
39
+ _MANAGERS: List[PackageManagerInfo] = [
40
+ PackageManagerInfo(
41
+ manager=PackageManager.APT,
42
+ description="Debian/Ubuntu",
43
+ detect_command="apt-get",
44
+ packages=["nodejs", "npm"],
45
+ pre_install=[["sudo", "apt-get", "update"]],
46
+ install_command=["sudo", "apt-get", "install", "-y"],
47
+ query_owner_command=["dpkg", "-S"],
48
+ remove_command=["sudo", "apt-get", "remove"],
49
+ ),
50
+ PackageManagerInfo(
51
+ manager=PackageManager.DNF,
52
+ description="Fedora/RHEL 8+",
53
+ detect_command="dnf",
54
+ packages=["nodejs", "npm"],
55
+ install_command=["sudo", "dnf", "install", "-y"],
56
+ query_owner_command=["rpm", "-qf"],
57
+ remove_command=["sudo", "dnf", "remove"],
58
+ ),
59
+ PackageManagerInfo(
60
+ manager=PackageManager.YUM,
61
+ description="RHEL/CentOS 7",
62
+ detect_command="yum",
63
+ packages=["nodejs", "npm"],
64
+ install_command=["sudo", "yum", "install", "-y"],
65
+ query_owner_command=["rpm", "-qf"],
66
+ remove_command=["sudo", "yum", "remove"],
67
+ ),
68
+ PackageManagerInfo(
69
+ manager=PackageManager.PACMAN,
70
+ description="Arch Linux",
71
+ detect_command="pacman",
72
+ packages=["nodejs", "npm"],
73
+ install_command=["sudo", "pacman", "-S", "--noconfirm"],
74
+ query_owner_command=["pacman", "-Qo"],
75
+ remove_command=["sudo", "pacman", "-R"],
76
+ ),
77
+ PackageManagerInfo(
78
+ manager=PackageManager.ZYPPER,
79
+ description="openSUSE",
80
+ detect_command="zypper",
81
+ packages=["nodejs", "npm"],
82
+ install_command=["sudo", "zypper", "install", "-y"],
83
+ query_owner_command=["rpm", "-qf"],
84
+ remove_command=["sudo", "zypper", "remove"],
85
+ ),
86
+ PackageManagerInfo(
87
+ manager=PackageManager.EMERGE,
88
+ description="Gentoo",
89
+ detect_command="emerge",
90
+ packages=["net-libs/nodejs"],
91
+ install_command=["sudo", "emerge", "--quiet-build"],
92
+ query_owner_command=["qfile", "-qC"],
93
+ remove_command=["sudo", "emerge", "--unmerge"],
94
+ ),
95
+ PackageManagerInfo(
96
+ manager=PackageManager.PKG,
97
+ description="FreeBSD",
98
+ detect_command="pkg",
99
+ packages=["node22", "npm-node22"],
100
+ install_command=["sudo", "pkg", "install", "-y"],
101
+ query_owner_command=["pkg", "which", "-q"],
102
+ remove_command=["sudo", "pkg", "delete"],
103
+ ),
104
+ PackageManagerInfo(
105
+ manager=PackageManager.HOMEBREW,
106
+ description="macOS/Linux (Homebrew)",
107
+ detect_command="brew",
108
+ packages=["node"],
109
+ install_command=["brew", "install"],
110
+ query_owner_command=["brew", "which-formula"],
111
+ remove_command=["brew", "uninstall"],
112
+ ),
113
+ ]
114
+
115
+ PACKAGE_MANAGER_BY_ENUM = {info.manager: info for info in _MANAGERS}
116
+
117
+
118
+ def detect_package_manager() -> Optional[PackageManagerInfo]:
119
+ """Detect the system package manager, or None on unsupported platforms."""
120
+ if platform.system() not in ("Linux", "FreeBSD", "Darwin"):
121
+ return None
122
+
123
+ for info in _MANAGERS:
124
+ if shutil.which(info.detect_command):
125
+ return info
126
+
127
+ return None
128
+
129
+
130
+ def _parse_package_name(manager: PackageManager, query_output: str) -> Optional[str]:
131
+ """Extract a package name from query-owner output."""
132
+ line = query_output.strip().split("\n")[0]
133
+ if not line:
134
+ return None
135
+
136
+ if manager == PackageManager.APT:
137
+ # dpkg -S output: "package: /path/to/file"
138
+ return line.split(":")[0].strip() or None
139
+ if manager == PackageManager.PACMAN:
140
+ # pacman -Qo output: "/path is owned by package version"
141
+ parts = line.split("is owned by")
142
+ if len(parts) > 1:
143
+ return parts[1].strip().rsplit(" ", 1)[0] or None
144
+ return None
145
+ # rpm -qf (dnf/yum/zypper): "package-version-release.arch"
146
+ # qfile -qC (emerge): "category/package"
147
+ # pkg which -q (FreeBSD): "package-name"
148
+ return line.strip() or None
149
+
150
+
151
+ def query_package_owner(
152
+ binary_path: str,
153
+ ) -> tuple:
154
+ """Identify the system package that owns a binary path.
155
+
156
+ Returns (package_name, remove_command_str) or (None, None).
157
+ """
158
+ mgr = detect_package_manager()
159
+ if not mgr or not mgr.query_owner_command or not mgr.remove_command:
160
+ return None, None
161
+
162
+ # brew which-formula takes the command name, not the full path.
163
+ query_arg = (
164
+ os.path.basename(binary_path)
165
+ if mgr.manager == PackageManager.HOMEBREW
166
+ else binary_path
167
+ )
168
+
169
+ try:
170
+ proc = subprocess.run(
171
+ [*mgr.query_owner_command, query_arg],
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=10,
175
+ check=False,
176
+ stdin=subprocess.DEVNULL,
177
+ )
178
+ if proc.returncode != 0 or not proc.stdout.strip():
179
+ return None, None
180
+ except Exception:
181
+ return None, None
182
+
183
+ package = _parse_package_name(mgr.manager, proc.stdout)
184
+ if not package:
185
+ return None, None
186
+
187
+ remove_cmd = " ".join([*mgr.remove_command, package])
188
+ return package, remove_cmd
@@ -6,7 +6,7 @@ import subprocess
6
6
  import sys
7
7
  from typing import List, Optional
8
8
 
9
- from code_aide.constants import PACKAGE_MANAGERS, TOOLS
9
+ from code_aide.constants import TOOLS
10
10
  from code_aide.console import (
11
11
  command_exists,
12
12
  error,
@@ -15,42 +15,38 @@ from code_aide.console import (
15
15
  success,
16
16
  warning,
17
17
  )
18
+ from code_aide.package_managers import (
19
+ _MANAGERS,
20
+ detect_package_manager,
21
+ )
18
22
 
19
23
 
20
- def detect_package_manager() -> Optional[str]:
21
- """Detect the Linux distribution and return package manager name."""
22
- if platform.system() not in ("Linux", "FreeBSD"):
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
24
+ def _print_all_install_hints() -> None:
25
+ """Print install commands for all known package managers."""
26
+ for mgr in _MANAGERS:
27
+ install_cmd = " ".join(mgr.install_command + mgr.packages)
28
+ print(f" {mgr.description}: {install_cmd}")
29
+ print(" Or visit: https://nodejs.org/")
30
30
 
31
31
 
32
32
  def install_nodejs_npm() -> bool:
33
33
  """Install Node.js and npm using the system package manager."""
34
- pkg_mgr_name = detect_package_manager()
34
+ mgr = detect_package_manager()
35
35
 
36
- if not pkg_mgr_name:
36
+ if not mgr:
37
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/")
38
+ _print_all_install_hints()
42
39
  return False
43
40
 
44
- config = PACKAGE_MANAGERS[pkg_mgr_name]
45
- info(f"Detected package manager: {pkg_mgr_name} ({config['description']})")
41
+ info(f"Detected package manager: {mgr.detect_command} ({mgr.description})")
46
42
  info("Installing Node.js and npm...")
47
43
 
48
44
  try:
49
- for pre_cmd in config["pre_install"]:
45
+ for pre_cmd in mgr.pre_install:
50
46
  info(f"Running: {' '.join(pre_cmd)}")
51
- run_command(pre_cmd, check=True, capture=False)
47
+ run_command(list(pre_cmd), check=True, capture=False)
52
48
 
53
- install_cmd = config["install_command"] + config["packages"]
49
+ install_cmd = list(mgr.install_command) + list(mgr.packages)
54
50
  run_command(install_cmd, check=True, capture=False)
55
51
 
56
52
  if not command_exists("npm"):
@@ -102,12 +98,7 @@ def check_prerequisites(
102
98
  else:
103
99
  error("npm is required but not installed.")
104
100
  error("Please install Node.js and npm first:")
105
- for manager_name, config in PACKAGE_MANAGERS.items():
106
- install_cmd = " ".join(
107
- config["install_command"] + config["packages"]
108
- )
109
- print(f" {config['description']}: {install_cmd}")
110
- print(" Or visit: https://nodejs.org/")
101
+ _print_all_install_hints()
111
102
  print("\nOr use -p/--install-prerequisites to install automatically")
112
103
  sys.exit(1)
113
104
 
@@ -26,7 +26,7 @@ class TestCmdList(unittest.TestCase):
26
26
  mock.patch.object(commands_tools, "is_tool_installed", return_value=False),
27
27
  mock.patch.object(commands_tools, "command_exists", return_value=False),
28
28
  mock.patch.object(
29
- commands_tools, "detect_package_manager", return_value=None
29
+ commands_tools, "_detect_package_manager", return_value=None
30
30
  ),
31
31
  ):
32
32
  buf = io.StringIO()
@@ -314,7 +314,7 @@ class TestCmdStatus(unittest.TestCase):
314
314
  ),
315
315
  mock.patch.object(commands_tools, "command_exists", return_value=False),
316
316
  mock.patch.object(
317
- commands_tools, "detect_package_manager", return_value=None
317
+ commands_tools, "_detect_package_manager", return_value=None
318
318
  ),
319
319
  ):
320
320
  buf = io.StringIO()
@@ -148,6 +148,67 @@ class TestExtractTarMember(unittest.TestCase):
148
148
  self.assertTrue(os.path.exists(extracted))
149
149
 
150
150
 
151
+ class TestInstallScriptEnv(unittest.TestCase):
152
+ """Tests for install script environment handling."""
153
+
154
+ def test_no_env_without_path_prepend(self):
155
+ self.assertIsNone(cli_install.get_install_script_env({}))
156
+
157
+ def test_prepends_expanded_paths(self):
158
+ with tempfile.TemporaryDirectory() as td:
159
+ with mock.patch.dict(
160
+ os.environ,
161
+ {"HOME": td, "PATH": "/usr/bin"},
162
+ clear=True,
163
+ ):
164
+ env = cli_install.get_install_script_env(
165
+ {"install_script_path_prepend": ["~/.local/bin"]}
166
+ )
167
+
168
+ self.assertIsNotNone(env)
169
+ self.assertEqual(
170
+ env["PATH"],
171
+ os.path.join(td, ".local", "bin") + os.pathsep + "/usr/bin",
172
+ )
173
+
174
+ def test_path_prepend_without_existing_path(self):
175
+ with tempfile.TemporaryDirectory() as td:
176
+ with mock.patch.dict(os.environ, {"HOME": td}, clear=True):
177
+ env = cli_install.get_install_script_env(
178
+ {"install_script_path_prepend": ["~/.local/bin"]}
179
+ )
180
+
181
+ self.assertIsNotNone(env)
182
+ self.assertEqual(env["PATH"], os.path.join(td, ".local", "bin"))
183
+
184
+ @mock.patch.object(cli_install, "fetch_url")
185
+ @mock.patch.object(cli_install.subprocess, "Popen")
186
+ def test_run_install_script_passes_env_to_bash(self, mock_popen, mock_fetch):
187
+ script_content = b"#!/usr/bin/env bash\n"
188
+ expected_sha256 = cli_install.hashlib.sha256(script_content).hexdigest()
189
+ process = mock.Mock()
190
+ process.communicate.return_value = (b"", b"")
191
+ process.returncode = 0
192
+ mock_popen.return_value = process
193
+ mock_fetch.return_value = (script_content, None)
194
+ env = {"PATH": "/tmp/bin:/usr/bin"}
195
+
196
+ result = cli_install.run_install_script(
197
+ "https://example.com/install.sh",
198
+ "Example",
199
+ expected_sha256,
200
+ env=env,
201
+ )
202
+
203
+ self.assertTrue(result)
204
+ mock_popen.assert_called_once_with(
205
+ ["bash"],
206
+ stdin=cli_install.subprocess.PIPE,
207
+ stderr=cli_install.subprocess.PIPE,
208
+ env=env,
209
+ )
210
+
211
+
151
212
  class TestInstallDirectDownloadDryrun(unittest.TestCase):
152
213
  """Tests for install_direct_download in dryrun mode."""
153
214
 
@@ -270,3 +331,38 @@ class TestInstallTool(unittest.TestCase):
270
331
  "/usr/local/bin/test-tool"
271
332
  )
272
333
  mock_info.assert_any_call("[DRYRUN] Would install npm package: test-tool")
334
+
335
+ def test_script_install_passes_configured_env(self):
336
+ tool_config = {
337
+ "name": "Test Tool",
338
+ "command": "test-tool",
339
+ "install_type": "script",
340
+ "install_url": "https://example.com/install.sh",
341
+ "install_sha256": "abc123",
342
+ "install_script_path_prepend": ["~/.local/bin"],
343
+ "next_steps": "Run test-tool",
344
+ }
345
+ env = {"PATH": "/tmp/bin:/usr/bin"}
346
+
347
+ with (
348
+ mock.patch.dict(cli_install.TOOLS, {"test": tool_config}, clear=True),
349
+ mock.patch.object(cli_install, "command_exists", return_value=False),
350
+ mock.patch.object(cli_install.platform, "system", return_value="Darwin"),
351
+ mock.patch.object(
352
+ cli_install, "get_install_script_env", return_value=env
353
+ ) as mock_env,
354
+ mock.patch.object(
355
+ cli_install, "run_install_script", return_value=True
356
+ ) as mock_script,
357
+ ):
358
+ result = cli_install.install_tool("test")
359
+
360
+ self.assertTrue(result)
361
+ mock_env.assert_called_once_with(tool_config)
362
+ mock_script.assert_called_once_with(
363
+ "https://example.com/install.sh",
364
+ "Test Tool",
365
+ "abc123",
366
+ False,
367
+ env=env,
368
+ )
@@ -244,6 +244,65 @@ class TestMigrateInstallMethod(unittest.TestCase):
244
244
  mock_dd.assert_called_once_with("cursor", tool_config)
245
245
  mock_script.assert_not_called()
246
246
 
247
+ def test_upgrade_script_passes_configured_env(self):
248
+ """Script upgrades pass per-tool install environment overrides."""
249
+ tool_config = {
250
+ "name": "Amp (Sourcegraph)",
251
+ "command": "amp",
252
+ "install_type": "script",
253
+ "install_url": "https://ampcode.com/install.sh",
254
+ "install_sha256": "abc123",
255
+ "install_script_path_prepend": ["~/.local/bin"],
256
+ }
257
+ env = {"PATH": "/tmp/bin:/usr/bin"}
258
+ with (
259
+ mock.patch.dict(cli_operations.TOOLS, {"amp": tool_config}),
260
+ mock.patch.object(cli_operations, "is_tool_installed", return_value=True),
261
+ mock.patch.object(
262
+ cli_operations, "is_deprecated_install", return_value=False
263
+ ),
264
+ mock.patch.object(
265
+ cli_operations,
266
+ "detect_install_method",
267
+ side_effect=[
268
+ {"method": "script", "detail": "native installer"},
269
+ {"method": "script", "detail": "native installer"},
270
+ ],
271
+ ),
272
+ mock.patch.object(
273
+ cli_operations, "get_install_script_env", return_value=env
274
+ ) as mock_env,
275
+ mock.patch.object(
276
+ cli_operations, "run_install_script", return_value=True
277
+ ) as mock_script,
278
+ mock.patch.object(
279
+ cli_operations,
280
+ "_get_upgrade_snapshot",
281
+ side_effect=[
282
+ {
283
+ "method": "script",
284
+ "detail": "native installer",
285
+ "version": "1.0.0",
286
+ },
287
+ {
288
+ "method": "script",
289
+ "detail": "native installer",
290
+ "version": "2.0.0",
291
+ },
292
+ ],
293
+ ),
294
+ ):
295
+ result = cli_operations.upgrade_tool("amp")
296
+
297
+ self.assertEqual(result, UpgradeResult.CHANGED)
298
+ mock_env.assert_called_once_with(tool_config)
299
+ mock_script.assert_called_once_with(
300
+ "https://ampcode.com/install.sh",
301
+ "Amp (Sourcegraph)",
302
+ "abc123",
303
+ env=env,
304
+ )
305
+
247
306
  def test_migration_fails_on_remove(self):
248
307
  """Migration fails if remove_tool returns False."""
249
308
  tool_config = {
@@ -0,0 +1,277 @@
1
+ """Unit tests for the package_managers module."""
2
+
3
+ import subprocess
4
+ import unittest
5
+ from unittest import mock
6
+
7
+ from code_aide.package_managers import (
8
+ PACKAGE_MANAGER_BY_ENUM,
9
+ PackageManager,
10
+ _MANAGERS,
11
+ _parse_package_name,
12
+ detect_package_manager,
13
+ query_package_owner,
14
+ )
15
+
16
+
17
+ class TestPackageManagerEnum(unittest.TestCase):
18
+ """Tests for the PackageManager enum."""
19
+
20
+ def test_all_managers_have_entries(self):
21
+ for mgr in PackageManager:
22
+ self.assertIn(mgr, PACKAGE_MANAGER_BY_ENUM)
23
+
24
+ def test_manager_list_matches_enum(self):
25
+ managers_in_list = {info.manager for info in _MANAGERS}
26
+ self.assertEqual(managers_in_list, set(PackageManager))
27
+
28
+ def test_no_duplicate_detect_commands(self):
29
+ commands = [info.detect_command for info in _MANAGERS]
30
+ self.assertEqual(len(commands), len(set(commands)))
31
+
32
+
33
+ class TestPackageManagerInfo(unittest.TestCase):
34
+ """Tests for PackageManagerInfo completeness."""
35
+
36
+ def test_all_managers_have_install_command(self):
37
+ for info in _MANAGERS:
38
+ self.assertTrue(
39
+ len(info.install_command) > 0,
40
+ f"{info.manager.name} missing install_command",
41
+ )
42
+
43
+ def test_all_managers_have_query_owner_command(self):
44
+ for info in _MANAGERS:
45
+ self.assertTrue(
46
+ len(info.query_owner_command) > 0,
47
+ f"{info.manager.name} missing query_owner_command",
48
+ )
49
+
50
+ def test_all_managers_have_remove_command(self):
51
+ for info in _MANAGERS:
52
+ self.assertTrue(
53
+ len(info.remove_command) > 0,
54
+ f"{info.manager.name} missing remove_command",
55
+ )
56
+
57
+ def test_all_managers_have_description(self):
58
+ for info in _MANAGERS:
59
+ self.assertTrue(
60
+ len(info.description) > 0,
61
+ f"{info.manager.name} missing description",
62
+ )
63
+
64
+ def test_all_managers_have_packages(self):
65
+ for info in _MANAGERS:
66
+ self.assertTrue(
67
+ len(info.packages) > 0,
68
+ f"{info.manager.name} missing packages",
69
+ )
70
+
71
+ def test_info_is_frozen(self):
72
+ info = PACKAGE_MANAGER_BY_ENUM[PackageManager.APT]
73
+ with self.assertRaises(AttributeError):
74
+ info.description = "changed"
75
+
76
+
77
+ class TestDetectPackageManager(unittest.TestCase):
78
+ """Tests for detect_package_manager."""
79
+
80
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Windows")
81
+ def test_returns_none_on_windows(self, _mock_sys):
82
+ self.assertIsNone(detect_package_manager())
83
+
84
+ @mock.patch("code_aide.package_managers.shutil.which", return_value=None)
85
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Linux")
86
+ def test_returns_none_when_no_manager_found(self, _mock_sys, _mock_which):
87
+ self.assertIsNone(detect_package_manager())
88
+
89
+ @mock.patch("code_aide.package_managers.shutil.which")
90
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Linux")
91
+ def test_detects_apt(self, _mock_sys, mock_which):
92
+ mock_which.side_effect = lambda cmd: (
93
+ "/usr/bin/apt-get" if cmd == "apt-get" else None
94
+ )
95
+ result = detect_package_manager()
96
+ self.assertIsNotNone(result)
97
+ self.assertEqual(result.manager, PackageManager.APT)
98
+
99
+ @mock.patch("code_aide.package_managers.shutil.which")
100
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Linux")
101
+ def test_detects_emerge(self, _mock_sys, mock_which):
102
+ mock_which.side_effect = lambda cmd: (
103
+ "/usr/bin/emerge" if cmd == "emerge" else None
104
+ )
105
+ result = detect_package_manager()
106
+ self.assertIsNotNone(result)
107
+ self.assertEqual(result.manager, PackageManager.EMERGE)
108
+
109
+ @mock.patch("code_aide.package_managers.shutil.which")
110
+ @mock.patch("code_aide.package_managers.platform.system", return_value="FreeBSD")
111
+ def test_detects_pkg_on_freebsd(self, _mock_sys, mock_which):
112
+ mock_which.side_effect = lambda cmd: "/usr/sbin/pkg" if cmd == "pkg" else None
113
+ result = detect_package_manager()
114
+ self.assertIsNotNone(result)
115
+ self.assertEqual(result.manager, PackageManager.PKG)
116
+
117
+ @mock.patch("code_aide.package_managers.shutil.which")
118
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Darwin")
119
+ def test_detects_brew_on_macos(self, _mock_sys, mock_which):
120
+ mock_which.side_effect = lambda cmd: (
121
+ "/opt/homebrew/bin/brew" if cmd == "brew" else None
122
+ )
123
+ result = detect_package_manager()
124
+ self.assertIsNotNone(result)
125
+ self.assertEqual(result.manager, PackageManager.HOMEBREW)
126
+
127
+ @mock.patch("code_aide.package_managers.shutil.which")
128
+ @mock.patch("code_aide.package_managers.platform.system", return_value="Linux")
129
+ def test_returns_first_match(self, _mock_sys, mock_which):
130
+ # apt-get is first in the list
131
+ mock_which.side_effect = lambda cmd: f"/usr/bin/{cmd}"
132
+ result = detect_package_manager()
133
+ self.assertEqual(result.manager, PackageManager.APT)
134
+
135
+
136
+ class TestParsePackageName(unittest.TestCase):
137
+ """Tests for _parse_package_name output parsing."""
138
+
139
+ def test_apt_dpkg_output(self):
140
+ self.assertEqual(
141
+ _parse_package_name(PackageManager.APT, "libfoo:amd64: /usr/lib/libfoo.so"),
142
+ "libfoo",
143
+ )
144
+
145
+ def test_apt_simple_output(self):
146
+ self.assertEqual(
147
+ _parse_package_name(PackageManager.APT, "claude-code: /usr/bin/claude"),
148
+ "claude-code",
149
+ )
150
+
151
+ def test_dnf_rpm_output(self):
152
+ self.assertEqual(
153
+ _parse_package_name(PackageManager.DNF, "claude-code-2.1.50-1.x86_64"),
154
+ "claude-code-2.1.50-1.x86_64",
155
+ )
156
+
157
+ def test_pacman_output(self):
158
+ self.assertEqual(
159
+ _parse_package_name(
160
+ PackageManager.PACMAN,
161
+ "/usr/bin/claude is owned by claude-code 2.1.50",
162
+ ),
163
+ "claude-code",
164
+ )
165
+
166
+ def test_pacman_no_match(self):
167
+ self.assertIsNone(
168
+ _parse_package_name(PackageManager.PACMAN, "error: no package"),
169
+ )
170
+
171
+ def test_emerge_output(self):
172
+ self.assertEqual(
173
+ _parse_package_name(PackageManager.EMERGE, "dev-util/claude-code"),
174
+ "dev-util/claude-code",
175
+ )
176
+
177
+ def test_pkg_output(self):
178
+ self.assertEqual(
179
+ _parse_package_name(PackageManager.PKG, "claude-code-2.1.50"),
180
+ "claude-code-2.1.50",
181
+ )
182
+
183
+ def test_brew_output(self):
184
+ self.assertEqual(
185
+ _parse_package_name(PackageManager.HOMEBREW, "gemini-cli"),
186
+ "gemini-cli",
187
+ )
188
+
189
+ def test_empty_output(self):
190
+ self.assertIsNone(_parse_package_name(PackageManager.APT, ""))
191
+ self.assertIsNone(_parse_package_name(PackageManager.APT, "\n"))
192
+
193
+ def test_multiline_takes_first(self):
194
+ self.assertEqual(
195
+ _parse_package_name(PackageManager.EMERGE, "dev-util/foo\ndev-util/bar"),
196
+ "dev-util/foo",
197
+ )
198
+
199
+
200
+ class TestQueryPackageOwner(unittest.TestCase):
201
+ """Tests for query_package_owner."""
202
+
203
+ @mock.patch("code_aide.package_managers.detect_package_manager", return_value=None)
204
+ def test_returns_none_when_no_manager(self, _mock):
205
+ pkg, cmd = query_package_owner("/usr/bin/example")
206
+ self.assertIsNone(pkg)
207
+ self.assertIsNone(cmd)
208
+
209
+ @mock.patch("code_aide.package_managers.subprocess.run")
210
+ @mock.patch("code_aide.package_managers.detect_package_manager")
211
+ def test_emerge_query(self, mock_detect, mock_run):
212
+ mock_detect.return_value = PACKAGE_MANAGER_BY_ENUM[PackageManager.EMERGE]
213
+ mock_run.return_value = subprocess.CompletedProcess(
214
+ args=[], returncode=0, stdout="dev-util/claude-code\n"
215
+ )
216
+ pkg, cmd = query_package_owner("/opt/bin/claude")
217
+ self.assertEqual(pkg, "dev-util/claude-code")
218
+ self.assertEqual(cmd, "sudo emerge --unmerge dev-util/claude-code")
219
+ mock_run.assert_called_once_with(
220
+ ["qfile", "-qC", "/opt/bin/claude"],
221
+ capture_output=True,
222
+ text=True,
223
+ timeout=10,
224
+ check=False,
225
+ stdin=subprocess.DEVNULL,
226
+ )
227
+
228
+ @mock.patch("code_aide.package_managers.subprocess.run")
229
+ @mock.patch("code_aide.package_managers.detect_package_manager")
230
+ def test_apt_query(self, mock_detect, mock_run):
231
+ mock_detect.return_value = PACKAGE_MANAGER_BY_ENUM[PackageManager.APT]
232
+ mock_run.return_value = subprocess.CompletedProcess(
233
+ args=[], returncode=0, stdout="claude-code: /usr/bin/claude\n"
234
+ )
235
+ pkg, cmd = query_package_owner("/usr/bin/claude")
236
+ self.assertEqual(pkg, "claude-code")
237
+ self.assertEqual(cmd, "sudo apt-get remove claude-code")
238
+
239
+ @mock.patch("code_aide.package_managers.subprocess.run")
240
+ @mock.patch("code_aide.package_managers.detect_package_manager")
241
+ def test_brew_uses_basename(self, mock_detect, mock_run):
242
+ mock_detect.return_value = PACKAGE_MANAGER_BY_ENUM[PackageManager.HOMEBREW]
243
+ mock_run.return_value = subprocess.CompletedProcess(
244
+ args=[], returncode=0, stdout="gemini-cli\n"
245
+ )
246
+ pkg, cmd = query_package_owner("/opt/homebrew/bin/gemini")
247
+ self.assertEqual(pkg, "gemini-cli")
248
+ self.assertEqual(cmd, "brew uninstall gemini-cli")
249
+ # Verify basename was passed, not full path
250
+ mock_run.assert_called_once_with(
251
+ ["brew", "which-formula", "gemini"],
252
+ capture_output=True,
253
+ text=True,
254
+ timeout=10,
255
+ check=False,
256
+ stdin=subprocess.DEVNULL,
257
+ )
258
+
259
+ @mock.patch("code_aide.package_managers.subprocess.run")
260
+ @mock.patch("code_aide.package_managers.detect_package_manager")
261
+ def test_returns_none_on_query_failure(self, mock_detect, mock_run):
262
+ mock_detect.return_value = PACKAGE_MANAGER_BY_ENUM[PackageManager.APT]
263
+ mock_run.return_value = subprocess.CompletedProcess(
264
+ args=[], returncode=1, stdout=""
265
+ )
266
+ pkg, cmd = query_package_owner("/usr/bin/example")
267
+ self.assertIsNone(pkg)
268
+ self.assertIsNone(cmd)
269
+
270
+ @mock.patch("code_aide.package_managers.subprocess.run")
271
+ @mock.patch("code_aide.package_managers.detect_package_manager")
272
+ def test_returns_none_on_exception(self, mock_detect, mock_run):
273
+ mock_detect.return_value = PACKAGE_MANAGER_BY_ENUM[PackageManager.APT]
274
+ mock_run.side_effect = OSError("timeout")
275
+ pkg, cmd = query_package_owner("/usr/bin/example")
276
+ self.assertIsNone(pkg)
277
+ self.assertIsNone(cmd)
@@ -1,104 +0,0 @@
1
- """Shared constants and mutable tool configuration for CLI modules."""
2
-
3
- import os
4
- import sys
5
- from typing import Any, Dict
6
-
7
- from code_aide.config import load_tools_config
8
-
9
-
10
- def _use_color() -> bool:
11
- """Determine whether to emit ANSI color codes.
12
-
13
- Checks (in priority order):
14
- NO_COLOR — if set (any value), no color (https://no-color.org/)
15
- FORCE_COLOR — if set (any value), force color
16
- CLICOLOR_FORCE — if non-empty, force color
17
- TERM=dumb — no color
18
- CLICOLOR=0 — no color
19
- stdout is not a TTY — no color
20
- """
21
- if os.environ.get("NO_COLOR") is not None:
22
- return False
23
- if os.environ.get("FORCE_COLOR") is not None:
24
- return True
25
- if os.environ.get("CLICOLOR_FORCE", ""):
26
- return True
27
- if os.environ.get("TERM") == "dumb":
28
- return False
29
- if os.environ.get("CLICOLOR") == "0":
30
- return False
31
- return sys.stdout.isatty()
32
-
33
-
34
- class Colors:
35
- if _use_color():
36
- RED = "\033[0;31m"
37
- GREEN = "\033[0;32m"
38
- YELLOW = "\033[1;33m"
39
- BLUE = "\033[0;34m"
40
- BOLD = "\033[1m"
41
- NC = "\033[0m"
42
- else:
43
- RED = ""
44
- GREEN = ""
45
- YELLOW = ""
46
- BLUE = ""
47
- BOLD = ""
48
- NC = ""
49
-
50
-
51
- PACKAGE_MANAGERS: Dict[str, Dict[str, Any]] = {
52
- "apt-get": {
53
- "detect_command": "apt-get",
54
- "packages": ["nodejs", "npm"],
55
- "pre_install": [["sudo", "apt-get", "update"]],
56
- "install_command": ["sudo", "apt-get", "install", "-y"],
57
- "description": "Debian/Ubuntu",
58
- },
59
- "dnf": {
60
- "detect_command": "dnf",
61
- "packages": ["nodejs", "npm"],
62
- "pre_install": [],
63
- "install_command": ["sudo", "dnf", "install", "-y"],
64
- "description": "Fedora/RHEL 8+",
65
- },
66
- "yum": {
67
- "detect_command": "yum",
68
- "packages": ["nodejs", "npm"],
69
- "pre_install": [],
70
- "install_command": ["sudo", "yum", "install", "-y"],
71
- "description": "RHEL/CentOS 7",
72
- },
73
- "pacman": {
74
- "detect_command": "pacman",
75
- "packages": ["nodejs", "npm"],
76
- "pre_install": [],
77
- "install_command": ["sudo", "pacman", "-S", "--noconfirm"],
78
- "description": "Arch Linux",
79
- },
80
- "zypper": {
81
- "detect_command": "zypper",
82
- "packages": ["nodejs", "npm"],
83
- "pre_install": [],
84
- "install_command": ["sudo", "zypper", "install", "-y"],
85
- "description": "openSUSE",
86
- },
87
- "emerge": {
88
- "detect_command": "emerge",
89
- "packages": ["net-libs/nodejs"],
90
- "pre_install": [],
91
- "install_command": ["sudo", "emerge", "--quiet-build"],
92
- "description": "Gentoo",
93
- },
94
- "pkg": {
95
- "detect_command": "pkg",
96
- "packages": ["node22", "npm-node22"],
97
- "pre_install": [],
98
- "install_command": ["sudo", "pkg", "install", "-y"],
99
- "description": "FreeBSD",
100
- },
101
- }
102
-
103
-
104
- TOOLS: Dict[str, Dict[str, Any]] = load_tools_config()
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