cgcardona / muse public
inspect.py python
169 lines 4.9 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """muse inspect [<ref>] — print structured JSON of the Muse commit graph.
2
3 Serializes the full commit graph reachable from a starting reference (default:
4 HEAD) into machine-readable output. Three formats are supported:
5
6 JSON (default)::
7
8 muse inspect
9 muse inspect abc1234 --depth 10
10 muse inspect --branches --format json
11
12 Graphviz DOT::
13
14 muse inspect --format dot | dot -Tsvg -o graph.svg
15
16 Mermaid.js::
17
18 muse inspect --format mermaid
19
20 Flags
21 -----
22 [<ref>] Optional starting commit or branch name (default: HEAD).
23 --depth N Limit traversal to N commits per branch (default: unlimited).
24 --branches Include all branch heads and their reachable commits.
25 --tags Include tag refs in the output (branch pointers always included).
26 --format Output format: json (default), dot, mermaid.
27 """
28 from __future__ import annotations
29
30 import asyncio
31 import logging
32 import pathlib
33 from typing import Optional
34
35 import typer
36 from sqlalchemy.ext.asyncio import AsyncSession
37
38 from maestro.muse_cli._repo import require_repo
39 from maestro.muse_cli.db import open_session
40 from maestro.muse_cli.errors import ExitCode
41 from maestro.services.muse_inspect import (
42 InspectFormat,
43 MuseInspectResult,
44 build_inspect_result,
45 render_dot,
46 render_json,
47 render_mermaid,
48 )
49
50 logger = logging.getLogger(__name__)
51
52 app = typer.Typer()
53
54
55 # ---------------------------------------------------------------------------
56 # Testable async core
57 # ---------------------------------------------------------------------------
58
59
60 async def _inspect_async(
61 *,
62 root: pathlib.Path,
63 session: AsyncSession,
64 ref: Optional[str],
65 depth: Optional[int],
66 branches: bool,
67 fmt: InspectFormat,
68 ) -> MuseInspectResult:
69 """Core inspect logic — fully injectable for tests.
70
71 Delegates graph traversal to :func:`~maestro.services.muse_inspect.build_inspect_result`
72 and renders the result to the requested format via ``typer.echo``.
73
74 Args:
75 root: Repository root path.
76 session: Open async DB session.
77 ref: Starting commit reference (None = HEAD).
78 depth: Maximum commits per branch (None = unlimited).
79 branches: Whether to traverse all branches.
80 fmt: Output format (json, dot, mermaid).
81
82 Returns:
83 The :class:`~maestro.services.muse_inspect.MuseInspectResult` so tests
84 can assert on the data model without parsing printed output.
85 """
86 result = await build_inspect_result(
87 session,
88 root,
89 ref=ref,
90 depth=depth,
91 include_branches=branches,
92 )
93
94 if fmt == InspectFormat.dot:
95 typer.echo(render_dot(result))
96 elif fmt == InspectFormat.mermaid:
97 typer.echo(render_mermaid(result))
98 else:
99 typer.echo(render_json(result))
100
101 return result
102
103
104 # ---------------------------------------------------------------------------
105 # Typer command
106 # ---------------------------------------------------------------------------
107
108
109 @app.callback(invoke_without_command=True)
110 def inspect(
111 ctx: typer.Context,
112 ref: Optional[str] = typer.Argument(
113 None,
114 help="Starting commit ID or branch name (default: HEAD).",
115 metavar="<ref>",
116 ),
117 depth: Optional[int] = typer.Option(
118 None,
119 "--depth",
120 help="Limit graph traversal to N commits per branch (default: unlimited).",
121 min=1,
122 ),
123 branches: bool = typer.Option(
124 False,
125 "--branches",
126 help="Include all branch heads and their reachable commits.",
127 ),
128 tags: bool = typer.Option(
129 False,
130 "--tags",
131 help="Include tag refs in the output (currently branch pointers always included).",
132 ),
133 fmt: InspectFormat = typer.Option(
134 InspectFormat.json,
135 "--format",
136 help="Output format: json (default), dot, mermaid.",
137 ),
138 ) -> None:
139 """Print structured output of the Muse commit graph.
140
141 Serializes the full commit graph reachable from the starting reference
142 (default: HEAD) into machine-readable output. Use ``--format json`` (the
143 default) for agent consumption, ``--format dot`` for Graphviz, or
144 ``--format mermaid`` for GitHub markdown embedding.
145 """
146 root = require_repo()
147
148 async def _run() -> None:
149 async with open_session() as session:
150 await _inspect_async(
151 root=root,
152 session=session,
153 ref=ref,
154 depth=depth,
155 branches=branches,
156 fmt=fmt,
157 )
158
159 try:
160 asyncio.run(_run())
161 except typer.Exit:
162 raise
163 except (ValueError, FileNotFoundError) as exc:
164 typer.echo(f"❌ muse inspect: {exc}", err=True)
165 raise typer.Exit(code=ExitCode.USER_ERROR)
166 except Exception as exc:
167 typer.echo(f"❌ muse inspect failed: {exc}", err=True)
168 logger.error("❌ muse inspect error: %s", exc, exc_info=True)
169 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)