open_cmd.py
python
| 1 | """muse open — open a Muse artifact with the macOS system default application. |
| 2 | |
| 3 | Dispatches to ``open`` (macOS ``NSWorkspace``), which opens the file in |
| 4 | whatever the system default app is for that file type: |
| 5 | |
| 6 | - ``.mid`` → Stori DAW or GarageBand |
| 7 | - ``.webp`` / ``.png`` → Preview |
| 8 | - ``.mp3`` → QuickTime |
| 9 | |
| 10 | macOS-only. Exits 1 with a clear error on other platforms. |
| 11 | """ |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | import asyncio |
| 15 | import logging |
| 16 | import pathlib |
| 17 | import platform |
| 18 | import subprocess |
| 19 | |
| 20 | import typer |
| 21 | |
| 22 | from maestro.muse_cli._repo import require_repo |
| 23 | from maestro.muse_cli.artifact_resolver import resolve_artifact_async |
| 24 | from maestro.muse_cli.db import open_session |
| 25 | from maestro.muse_cli.errors import ExitCode |
| 26 | |
| 27 | logger = logging.getLogger(__name__) |
| 28 | |
| 29 | app = typer.Typer() |
| 30 | |
| 31 | |
| 32 | def _guard_macos() -> None: |
| 33 | """Exit 1 with a clear message if not running on macOS.""" |
| 34 | if platform.system() != "Darwin": |
| 35 | typer.echo("❌ muse open requires macOS.") |
| 36 | raise typer.Exit(code=ExitCode.USER_ERROR) |
| 37 | |
| 38 | |
| 39 | @app.callback(invoke_without_command=True) |
| 40 | def open_artifact( |
| 41 | ctx: typer.Context, |
| 42 | path_or_id: str = typer.Argument(..., help="File path or short commit ID."), |
| 43 | ) -> None: |
| 44 | """Open an artifact with the macOS system default application.""" |
| 45 | _guard_macos() |
| 46 | root = require_repo() |
| 47 | |
| 48 | async def _run() -> None: |
| 49 | async with open_session() as session: |
| 50 | path = await resolve_artifact_async(path_or_id, root=root, session=session) |
| 51 | subprocess.run(["open", str(path)], check=True) |
| 52 | typer.echo(f"✅ Opened {path}") |
| 53 | logger.info("✅ muse open: %s", path) |
| 54 | |
| 55 | try: |
| 56 | asyncio.run(_run()) |
| 57 | except typer.Exit: |
| 58 | raise |
| 59 | except subprocess.CalledProcessError as exc: |
| 60 | typer.echo(f"❌ muse open failed: {exc}") |
| 61 | logger.error("❌ muse open subprocess error: %s", exc) |
| 62 | raise typer.Exit(code=ExitCode.INTERNAL_ERROR) |
| 63 | except Exception as exc: |
| 64 | typer.echo(f"❌ muse open failed: {exc}") |
| 65 | logger.error("❌ muse open error: %s", exc, exc_info=True) |
| 66 | raise typer.Exit(code=ExitCode.INTERNAL_ERROR) |