Add system dependencies support for tools

Tools can now declare system-level package dependencies (apt, brew, pacman, dnf)
that get checked and optionally installed when the tool is installed or run.

Features:
- SystemDependency dataclass with short form (string) and long form (dict)
- New system_deps.py module with platform detection and installation
- `cmdforge system-deps <tool>` command to check/install system packages
- `cmdforge check` now shows both CmdForge and system dependencies
- `cmdforge registry install` prompts for system dep installation
- GUI: System Dependencies section in Tool Builder with add/edit dialog
- Runner warns about missing system deps before execution
- Integration with project install (manifest and lock-based)

Also includes:
- Quote paths in wrapper scripts for spaces support
- Tests for value type preservation in code steps
- Unskip invalid tool name test

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-28 16:31:37 -04:00
co-authored by Claude Opus 4.5
parent 8eff55ed1e
commit 071ade0ffb
15 changed files with 1399 additions and 62 deletions
+12 -2
View File
@@ -148,6 +148,7 @@ def cmd_install_deps(args):
from ..resolver import ToolResolver, install_from_registry, ToolNotFoundError
from ..registry_client import RegistryError
from ..lockfile import Lockfile
from ..system_deps import prompt_install_missing
manifest = load_manifest()
@@ -252,9 +253,13 @@ def cmd_install_deps(args):
print(f"Installing {node.qualified_name}...", end=" ", flush=True)
spec = node.qualified_name
install_version = node.resolved_version or node.version_constraint
install_from_registry(spec, version=install_version)
resolved = install_from_registry(spec, version=install_version)
print(f"v{node.resolved_version or 'latest'}")
installed += 1
if resolved.tool.system_dependencies:
print()
tool_ref = f"{resolved.owner}/{resolved.tool.name}" if resolved.owner else resolved.tool.name
prompt_install_missing(resolved.tool.system_dependencies, tool_ref)
except RegistryError as e:
print(f"FAILED: {e.message}")
failed.append((node.qualified_name, e.message))
@@ -278,6 +283,7 @@ def _install_from_lock(lock: "Lockfile", args) -> int:
"""Install exact versions from lock file."""
from ..resolver import ToolResolver, ToolNotFoundError, install_from_registry
from ..registry_client import RegistryError
from ..system_deps import prompt_install_missing
verbose = getattr(args, 'verbose', False)
dry_run = getattr(args, 'dry_run', False)
@@ -333,9 +339,13 @@ def _install_from_lock(lock: "Lockfile", args) -> int:
try:
print(f"Installing {pkg.name}@{pkg.version}...", end=" ", flush=True)
# Use correct API: install_from_registry(spec, version)
install_from_registry(pkg.name, version=pkg.version)
resolved = install_from_registry(pkg.name, version=pkg.version)
print("OK")
installed += 1
if resolved.tool.system_dependencies:
print()
tool_ref = f"{resolved.owner}/{resolved.tool.name}" if resolved.owner else resolved.tool.name
prompt_install_missing(resolved.tool.system_dependencies, tool_ref)
except RegistryError as e:
print(f"FAILED: {e.message}")
failed.append((pkg.name, e.message))