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:
+43
-2
@@ -17,6 +17,24 @@ from .profiles import load_profile
|
||||
MAX_TOOL_DEPTH = 10
|
||||
|
||||
|
||||
def check_system_dependencies(tool: Tool) -> list:
|
||||
"""
|
||||
Check if system dependencies are satisfied.
|
||||
|
||||
Args:
|
||||
tool: Tool to check system dependencies for
|
||||
|
||||
Returns:
|
||||
List of missing SystemDependency objects
|
||||
"""
|
||||
from .system_deps import check_system_dep
|
||||
|
||||
if not tool.system_dependencies:
|
||||
return []
|
||||
|
||||
return [d for d in tool.system_dependencies if not check_system_dep(d)]
|
||||
|
||||
|
||||
def auto_install_dependencies(tool: Tool, verbose: bool = False) -> list[str]:
|
||||
"""
|
||||
Automatically install missing dependencies for a tool.
|
||||
@@ -155,7 +173,8 @@ def substitute_variables(template: str, variables: dict, warn_non_scalar: bool =
|
||||
for name, value in variables.items():
|
||||
if name == "settings":
|
||||
continue # Already handled above
|
||||
result = result.replace(f"{{{name}}}", str(value) if value else "")
|
||||
# Use 'is not None' to preserve falsey values like 0 and False
|
||||
result = result.replace(f"{{{name}}}", "" if value is None else str(value))
|
||||
|
||||
# Finally, restore escaped braces as single braces
|
||||
result = result.replace(ESCAPE_OPEN, "{").replace(ESCAPE_CLOSE, "}")
|
||||
@@ -236,7 +255,9 @@ def execute_code_step(
|
||||
output_vars = [v.strip() for v in step.output_var.split(',')]
|
||||
outputs = {}
|
||||
for var in output_vars:
|
||||
outputs[var] = str(local_vars.get(var, ""))
|
||||
# Preserve original types for code→code workflows
|
||||
# Template substitution will convert to string when needed
|
||||
outputs[var] = local_vars.get(var)
|
||||
|
||||
return outputs, True
|
||||
|
||||
@@ -388,6 +409,17 @@ def run_tool(
|
||||
Returns:
|
||||
Tuple of (output_text, exit_code)
|
||||
"""
|
||||
def _format_tool_ref(current_tool: Tool) -> str:
|
||||
"""Best-effort tool reference for messages (owner/name if available)."""
|
||||
if current_tool.path:
|
||||
tool_dir = current_tool.path.parent
|
||||
# Handle both global and local ~/.cmdforge/<owner>/<name> layouts
|
||||
if tool_dir.parent.name == ".cmdforge":
|
||||
return tool_dir.name
|
||||
if tool_dir.parent.parent.name == ".cmdforge":
|
||||
return f"{tool_dir.parent.name}/{tool_dir.name}"
|
||||
return current_tool.name
|
||||
|
||||
# Initialize call stack
|
||||
if _call_stack is None:
|
||||
_call_stack = []
|
||||
@@ -412,6 +444,15 @@ def run_tool(
|
||||
print(f"Or use --auto-install to install automatically", file=sys.stderr)
|
||||
# Continue anyway - the actual step execution will fail with a better error
|
||||
|
||||
# Check system dependencies on first level only
|
||||
if _depth == 0 and tool.system_dependencies:
|
||||
missing_sys = check_system_dependencies(tool)
|
||||
if missing_sys:
|
||||
names = ", ".join(d.name for d in missing_sys)
|
||||
print(f"Warning: Missing system packages: {names}", file=sys.stderr)
|
||||
print(f"Run: cmdforge system-deps {_format_tool_ref(tool)} install", file=sys.stderr)
|
||||
# Continue execution - let it fail naturally if dep is actually needed
|
||||
|
||||
# Initialize variables with input and arguments
|
||||
variables = {"input": input_text}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user