#!/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 # 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 --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"), ("build_command.py", "register_build_command"), ("dev_command.py", "register_dev_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"), ] 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()