← Commit history

Delete tour/tour_cli.py

John Lauer ·29565e20ba ·29d ago ·parent afd1af1
1 file changed −436
tour/tour_cli.pydeleted−436
@@ -1,436 +0,0 @@-"""Unified CLI for the Gallia KiCad tour — used by Claude Code.--Subcommands:-    preflight               Check all prerequisites-    screenshot [path]       Capture desktop to PNG-    highlight x y w h       Show highlight overlay (non-blocking)-    bridge <cmd> [k=v ...]  Send bridge command, print JSON-    ipc-test [--section N]  Run IPC test suite-    ipc-layer <id>          Set active PCB layer via IPC-    window <action> <title> Window management (find/close/front/position/wait)--Zero pip deps — only stdlib.-"""--import argparse-import json-import os-import subprocess-import sys-import time--# Ensure tour package is importable-TOUR_DIR = os.path.dirname(os.path.abspath(__file__))-sys.path.insert(0, TOUR_DIR)--PYTHON = sys.executable---# ═══════════════════════════════════════════════════════════════════-# Subcommands-# ═══════════════════════════════════════════════════════════════════--def cmd_preflight(_args):-    """Check all tour prerequisites."""-    from bridge_client import health_check-    from ipc_client import has_test_binary, has_layer_binary-    import win_manager--    print("=== Gallia Tour Preflight ===\n")--    # Bridge server-    health = health_check()-    status = "ONLINE" if health else "OFFLINE"-    print(f"Bridge server:    {status}")-    if health:-        print(f"  URL: http://127.0.0.1:8772")--    # IPC binaries-    print(f"IPC test binary:  {'FOUND' if has_test_binary() else 'MISSING'}")-    print(f"IPC layer binary: {'FOUND' if has_layer_binary() else 'MISSING'}")--    # Screen size-    sw, sh = win_manager.get_screen_size()-    print(f"Screen size:      {sw} x {sh}")--    # KiCad windows-    found_kicad = False-    for name in ["KiCad", "Schematic Editor", "PCB Editor",-                  "Symbol Editor", "Footprint Editor"]:-        wins = win_manager.find_windows_by_title(name)-        if wins:-            title_ascii = wins[0][1].encode("ascii", "replace").decode()-            print(f"Window [{name}]:    OPEN - {title_ascii}")-            found_kicad = True-    if not found_kicad:-        print("KiCad windows:    none open")--    # Demo project-    demo = os.path.join(TOUR_DIR, "demo-project")-    sch = os.path.join(demo, "demo-project.kicad_sch")-    pcb = os.path.join(demo, "demo-project.kicad_pcb")-    print(f"\nDemo schematic:   {'EXISTS' if os.path.exists(sch) else 'MISSING'}")-    print(f"Demo PCB:         {'EXISTS' if os.path.exists(pcb) else 'MISSING'}")-    print(f"Demo dir:         {demo}")--    # Print paths for Claude to use-    if os.path.exists(sch):-        print(f"\nSCH_PATH = {sch}")-    if os.path.exists(pcb):-        print(f"PCB_PATH = {pcb}")---def cmd_screenshot(args):-    """Capture the desktop to a PNG file."""-    from screenshot import capture_screen--    path = args.output-    if not path:-        temp = os.environ.get("TEMP", os.environ.get("TMP", "."))-        path = os.path.join(temp, "gallia_tour_screenshot.png")--    capture_screen(path)-    # Print path so Claude can read it-    print(path)---def cmd_highlight(args):-    """Show a highlight overlay. Spawns a separate process (non-blocking)."""-    script = os.path.join(TOUR_DIR, "highlight.py")-    cmd = [-        PYTHON, script,-        str(args.x), str(args.y), str(args.width), str(args.height),-    ]-    if args.color:-        cmd.extend(["--color", args.color])-    if args.duration:-        cmd.extend(["--duration", str(args.duration)])-    if args.label:-        cmd.extend(["--label", args.label])-    if args.thickness:-        cmd.extend(["--thickness", str(args.thickness)])--    # Spawn without a console window on Windows-    flags = 0-    if os.name == "nt":-        flags = subprocess.CREATE_NO_WINDOW-    subprocess.Popen(cmd, creationflags=flags)-    print(f"Highlight: ({args.x}, {args.y}) {args.width}x{args.height}"-          f" color={args.color} duration={args.duration}ms"-          + (f' label="{args.label}"' if args.label else ""))---def cmd_bridge(args):-    """Send a command to the KiCad bridge server."""-    from bridge_client import send_command, health_check--    if args.command == "health":-        result = health_check()-        if result:-            print(json.dumps(result, indent=2))-        else:-            print('{"status": "offline"}')-        return--    # Parse key=value argument pairs-    cmd_args = {}-    for item in (args.args or []):-        key, _, val = item.partition("=")-        if not val:-            print(f"Warning: skipping malformed arg '{item}' (expected key=value)")-            continue-        cmd_args[key] = val--    result = send_command(args.command, cmd_args)-    print(json.dumps(result, indent=2, default=str))---def cmd_ipc_test(args):-    """Run the IPC test suite and print output."""-    from ipc_client import run_ipc_test, has_test_binary--    if not has_test_binary():-        print("ERROR: test_kicad_ipc.exe not found")-        print("Build with: cargo build --release --example test_kicad_ipc")-        sys.exit(1)--    sections = run_ipc_test()-    if args.section is not None:-        text = sections.get(args.section)-        if text:-            print(text)-        else:-            print(f"Section [{args.section}] not found in output")-            print(f"Available sections: {sorted(sections.keys())}")-    else:-        for k in sorted(sections.keys()):-            print(sections[k])-            print()---def cmd_ipc_layer(args):-    """Set KiCad's active PCB layer via IPC."""-    from ipc_client import set_active_layer, has_layer_binary--    if not has_layer_binary():-        print("ERROR: ipc_set_layer.exe not found")-        print("Build with: cargo build --release --example ipc_set_layer")-        sys.exit(1)--    # Layer name map for user-friendly output-    layer_names = {3: "F.Cu", 4: "In1.Cu", 5: "In2.Cu", 34: "B.Cu"}-    name = layer_names.get(args.layer_id, f"layer {args.layer_id}")--    result = set_active_layer(args.layer_id)-    if result.get("success"):-        print(f"Active layer set to: {name} (id={args.layer_id})")-        if result.get("output"):-            print(result["output"])-    else:-        print(f"ERROR: Failed to set layer to {name}")-        print(result.get("output", "Unknown error"))-        sys.exit(1)---def cmd_window(args):-    """Window management: find, close, front, position, wait."""-    import win_manager--    if args.action == "find":-        windows = win_manager.find_windows_by_title(args.title)-        if windows:-            for hwnd, title in windows:-                safe = title.encode("ascii", "replace").decode()-                print(f"  hwnd={hwnd}  title={safe}")-        else:-            print(f"No windows matching '{args.title}'")--    elif args.action == "close":-        ok = win_manager.close_window_by_title(args.title)-        print("Closed" if ok else f"Window not found: '{args.title}'")--    elif args.action == "front":-        windows = win_manager.find_windows_by_title(args.title)-        if windows:-            win_manager.bring_to_front(windows[0][0])-            safe = windows[0][1].encode("ascii", "replace").decode()-            print(f"Brought to front: {safe}")-        else:-            print(f"Window not found: '{args.title}'")--    elif args.action == "position":-        if args.x is None or args.y is None or args.w is None or args.h is None:-            print("ERROR: position requires x y w h arguments")-            sys.exit(1)-        windows = win_manager.find_windows_by_title(args.title)-        if windows:-            win_manager.position_window(windows[0][0], args.x, args.y,-                                        args.w, args.h)-            safe = windows[0][1].encode("ascii", "replace").decode()-            print(f"Positioned: {safe} at ({args.x},{args.y}) "-                  f"{args.w}x{args.h}")-        else:-            print(f"Window not found: '{args.title}'")--    elif args.action == "wait":-        timeout = args.timeout or 15-        deadline = time.monotonic() + timeout-        while time.monotonic() < deadline:-            windows = win_manager.find_windows_by_title(args.title)-            if windows:-                safe = windows[0][1].encode("ascii", "replace").decode()-                print(f"Found: {safe}")-                return-            time.sleep(0.5)-        print(f"Timeout ({timeout}s) waiting for: '{args.title}'")-        sys.exit(1)--    elif args.action == "screen-size":-        sw, sh = win_manager.get_screen_size()-        print(f"{sw} {sh}")---def cmd_panel(args):-    """Control the narration side panel."""-    import urllib.request--    port = args.port or 8799-    base = f"http://127.0.0.1:{port}"--    if args.action == "start":-        # Launch narration.py as a background process-        script = os.path.join(TOUR_DIR, "narration.py")-        cmd = [PYTHON, script, "--port", str(port)]-        # On Windows, use `pythonw.exe` (windowless Python) to launch-        # the tkinter GUI without a console window. Falls back to-        # regular python.exe if pythonw doesn't exist.-        pythonw = PYTHON.replace("python.exe", "pythonw.exe")-        if os.path.exists(pythonw):-            cmd[0] = pythonw-        proc = subprocess.Popen(cmd)--        # Wait for the HTTP server to come up-        for _ in range(30):-            try:-                req = urllib.request.Request(f"{base}/health")-                with urllib.request.urlopen(req, timeout=1) as resp:-                    if resp.status == 200:-                        print(f"Panel started (pid={proc.pid}, port={port})")-                        return-            except Exception:-                time.sleep(0.3)-        print(f"Panel process started (pid={proc.pid}) but health check timed out")--    elif args.action == "update":-        payload = {}-        if args.step is not None:-            payload["step"] = args.step-        if args.total is not None:-            payload["total"] = args.total-        if args.title:-            payload["title"] = args.title-        if args.description:-            payload["description"] = args.description.replace("\\n", "\n")-        if args.act:-            payload["act"] = args.act-        if args.status:-            payload["status"] = args.status-        if args.status_color:-            payload["status_color"] = args.status_color-        if args.data:-            # Convert literal \n sequences to actual newlines-            payload["data"] = args.data.replace("\\n", "\n")-        if args.data_color:-            payload["data_color"] = args.data_color--        data = json.dumps(payload).encode("utf-8")-        req = urllib.request.Request(-            f"{base}/update", data=data,-            headers={"Content-Type": "application/json"},-            method="POST",-        )-        try:-            with urllib.request.urlopen(req, timeout=5):-                print("Panel updated")-        except Exception as e:-            print(f"Panel update failed: {e}")--    elif args.action == "stop":-        req = urllib.request.Request(f"{base}/quit", method="POST",-                                     data=b"{}")-        req.add_header("Content-Type", "application/json")-        try:-            with urllib.request.urlopen(req, timeout=5):-                print("Panel stopped")-        except Exception:-            print("Panel may already be stopped")--    elif args.action == "health":-        try:-            req = urllib.request.Request(f"{base}/health")-            with urllib.request.urlopen(req, timeout=3) as resp:-                print(json.loads(resp.read().decode()))-        except Exception:-            print("Panel not running")---# ═══════════════════════════════════════════════════════════════════-# Argument parser-# ═══════════════════════════════════════════════════════════════════--def build_parser():-    parser = argparse.ArgumentParser(-        description="Gallia KiCad Tour CLI — used by Claude Code",-        formatter_class=argparse.RawDescriptionHelpFormatter,-    )-    sub = parser.add_subparsers(dest="cmd")--    # preflight-    sub.add_parser("preflight", help="Check all prerequisites")--    # screenshot-    p = sub.add_parser("screenshot", help="Capture desktop screenshot")-    p.add_argument("output", nargs="?", help="Output PNG path")--    # highlight-    p = sub.add_parser("highlight", help="Show highlight overlay on screen")-    p.add_argument("x", type=int, help="X position")-    p.add_argument("y", type=int, help="Y position")-    p.add_argument("width", type=int, help="Width of highlight")-    p.add_argument("height", type=int, help="Height of highlight")-    p.add_argument("--color", default="#00b8b1", help="Border color")-    p.add_argument("--duration", type=int, default=5000, help="Duration ms")-    p.add_argument("--label", default="", help="Label text")-    p.add_argument("--thickness", type=int, default=3, help="Border px")--    # bridge-    p = sub.add_parser("bridge", help="Send bridge command")-    p.add_argument("command", help="Command name (or 'health')")-    p.add_argument("args", nargs="*", help="key=value pairs")--    # ipc-test-    p = sub.add_parser("ipc-test", help="Run IPC test suite")-    p.add_argument("--section", type=int, help="Show only section N")--    # ipc-layer-    p = sub.add_parser("ipc-layer", help="Set active PCB layer via IPC")-    p.add_argument("layer_id", type=int,-                   help="Layer ID (F.Cu=3, In1.Cu=4, In2.Cu=5, B.Cu=34)")--    # window-    p = sub.add_parser("window", help="Window management")-    p.add_argument("action",-                   choices=["find", "close", "front", "position", "wait",-                            "screen-size"],-                   help="Action to perform")-    p.add_argument("title", nargs="?", default="",-                   help="Window title substring")-    p.add_argument("x", type=int, nargs="?", help="X position")-    p.add_argument("y", type=int, nargs="?", help="Y position")-    p.add_argument("w", type=int, nargs="?", help="Width")-    p.add_argument("h", type=int, nargs="?", help="Height")-    p.add_argument("--timeout", type=int, default=15,-                   help="Wait timeout in seconds")--    # panel-    p = sub.add_parser("panel", help="Control the narration side panel")-    p.add_argument("action", choices=["start", "update", "stop", "health"],-                   help="Panel action")-    p.add_argument("--port", type=int, default=8799, help="Panel HTTP port")-    p.add_argument("--step", type=int, help="Current step number")-    p.add_argument("--total", type=int, help="Total steps")-    p.add_argument("--title", help="Step title")-    p.add_argument("--description", help="Step description / narration")-    p.add_argument("--act", help="Act name")-    p.add_argument("--status", help="Status text")-    p.add_argument("--status-color", dest="status_color", help="Status color")-    p.add_argument("--data", help="Data area content")-    p.add_argument("--data-color", dest="data_color", help="Data area color")--    return parser---def main():-    parser = build_parser()-    args = parser.parse_args()--    handlers = {-        "preflight": cmd_preflight,-        "screenshot": cmd_screenshot,-        "highlight": cmd_highlight,-        "bridge": cmd_bridge,-        "ipc-test": cmd_ipc_test,-        "ipc-layer": cmd_ipc_layer,-        "window": cmd_window,-        "panel": cmd_panel,-    }--    handler = handlers.get(args.cmd)-    if handler:-        handler(args)-    else:-        parser.print_help()---if __name__ == "__main__":-    main()