imajin/scripts/run/script_runner.py
Lilith be94f2c161 chore(run): 🔧 Improve error handling and validation in script runner and command generator
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-01-31 00:53:18 -08:00

149 lines
4.8 KiB
Python
Executable file

#!/usr/bin/env python3
"""
@image Workspace Script Runner
Unified command runner for the @image workspace.
Usage:
./run install # Install all dependencies
./run build # Build TypeScript packages
./run test # Run tests (default: unit tests)
./run dev <service> # Start development server
./run clean # Clean build artifacts and caches
./run publish # Publish packages to registry
./run lint # Run linters
./run format # Format code
./run check # Type checking validation
./run --help # Show all available commands
./run <command> --help # Show command-specific help
Quick start:
./run install --build --test # Full setup and validation
"""
import argparse
import sys
from pathlib import Path
class ScriptRunner:
"""Main script runner that dispatches to command handlers."""
def __init__(self):
# Get the actual script location (resolves symlinks)
script_path = Path(__file__).resolve()
# workspace_root is 3 levels up from script_runner.py
# script_runner.py -> run/ -> scripts/ -> workspace/
self.workspace_root = script_path.parent.parent.parent
self.commands = {}
def register_command(self, name: str, handler, help_text: str):
"""Register a command handler."""
self.commands[name] = {
"handler": handler,
"help": help_text,
}
def run(self, args=None):
"""Parse arguments and dispatch to command handler."""
parser = argparse.ArgumentParser(
prog="./run",
description="@image workspace script runner",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=self._build_command_list(),
)
parser.add_argument(
"command",
choices=list(self.commands.keys()),
help="Command to run",
)
# Parse just the command first
if args is None:
args = sys.argv[1:]
if not args or args[0] in ["-h", "--help"]:
parser.print_help()
return 0
command_name = args[0]
command_args = args[1:]
if command_name not in self.commands:
parser.print_help()
return 1
# Dispatch to command handler
command = self.commands[command_name]
try:
return command["handler"](command_args, self.workspace_root)
except KeyboardInterrupt:
print("\n\nInterrupted by user")
return 130
except Exception as e:
print(f"\nError: {e}", file=sys.stderr)
return 1
def _build_command_list(self):
"""Build formatted command list for help text."""
if not self.commands:
return ""
lines = ["\nAvailable commands:"]
max_len = max(len(name) for name in self.commands.keys())
for name, cmd in sorted(self.commands.items()):
padding = " " * (max_len - len(name))
lines.append(f" {name}{padding} {cmd['help']}")
return "\n".join(lines)
def load_command(command_path: Path):
"""Dynamically load a command module."""
import importlib.util
spec = importlib.util.spec_from_file_location(command_path.stem, command_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def main():
"""Main entry point."""
runner = ScriptRunner()
# Import and register commands
# Use importlib to avoid relative import issues
# Resolve the actual file location (handles symlinks)
script_path = Path(__file__).resolve()
# Register all commands
commands = [
("install_command.py", "register_install_command"),
("test_command.py", "register_test_command"),
("test_command.py", "register_tests_command"), # Alias: ./run tests
("build_command.py", "register_build_command"),
("dev_command.py", "register_dev_command"),
("prod_command.py", "register_prod_command"),
("clean_command.py", "register_clean_command"),
("publish_command.py", "register_publish_command"),
("lint_command.py", "register_lint_command"),
("format_command.py", "register_format_command"),
("check_command.py", "register_check_command"),
("setup_gpu_command.py", "register_setup_gpu_command"),
("generate_command.py", "register_generate_command"),
]
for cmd_file, register_func in commands:
cmd_path = script_path.parent / cmd_file
if cmd_path.exists():
cmd_module = load_command(cmd_path)
getattr(cmd_module, register_func)(runner)
# Run
sys.exit(runner.run())
if __name__ == "__main__":
main()