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

1"""CLI command for displaying project structure.""" 

2 

3import json 

4from pathlib import Path 

5from typing import TYPE_CHECKING, Any, Union 

6 

7import click 

8 

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 

15 

16if TYPE_CHECKING: # pragma: no cover 

17 from prosemark.domain.models import BinderItem 

18 

19 

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. 

33 

34 If NODE_ID is provided, only show the subtree starting from that node. 

35 """ 

36 try: 

37 project_root = path or Path.cwd() 

38 

39 # Wire up dependencies 

40 binder_repo = BinderRepoFs(project_root) 

41 logger = LoggerStdout() 

42 

43 # Execute use case 

44 interactor = ShowStructure( 

45 binder_repo=binder_repo, 

46 logger=logger, 

47 ) 

48 

49 # Parse node ID if provided 

50 parsed_node_id = NodeId(node_id) if node_id else None 

51 

52 structure_str = interactor.execute(node_id=parsed_node_id) 

53 

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() 

60 

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 

72 

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)) 

75 

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