codemesh 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.
- codemesh/__init__.py +5 -0
- codemesh/__main__.py +8 -0
- codemesh/cli/__init__.py +3 -0
- codemesh/cli/init.py +208 -0
- codemesh/cli/install_cmd.py +208 -0
- codemesh/cli/main.py +469 -0
- codemesh/context/__init__.py +3 -0
- codemesh/context/builder.py +388 -0
- codemesh/db/__init__.py +3 -0
- codemesh/db/connection.py +66 -0
- codemesh/db/queries.py +696 -0
- codemesh/db/schema.py +125 -0
- codemesh/embedding/__init__.py +3 -0
- codemesh/extraction/__init__.py +7 -0
- codemesh/extraction/languages/__init__.py +95 -0
- codemesh/extraction/languages/c_family.py +614 -0
- codemesh/extraction/languages/go.py +397 -0
- codemesh/extraction/languages/java.py +603 -0
- codemesh/extraction/languages/python.py +718 -0
- codemesh/extraction/languages/rust.py +435 -0
- codemesh/extraction/languages/swift.py +464 -0
- codemesh/extraction/languages/typescript.py +1222 -0
- codemesh/extraction/orchestrator.py +218 -0
- codemesh/graph/__init__.py +8 -0
- codemesh/graph/query_manager.py +117 -0
- codemesh/graph/traverser.py +107 -0
- codemesh/indexer.py +240 -0
- codemesh/mcp/__init__.py +3 -0
- codemesh/mcp/server.py +60 -0
- codemesh/mcp/tools.py +605 -0
- codemesh/querier.py +269 -0
- codemesh/resolution/__init__.py +7 -0
- codemesh/resolution/frameworks/__init__.py +15 -0
- codemesh/resolution/frameworks/django.py +30 -0
- codemesh/resolution/frameworks/fastapi.py +23 -0
- codemesh/resolution/import_resolver.py +69 -0
- codemesh/resolution/name_matcher.py +30 -0
- codemesh/resolution/resolver.py +268 -0
- codemesh/retrieval/__init__.py +7 -0
- codemesh/search/__init__.py +3 -0
- codemesh/sync/__init__.py +3 -0
- codemesh/sync/watcher.py +135 -0
- codemesh/types.py +148 -0
- codemesh/viz/__init__.py +0 -0
- codemesh/viz/graph_builder.py +162 -0
- codemesh/viz/server.py +122 -0
- codemesh/viz/templates/index.html +359 -0
- codemesh-0.1.1.dist-info/METADATA +337 -0
- codemesh-0.1.1.dist-info/RECORD +52 -0
- codemesh-0.1.1.dist-info/WHEEL +4 -0
- codemesh-0.1.1.dist-info/entry_points.txt +2 -0
- codemesh-0.1.1.dist-info/licenses/LICENSE +21 -0
codemesh/cli/main.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
"""CodeMesh CLI entry point."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="codemesh",
|
|
12
|
+
help="BM25 keyword search + graph walk for code intelligence",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def init(
|
|
19
|
+
path: str = typer.Argument(".", help="Path to the project to initialize"),
|
|
20
|
+
interactive: bool = typer.Option(
|
|
21
|
+
False, "-i", "--interactive", help="Interactive mode — prompts before overwriting files"
|
|
22
|
+
),
|
|
23
|
+
index_project: bool = typer.Option(
|
|
24
|
+
False, "--index", help="Also index the project after initialization"
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize CodeMesh in a project.
|
|
28
|
+
|
|
29
|
+
Creates .codemesh/ directory and writes agent instruction files
|
|
30
|
+
(CLAUDE.md, .cursor/rules/codemesh.mdc, AGENTS.md).
|
|
31
|
+
"""
|
|
32
|
+
from codemesh.cli.init import init_project
|
|
33
|
+
|
|
34
|
+
root = Path(path).resolve()
|
|
35
|
+
if not root.exists():
|
|
36
|
+
typer.echo(f"Error: {root} does not exist", err=True)
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
created = init_project(root, interactive=interactive)
|
|
40
|
+
typer.echo(f"CodeMesh initialized in {root}")
|
|
41
|
+
for key, val in created.items():
|
|
42
|
+
typer.echo(f" {key}: {val}")
|
|
43
|
+
|
|
44
|
+
if index_project:
|
|
45
|
+
from codemesh.indexer import index_project as do_index
|
|
46
|
+
|
|
47
|
+
typer.echo(f"\nIndexing {root}...")
|
|
48
|
+
stats = do_index(root, quiet=True)
|
|
49
|
+
typer.echo(
|
|
50
|
+
f"Done! {stats['nodes']} nodes, {stats['edges']} edges "
|
|
51
|
+
f"indexed in {stats.get('time_seconds', 0):.1f}s."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def install(
|
|
57
|
+
target: str = typer.Option(
|
|
58
|
+
"auto",
|
|
59
|
+
"--target",
|
|
60
|
+
"-t",
|
|
61
|
+
help="Agent(s) to configure: auto, all, claude, cursor, codex, or comma-separated list",
|
|
62
|
+
),
|
|
63
|
+
global_config: bool = typer.Option(
|
|
64
|
+
True, "--global/--local", help="Write global config (default) or project-local"
|
|
65
|
+
),
|
|
66
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Non-interactive mode"),
|
|
67
|
+
path: str = typer.Option(".", "--path", "-p", help="Project path for local config"),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Install CodeMesh MCP server configuration for AI coding agents.
|
|
70
|
+
|
|
71
|
+
Auto-detects installed agents and writes MCP server config + permissions.
|
|
72
|
+
Supports Claude Code, Cursor, and Codex CLI.
|
|
73
|
+
"""
|
|
74
|
+
from codemesh.cli.install_cmd import (
|
|
75
|
+
detect_agents,
|
|
76
|
+
install_claude,
|
|
77
|
+
install_codex,
|
|
78
|
+
install_cursor,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
root = Path(path).resolve()
|
|
82
|
+
targets = target.lower().split(",") if target not in ("auto", "all") else [target]
|
|
83
|
+
|
|
84
|
+
if "auto" in targets:
|
|
85
|
+
detected = detect_agents()
|
|
86
|
+
if not detected:
|
|
87
|
+
typer.echo("No AI coding agents detected. Use --target to specify manually.")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
targets = detected
|
|
90
|
+
if not yes:
|
|
91
|
+
typer.echo(f"Detected agents: {', '.join(targets)}")
|
|
92
|
+
typer.confirm("Configure these agents?", abort=True)
|
|
93
|
+
|
|
94
|
+
if "all" in targets:
|
|
95
|
+
targets = ["claude", "cursor", "codex"]
|
|
96
|
+
|
|
97
|
+
results = {}
|
|
98
|
+
for agent in targets:
|
|
99
|
+
agent = agent.strip()
|
|
100
|
+
if agent == "claude":
|
|
101
|
+
r = install_claude(root, global_config=global_config)
|
|
102
|
+
results["claude"] = r
|
|
103
|
+
elif agent == "cursor":
|
|
104
|
+
r = install_cursor(root)
|
|
105
|
+
results["cursor"] = r
|
|
106
|
+
elif agent == "codex":
|
|
107
|
+
r = install_codex(root)
|
|
108
|
+
results["codex"] = r
|
|
109
|
+
else:
|
|
110
|
+
typer.echo(f"Unknown agent: {agent}", err=True)
|
|
111
|
+
|
|
112
|
+
typer.echo("CodeMesh MCP server configured:")
|
|
113
|
+
for agent, r in results.items():
|
|
114
|
+
for key, val in r.items():
|
|
115
|
+
if val:
|
|
116
|
+
typer.echo(f" {agent}/{key}: {val}")
|
|
117
|
+
|
|
118
|
+
# Also init the project if not already
|
|
119
|
+
codemesh_dir = root / ".codemesh"
|
|
120
|
+
if not codemesh_dir.exists():
|
|
121
|
+
from codemesh.cli.init import init_project
|
|
122
|
+
|
|
123
|
+
init_project(root)
|
|
124
|
+
typer.echo(f"\nInitialized .codemesh/ in {root}")
|
|
125
|
+
|
|
126
|
+
typer.echo("\nRestart your agent(s) for the MCP server to load.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def index(
|
|
131
|
+
path: str = typer.Argument(".", help="Path to the codebase to index"),
|
|
132
|
+
workers: int | None = typer.Option(None, "--workers", "-w", help="Number of parallel workers"),
|
|
133
|
+
force: bool = typer.Option(
|
|
134
|
+
False, "--force", "-f", help="Force re-index even if already indexed"
|
|
135
|
+
),
|
|
136
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Index a codebase for BM25 search."""
|
|
139
|
+
from codemesh.indexer import index_project
|
|
140
|
+
|
|
141
|
+
root = Path(path).resolve()
|
|
142
|
+
if not root.exists():
|
|
143
|
+
typer.echo(f"Error: {root} does not exist", err=True)
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
typer.echo(f"Indexing {root}...")
|
|
147
|
+
stats = index_project(root, max_workers=workers, quiet=quiet)
|
|
148
|
+
if quiet:
|
|
149
|
+
typer.echo(
|
|
150
|
+
f"Done! {stats['nodes']} nodes, {stats['edges']} edges "
|
|
151
|
+
f"indexed in {stats.get('time_seconds', 0):.1f}s."
|
|
152
|
+
)
|
|
153
|
+
# When not quiet, the progress bar already shows completion
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def sync(
|
|
158
|
+
path: str = typer.Argument(".", help="Path to watch for changes"),
|
|
159
|
+
debounce: float = typer.Option(1.0, "--debounce", "-d", help="Debounce delay in seconds"),
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Watch for file changes and auto-sync the index.
|
|
162
|
+
|
|
163
|
+
Uses native OS file events (FSEvents/inotify) with debounced auto-sync.
|
|
164
|
+
The graph stays current as you code.
|
|
165
|
+
"""
|
|
166
|
+
from codemesh.indexer import sync_project
|
|
167
|
+
|
|
168
|
+
root = Path(path).resolve()
|
|
169
|
+
typer.echo(f"Watching {root} for changes... (Ctrl+C to stop)")
|
|
170
|
+
sync_project(root, debounce_delay=debounce)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def query(
|
|
175
|
+
q: str = typer.Argument(..., help="Query string"),
|
|
176
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
177
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Max results"),
|
|
178
|
+
fmt: str = typer.Option(
|
|
179
|
+
"xml", "--format", "-f", help="Output format: xml, markdown, structured, or json"
|
|
180
|
+
),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Query the indexed codebase."""
|
|
183
|
+
from codemesh.querier import query_codebase
|
|
184
|
+
|
|
185
|
+
root = Path(path).resolve()
|
|
186
|
+
result = query_codebase(root, q, limit=limit, fmt=fmt)
|
|
187
|
+
typer.echo(result)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command()
|
|
191
|
+
def callers(
|
|
192
|
+
symbol: str = typer.Argument(..., help="Symbol to find callers for"),
|
|
193
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Find all functions/methods that call a specific symbol."""
|
|
196
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
197
|
+
from codemesh.db.schema import init_db
|
|
198
|
+
from codemesh.graph.query_manager import QueryManager
|
|
199
|
+
|
|
200
|
+
root = Path(path).resolve()
|
|
201
|
+
init_db(get_db_path(root))
|
|
202
|
+
with get_connection(get_db_path(root)) as conn:
|
|
203
|
+
qm = QueryManager(conn)
|
|
204
|
+
callers = qm.find_callers(symbol)
|
|
205
|
+
if not callers:
|
|
206
|
+
typer.echo(f'No callers found for "{symbol}"')
|
|
207
|
+
return
|
|
208
|
+
typer.echo(f'Callers of "{symbol}" ({len(callers)}):')
|
|
209
|
+
typer.echo("")
|
|
210
|
+
for n in callers:
|
|
211
|
+
sig = f" {n.qualified_name} ({n.kind.value}) - {n.file_path}:{n.start_line}"
|
|
212
|
+
typer.echo(sig)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command()
|
|
216
|
+
def callees(
|
|
217
|
+
symbol: str = typer.Argument(..., help="Symbol to find callees for"),
|
|
218
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Find all functions/methods that a specific symbol calls."""
|
|
221
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
222
|
+
from codemesh.db.schema import init_db
|
|
223
|
+
from codemesh.graph.query_manager import QueryManager
|
|
224
|
+
|
|
225
|
+
root = Path(path).resolve()
|
|
226
|
+
init_db(get_db_path(root))
|
|
227
|
+
with get_connection(get_db_path(root)) as conn:
|
|
228
|
+
qm = QueryManager(conn)
|
|
229
|
+
callees = qm.find_callees(symbol)
|
|
230
|
+
if not callees:
|
|
231
|
+
typer.echo(f'No callees found for "{symbol}"')
|
|
232
|
+
return
|
|
233
|
+
typer.echo(f'Callees of "{symbol}" ({len(callees)}):')
|
|
234
|
+
typer.echo("")
|
|
235
|
+
for n in callees:
|
|
236
|
+
sig = f" {n.qualified_name} ({n.kind.value}) - {n.file_path}:{n.start_line}"
|
|
237
|
+
typer.echo(sig)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command()
|
|
241
|
+
def impact(
|
|
242
|
+
symbol: str = typer.Argument(..., help="Symbol to analyze impact for"),
|
|
243
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
244
|
+
depth: int = typer.Option(3, "--depth", "-d", help="Max traversal depth"),
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Analyze what code is affected by changing a symbol."""
|
|
247
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
248
|
+
from codemesh.db.queries import get_node
|
|
249
|
+
from codemesh.db.schema import init_db
|
|
250
|
+
from codemesh.graph.query_manager import QueryManager
|
|
251
|
+
|
|
252
|
+
root = Path(path).resolve()
|
|
253
|
+
init_db(get_db_path(root))
|
|
254
|
+
with get_connection(get_db_path(root)) as conn:
|
|
255
|
+
qm = QueryManager(conn)
|
|
256
|
+
subgraph = qm.what_breaks_if_changed(symbol)
|
|
257
|
+
affected = [n for nid in subgraph.nodes if (n := get_node(conn, nid)) is not None]
|
|
258
|
+
if not affected:
|
|
259
|
+
typer.echo(f'No dependents found for "{symbol}"')
|
|
260
|
+
return
|
|
261
|
+
typer.echo(f'Impact of changing "{symbol}" — {len(affected)} affected symbols:')
|
|
262
|
+
typer.echo("")
|
|
263
|
+
# Group by file
|
|
264
|
+
by_file: dict[str, list] = {}
|
|
265
|
+
for n in affected:
|
|
266
|
+
fp = str(n.file_path)
|
|
267
|
+
by_file.setdefault(fp, []).append(n)
|
|
268
|
+
for fp, nodes in sorted(by_file.items()):
|
|
269
|
+
typer.echo(fp)
|
|
270
|
+
for n in nodes:
|
|
271
|
+
typer.echo(f" {n.kind.value:10s} {n.name}:{n.start_line}")
|
|
272
|
+
typer.echo("")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command()
|
|
276
|
+
def context(
|
|
277
|
+
symbol: str = typer.Argument(..., help="Symbol to get context for"),
|
|
278
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
279
|
+
tokens: int = typer.Option(8000, "--tokens", "-t", help="Token budget"),
|
|
280
|
+
fmt: str = typer.Option(
|
|
281
|
+
"xml", "--format", "-f", help="Output format: xml, markdown, or structured"
|
|
282
|
+
),
|
|
283
|
+
max_nodes: int = typer.Option(50, "--max-nodes", "-n", help="Max nodes to include"),
|
|
284
|
+
max_code: int = typer.Option(10, "--max-code", "-c", help="Max code blocks"),
|
|
285
|
+
no_code: bool = typer.Option(False, "--no-code", help="Exclude code blocks"),
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Get context for a symbol (or general task).
|
|
288
|
+
|
|
289
|
+
Builds structured context with Entry Points, Related Symbols, and Code.
|
|
290
|
+
Similar to a context command for code intelligence.
|
|
291
|
+
"""
|
|
292
|
+
from codemesh.querier import get_context
|
|
293
|
+
|
|
294
|
+
root = Path(path).resolve()
|
|
295
|
+
|
|
296
|
+
# If format is structured, we need to handle it differently
|
|
297
|
+
if fmt == "structured":
|
|
298
|
+
from codemesh.context.builder import ContextBuilder, ContextFormat, ContextOptions
|
|
299
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
300
|
+
from codemesh.db.queries import get_node, search_nodes_fts
|
|
301
|
+
from codemesh.db.schema import init_db
|
|
302
|
+
from codemesh.graph.traverser import GraphTraverser
|
|
303
|
+
|
|
304
|
+
init_db(get_db_path(root))
|
|
305
|
+
with get_connection(get_db_path(root)) as conn:
|
|
306
|
+
# Search for the symbol
|
|
307
|
+
results = search_nodes_fts(conn, symbol, limit=max_nodes)
|
|
308
|
+
if not results:
|
|
309
|
+
typer.echo(f"No results for: {symbol}")
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Separate entry points (top results) from related (graph expansion)
|
|
313
|
+
traverser = GraphTraverser()
|
|
314
|
+
bm25_ids = {n.id for n, _ in results[:10]}
|
|
315
|
+
expanded = list(results[:10])
|
|
316
|
+
|
|
317
|
+
for node, _score in results[:5]:
|
|
318
|
+
subgraph = traverser.traverse(conn, [node.id], max_depth=1, max_nodes=20)
|
|
319
|
+
for nid, tr in subgraph.nodes.items():
|
|
320
|
+
if nid not in bm25_ids and len(expanded) < max_nodes:
|
|
321
|
+
bm25_ids.add(nid)
|
|
322
|
+
n = get_node(conn, nid)
|
|
323
|
+
if n is not None:
|
|
324
|
+
expanded.append((n, tr.score))
|
|
325
|
+
|
|
326
|
+
entry_points = expanded[:5]
|
|
327
|
+
related = expanded[5:max_nodes]
|
|
328
|
+
|
|
329
|
+
builder = ContextBuilder(conn, root)
|
|
330
|
+
context = builder.build(
|
|
331
|
+
expanded[:max_code] if not no_code else [],
|
|
332
|
+
symbol,
|
|
333
|
+
ContextOptions(
|
|
334
|
+
max_snippets=max_code if not no_code else 0,
|
|
335
|
+
max_tokens=tokens * 4,
|
|
336
|
+
format=ContextFormat.STRUCTURED,
|
|
337
|
+
),
|
|
338
|
+
entry_points=entry_points,
|
|
339
|
+
related=related,
|
|
340
|
+
)
|
|
341
|
+
typer.echo(context)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
result = get_context(root, symbol, max_tokens=tokens)
|
|
345
|
+
typer.echo(result)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@app.command()
|
|
349
|
+
def serve(
|
|
350
|
+
transport: str = typer.Option("stdio", "--transport", help="Transport: stdio or sse"),
|
|
351
|
+
port: int = typer.Option(3000, "--port", "-p", help="Port for SSE transport"),
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Start the MCP server."""
|
|
354
|
+
from codemesh.mcp.server import run_server
|
|
355
|
+
|
|
356
|
+
run_server(transport=transport, port=port)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@app.command()
|
|
360
|
+
def graph(
|
|
361
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
362
|
+
port: int = typer.Option(8765, "--port", help="Port for the visualization server"),
|
|
363
|
+
symbol: str | None = typer.Option(None, "--symbol", "-s", help="Focus on a specific symbol"),
|
|
364
|
+
kind: str | None = typer.Option(None, "--kind", "-k", help="Filter by node kind"),
|
|
365
|
+
depth: int = typer.Option(3, "--depth", "-d", help="BFS depth for symbol focus"),
|
|
366
|
+
export_json: str | None = typer.Option(None, "--json", help="Export graph as JSON to file"),
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Open interactive graph visualization in browser."""
|
|
369
|
+
import json as json_mod
|
|
370
|
+
from pathlib import Path as Path2
|
|
371
|
+
|
|
372
|
+
from codemesh.viz.graph_builder import build_graph
|
|
373
|
+
|
|
374
|
+
root = Path(path).resolve()
|
|
375
|
+
|
|
376
|
+
if export_json:
|
|
377
|
+
g = build_graph(
|
|
378
|
+
root, kind_filter=[kind] if kind else None, symbol_focus=symbol, depth=depth
|
|
379
|
+
)
|
|
380
|
+
Path2(export_json).write_text(json_mod.dumps(g, indent=2, default=str))
|
|
381
|
+
typer.echo(
|
|
382
|
+
f"Graph exported to {export_json} ({len(g['nodes'])} nodes, {len(g['edges'])} edges)"
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
from codemesh.viz.server import run_server
|
|
387
|
+
|
|
388
|
+
run_server(root=root, port=port, open_browser=True)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@app.command()
|
|
392
|
+
def files(
|
|
393
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Show project file structure from the index."""
|
|
396
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
397
|
+
from codemesh.db.schema import init_db
|
|
398
|
+
|
|
399
|
+
root = Path(path).resolve()
|
|
400
|
+
init_db(get_db_path(root))
|
|
401
|
+
with get_connection(get_db_path(root)) as conn:
|
|
402
|
+
# Get file nodes
|
|
403
|
+
file_rows = conn.execute(
|
|
404
|
+
"SELECT DISTINCT file_path, language FROM nodes WHERE kind = 'file' ORDER BY file_path"
|
|
405
|
+
).fetchall()
|
|
406
|
+
if not file_rows:
|
|
407
|
+
typer.echo("No files indexed. Run 'codemesh index' first.")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# Count nodes per file
|
|
411
|
+
counts = conn.execute(
|
|
412
|
+
"SELECT file_path, kind, COUNT(*) as cnt FROM nodes GROUP BY file_path, kind ORDER BY file_path"
|
|
413
|
+
).fetchall()
|
|
414
|
+
|
|
415
|
+
by_file: dict[str, dict[str, int]] = {}
|
|
416
|
+
for row in counts:
|
|
417
|
+
fp = row["file_path"]
|
|
418
|
+
by_file.setdefault(fp, {})[row["kind"]] = row["cnt"]
|
|
419
|
+
|
|
420
|
+
typer.echo(f"Indexed files: {len(file_rows)}")
|
|
421
|
+
typer.echo("")
|
|
422
|
+
for row in file_rows:
|
|
423
|
+
fp = row["file_path"]
|
|
424
|
+
lang = row["language"]
|
|
425
|
+
kinds = by_file.get(fp, {})
|
|
426
|
+
total = sum(kinds.values())
|
|
427
|
+
kind_str = ", ".join(f"{k}={v}" for k, v in sorted(kinds.items()) if k != "file")
|
|
428
|
+
typer.echo(f" {fp} ({lang}, {total} nodes: {kind_str})")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@app.command()
|
|
432
|
+
def status(
|
|
433
|
+
path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Show index status and statistics."""
|
|
436
|
+
from codemesh.db.connection import get_connection, get_db_path
|
|
437
|
+
from codemesh.db.schema import init_db
|
|
438
|
+
|
|
439
|
+
root = Path(path).resolve()
|
|
440
|
+
init_db(get_db_path(root))
|
|
441
|
+
with get_connection(get_db_path(root)) as conn:
|
|
442
|
+
node_count = conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
|
|
443
|
+
edge_count = conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
|
|
444
|
+
file_count = conn.execute(
|
|
445
|
+
"SELECT COUNT(DISTINCT file_path) FROM nodes WHERE kind = 'file'"
|
|
446
|
+
).fetchone()[0]
|
|
447
|
+
|
|
448
|
+
# Node kinds breakdown
|
|
449
|
+
kinds = conn.execute(
|
|
450
|
+
"SELECT kind, COUNT(*) as cnt FROM nodes GROUP BY kind ORDER BY cnt DESC"
|
|
451
|
+
).fetchall()
|
|
452
|
+
|
|
453
|
+
# Edge kinds breakdown
|
|
454
|
+
edge_kinds = conn.execute(
|
|
455
|
+
"SELECT kind, COUNT(*) as cnt FROM edges GROUP BY kind ORDER BY cnt DESC"
|
|
456
|
+
).fetchall()
|
|
457
|
+
|
|
458
|
+
typer.echo("CodeMesh Index Status")
|
|
459
|
+
typer.echo("=" * 40)
|
|
460
|
+
typer.echo(f" Files: {file_count}")
|
|
461
|
+
typer.echo(f" Nodes: {node_count}")
|
|
462
|
+
typer.echo(f" Edges: {edge_count}")
|
|
463
|
+
typer.echo("")
|
|
464
|
+
typer.echo(" Node kinds:")
|
|
465
|
+
for row in kinds:
|
|
466
|
+
typer.echo(f" {row['kind']:12s} {row['cnt']}")
|
|
467
|
+
typer.echo(" Edge kinds:")
|
|
468
|
+
for row in edge_kinds:
|
|
469
|
+
typer.echo(f" {row['kind']:12s} {row['cnt']}")
|