crackerjack 0.29.0__py3-none-any.whl → 0.31.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,395 @@
1
+ import shutil
2
+ from collections.abc import Iterator
3
+ from pathlib import Path
4
+
5
+ from crackerjack.errors import ErrorCode, FileError, ResourceError
6
+
7
+
8
+ class FileSystemService:
9
+ def read_file(self, path: str | Path) -> str:
10
+ try:
11
+ path_obj = Path(path) if isinstance(path, str) else path
12
+ if not path_obj.exists():
13
+ raise FileError(
14
+ message=f"File does not exist: {path_obj}",
15
+ details=f"Attempted to read file at {path_obj.absolute()}",
16
+ recovery="Check file path and ensure file exists",
17
+ )
18
+ return path_obj.read_text(encoding="utf-8")
19
+ except PermissionError as e:
20
+ raise FileError(
21
+ message=f"Permission denied reading file: {path}",
22
+ error_code=ErrorCode.PERMISSION_ERROR,
23
+ details=str(e),
24
+ recovery="Check file permissions and user access rights",
25
+ ) from e
26
+ except UnicodeDecodeError as e:
27
+ raise FileError(
28
+ message=f"Unable to decode file as UTF-8: {path}",
29
+ error_code=ErrorCode.FILE_READ_ERROR,
30
+ details=str(e),
31
+ recovery="Ensure file is text-based and UTF-8 encoded",
32
+ ) from e
33
+ except OSError as e:
34
+ raise FileError(
35
+ message=f"System error reading file: {path}",
36
+ error_code=ErrorCode.FILE_READ_ERROR,
37
+ details=str(e),
38
+ recovery="Check disk space and file system integrity",
39
+ ) from e
40
+
41
+ def write_file(self, path: str | Path, content: str) -> None:
42
+ try:
43
+ path_obj = Path(path) if isinstance(path, str) else path
44
+ try:
45
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
46
+ except OSError as e:
47
+ raise FileError(
48
+ message=f"Cannot create parent directories for: {path}",
49
+ error_code=ErrorCode.FILE_WRITE_ERROR,
50
+ details=str(e),
51
+ recovery="Check disk space and directory permissions",
52
+ ) from e
53
+ path_obj.write_text(content, encoding="utf-8")
54
+ except PermissionError as e:
55
+ raise FileError(
56
+ message=f"Permission denied writing file: {path}",
57
+ error_code=ErrorCode.PERMISSION_ERROR,
58
+ details=str(e),
59
+ recovery="Check file and directory permissions",
60
+ ) from e
61
+ except OSError as e:
62
+ if "No space left on device" in str(e):
63
+ raise ResourceError(
64
+ message=f"Insufficient disk space to write file: {path}",
65
+ details=str(e),
66
+ recovery="Free up disk space and try again",
67
+ ) from e
68
+ raise FileError(
69
+ message=f"System error writing file: {path}",
70
+ error_code=ErrorCode.FILE_WRITE_ERROR,
71
+ details=str(e),
72
+ recovery="Check disk space and file system integrity",
73
+ ) from e
74
+
75
+ def exists(self, path: str | Path) -> bool:
76
+ try:
77
+ path_obj = Path(path) if isinstance(path, str) else path
78
+ return path_obj.exists()
79
+ except OSError:
80
+ return False
81
+
82
+ def mkdir(self, path: str | Path, parents: bool = False) -> None:
83
+ try:
84
+ path_obj = Path(path) if isinstance(path, str) else path
85
+ path_obj.mkdir(parents=parents, exist_ok=True)
86
+ except PermissionError as e:
87
+ raise FileError(
88
+ message=f"Permission denied creating directory: {path}",
89
+ error_code=ErrorCode.PERMISSION_ERROR,
90
+ details=str(e),
91
+ recovery="Check parent directory permissions",
92
+ ) from e
93
+ except FileExistsError as e:
94
+ if not parents:
95
+ raise FileError(
96
+ message=f"Directory already exists: {path}",
97
+ details=str(e),
98
+ recovery="Use exist_ok=True or check if directory exists first",
99
+ ) from e
100
+ except OSError as e:
101
+ if "No space left on device" in str(e):
102
+ raise ResourceError(
103
+ message=f"Insufficient disk space to create directory: {path}",
104
+ details=str(e),
105
+ recovery="Free up disk space and try again",
106
+ ) from e
107
+ raise FileError(
108
+ message=f"System error creating directory: {path}",
109
+ error_code=ErrorCode.FILE_WRITE_ERROR,
110
+ details=str(e),
111
+ recovery="Check disk space and file system integrity",
112
+ ) from e
113
+
114
+ def glob(self, pattern: str, path: str | Path | None = None) -> list[Path]:
115
+ base_path = Path(path) if path else Path.cwd()
116
+ try:
117
+ if not base_path.exists():
118
+ raise FileError(
119
+ message=f"Base path does not exist: {base_path}",
120
+ details=f"Attempted to glob in {base_path.absolute()}",
121
+ recovery="Check base path and ensure directory exists",
122
+ )
123
+ return list(base_path.glob(pattern))
124
+ except PermissionError as e:
125
+ raise FileError(
126
+ message=f"Permission denied accessing directory: {base_path}",
127
+ error_code=ErrorCode.PERMISSION_ERROR,
128
+ details=str(e),
129
+ recovery="Check directory permissions",
130
+ ) from e
131
+ except OSError as e:
132
+ raise FileError(
133
+ message=f"System error during glob operation: {pattern}",
134
+ error_code=ErrorCode.FILE_READ_ERROR,
135
+ details=str(e),
136
+ recovery="Check path validity and file system integrity",
137
+ ) from e
138
+
139
+ def rglob(self, pattern: str, path: str | Path | None = None) -> list[Path]:
140
+ base_path = Path(path) if path else Path.cwd()
141
+ try:
142
+ if not base_path.exists():
143
+ raise FileError(
144
+ message=f"Base path does not exist: {base_path}",
145
+ details=f"Attempted to rglob in {base_path.absolute()}",
146
+ recovery="Check base path and ensure directory exists",
147
+ )
148
+ return list(base_path.rglob(pattern))
149
+ except PermissionError as e:
150
+ raise FileError(
151
+ message=f"Permission denied accessing directory: {base_path}",
152
+ error_code=ErrorCode.PERMISSION_ERROR,
153
+ details=str(e),
154
+ recovery="Check directory permissions",
155
+ ) from e
156
+ except OSError as e:
157
+ raise FileError(
158
+ message=f"System error during recursive glob operation: {pattern}",
159
+ error_code=ErrorCode.FILE_READ_ERROR,
160
+ details=str(e),
161
+ recovery="Check path validity and file system integrity",
162
+ ) from e
163
+
164
+ def copy_file(self, src: str | Path, dst: str | Path) -> None:
165
+ src_path, dst_path = self._normalize_copy_paths(src, dst)
166
+ self._validate_copy_source(src_path)
167
+ self._prepare_copy_destination(dst_path)
168
+ self._perform_file_copy(src_path, dst_path, src, dst)
169
+
170
+ def _normalize_copy_paths(
171
+ self, src: str | Path, dst: str | Path
172
+ ) -> tuple[Path, Path]:
173
+ src_path = Path(src) if isinstance(src, str) else src
174
+ dst_path = Path(dst) if isinstance(dst, str) else dst
175
+ return src_path, dst_path
176
+
177
+ def _validate_copy_source(self, src_path: Path) -> None:
178
+ if not src_path.exists():
179
+ raise FileError(
180
+ message=f"Source file does not exist: {src_path}",
181
+ details=f"Attempted to copy from {src_path.absolute()}",
182
+ recovery="Check source file path and ensure file exists",
183
+ )
184
+ if not src_path.is_file():
185
+ raise FileError(
186
+ message=f"Source is not a file: {src_path}",
187
+ details=f"Source is a {src_path.stat().st_mode} type",
188
+ recovery="Ensure source path points to a regular file",
189
+ )
190
+
191
+ def _prepare_copy_destination(self, dst_path: Path) -> None:
192
+ try:
193
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
194
+ except OSError as e:
195
+ raise FileError(
196
+ message=f"Cannot create destination parent directories: {dst_path.parent}",
197
+ error_code=ErrorCode.FILE_WRITE_ERROR,
198
+ details=str(e),
199
+ recovery="Check disk space and directory permissions",
200
+ ) from e
201
+
202
+ def _perform_file_copy(
203
+ self, src_path: Path, dst_path: Path, src: str | Path, dst: str | Path
204
+ ) -> None:
205
+ try:
206
+ shutil.copy2(src_path, dst_path)
207
+ except PermissionError as e:
208
+ raise FileError(
209
+ message=f"Permission denied copying file: {src} -> {dst}",
210
+ error_code=ErrorCode.PERMISSION_ERROR,
211
+ details=str(e),
212
+ recovery="Check file and directory permissions",
213
+ ) from e
214
+ except shutil.SameFileError as e:
215
+ raise FileError(
216
+ message=f"Source and destination are the same file: {src}",
217
+ error_code=ErrorCode.FILE_WRITE_ERROR,
218
+ details=str(e),
219
+ recovery="Ensure source and destination paths are different",
220
+ ) from e
221
+ except OSError as e:
222
+ if "No space left on device" in str(e):
223
+ raise ResourceError(
224
+ message=f"Insufficient disk space to copy file: {src} -> {dst}",
225
+ details=str(e),
226
+ recovery="Free up disk space and try again",
227
+ ) from e
228
+ raise FileError(
229
+ message=f"System error copying file: {src} -> {dst}",
230
+ error_code=ErrorCode.FILE_WRITE_ERROR,
231
+ details=str(e),
232
+ recovery="Check disk space and file system integrity",
233
+ ) from e
234
+
235
+ def remove_file(self, path: str | Path) -> None:
236
+ try:
237
+ path_obj = Path(path) if isinstance(path, str) else path
238
+ if path_obj.exists():
239
+ if not path_obj.is_file():
240
+ raise FileError(
241
+ message=f"Path is not a file: {path_obj}",
242
+ details=f"Path type: {path_obj.stat().st_mode}",
243
+ recovery="Use appropriate method for directory removal",
244
+ )
245
+ path_obj.unlink()
246
+ except PermissionError as e:
247
+ raise FileError(
248
+ message=f"Permission denied removing file: {path}",
249
+ error_code=ErrorCode.PERMISSION_ERROR,
250
+ details=str(e),
251
+ recovery="Check file permissions and ownership",
252
+ ) from e
253
+ except OSError as e:
254
+ raise FileError(
255
+ message=f"System error removing file: {path}",
256
+ error_code=ErrorCode.FILE_WRITE_ERROR,
257
+ details=str(e),
258
+ recovery="Check file system integrity and try again",
259
+ ) from e
260
+
261
+ def get_file_size(self, path: str | Path) -> int:
262
+ try:
263
+ path_obj = Path(path) if isinstance(path, str) else path
264
+ if not path_obj.exists():
265
+ raise FileError(
266
+ message=f"File does not exist: {path_obj}",
267
+ details=f"Attempted to get size of {path_obj.absolute()}",
268
+ recovery="Check file path and ensure file exists",
269
+ )
270
+ if not path_obj.is_file():
271
+ raise FileError(
272
+ message=f"Path is not a file: {path_obj}",
273
+ details=f"Path type: {path_obj.stat().st_mode}",
274
+ recovery="Ensure path points to a regular file",
275
+ )
276
+ return path_obj.stat().st_size
277
+ except PermissionError as e:
278
+ raise FileError(
279
+ message=f"Permission denied accessing file: {path}",
280
+ error_code=ErrorCode.PERMISSION_ERROR,
281
+ details=str(e),
282
+ recovery="Check file permissions",
283
+ ) from e
284
+ except OSError as e:
285
+ raise FileError(
286
+ message=f"System error getting file size: {path}",
287
+ error_code=ErrorCode.FILE_READ_ERROR,
288
+ details=str(e),
289
+ recovery="Check file system integrity",
290
+ ) from e
291
+
292
+ def get_file_mtime(self, path: str | Path) -> float:
293
+ try:
294
+ path_obj = Path(path) if isinstance(path, str) else path
295
+ if not path_obj.exists():
296
+ raise FileError(
297
+ message=f"File does not exist: {path_obj}",
298
+ details=f"Attempted to get mtime of {path_obj.absolute()}",
299
+ recovery="Check file path and ensure file exists",
300
+ )
301
+ if not path_obj.is_file():
302
+ raise FileError(
303
+ message=f"Path is not a file: {path_obj}",
304
+ details=f"Path type: {path_obj.stat().st_mode}",
305
+ recovery="Ensure path points to a regular file",
306
+ )
307
+ return path_obj.stat().st_mtime
308
+ except PermissionError as e:
309
+ raise FileError(
310
+ message=f"Permission denied accessing file: {path}",
311
+ error_code=ErrorCode.PERMISSION_ERROR,
312
+ details=str(e),
313
+ recovery="Check file permissions",
314
+ ) from e
315
+ except OSError as e:
316
+ raise FileError(
317
+ message=f"System error getting file modification time: {path}",
318
+ error_code=ErrorCode.FILE_READ_ERROR,
319
+ details=str(e),
320
+ recovery="Check file system integrity",
321
+ ) from e
322
+
323
+ def read_file_chunked(
324
+ self,
325
+ path: str | Path,
326
+ chunk_size: int = 8192,
327
+ ) -> Iterator[str]:
328
+ try:
329
+ path_obj = Path(path) if isinstance(path, str) else path
330
+ if not path_obj.exists():
331
+ raise FileError(
332
+ message=f"File does not exist: {path_obj}",
333
+ details=f"Attempted to read file at {path_obj.absolute()}",
334
+ recovery="Check file path and ensure file exists",
335
+ )
336
+
337
+ with path_obj.open(encoding="utf-8") as file:
338
+ while chunk := file.read(chunk_size):
339
+ yield chunk
340
+
341
+ except PermissionError as e:
342
+ raise FileError(
343
+ message=f"Permission denied reading file: {path}",
344
+ error_code=ErrorCode.PERMISSION_ERROR,
345
+ details=str(e),
346
+ recovery="Check file permissions",
347
+ ) from e
348
+ except UnicodeDecodeError as e:
349
+ raise FileError(
350
+ message=f"File encoding error: {path}",
351
+ error_code=ErrorCode.FILE_READ_ERROR,
352
+ details=str(e),
353
+ recovery="Ensure file is encoded in UTF-8",
354
+ ) from e
355
+ except OSError as e:
356
+ raise FileError(
357
+ message=f"System error reading file: {path}",
358
+ error_code=ErrorCode.FILE_READ_ERROR,
359
+ details=str(e),
360
+ recovery="Check file system integrity",
361
+ ) from e
362
+
363
+ def read_lines_streaming(self, path: str | Path) -> Iterator[str]:
364
+ try:
365
+ path_obj = Path(path) if isinstance(path, str) else path
366
+ if not path_obj.exists():
367
+ raise FileError(
368
+ message=f"File does not exist: {path_obj}",
369
+ details=f"Attempted to read file at {path_obj.absolute()}",
370
+ recovery="Check file path and ensure file exists",
371
+ )
372
+ with path_obj.open(encoding="utf-8") as file:
373
+ for line in file:
374
+ yield line.rstrip("\n\r")
375
+ except PermissionError as e:
376
+ raise FileError(
377
+ message=f"Permission denied reading file: {path}",
378
+ error_code=ErrorCode.PERMISSION_ERROR,
379
+ details=str(e),
380
+ recovery="Check file permissions",
381
+ ) from e
382
+ except UnicodeDecodeError as e:
383
+ raise FileError(
384
+ message=f"File encoding error: {path}",
385
+ error_code=ErrorCode.FILE_READ_ERROR,
386
+ details=str(e),
387
+ recovery="Ensure file is encoded in UTF-8",
388
+ ) from e
389
+ except OSError as e:
390
+ raise FileError(
391
+ message=f"System error reading file: {path}",
392
+ error_code=ErrorCode.FILE_READ_ERROR,
393
+ details=str(e),
394
+ recovery="Check file system integrity",
395
+ ) from e
@@ -0,0 +1,165 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ from rich.console import Console
5
+
6
+
7
+ class GitService:
8
+ def __init__(self, console: Console, pkg_path: Path | None = None) -> None:
9
+ self.console = console
10
+ self.pkg_path = pkg_path or Path.cwd()
11
+
12
+ def _run_git_command(self, args: list[str]) -> subprocess.CompletedProcess[str]:
13
+ cmd = ["git", *args]
14
+ return subprocess.run(
15
+ cmd,
16
+ check=False,
17
+ cwd=self.pkg_path,
18
+ capture_output=True,
19
+ text=True,
20
+ timeout=60,
21
+ )
22
+
23
+ def is_git_repo(self) -> bool:
24
+ try:
25
+ result = self._run_git_command(["rev-parse", "--git-dir"])
26
+ return result.returncode == 0
27
+ except (subprocess.SubprocessError, OSError, FileNotFoundError):
28
+ return False
29
+
30
+ def get_changed_files(self) -> list[str]:
31
+ try:
32
+ staged_result = self._run_git_command(["diff", "--cached", "--name-only"])
33
+ staged_files = (
34
+ staged_result.stdout.strip().split("\n")
35
+ if staged_result.stdout.strip()
36
+ else []
37
+ )
38
+ unstaged_result = self._run_git_command(["diff", "--name-only"])
39
+ unstaged_files = (
40
+ unstaged_result.stdout.strip().split("\n")
41
+ if unstaged_result.stdout.strip()
42
+ else []
43
+ )
44
+ untracked_result = self._run_git_command(
45
+ ["ls-files", "--others", "--exclude-standard"],
46
+ )
47
+ untracked_files = (
48
+ untracked_result.stdout.strip().split("\n")
49
+ if untracked_result.stdout.strip()
50
+ else []
51
+ )
52
+ all_files = set(staged_files + unstaged_files + untracked_files)
53
+ return [f for f in all_files if f]
54
+ except Exception as e:
55
+ self.console.print(f"[yellow]⚠️[/yellow] Error getting changed files: {e}")
56
+ return []
57
+
58
+ def get_staged_files(self) -> list[str]:
59
+ try:
60
+ result = self._run_git_command(["diff", "--cached", "--name-only"])
61
+ return result.stdout.strip().split("\n") if result.stdout.strip() else []
62
+ except Exception as e:
63
+ self.console.print(f"[yellow]⚠️[/yellow] Error getting staged files: {e}")
64
+ return []
65
+
66
+ def add_files(self, files: list[str]) -> bool:
67
+ try:
68
+ for file in files:
69
+ result = self._run_git_command(["add", file])
70
+ if result.returncode != 0:
71
+ self.console.print(
72
+ f"[red]❌[/red] Failed to add {file}: {result.stderr}",
73
+ )
74
+ return False
75
+ return True
76
+ except Exception as e:
77
+ self.console.print(f"[red]❌[/red] Error adding files: {e}")
78
+ return False
79
+
80
+ def commit(self, message: str) -> bool:
81
+ try:
82
+ result = self._run_git_command(["commit", "-m", message])
83
+ if result.returncode == 0:
84
+ self.console.print(f"[green]✅[/green] Committed: {message}")
85
+ return True
86
+ self.console.print(f"[red]❌[/red] Commit failed: {result.stderr}")
87
+ return False
88
+ except Exception as e:
89
+ self.console.print(f"[red]❌[/red] Error committing: {e}")
90
+ return False
91
+
92
+ def push(self) -> bool:
93
+ try:
94
+ result = self._run_git_command(["push"])
95
+ if result.returncode == 0:
96
+ self.console.print("[green]✅[/green] Pushed to remote")
97
+ return True
98
+ self.console.print(f"[red]❌[/red] Push failed: {result.stderr}")
99
+ return False
100
+ except Exception as e:
101
+ self.console.print(f"[red]❌[/red] Error pushing: {e}")
102
+ return False
103
+
104
+ def get_current_branch(self) -> str | None:
105
+ try:
106
+ result = self._run_git_command(["branch", "--show-current"])
107
+ return result.stdout.strip() if result.returncode == 0 else None
108
+ except (subprocess.SubprocessError, OSError, FileNotFoundError):
109
+ return None
110
+
111
+ def get_commit_message_suggestions(self, files: list[str]) -> list[str]:
112
+ if not files:
113
+ return ["Update project files"]
114
+ file_categories = self._categorize_files(files)
115
+ messages = self._generate_category_messages(file_categories)
116
+ messages.extend(self._generate_specific_messages(files))
117
+
118
+ return messages[:5]
119
+
120
+ def _categorize_files(self, files: list[str]) -> set[str]:
121
+ categories = {
122
+ "docs": ["README", "CLAUDE", "docs/", ".md"],
123
+ "tests": ["test_", "tests/", "conftest.py"],
124
+ "config": ["pyproject.toml", ".yaml", ".yml", ".json", ".gitignore"],
125
+ "ci": [".github/", "ci/", ".pre-commit"],
126
+ "deps": ["requirements", "uv.lock", "Pipfile"],
127
+ }
128
+ file_categories: set[str] = set()
129
+ for file in files:
130
+ category = self._get_file_category(file, categories)
131
+ file_categories.add(category)
132
+
133
+ return file_categories
134
+
135
+ def _get_file_category(self, file: str, categories: dict[str, list[str]]) -> str:
136
+ for category, patterns in categories.items():
137
+ if any(pattern in file for pattern in patterns):
138
+ return category
139
+ return "core"
140
+
141
+ def _generate_category_messages(self, file_categories: set[str]) -> list[str]:
142
+ if len(file_categories) == 1:
143
+ return self._generate_single_category_message(next(iter(file_categories)))
144
+ return [f"Update {', '.join(sorted(file_categories))}"]
145
+
146
+ def _generate_single_category_message(self, category: str) -> list[str]:
147
+ category_messages = {
148
+ "docs": "Update documentation",
149
+ "tests": "Update tests",
150
+ "config": "Update configuration",
151
+ "ci": "Update CI/CD configuration",
152
+ "deps": "Update dependencies",
153
+ }
154
+ return [category_messages.get(category, "Update core functionality")]
155
+
156
+ def _generate_specific_messages(self, files: list[str]) -> list[str]:
157
+ messages: list[str] = []
158
+ if "pyproject.toml" in files:
159
+ messages.append("Update project configuration")
160
+ if any("test_" in f for f in files):
161
+ messages.append("Improve test coverage")
162
+ if "README.md" in files:
163
+ messages.append("Update README documentation")
164
+
165
+ return messages