claude-mpm 4.1.7__py3-none-any.whl → 4.1.10__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/OUTPUT_STYLE.md +73 -0
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agent-manager.md +248 -10
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +4 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +547 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +223 -25
- claude_mpm/cli/commands/configure_tui.py +65 -61
- claude_mpm/cli/commands/debug.py +1387 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +29 -0
- claude_mpm/cli/parsers/configure_parser.py +23 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/config/socketio_config.py +21 -21
- claude_mpm/constants.py +3 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +428 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +846 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +39 -0
- claude_mpm/dashboard/static/js/socket-client.js +414 -20
- claude_mpm/dashboard/templates/index.html +184 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +728 -0
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -5
- claude_mpm/services/cli/agent_cleanup_service.py +1 -2
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -10
- claude_mpm/services/core/cache_manager.py +1 -2
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
- claude_mpm/services/event_bus/direct_relay.py +98 -20
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +95 -65
- claude_mpm/services/socketio/server/core.py +125 -17
- claude_mpm/services/socketio/server/main.py +44 -5
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +778 -0
- claude_mpm/tools/code_tree_builder.py +632 -0
- claude_mpm/tools/code_tree_events.py +318 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Code Tree Builder
|
|
4
|
+
=================
|
|
5
|
+
|
|
6
|
+
WHY: Builds hierarchical file system structure with intelligent filtering
|
|
7
|
+
and incremental processing support for large codebases.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Support .gitignore patterns for filtering
|
|
11
|
+
- Incremental processing with resume capability
|
|
12
|
+
- Efficient directory traversal with progress tracking
|
|
13
|
+
- Cache file metadata to detect changes
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import fnmatch
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Set
|
|
23
|
+
|
|
24
|
+
from ..core.logging_config import get_logger
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FileMetadata:
|
|
29
|
+
"""Metadata for a file in the tree."""
|
|
30
|
+
|
|
31
|
+
path: str
|
|
32
|
+
size: int
|
|
33
|
+
modified: float
|
|
34
|
+
hash: Optional[str] = None
|
|
35
|
+
language: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TreeNode:
|
|
40
|
+
"""Node in the file tree."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
path: str
|
|
44
|
+
type: str # 'file' or 'directory'
|
|
45
|
+
children: List["TreeNode"] = field(default_factory=list)
|
|
46
|
+
metadata: Optional[FileMetadata] = None
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
49
|
+
"""Convert to dictionary representation."""
|
|
50
|
+
result = {"name": self.name, "path": self.path, "type": self.type}
|
|
51
|
+
|
|
52
|
+
if self.metadata:
|
|
53
|
+
result["metadata"] = {
|
|
54
|
+
"size": self.metadata.size,
|
|
55
|
+
"modified": self.metadata.modified,
|
|
56
|
+
"language": self.metadata.language,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if self.children:
|
|
60
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GitignoreParser:
|
|
66
|
+
"""Parser for .gitignore patterns.
|
|
67
|
+
|
|
68
|
+
WHY: Respecting .gitignore patterns ensures we don't analyze files
|
|
69
|
+
that shouldn't be included in the codebase analysis.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, root_dir: Path):
|
|
73
|
+
self.root_dir = root_dir
|
|
74
|
+
self.patterns = []
|
|
75
|
+
self.logger = get_logger(__name__)
|
|
76
|
+
self._load_patterns()
|
|
77
|
+
|
|
78
|
+
def _load_patterns(self):
|
|
79
|
+
"""Load .gitignore patterns from file."""
|
|
80
|
+
gitignore_path = self.root_dir / ".gitignore"
|
|
81
|
+
|
|
82
|
+
if gitignore_path.exists():
|
|
83
|
+
try:
|
|
84
|
+
with open(gitignore_path) as f:
|
|
85
|
+
for line in f:
|
|
86
|
+
line = line.strip()
|
|
87
|
+
# Skip comments and empty lines
|
|
88
|
+
if line and not line.startswith("#"):
|
|
89
|
+
self.patterns.append(line)
|
|
90
|
+
|
|
91
|
+
self.logger.debug(f"Loaded {len(self.patterns)} .gitignore patterns")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.logger.warning(f"Failed to load .gitignore: {e}")
|
|
94
|
+
|
|
95
|
+
def should_ignore(self, path: Path) -> bool:
|
|
96
|
+
"""Check if path should be ignored based on patterns.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
path: Path to check
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if path should be ignored
|
|
103
|
+
"""
|
|
104
|
+
# Get relative path from root
|
|
105
|
+
try:
|
|
106
|
+
rel_path = path.relative_to(self.root_dir)
|
|
107
|
+
except ValueError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
rel_str = str(rel_path)
|
|
111
|
+
|
|
112
|
+
for pattern in self.patterns:
|
|
113
|
+
# Handle directory patterns
|
|
114
|
+
if pattern.endswith("/"):
|
|
115
|
+
if path.is_dir() and fnmatch.fnmatch(rel_str, pattern[:-1]):
|
|
116
|
+
return True
|
|
117
|
+
# Handle negation patterns
|
|
118
|
+
elif pattern.startswith("!"):
|
|
119
|
+
if fnmatch.fnmatch(rel_str, pattern[1:]):
|
|
120
|
+
return False
|
|
121
|
+
# Regular patterns
|
|
122
|
+
else:
|
|
123
|
+
if fnmatch.fnmatch(rel_str, pattern):
|
|
124
|
+
return True
|
|
125
|
+
# Also check if any parent directory matches
|
|
126
|
+
for parent in rel_path.parents:
|
|
127
|
+
if fnmatch.fnmatch(str(parent), pattern):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CodeTreeBuilder:
|
|
134
|
+
"""Builds hierarchical code tree with filtering and caching.
|
|
135
|
+
|
|
136
|
+
WHY: Efficient tree building is crucial for large codebases. This class
|
|
137
|
+
handles incremental processing, caching, and intelligent filtering.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Default ignore patterns
|
|
141
|
+
DEFAULT_IGNORE = [
|
|
142
|
+
"__pycache__",
|
|
143
|
+
".git",
|
|
144
|
+
"node_modules",
|
|
145
|
+
".venv",
|
|
146
|
+
"venv",
|
|
147
|
+
"env",
|
|
148
|
+
"dist",
|
|
149
|
+
"build",
|
|
150
|
+
".pytest_cache",
|
|
151
|
+
".mypy_cache",
|
|
152
|
+
".tox",
|
|
153
|
+
".eggs",
|
|
154
|
+
"*.egg-info",
|
|
155
|
+
".coverage",
|
|
156
|
+
"htmlcov",
|
|
157
|
+
".hypothesis",
|
|
158
|
+
".ruff_cache",
|
|
159
|
+
".DS_Store",
|
|
160
|
+
"Thumbs.db",
|
|
161
|
+
"*.pyc",
|
|
162
|
+
"*.pyo",
|
|
163
|
+
"*.pyd",
|
|
164
|
+
".Python",
|
|
165
|
+
"*.so",
|
|
166
|
+
"*.dll",
|
|
167
|
+
"*.dylib",
|
|
168
|
+
".idea",
|
|
169
|
+
".vscode",
|
|
170
|
+
"*.swp",
|
|
171
|
+
"*.swo",
|
|
172
|
+
"*~",
|
|
173
|
+
".env",
|
|
174
|
+
".env.local",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
# Language detection by extension
|
|
178
|
+
LANGUAGE_MAP = {
|
|
179
|
+
".py": "python",
|
|
180
|
+
".pyw": "python",
|
|
181
|
+
".pyx": "python",
|
|
182
|
+
".pxd": "python",
|
|
183
|
+
".pyi": "python",
|
|
184
|
+
".js": "javascript",
|
|
185
|
+
".jsx": "javascript",
|
|
186
|
+
".ts": "typescript",
|
|
187
|
+
".tsx": "typescript",
|
|
188
|
+
".mjs": "javascript",
|
|
189
|
+
".cjs": "javascript",
|
|
190
|
+
".java": "java",
|
|
191
|
+
".kt": "kotlin",
|
|
192
|
+
".scala": "scala",
|
|
193
|
+
".go": "go",
|
|
194
|
+
".rs": "rust",
|
|
195
|
+
".c": "c",
|
|
196
|
+
".h": "c",
|
|
197
|
+
".cpp": "cpp",
|
|
198
|
+
".cc": "cpp",
|
|
199
|
+
".cxx": "cpp",
|
|
200
|
+
".hpp": "cpp",
|
|
201
|
+
".cs": "csharp",
|
|
202
|
+
".rb": "ruby",
|
|
203
|
+
".php": "php",
|
|
204
|
+
".swift": "swift",
|
|
205
|
+
".m": "objc",
|
|
206
|
+
".mm": "objc",
|
|
207
|
+
".r": "r",
|
|
208
|
+
".R": "r",
|
|
209
|
+
".lua": "lua",
|
|
210
|
+
".pl": "perl",
|
|
211
|
+
".pm": "perl",
|
|
212
|
+
".sh": "bash",
|
|
213
|
+
".bash": "bash",
|
|
214
|
+
".zsh": "zsh",
|
|
215
|
+
".fish": "fish",
|
|
216
|
+
".ps1": "powershell",
|
|
217
|
+
".psm1": "powershell",
|
|
218
|
+
".vim": "vim",
|
|
219
|
+
".el": "elisp",
|
|
220
|
+
".clj": "clojure",
|
|
221
|
+
".cljs": "clojure",
|
|
222
|
+
".ex": "elixir",
|
|
223
|
+
".exs": "elixir",
|
|
224
|
+
".erl": "erlang",
|
|
225
|
+
".hrl": "erlang",
|
|
226
|
+
".fs": "fsharp",
|
|
227
|
+
".fsx": "fsharp",
|
|
228
|
+
".ml": "ocaml",
|
|
229
|
+
".mli": "ocaml",
|
|
230
|
+
".dart": "dart",
|
|
231
|
+
".nim": "nim",
|
|
232
|
+
".nims": "nim",
|
|
233
|
+
".zig": "zig",
|
|
234
|
+
".v": "v",
|
|
235
|
+
".vv": "v",
|
|
236
|
+
".sql": "sql",
|
|
237
|
+
".md": "markdown",
|
|
238
|
+
".rst": "restructuredtext",
|
|
239
|
+
".tex": "latex",
|
|
240
|
+
".json": "json",
|
|
241
|
+
".xml": "xml",
|
|
242
|
+
".yaml": "yaml",
|
|
243
|
+
".yml": "yaml",
|
|
244
|
+
".toml": "toml",
|
|
245
|
+
".ini": "ini",
|
|
246
|
+
".cfg": "ini",
|
|
247
|
+
".conf": "conf",
|
|
248
|
+
".dockerfile": "dockerfile",
|
|
249
|
+
".Dockerfile": "dockerfile",
|
|
250
|
+
".html": "html",
|
|
251
|
+
".htm": "html",
|
|
252
|
+
".css": "css",
|
|
253
|
+
".scss": "scss",
|
|
254
|
+
".sass": "sass",
|
|
255
|
+
".less": "less",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def __init__(self, cache_dir: Optional[Path] = None):
|
|
259
|
+
"""Initialize tree builder.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
cache_dir: Directory for caching tree data
|
|
263
|
+
"""
|
|
264
|
+
self.logger = get_logger(__name__)
|
|
265
|
+
self.cache_dir = cache_dir or Path.home() / ".claude-mpm" / "tree-cache"
|
|
266
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
# Statistics
|
|
269
|
+
self.stats = {
|
|
270
|
+
"directories_scanned": 0,
|
|
271
|
+
"files_found": 0,
|
|
272
|
+
"files_ignored": 0,
|
|
273
|
+
"total_size": 0,
|
|
274
|
+
"languages": set(),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def build_tree(
|
|
278
|
+
self,
|
|
279
|
+
root_path: Path,
|
|
280
|
+
file_extensions: Optional[List[str]] = None,
|
|
281
|
+
ignore_patterns: Optional[List[str]] = None,
|
|
282
|
+
max_depth: Optional[int] = None,
|
|
283
|
+
use_gitignore: bool = True,
|
|
284
|
+
calculate_hashes: bool = False,
|
|
285
|
+
progress_callback: Optional[callable] = None,
|
|
286
|
+
) -> TreeNode:
|
|
287
|
+
"""Build file tree from directory.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
root_path: Root directory to scan
|
|
291
|
+
file_extensions: File extensions to include (None for all)
|
|
292
|
+
ignore_patterns: Additional ignore patterns
|
|
293
|
+
max_depth: Maximum directory depth to traverse
|
|
294
|
+
use_gitignore: Whether to use .gitignore patterns
|
|
295
|
+
calculate_hashes: Whether to calculate file hashes
|
|
296
|
+
progress_callback: Callback for progress updates
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Root TreeNode of the built tree
|
|
300
|
+
"""
|
|
301
|
+
self.stats = {
|
|
302
|
+
"directories_scanned": 0,
|
|
303
|
+
"files_found": 0,
|
|
304
|
+
"files_ignored": 0,
|
|
305
|
+
"total_size": 0,
|
|
306
|
+
"languages": set(),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Load gitignore patterns if requested
|
|
310
|
+
gitignore_parser = GitignoreParser(root_path) if use_gitignore else None
|
|
311
|
+
|
|
312
|
+
# Combine ignore patterns
|
|
313
|
+
all_ignore_patterns = set(self.DEFAULT_IGNORE)
|
|
314
|
+
if ignore_patterns:
|
|
315
|
+
all_ignore_patterns.update(ignore_patterns)
|
|
316
|
+
|
|
317
|
+
# Build the tree
|
|
318
|
+
return self._build_node(
|
|
319
|
+
root_path,
|
|
320
|
+
root_path,
|
|
321
|
+
file_extensions,
|
|
322
|
+
all_ignore_patterns,
|
|
323
|
+
gitignore_parser,
|
|
324
|
+
max_depth,
|
|
325
|
+
0,
|
|
326
|
+
calculate_hashes,
|
|
327
|
+
progress_callback,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _build_node(
|
|
332
|
+
self,
|
|
333
|
+
path: Path,
|
|
334
|
+
root_path: Path,
|
|
335
|
+
file_extensions: Optional[List[str]],
|
|
336
|
+
ignore_patterns: Set[str],
|
|
337
|
+
gitignore_parser: Optional[GitignoreParser],
|
|
338
|
+
max_depth: Optional[int],
|
|
339
|
+
current_depth: int,
|
|
340
|
+
calculate_hashes: bool,
|
|
341
|
+
progress_callback: Optional[callable],
|
|
342
|
+
) -> Optional[TreeNode]:
|
|
343
|
+
"""Recursively build tree node.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
path: Current path to process
|
|
347
|
+
root_path: Root directory of tree
|
|
348
|
+
file_extensions: File extensions to include
|
|
349
|
+
ignore_patterns: Patterns to ignore
|
|
350
|
+
gitignore_parser: Gitignore parser instance
|
|
351
|
+
max_depth: Maximum depth to traverse
|
|
352
|
+
current_depth: Current depth in tree
|
|
353
|
+
calculate_hashes: Whether to calculate file hashes
|
|
354
|
+
progress_callback: Progress callback function
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
TreeNode or None if path should be ignored
|
|
358
|
+
"""
|
|
359
|
+
# Check if we should ignore this path
|
|
360
|
+
if self._should_ignore(path, ignore_patterns):
|
|
361
|
+
self.stats["files_ignored"] += 1
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
# Check gitignore
|
|
365
|
+
if gitignore_parser and gitignore_parser.should_ignore(path):
|
|
366
|
+
self.stats["files_ignored"] += 1
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# Check depth limit
|
|
370
|
+
if max_depth is not None and current_depth > max_depth:
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
# Create node
|
|
374
|
+
node = TreeNode(
|
|
375
|
+
name=path.name,
|
|
376
|
+
path=str(path),
|
|
377
|
+
type="directory" if path.is_dir() else "file",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Handle files
|
|
381
|
+
if path.is_file():
|
|
382
|
+
# Check file extension filter
|
|
383
|
+
if file_extensions:
|
|
384
|
+
if not any(path.suffix == ext for ext in file_extensions):
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
# Get file metadata
|
|
388
|
+
stat = path.stat()
|
|
389
|
+
metadata = FileMetadata(
|
|
390
|
+
path=str(path),
|
|
391
|
+
size=stat.st_size,
|
|
392
|
+
modified=stat.st_mtime,
|
|
393
|
+
language=self._detect_language(path),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Calculate hash if requested
|
|
397
|
+
if calculate_hashes:
|
|
398
|
+
metadata.hash = self._calculate_file_hash(path)
|
|
399
|
+
|
|
400
|
+
node.metadata = metadata
|
|
401
|
+
|
|
402
|
+
# Update statistics
|
|
403
|
+
self.stats["files_found"] += 1
|
|
404
|
+
self.stats["total_size"] += stat.st_size
|
|
405
|
+
if metadata.language:
|
|
406
|
+
self.stats["languages"].add(metadata.language)
|
|
407
|
+
|
|
408
|
+
# Progress callback
|
|
409
|
+
if progress_callback:
|
|
410
|
+
progress_callback(path, self.stats)
|
|
411
|
+
|
|
412
|
+
# Handle directories
|
|
413
|
+
elif path.is_dir():
|
|
414
|
+
self.stats["directories_scanned"] += 1
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
# Process children
|
|
418
|
+
children = []
|
|
419
|
+
for child_path in sorted(path.iterdir()):
|
|
420
|
+
child_node = self._build_node(
|
|
421
|
+
child_path,
|
|
422
|
+
root_path,
|
|
423
|
+
file_extensions,
|
|
424
|
+
ignore_patterns,
|
|
425
|
+
gitignore_parser,
|
|
426
|
+
max_depth,
|
|
427
|
+
current_depth + 1,
|
|
428
|
+
calculate_hashes,
|
|
429
|
+
progress_callback,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if child_node:
|
|
433
|
+
children.append(child_node)
|
|
434
|
+
|
|
435
|
+
node.children = children
|
|
436
|
+
|
|
437
|
+
except PermissionError:
|
|
438
|
+
self.logger.warning(f"Permission denied: {path}")
|
|
439
|
+
|
|
440
|
+
return node
|
|
441
|
+
|
|
442
|
+
def _should_ignore(self, path: Path, ignore_patterns: Set[str]) -> bool:
|
|
443
|
+
"""Check if path matches any ignore pattern.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
path: Path to check
|
|
447
|
+
ignore_patterns: Set of ignore patterns
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
True if path should be ignored
|
|
451
|
+
"""
|
|
452
|
+
name = path.name
|
|
453
|
+
|
|
454
|
+
return any(fnmatch.fnmatch(name, pattern) for pattern in ignore_patterns)
|
|
455
|
+
|
|
456
|
+
def _detect_language(self, path: Path) -> Optional[str]:
|
|
457
|
+
"""Detect programming language from file extension.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
path: File path
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Language name or None
|
|
464
|
+
"""
|
|
465
|
+
suffix = path.suffix.lower()
|
|
466
|
+
|
|
467
|
+
# Special case for Dockerfile
|
|
468
|
+
if path.name.lower() in ("dockerfile", "dockerfile.*"):
|
|
469
|
+
return "dockerfile"
|
|
470
|
+
|
|
471
|
+
return self.LANGUAGE_MAP.get(suffix)
|
|
472
|
+
|
|
473
|
+
def _calculate_file_hash(self, path: Path) -> str:
|
|
474
|
+
"""Calculate MD5 hash of file contents.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
path: File path
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
MD5 hash string
|
|
481
|
+
"""
|
|
482
|
+
hasher = hashlib.md5()
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
with open(path, "rb") as f:
|
|
486
|
+
# Read in chunks for large files
|
|
487
|
+
while chunk := f.read(8192):
|
|
488
|
+
hasher.update(chunk)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
self.logger.warning(f"Failed to hash {path}: {e}")
|
|
491
|
+
return ""
|
|
492
|
+
|
|
493
|
+
return hasher.hexdigest()
|
|
494
|
+
|
|
495
|
+
def save_tree(self, tree: TreeNode, output_path: Path):
|
|
496
|
+
"""Save tree to JSON file.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
tree: Root tree node
|
|
500
|
+
output_path: Output file path
|
|
501
|
+
"""
|
|
502
|
+
tree_dict = tree.to_dict()
|
|
503
|
+
tree_dict["stats"] = {
|
|
504
|
+
"directories_scanned": self.stats["directories_scanned"],
|
|
505
|
+
"files_found": self.stats["files_found"],
|
|
506
|
+
"files_ignored": self.stats["files_ignored"],
|
|
507
|
+
"total_size": self.stats["total_size"],
|
|
508
|
+
"languages": list(self.stats["languages"]),
|
|
509
|
+
"generated_at": datetime.utcnow().isoformat(),
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
with open(output_path, "w") as f:
|
|
513
|
+
json.dump(tree_dict, f, indent=2)
|
|
514
|
+
|
|
515
|
+
self.logger.info(f"Saved tree to {output_path}")
|
|
516
|
+
|
|
517
|
+
def load_tree(self, input_path: Path) -> TreeNode:
|
|
518
|
+
"""Load tree from JSON file.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
input_path: Input file path
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Root tree node
|
|
525
|
+
"""
|
|
526
|
+
with open(input_path) as f:
|
|
527
|
+
tree_dict = json.load(f)
|
|
528
|
+
|
|
529
|
+
# Remove stats if present
|
|
530
|
+
if "stats" in tree_dict:
|
|
531
|
+
del tree_dict["stats"]
|
|
532
|
+
|
|
533
|
+
return self._dict_to_node(tree_dict)
|
|
534
|
+
|
|
535
|
+
def _dict_to_node(self, node_dict: Dict[str, Any]) -> TreeNode:
|
|
536
|
+
"""Convert dictionary to TreeNode.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
node_dict: Node dictionary
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
TreeNode instance
|
|
543
|
+
"""
|
|
544
|
+
node = TreeNode(
|
|
545
|
+
name=node_dict["name"], path=node_dict["path"], type=node_dict["type"]
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if "metadata" in node_dict:
|
|
549
|
+
meta = node_dict["metadata"]
|
|
550
|
+
node.metadata = FileMetadata(
|
|
551
|
+
path=node.path,
|
|
552
|
+
size=meta.get("size", 0),
|
|
553
|
+
modified=meta.get("modified", 0),
|
|
554
|
+
hash=meta.get("hash"),
|
|
555
|
+
language=meta.get("language"),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if "children" in node_dict:
|
|
559
|
+
node.children = [
|
|
560
|
+
self._dict_to_node(child_dict) for child_dict in node_dict["children"]
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
return node
|
|
564
|
+
|
|
565
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
566
|
+
"""Get current statistics.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
Statistics dictionary
|
|
570
|
+
"""
|
|
571
|
+
return {**self.stats, "languages": list(self.stats["languages"])}
|
|
572
|
+
|
|
573
|
+
def compare_trees(
|
|
574
|
+
self, old_tree: TreeNode, new_tree: TreeNode
|
|
575
|
+
) -> Dict[str, List[str]]:
|
|
576
|
+
"""Compare two trees to find differences.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
old_tree: Previous tree
|
|
580
|
+
new_tree: Current tree
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Dictionary with added, removed, and modified files
|
|
584
|
+
"""
|
|
585
|
+
old_files = self._get_all_files(old_tree)
|
|
586
|
+
new_files = self._get_all_files(new_tree)
|
|
587
|
+
|
|
588
|
+
old_paths = set(old_files.keys())
|
|
589
|
+
new_paths = set(new_files.keys())
|
|
590
|
+
|
|
591
|
+
added = list(new_paths - old_paths)
|
|
592
|
+
removed = list(old_paths - new_paths)
|
|
593
|
+
|
|
594
|
+
# Check for modifications
|
|
595
|
+
modified = []
|
|
596
|
+
for path in old_paths & new_paths:
|
|
597
|
+
old_meta = old_files[path]
|
|
598
|
+
new_meta = new_files[path]
|
|
599
|
+
|
|
600
|
+
# Compare modification times or hashes
|
|
601
|
+
if old_meta.hash and new_meta.hash:
|
|
602
|
+
if old_meta.hash != new_meta.hash:
|
|
603
|
+
modified.append(path)
|
|
604
|
+
elif old_meta.modified != new_meta.modified:
|
|
605
|
+
modified.append(path)
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
"added": sorted(added),
|
|
609
|
+
"removed": sorted(removed),
|
|
610
|
+
"modified": sorted(modified),
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
def _get_all_files(self, tree: TreeNode) -> Dict[str, FileMetadata]:
|
|
614
|
+
"""Get all files from tree.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
tree: Root tree node
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Dictionary mapping file paths to metadata
|
|
621
|
+
"""
|
|
622
|
+
files = {}
|
|
623
|
+
|
|
624
|
+
def traverse(node: TreeNode):
|
|
625
|
+
if node.type == "file" and node.metadata:
|
|
626
|
+
files[node.path] = node.metadata
|
|
627
|
+
elif node.children:
|
|
628
|
+
for child in node.children:
|
|
629
|
+
traverse(child)
|
|
630
|
+
|
|
631
|
+
traverse(tree)
|
|
632
|
+
return files
|