Coverage for src/prosemark/cli/add.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-24 18:08 +0000

1"""CLI command for adding nodes to the binder.""" 

2 

3from pathlib import Path 

4 

5import click 

6 

7from prosemark.adapters.binder_repo_fs import BinderRepoFs 

8from prosemark.adapters.clock_system import ClockSystem 

9from prosemark.adapters.console_pretty import ConsolePretty 

10from prosemark.adapters.editor_launcher_system import EditorLauncherSystem 

11from prosemark.adapters.id_generator_uuid7 import IdGeneratorUuid7 

12from prosemark.adapters.logger_stdout import LoggerStdout 

13from prosemark.adapters.node_repo_fs import NodeRepoFs 

14from prosemark.app.use_cases import AddNode, InitProject 

15from prosemark.domain.models import NodeId 

16from prosemark.exceptions import FileSystemError, NodeNotFoundError 

17 

18 

19@click.command() 

20@click.argument('title') 

21@click.option('--parent', help='Parent node ID') 

22@click.option('--position', type=int, help="Position in parent's children") 

23@click.option('--path', '-p', type=click.Path(path_type=Path), help='Project directory') 

24def add_command(title: str, parent: str | None, position: int | None, path: Path | None) -> None: # noqa: PLR0914 

25 """Add a new node to the binder hierarchy.""" 

26 try: 

27 project_root = path or Path.cwd() 

28 

29 # Auto-initialize project if it doesn't exist 

30 binder_path = project_root / '_binder.md' 

31 if not binder_path.exists(): 

32 # Initialize empty project silently 

33 from prosemark.cli.init import FileSystemConfigPort 

34 

35 binder_repo_init = BinderRepoFs(project_root) 

36 config_port = FileSystemConfigPort() 

37 console_port = ConsolePretty() 

38 logger_init = LoggerStdout() 

39 clock_init = ClockSystem() 

40 

41 init_interactor = InitProject( 

42 binder_repo=binder_repo_init, 

43 config_port=config_port, 

44 console_port=console_port, 

45 logger=logger_init, 

46 clock=clock_init, 

47 ) 

48 init_interactor.execute(project_root) 

49 

50 # Wire up dependencies 

51 binder_repo = BinderRepoFs(project_root) 

52 clock = ClockSystem() 

53 editor = EditorLauncherSystem() 

54 node_repo = NodeRepoFs(project_root, editor, clock) 

55 id_generator = IdGeneratorUuid7() 

56 logger = LoggerStdout() 

57 

58 # Execute use case 

59 interactor = AddNode( 

60 binder_repo=binder_repo, 

61 node_repo=node_repo, 

62 id_generator=id_generator, 

63 logger=logger, 

64 clock=clock, 

65 ) 

66 

67 # Validate position if provided 

68 if position is not None and position < 0: 

69 click.echo('Error: Invalid position index', err=True) 

70 raise SystemExit(2) 

71 

72 parent_id = None 

73 if parent: 

74 try: 

75 parent_id = NodeId(parent) 

76 except ValueError as err: 

77 # Invalid parent ID format, treat as "parent not found" 

78 click.echo('Error: Parent node not found', err=True) 

79 raise SystemExit(1) from err 

80 node_id = interactor.execute( 

81 title=title, 

82 synopsis=None, 

83 parent_id=parent_id, 

84 position=position, 

85 ) 

86 

87 # Success output 

88 click.echo(f'Added "{title}" ({node_id})') 

89 click.echo(f'Created files: {node_id}.md, {node_id}.notes.md') 

90 click.echo('Updated binder structure') 

91 

92 except NodeNotFoundError as err: 

93 click.echo('Error: Parent node not found', err=True) 

94 raise SystemExit(1) from err 

95 except ValueError as err: 

96 click.echo('Error: Invalid position index', err=True) 

97 raise SystemExit(2) from err 

98 except FileSystemError as err: 

99 click.echo(f'Error: File creation failed - {err}', err=True) 

100 raise SystemExit(3) from err