Coverage for src/prosemark/cli/structure.py: 100%
47 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-28 19:17 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-28 19:17 +0000
1"""CLI command for displaying project structure."""
3import json
4from pathlib import Path
5from typing import TYPE_CHECKING, Any, Union
7import click
9from prosemark.adapters.binder_repo_fs import BinderRepoFs
10from prosemark.adapters.logger_stdout import LoggerStdout
11from prosemark.app.use_cases import ShowStructure
12from prosemark.domain.binder import Item
13from prosemark.domain.models import NodeId
14from prosemark.exceptions import FileSystemError, NodeIdentityError, NodeNotFoundError
16if TYPE_CHECKING: # pragma: no cover
17 from prosemark.domain.models import BinderItem
20@click.command()
21@click.option(
22 '--format',
23 '-f',
24 'output_format',
25 default='tree',
26 type=click.Choice(['tree', 'json']),
27 help='Output format',
28)
29@click.option('--path', '-p', type=click.Path(path_type=Path), help='Project directory')
30@click.argument('node_id', required=False)
31def structure_command(output_format: str, path: Path | None, node_id: str | None) -> None:
32 """Display project hierarchy.
34 If NODE_ID is provided, only show the subtree starting from that node.
35 """
36 try:
37 project_root = path or Path.cwd()
39 # Wire up dependencies
40 binder_repo = BinderRepoFs(project_root)
41 logger = LoggerStdout()
43 # Execute use case
44 interactor = ShowStructure(
45 binder_repo=binder_repo,
46 logger=logger,
47 )
49 # Parse node ID if provided
50 parsed_node_id = NodeId(node_id) if node_id else None
52 structure_str = interactor.execute(node_id=parsed_node_id)
54 if output_format == 'tree':
55 click.echo('Project Structure:')
56 click.echo(structure_str)
57 elif output_format == 'json': # pragma: no branch
58 # For JSON format, we need to convert the tree to JSON
59 binder = binder_repo.load()
61 def item_to_dict(item: Union[Item, 'BinderItem']) -> dict[str, Any]:
62 result: dict[str, Any] = {
63 'display_title': item.display_title,
64 }
65 node_id = item.id if hasattr(item, 'id') else (item.node_id if hasattr(item, 'node_id') else None)
66 if node_id:
67 result['node_id'] = str(node_id)
68 item_children = item.children if hasattr(item, 'children') else []
69 if item_children:
70 result['children'] = [item_to_dict(child) for child in item_children]
71 return result
73 data: dict[str, list[dict[str, Any]]] = {'roots': [item_to_dict(item) for item in binder.roots]}
74 click.echo(json.dumps(data, indent=2))
76 except NodeNotFoundError as e:
77 click.echo(f'Error: {e}', err=True)
78 raise SystemExit(1) from e
79 except FileSystemError as e:
80 click.echo(f'Error: {e}', err=True)
81 raise SystemExit(1) from e
82 except (ValueError, NodeIdentityError) as e:
83 # Invalid node ID format
84 click.echo(f'Error: Invalid node ID format: {e}', err=True)
85 raise SystemExit(1) from e