codebase-digest-ai 0.1.1__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.
@@ -0,0 +1,284 @@
1
+ """Main CLI application."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+ from rich.table import Table
9
+
10
+ from ..analyzer import CodebaseAnalyzer
11
+ from ..exporters import HTMLExporter, MarkdownExporter, JSONExporter, GraphExporter, ReadmeExporter
12
+
13
+ app = typer.Typer(
14
+ name="codebase-digest",
15
+ help="AI-native code intelligence engine for semantic codebase analysis",
16
+ no_args_is_help=True
17
+ )
18
+ console = Console()
19
+
20
+
21
+ @app.command()
22
+ def build(
23
+ path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)"),
24
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory (default: .digest)"),
25
+ format: str = typer.Option("all", "--format", "-f", help="Output format: html, markdown, json, or all"),
26
+ graph: bool = typer.Option(False, "--graph", help="Generate interactive call graph visualization"),
27
+ graph_depth: Optional[int] = typer.Option(None, "--graph-depth", help="Limit graph to N hops from entrypoints (default: no limit)")
28
+ ):
29
+ """Build complete codebase analysis and generate reports."""
30
+
31
+ # Set defaults
32
+ if path is None:
33
+ path = Path.cwd()
34
+ if output is None:
35
+ output = path / ".digest"
36
+
37
+ # Validate inputs
38
+ if not path.exists():
39
+ console.print(f"[red]Error: Path {path} does not exist[/red]")
40
+ raise typer.Exit(1)
41
+
42
+ if not path.is_dir():
43
+ console.print(f"[red]Error: Path {path} is not a directory[/red]")
44
+ raise typer.Exit(1)
45
+
46
+ # Create output directory
47
+ output.mkdir(exist_ok=True)
48
+
49
+ console.print(f"[blue]Analyzing codebase at:[/blue] {path}")
50
+ console.print(f"[blue]Output directory:[/blue] {output}")
51
+
52
+ # Analyze codebase
53
+ with Progress(
54
+ SpinnerColumn(),
55
+ TextColumn("[progress.description]{task.description}"),
56
+ console=console
57
+ ) as progress:
58
+
59
+ task = progress.add_task("Analyzing codebase...", total=None)
60
+ analyzer = CodebaseAnalyzer(path)
61
+ analysis = analyzer.analyze()
62
+ progress.update(task, description="Analysis complete!")
63
+
64
+ # Generate reports
65
+ formats_to_generate = []
66
+ if format == "all":
67
+ formats_to_generate = ["html", "markdown", "json"]
68
+ else:
69
+ formats_to_generate = [format]
70
+
71
+ for fmt in formats_to_generate:
72
+ if fmt == "html":
73
+ html_exporter = HTMLExporter(analysis)
74
+ html_exporter.export(output / "report.html")
75
+ console.print(f"[green]✓[/green] Generated HTML report: {output / 'report.html'}")
76
+
77
+ elif fmt == "markdown":
78
+ md_exporter = MarkdownExporter(analysis)
79
+ md_exporter.export(output / "architecture.md")
80
+ console.print(f"[green]✓[/green] Generated Markdown report: {output / 'architecture.md'}")
81
+
82
+ elif fmt == "json":
83
+ json_exporter = JSONExporter(analysis)
84
+ json_exporter.export(output / "entities.json")
85
+ console.print(f"[green]✓[/green] Generated JSON data: {output / 'entities.json'}")
86
+
87
+ else:
88
+ console.print(f"[red]Error: Unknown format '{fmt}'[/red]")
89
+ raise typer.Exit(1)
90
+
91
+ # Generate additional files
92
+ _generate_flows_md(analysis, output / "flows.md")
93
+ _generate_ai_context_md(analysis, output / "ai-context.md")
94
+
95
+ # Generate project README.md
96
+ readme_exporter = ReadmeExporter(analysis)
97
+ readme_exporter.export(output / "README.md")
98
+ console.print(f"[green]✓[/green] Generated project README: {output / 'README.md'}")
99
+
100
+ # Always generate interactive call graph
101
+ try:
102
+ graph_exporter = GraphExporter(analysis, max_depth=graph_depth)
103
+ graph_exporter.export(output / "callgraph.html")
104
+ console.print(f"[green]✓[/green] Generated interactive call graph: {output / 'callgraph.html'}")
105
+
106
+ # Show graph statistics
107
+ stats = graph_exporter.get_graph_stats()
108
+ console.print(f"[blue]Graph Stats:[/blue] {stats['nodes']} nodes, {stats['edges']} edges, {stats['components']} components")
109
+
110
+ except ImportError:
111
+ console.print(f"[red]Error: pyvis not installed. Install with: pip install pyvis[/red]")
112
+ except Exception as e:
113
+ console.print(f"[red]Error generating call graph: {e}[/red]")
114
+
115
+ # Generate additional call graph if explicitly requested (for backward compatibility)
116
+ if graph:
117
+ console.print("[blue]Note: Call graph is now generated by default[/blue]")
118
+
119
+ total_files = len(formats_to_generate) + 4 # +4 for flows.md, ai-context.md, README.md, callgraph.html
120
+ console.print(f"\n[green]Analysis complete![/green] Generated {total_files} files in {output}")
121
+
122
+
123
+ @app.command()
124
+ def stats(
125
+ path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)")
126
+ ):
127
+ """Show quick statistics about the codebase."""
128
+
129
+ if path is None:
130
+ path = Path.cwd()
131
+
132
+ if not path.exists() or not path.is_dir():
133
+ console.print(f"[red]Error: Invalid directory {path}[/red]")
134
+ raise typer.Exit(1)
135
+
136
+ console.print(f"[blue]Analyzing:[/blue] {path}")
137
+
138
+ with Progress(
139
+ SpinnerColumn(),
140
+ TextColumn("[progress.description]{task.description}"),
141
+ console=console
142
+ ) as progress:
143
+
144
+ task = progress.add_task("Gathering statistics...", total=None)
145
+ analyzer = CodebaseAnalyzer(path)
146
+ analysis = analyzer.analyze()
147
+ progress.update(task, description="Complete!")
148
+
149
+ # Create statistics table
150
+ table = Table(title=f"Codebase Statistics - {path.name}")
151
+ table.add_column("Metric", style="cyan")
152
+ table.add_column("Value", style="green")
153
+
154
+ table.add_row("Total Files", str(analysis.total_files))
155
+ table.add_row("Lines of Code", f"{analysis.total_lines:,}")
156
+ table.add_row("Languages", ", ".join(sorted(analysis.languages)))
157
+ table.add_row("Functions", str(len([s for s in analysis.symbols if s.type == 'function'])))
158
+ table.add_row("Classes", str(len([s for s in analysis.symbols if s.type == 'class'])))
159
+ table.add_row("Domain Entities", str(len(analysis.domain_entities)))
160
+ table.add_row("Execution Flows", str(len(analysis.execution_flows)))
161
+ table.add_row("Complexity Score", f"{analysis.complexity_score:.1f}")
162
+
163
+ console.print(table)
164
+
165
+
166
+ @app.command()
167
+ def query(
168
+ search_term: str = typer.Argument(..., help="Search term or pattern"),
169
+ path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)")
170
+ ):
171
+ """Search for patterns in the codebase analysis."""
172
+
173
+ if path is None:
174
+ path = Path.cwd()
175
+
176
+ if not path.exists() or not path.is_dir():
177
+ console.print(f"[red]Error: Invalid directory {path}[/red]")
178
+ raise typer.Exit(1)
179
+
180
+ console.print(f"[blue]Searching for:[/blue] '{search_term}' in {path}")
181
+
182
+ with Progress(
183
+ SpinnerColumn(),
184
+ TextColumn("[progress.description]{task.description}"),
185
+ console=console
186
+ ) as progress:
187
+
188
+ task = progress.add_task("Analyzing and searching...", total=None)
189
+ analyzer = CodebaseAnalyzer(path)
190
+ analysis = analyzer.analyze()
191
+ progress.update(task, description="Searching...")
192
+
193
+ results = _search_analysis(analysis, search_term.lower())
194
+ progress.update(task, description="Complete!")
195
+
196
+ if not results:
197
+ console.print(f"[yellow]No results found for '{search_term}'[/yellow]")
198
+ return
199
+
200
+ console.print(f"\n[green]Found {len(results)} results:[/green]")
201
+
202
+ for result in results:
203
+ console.print(f"[cyan]•[/cyan] {result}")
204
+
205
+
206
+ def _generate_flows_md(analysis, output_path: Path):
207
+ """Generate flows.md file."""
208
+ content = "# Execution Flows\n\n"
209
+
210
+ if not analysis.execution_flows:
211
+ content += "No execution flows detected.\n"
212
+ else:
213
+ for flow in analysis.execution_flows:
214
+ content += f"## {flow.name}\n\n"
215
+ content += f"{flow.description}\n\n"
216
+ content += f"**Entry Point:** `{flow.entry_point}`\n\n"
217
+ content += "**Flow Steps:**\n"
218
+ for i, step in enumerate(flow.steps, 1):
219
+ content += f"{i}. `{step}`\n"
220
+ content += "\n"
221
+
222
+ if flow.files_involved:
223
+ content += "**Files Involved:**\n"
224
+ for file_path in sorted(flow.files_involved):
225
+ rel_path = file_path.relative_to(analysis.root_path) if file_path.is_absolute() else file_path
226
+ content += f"- {rel_path}\n"
227
+ content += "\n"
228
+
229
+ output_path.write_text(content, encoding='utf-8')
230
+
231
+
232
+ def _generate_ai_context_md(analysis, output_path: Path):
233
+ """Generate AI-optimized context file."""
234
+ content = f"""# AI Context - {analysis.root_path.name}
235
+
236
+ ## System Overview
237
+ This codebase contains {analysis.total_files} files with {analysis.total_lines:,} lines of code across {len(analysis.languages)} languages.
238
+
239
+ ## Key Components
240
+ """
241
+
242
+ # Add top symbols
243
+ for symbol in analysis.symbols[:20]:
244
+ rel_path = symbol.file_path.relative_to(analysis.root_path)
245
+ content += f"- `{symbol.name}` ({symbol.type}) - {rel_path}:{symbol.line_number}\n"
246
+
247
+ content += "\n## Domain Entities\n"
248
+ for entity in analysis.domain_entities:
249
+ rel_path = entity.file_path.relative_to(analysis.root_path)
250
+ content += f"- `{entity.name}` - {rel_path}\n"
251
+
252
+ content += "\n## Execution Flows\n"
253
+ for flow in analysis.execution_flows:
254
+ content += f"- {flow.name}: {' → '.join(flow.steps[:3])}{'...' if len(flow.steps) > 3 else ''}\n"
255
+
256
+ output_path.write_text(content, encoding='utf-8')
257
+
258
+
259
+ def _search_analysis(analysis, search_term: str):
260
+ """Search through analysis results."""
261
+ results = []
262
+
263
+ # Search symbols
264
+ for symbol in analysis.symbols:
265
+ if search_term in symbol.name.lower():
266
+ rel_path = symbol.file_path.relative_to(analysis.root_path)
267
+ results.append(f"Symbol: {symbol.name} ({symbol.type}) in {rel_path}:{symbol.line_number}")
268
+
269
+ # Search domain entities
270
+ for entity in analysis.domain_entities:
271
+ if search_term in entity.name.lower():
272
+ rel_path = entity.file_path.relative_to(analysis.root_path)
273
+ results.append(f"Entity: {entity.name} in {rel_path}")
274
+
275
+ # Search execution flows
276
+ for flow in analysis.execution_flows:
277
+ if search_term in flow.name.lower() or any(search_term in step.lower() for step in flow.steps):
278
+ results.append(f"Flow: {flow.name} - {flow.description}")
279
+
280
+ return results
281
+
282
+
283
+ if __name__ == "__main__":
284
+ app()
@@ -0,0 +1,9 @@
1
+ """Export modules for generating reports and documentation."""
2
+
3
+ from .html_exporter import HTMLExporter
4
+ from .markdown_exporter import MarkdownExporter
5
+ from .json_exporter import JSONExporter
6
+ from .graph_exporter import GraphExporter
7
+ from .readme_exporter import ReadmeExporter
8
+
9
+ __all__ = ["HTMLExporter", "MarkdownExporter", "JSONExporter", "GraphExporter", "ReadmeExporter"]