1 from __future__ import annotations

2

3 import tempfile

4 import typing as t

5 from contextlib import suppress

6 from pathlib import Path

7

8 from session_buddy.di.container import depends

9

10 if t.TYPE_CHECKING:

11 from session_buddy.core import (

12 SessionLifecycleManager as SessionLifecycleManagerT,

13 )

14 from session_buddy.core.permissions import (

15 SessionPermissionsManager as SessionPermissionsManagerT,

16 )

17 from session_buddy.utils.logging import ( # type: ignore[attr-defined]

18 SessionLogger as SessionLoggerT,

19 )

20

21 from .config import SessionPaths

22 from .constants import CLAUDE_DIR_KEY, COMMANDS_DIR_KEY, LOGS_DIR_KEY

23

24_configured = False

25

26

27# Type variable for use with t.cast() in legacy type contexts

28

29

30 def get_sync_typed[T](key: type[T]) -> T:

31 """Type-safe wrapper for depends.get_sync.

32

33 This helper provides proper type information for the dependency injection

34 container, avoiding 'no-any-return' type checker errors.

35

36 Args:

37 key: The class type to retrieve from the DI container

38

39 Returns:

40 The instance of type T from the DI container

41

42 Example:

43 >>> from session_buddy.core.session_manager import SessionLifecycleManager

44 >>> manager = get_sync_typed(SessionLifecycleManager) # Properly typed

45

46 """

47 result = depends.get_sync(key)

48 # Trust the DI container - type checker will verify usage

49 return t.cast("T", result) # Use T directly from type parameter

50

51

52 def configure(*, force: bool = False) -> None:

53 """Register default dependencies for the session-buddy MCP stack.

54

55 This function sets up the dependency injection container with type-safe

56 configuration and singleton instances for the session management system.

57

58 Args:

59 force: If True, re-registers all dependencies even if already configured.

60 Used primarily for testing to reset singleton state.

61

62 Example:

63 >>> from session_buddy.di import configure

64 >>> configure() # First call registers dependencies

65 >>> configure() # Subsequent calls are no-ops unless force=True

66 >>> configure(force=True) # Re-registers all dependencies

67

68 """

69 global _configured

70 if _configured and not force:

71 return

72

73 # Register type-safe path configuration

74 paths = SessionPaths.from_home()

75 paths.ensure_directories()

76 depends.set(SessionPaths, paths)

77

78 # Register services with type-safe path access

79 _register_logger(paths.logs_dir, force)

80 _register_session_logger(paths.logs_dir, force) # Register SessionLogger

81 _register_permissions_manager(paths.claude_dir, force)

82 _register_lifecycle_manager(force)

83

84 _configured = True

85

86

87 def reset() -> None:

88 """Reset dependencies to defaults."""

89 # Reset singleton instances that have class-level state

90 with suppress(ImportError, AttributeError):

91 from session_buddy.core.permissions import SessionPermissionsManager

92

93 SessionPermissionsManager.reset_singleton()

94

95 configure(force=True)

96

97

98 def _register_logger(logs_dir: Path, force: bool) -> None:

99 """Register ACB logger adapter with the given logs directory.

100

101 Args:

102 logs_dir: Directory for session log files

103 force: If True, re-registers even if already registered

104

105 Note:

106 This function configures ACB's logger adapter but does NOT register it

107 in the DI container to avoid type resolution conflicts. Components

108 should use direct logging imports instead of DI lookup.

109

110 """

111 # Skip registration entirely - we don't need to register the logger in DI

112 # Components should use `import logging; logger = logging.getLogger(__name__)`

113 # This avoids DI type confusion when crackerjack runs

114

115

116 def _register_session_logger(logs_dir: Path, force: bool) -> None:

117 """Register SessionLogger with the given logs directory.

118

119 Args:

120 logs_dir: Directory for session log files

121 force: If True, re-registers even if already registered

122

123 """

124 from session_buddy.utils.logging import SessionLogger

125

126 if not force: 126 ↛ 127 line 126 didn't jump to line 127 because the condition on line 126 was never true

127 with suppress(Exception):

128 existing = depends.get_sync(SessionLogger)

129 if isinstance(existing, SessionLogger):

130 return

131

132 # Create SessionLogger instance with fallback to temp logs if needed

133 try:

134 session_logger = SessionLogger(logs_dir)

135 except Exception:

136 tmp_logs = Path(tempfile.gettempdir()) / "session-buddy" / "logs"

137 tmp_logs.mkdir(parents=True, exist_ok=True)

138 depends.set(LOGS_DIR_KEY, tmp_logs)

139 session_logger = SessionLogger(tmp_logs)

140 depends.set(SessionLogger, session_logger)

141

142

143 def _register_permissions_manager(claude_dir: Path, force: bool) -> None:

144 """Register SessionPermissionsManager with the given Claude directory.

145

146 Args:

147 claude_dir: Root Claude directory for session data

148 force: If True, re-registers even if already registered

149

150 Note:

151 Accepts Path directly instead of resolving from string keys,

152 following ACB's type-based dependency injection pattern.

153

154 """

155 from session_buddy.core.permissions import SessionPermissionsManager

156

157 if not force: 157 ↛ 158 line 157 didn't jump to line 158 because the condition on line 157 was never true

158 with suppress(Exception): # Catch all DI resolution errors

159 existing = depends.get_sync(SessionPermissionsManager)

160 if isinstance(existing, SessionPermissionsManager):

161 return

162

163 # Create and register permissions manager instance

164 permissions_manager = SessionPermissionsManager(claude_dir)

165 depends.set(SessionPermissionsManager, permissions_manager)

166

167

168 def _register_lifecycle_manager(force: bool) -> None:

169 """Register SessionLifecycleManager with the DI container.

170

171 Args:

172 force: If True, re-registers even if already registered

173

174 """

175 from session_buddy.core.session_manager import SessionLifecycleManager

176

177 if not force: 177 ↛ 178 line 177 didn't jump to line 178 because the condition on line 177 was never true

178 with suppress(Exception): # Catch all DI resolution errors

179 existing = depends.get_sync(SessionLifecycleManager)

180 if isinstance(existing, SessionLifecycleManager):

181 return

182

183 # Create and register lifecycle manager instance

184 lifecycle_manager = SessionLifecycleManager()

185 depends.set(SessionLifecycleManager, lifecycle_manager)

186

187

188__all__ = [

189 # Legacy string keys (deprecated - use SessionPaths instead)

190 "CLAUDE_DIR_KEY",

191 "COMMANDS_DIR_KEY",

192 "LOGS_DIR_KEY",

193 "SessionPaths",

194 "configure",

195 "depends",

196 "get_sync_typed",

197 "reset",

198]