model-boss/scripts/run/script_runner.py
2026-01-18 17:10:38 -08:00

126 lines
3.6 KiB
Python
Executable file

#!/usr/bin/env python3
"""
@model-boss Workspace Script Runner
Unified command runner for the @model-boss workspace.
Usage:
./run dev <service> # Start development server
./run prod <service> # Start production server
./run --help # Show all available commands
./run <command> --help # Show command-specific help
"""
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="@model-boss 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()
# Resolve the actual file location (handles symlinks)
script_path = Path(__file__).resolve()
# Register all commands
commands = [
("dev_command.py", "register_dev_command"),
("prod_command.py", "register_prod_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()