workspace-graph 0.1.0__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.
- codegraph/__init__.py +1 -0
- codegraph/__main__.py +3 -0
- codegraph/builder.py +300 -0
- codegraph/cli.py +208 -0
- codegraph/commands/__init__.py +0 -0
- codegraph/commands/build.py +13 -0
- codegraph/commands/callees.py +22 -0
- codegraph/commands/callers.py +22 -0
- codegraph/commands/clean.py +16 -0
- codegraph/commands/context.py +51 -0
- codegraph/commands/cross_service.py +41 -0
- codegraph/commands/impact.py +23 -0
- codegraph/commands/opencode_plugin.py +38 -0
- codegraph/commands/orphans.py +33 -0
- codegraph/commands/query_cmd.py +25 -0
- codegraph/commands/routes.py +42 -0
- codegraph/commands/status.py +77 -0
- codegraph/commands/trace.py +19 -0
- codegraph/config.py +81 -0
- codegraph/cross_service.py +114 -0
- codegraph/discover.py +167 -0
- codegraph/graph/__init__.py +0 -0
- codegraph/graph/serialize.py +141 -0
- codegraph/graph/types.py +66 -0
- codegraph/plugin.py +61 -0
- codegraph/query.py +435 -0
- codegraph/server.py +299 -0
- workspace_graph-0.1.0.dist-info/METADATA +250 -0
- workspace_graph-0.1.0.dist-info/RECORD +31 -0
- workspace_graph-0.1.0.dist-info/WHEEL +4 -0
- workspace_graph-0.1.0.dist-info/entry_points.txt +2 -0
codegraph/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
codegraph/__main__.py
ADDED
codegraph/builder.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from codegraph.config import load_config
|
|
11
|
+
from codegraph.cross_service import detect_cross_service_edges
|
|
12
|
+
from codegraph.discover import BUILT_LANGUAGES, resolve_entries
|
|
13
|
+
from codegraph.graph.serialize import write_graph, write_manifest
|
|
14
|
+
from codegraph.graph.types import UnifiedGraph, WorkspaceEntry, make_unified_graph
|
|
15
|
+
from codegraph.plugin import run_plugins
|
|
16
|
+
|
|
17
|
+
TOOL_BY_LANGUAGE: dict[str, str] = {
|
|
18
|
+
"go": "gograph",
|
|
19
|
+
"python": "pygraph",
|
|
20
|
+
"typescript": "tsgraph",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
GRAPH_FILE_BY_LANGUAGE: dict[str, str] = {
|
|
24
|
+
"go": ".gograph/graph.json",
|
|
25
|
+
"python": ".pygraph/graph.json",
|
|
26
|
+
"typescript": ".tsgraph/graph.json",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_cmd(tool: str, entry_path: Path) -> list[str]:
|
|
31
|
+
if tool in ("gograph", "tsgraph"):
|
|
32
|
+
return [tool, "build", str(entry_path)]
|
|
33
|
+
return [tool, "build", "--root", str(entry_path)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _run_tool_build(entry: WorkspaceEntry, root_path: Path) -> WorkspaceEntry:
|
|
37
|
+
tool = TOOL_BY_LANGUAGE.get(entry.language)
|
|
38
|
+
if tool is None:
|
|
39
|
+
entry.build_status = "unsupported"
|
|
40
|
+
return entry
|
|
41
|
+
|
|
42
|
+
entry_path = root_path / entry.path
|
|
43
|
+
if not entry_path.is_dir():
|
|
44
|
+
entry.build_status = "failed"
|
|
45
|
+
return entry
|
|
46
|
+
|
|
47
|
+
start = time.monotonic()
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
_build_cmd(tool, entry_path),
|
|
51
|
+
capture_output=True, text=True, timeout=120,
|
|
52
|
+
cwd=str(entry_path),
|
|
53
|
+
)
|
|
54
|
+
elapsed = int((time.monotonic() - start) * 1000)
|
|
55
|
+
entry.build_duration_ms = elapsed
|
|
56
|
+
|
|
57
|
+
if result.returncode != 0:
|
|
58
|
+
entry.build_status = "failed"
|
|
59
|
+
return entry
|
|
60
|
+
|
|
61
|
+
entry.build_status = "ok"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
version_result = subprocess.run(
|
|
65
|
+
[tool, "--version"],
|
|
66
|
+
capture_output=True, text=True, timeout=10,
|
|
67
|
+
)
|
|
68
|
+
ver = version_result.stdout.strip() if version_result.returncode == 0 else ""
|
|
69
|
+
entry.tool_version = ver
|
|
70
|
+
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
|
|
71
|
+
entry.tool_version = ""
|
|
72
|
+
|
|
73
|
+
except (OSError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
74
|
+
entry.build_status = "failed"
|
|
75
|
+
|
|
76
|
+
return entry
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _prefix_ids(items: list[dict[str, Any]], prefix: str, id_fields: set[str]) -> None:
|
|
80
|
+
for item in items:
|
|
81
|
+
for field in id_fields:
|
|
82
|
+
if field in item and isinstance(item[field], str):
|
|
83
|
+
item[field] = f"{prefix}::{item[field]}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
SYMBOL_ID_FIELDS = {"id"}
|
|
87
|
+
CALL_ID_FIELDS = {"caller_symbol_id"}
|
|
88
|
+
FILE_ID_FIELDS = {"id"}
|
|
89
|
+
PACKAGE_ID_FIELDS = {"id"}
|
|
90
|
+
TEST_EDGE_ID_FIELDS = {"test_func", "target"}
|
|
91
|
+
ERROR_ID_FIELDS = {"function_name"}
|
|
92
|
+
MUTATION_ID_FIELDS = {"function_name"}
|
|
93
|
+
ENV_ID_FIELDS = {"function_name"}
|
|
94
|
+
IMPLEMENTS_ID_FIELDS = {"interface", "concrete"}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _stamp_and_collect(
|
|
98
|
+
entry: WorkspaceEntry,
|
|
99
|
+
root_path: Path,
|
|
100
|
+
unified: UnifiedGraph,
|
|
101
|
+
) -> None:
|
|
102
|
+
graph_rel = GRAPH_FILE_BY_LANGUAGE.get(entry.language)
|
|
103
|
+
if graph_rel is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
graph_path = root_path / entry.path / graph_rel
|
|
107
|
+
if not graph_path.exists():
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
raw = graph_path.read_text()
|
|
112
|
+
data: dict[str, Any] = json.loads(raw)
|
|
113
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
entry_name = entry.name
|
|
117
|
+
prefix = entry_name
|
|
118
|
+
|
|
119
|
+
packages = data.get("packages", [])
|
|
120
|
+
_prefix_ids(packages, prefix, PACKAGE_ID_FIELDS)
|
|
121
|
+
for p in packages:
|
|
122
|
+
p["entry_name"] = entry_name
|
|
123
|
+
p["language"] = entry.language
|
|
124
|
+
p["type"] = entry.type
|
|
125
|
+
unified.packages.extend(packages)
|
|
126
|
+
|
|
127
|
+
files = data.get("files", [])
|
|
128
|
+
_prefix_ids(files, prefix, FILE_ID_FIELDS)
|
|
129
|
+
for f in files:
|
|
130
|
+
f["entry_name"] = entry_name
|
|
131
|
+
f["language"] = entry.language
|
|
132
|
+
f["type"] = entry.type
|
|
133
|
+
unified.files.extend(files)
|
|
134
|
+
|
|
135
|
+
symbols = data.get("symbols", [])
|
|
136
|
+
_prefix_ids(symbols, prefix, SYMBOL_ID_FIELDS)
|
|
137
|
+
for s in symbols:
|
|
138
|
+
s["entry_name"] = entry_name
|
|
139
|
+
s["language"] = entry.language
|
|
140
|
+
s["type"] = entry.type
|
|
141
|
+
unified.symbols.extend(symbols)
|
|
142
|
+
entry.symbol_count = len(symbols)
|
|
143
|
+
|
|
144
|
+
calls = data.get("calls", [])
|
|
145
|
+
_prefix_ids(calls, prefix, CALL_ID_FIELDS)
|
|
146
|
+
for c in calls:
|
|
147
|
+
c["entry_name"] = entry_name
|
|
148
|
+
c["language"] = entry.language
|
|
149
|
+
c["type"] = entry.type
|
|
150
|
+
unified.calls.extend(calls)
|
|
151
|
+
entry.call_count = len(calls)
|
|
152
|
+
|
|
153
|
+
imports = data.get("imports", [])
|
|
154
|
+
for im in imports:
|
|
155
|
+
im["entry_name"] = entry_name
|
|
156
|
+
im["language"] = entry.language
|
|
157
|
+
im["type"] = entry.type
|
|
158
|
+
unified.imports.extend(imports)
|
|
159
|
+
|
|
160
|
+
routes = data.get("routes", [])
|
|
161
|
+
for r in routes:
|
|
162
|
+
r["entry_name"] = entry_name
|
|
163
|
+
r["language"] = entry.language
|
|
164
|
+
r["type"] = entry.type
|
|
165
|
+
unified.routes.extend(routes)
|
|
166
|
+
entry.route_count = len(routes)
|
|
167
|
+
|
|
168
|
+
all_lists: list[tuple[list[dict[str, Any]] | None, str, set[str]]] = [
|
|
169
|
+
(data.get("env_reads"), "env_reads", ENV_ID_FIELDS),
|
|
170
|
+
(data.get("errors"), "errors", ERROR_ID_FIELDS),
|
|
171
|
+
(data.get("test_edges"), "test_edges", TEST_EDGE_ID_FIELDS),
|
|
172
|
+
(data.get("mutations"), "mutations", MUTATION_ID_FIELDS),
|
|
173
|
+
(data.get("implements"), "implements", IMPLEMENTS_ID_FIELDS),
|
|
174
|
+
(data.get("blueprints"), "blueprints", set()),
|
|
175
|
+
(data.get("blueprint_registrations"), "blueprint_registrations", set()),
|
|
176
|
+
(data.get("template_refs"), "template_refs", set()),
|
|
177
|
+
(data.get("extensions"), "extensions", set()),
|
|
178
|
+
(data.get("dependencies"), "dependencies", set()),
|
|
179
|
+
(data.get("http_calls"), "http_calls", set()),
|
|
180
|
+
]
|
|
181
|
+
for items, field_name, id_fields in all_lists:
|
|
182
|
+
if items:
|
|
183
|
+
_prefix_ids(items, prefix, id_fields)
|
|
184
|
+
for item in items:
|
|
185
|
+
item["entry_name"] = entry_name
|
|
186
|
+
item["language"] = entry.language
|
|
187
|
+
item["type"] = entry.type
|
|
188
|
+
getattr(unified, field_name).extend(items)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_single(entry: WorkspaceEntry, root_path: Path) -> None:
|
|
192
|
+
if entry.language not in BUILT_LANGUAGES:
|
|
193
|
+
entry.build_status = "unsupported"
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
_run_tool_build(entry, root_path)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_entry(entry: WorkspaceEntry, root_path: Path) -> WorkspaceEntry:
|
|
200
|
+
result = _run_tool_build(entry, root_path)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_one(entry: WorkspaceEntry, root_path: Path) -> WorkspaceEntry:
|
|
205
|
+
if entry.language not in BUILT_LANGUAGES:
|
|
206
|
+
entry.build_status = "unsupported"
|
|
207
|
+
return entry
|
|
208
|
+
start = time.monotonic()
|
|
209
|
+
print(f" [{entry.name}] building ({entry.language})...", flush=True)
|
|
210
|
+
entry = _run_tool_build(entry, root_path)
|
|
211
|
+
elapsed = time.monotonic() - start
|
|
212
|
+
if entry.build_status == "ok":
|
|
213
|
+
print(f" [{entry.name}] done ({elapsed:.1f}s)", flush=True)
|
|
214
|
+
elif entry.build_status == "failed":
|
|
215
|
+
print(f" [{entry.name}] FAILED ({elapsed:.1f}s)", flush=True)
|
|
216
|
+
else:
|
|
217
|
+
print(f" [{entry.name}] {entry.build_status}", flush=True)
|
|
218
|
+
return entry
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_all(
|
|
222
|
+
root: str,
|
|
223
|
+
entries: list[WorkspaceEntry] | None = None,
|
|
224
|
+
max_workers: int = 4,
|
|
225
|
+
) -> UnifiedGraph:
|
|
226
|
+
root_path = Path(root).resolve()
|
|
227
|
+
config = load_config(root)
|
|
228
|
+
|
|
229
|
+
if entries is None:
|
|
230
|
+
entries = resolve_entries(config, root)
|
|
231
|
+
|
|
232
|
+
unified = make_unified_graph(workspace_root=str(root_path))
|
|
233
|
+
|
|
234
|
+
buildable = [e for e in entries if e.language in BUILT_LANGUAGES]
|
|
235
|
+
skipped = [e for e in entries if e.language not in BUILT_LANGUAGES]
|
|
236
|
+
|
|
237
|
+
for e in skipped:
|
|
238
|
+
e.build_status = "unsupported"
|
|
239
|
+
if unified.manifest is not None:
|
|
240
|
+
unified.manifest.entries.append(e)
|
|
241
|
+
|
|
242
|
+
print(f"Building {len(buildable)} entries ({len(skipped)} skipped)...")
|
|
243
|
+
overall_start = time.monotonic()
|
|
244
|
+
|
|
245
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
246
|
+
futures = {
|
|
247
|
+
pool.submit(_build_one, entry, root_path): entry
|
|
248
|
+
for entry in buildable
|
|
249
|
+
}
|
|
250
|
+
for future in concurrent.futures.as_completed(futures):
|
|
251
|
+
entry = futures[future]
|
|
252
|
+
try:
|
|
253
|
+
result = future.result()
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
print(f" [{entry.name}] ERROR: {exc}", flush=True)
|
|
256
|
+
result = entry
|
|
257
|
+
result.build_status = "failed"
|
|
258
|
+
|
|
259
|
+
if result.build_status == "ok":
|
|
260
|
+
_stamp_and_collect(result, root_path, unified)
|
|
261
|
+
|
|
262
|
+
if unified.manifest is not None:
|
|
263
|
+
unified.manifest.entries.append(result)
|
|
264
|
+
|
|
265
|
+
total_time = time.monotonic() - overall_start
|
|
266
|
+
ok_count = sum(1 for e in buildable if e.build_status == "ok")
|
|
267
|
+
fail_count = sum(1 for e in buildable if e.build_status == "failed")
|
|
268
|
+
print(
|
|
269
|
+
f"Built {ok_count}/{len(buildable)} entries "
|
|
270
|
+
f"({fail_count} failed) in {total_time:.1f}s"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return unified
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def build_and_write(
|
|
277
|
+
root: str,
|
|
278
|
+
entry_name: str | None = None,
|
|
279
|
+
) -> Path:
|
|
280
|
+
root_path = Path(root).resolve()
|
|
281
|
+
config = load_config(root)
|
|
282
|
+
entries = resolve_entries(config, root)
|
|
283
|
+
|
|
284
|
+
if entry_name is not None:
|
|
285
|
+
entries = [e for e in entries if e.name == entry_name]
|
|
286
|
+
|
|
287
|
+
unified = build_all(root, entries)
|
|
288
|
+
|
|
289
|
+
unified.cross_service_edges = detect_cross_service_edges(unified)
|
|
290
|
+
run_plugins(unified, root)
|
|
291
|
+
|
|
292
|
+
out_dir = root_path / ".codegraph"
|
|
293
|
+
graph_path = out_dir / "workspace.graph.json"
|
|
294
|
+
write_graph(unified, graph_path)
|
|
295
|
+
|
|
296
|
+
if unified.manifest is not None:
|
|
297
|
+
manifest_path = out_dir / "manifest.json"
|
|
298
|
+
write_manifest(unified.manifest, manifest_path)
|
|
299
|
+
|
|
300
|
+
return graph_path
|
codegraph/cli.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from codegraph.commands.build import run as run_build
|
|
8
|
+
from codegraph.commands.callees import run as run_callees
|
|
9
|
+
from codegraph.commands.callers import run as run_callers
|
|
10
|
+
from codegraph.commands.clean import run as run_clean
|
|
11
|
+
from codegraph.commands.context import run as run_context
|
|
12
|
+
from codegraph.commands.cross_service import run as run_cross_service
|
|
13
|
+
from codegraph.commands.impact import run as run_impact
|
|
14
|
+
from codegraph.commands.opencode_plugin import run as run_opencode_plugin
|
|
15
|
+
from codegraph.commands.orphans import run as run_orphans
|
|
16
|
+
from codegraph.commands.query_cmd import run as run_query
|
|
17
|
+
from codegraph.commands.routes import run as run_routes
|
|
18
|
+
from codegraph.commands.status import run as run_status
|
|
19
|
+
from codegraph.commands.trace import run as run_trace
|
|
20
|
+
from codegraph.graph.serialize import read_graph
|
|
21
|
+
from codegraph.query import WorkspaceQuery
|
|
22
|
+
from codegraph.server import run_server
|
|
23
|
+
|
|
24
|
+
app = typer.Typer()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_nearest_graph(root: str) -> Path | None:
|
|
28
|
+
path = Path(root).resolve()
|
|
29
|
+
for parent in [path] + list(path.parents):
|
|
30
|
+
candidate = parent / ".codegraph" / "workspace.graph.json"
|
|
31
|
+
if candidate.exists():
|
|
32
|
+
return candidate
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_query(root: str) -> WorkspaceQuery:
|
|
37
|
+
graph_path = Path(root) / ".codegraph" / "workspace.graph.json"
|
|
38
|
+
if not graph_path.exists():
|
|
39
|
+
nearest = _find_nearest_graph(root)
|
|
40
|
+
if nearest:
|
|
41
|
+
typer.echo(
|
|
42
|
+
f"Error: no graph found at {graph_path}.\n"
|
|
43
|
+
f"Found one at {nearest} — did you mean "
|
|
44
|
+
f"'--root {nearest.parent.parent}'?"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
typer.echo(
|
|
48
|
+
f"Error: no graph found at {graph_path}.\n"
|
|
49
|
+
f"Run 'codegraph build' in your workspace root first."
|
|
50
|
+
)
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
graph = read_graph(graph_path)
|
|
53
|
+
return WorkspaceQuery(graph, root=root)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def status(
|
|
58
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Detect and display workspace entries"""
|
|
61
|
+
run_status(root)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def build(
|
|
66
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
67
|
+
entry: str | None = typer.Option(
|
|
68
|
+
None, "--entry", help="Build only a specific entry by name"
|
|
69
|
+
),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Build graph for all (or one) workspace entries"""
|
|
72
|
+
run_build(root, entry)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
def clean(
|
|
77
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Remove .codegraph/ output directory"""
|
|
80
|
+
run_clean(root)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def query(
|
|
85
|
+
pattern: str,
|
|
86
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Search symbols by pattern (regex or substring)"""
|
|
89
|
+
q = _load_query(root)
|
|
90
|
+
run_query(q, pattern)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def callers(
|
|
95
|
+
name: str,
|
|
96
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Show who calls the given symbol"""
|
|
99
|
+
q = _load_query(root)
|
|
100
|
+
run_callers(q, name)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def callees(
|
|
105
|
+
name: str,
|
|
106
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Show what the given symbol calls"""
|
|
109
|
+
q = _load_query(root)
|
|
110
|
+
run_callees(q, name)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def routes(
|
|
115
|
+
entry: str | None = typer.Option(
|
|
116
|
+
None, "--entry", help="Filter by entry name"
|
|
117
|
+
),
|
|
118
|
+
type: str | None = typer.Option(
|
|
119
|
+
None, "--type", help="Filter by entry type (service, frontend, etc.)"
|
|
120
|
+
),
|
|
121
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
122
|
+
) -> None:
|
|
123
|
+
"""List all HTTP routes across the workspace"""
|
|
124
|
+
q = _load_query(root)
|
|
125
|
+
run_routes(q, entry_filter=entry, type_filter=type)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command()
|
|
129
|
+
def impact(
|
|
130
|
+
name: str,
|
|
131
|
+
max_depth: int | None = typer.Option(
|
|
132
|
+
None, "--max-depth", "-d", help="Maximum depth for BFS traversal"
|
|
133
|
+
),
|
|
134
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Show downstream impact (BFS from symbol)"""
|
|
137
|
+
q = _load_query(root)
|
|
138
|
+
run_impact(q, name, max_depth=max_depth)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def orphans(
|
|
143
|
+
all: bool = typer.Option(
|
|
144
|
+
False, "--all", help="Include public uncalled symbols"
|
|
145
|
+
),
|
|
146
|
+
exclude_type: str | None = typer.Option(
|
|
147
|
+
None, "--exclude-type", help="Exclude entries of a given type"
|
|
148
|
+
),
|
|
149
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""List unreachable symbols (dead code)"""
|
|
152
|
+
q = _load_query(root)
|
|
153
|
+
run_orphans(q, include_public=all, exclude_type=exclude_type)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def context(
|
|
158
|
+
name: str,
|
|
159
|
+
source: bool = typer.Option(
|
|
160
|
+
False, "--source", help="Include full source code"
|
|
161
|
+
),
|
|
162
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Show symbol with callers, callees, tests"""
|
|
165
|
+
q = _load_query(root)
|
|
166
|
+
run_context(q, name, show_source=source)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command()
|
|
170
|
+
def trace(
|
|
171
|
+
message: str,
|
|
172
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Find error messages and trace their call paths"""
|
|
175
|
+
q = _load_query(root)
|
|
176
|
+
run_trace(q, message)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command(name="add-opencode-plugin")
|
|
180
|
+
def add_opencode_plugin(
|
|
181
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Create .opencode.json with codegraph MCP config + architect agent"""
|
|
184
|
+
q = _load_query(root)
|
|
185
|
+
run_opencode_plugin(q, root)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.command(name="cross-service")
|
|
189
|
+
def cross_service(
|
|
190
|
+
source_entry: str | None = typer.Option(
|
|
191
|
+
None, "--source-entry", "-s", help="Filter by source entry name"
|
|
192
|
+
),
|
|
193
|
+
target_entry: str | None = typer.Option(
|
|
194
|
+
None, "--target-entry", "-t", help="Filter by target entry name"
|
|
195
|
+
),
|
|
196
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Show cross-service HTTP call edges"""
|
|
199
|
+
q = _load_query(root)
|
|
200
|
+
run_cross_service(q, source_entry=source_entry, target_entry=target_entry)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command()
|
|
204
|
+
def mcp(
|
|
205
|
+
root: str = typer.Option(".", "--root", help="Workspace root directory"),
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Start MCP stdio server for AI agent integration"""
|
|
208
|
+
run_server(root)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codegraph.builder import build_and_write
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(root: str, entry_name: str | None = None) -> None:
|
|
7
|
+
try:
|
|
8
|
+
out_path = build_and_write(root, entry_name=entry_name)
|
|
9
|
+
print(f"Built {out_path}")
|
|
10
|
+
except FileNotFoundError as e:
|
|
11
|
+
print(f"Error: {e}")
|
|
12
|
+
except Exception as e:
|
|
13
|
+
print(f"Build failed: {e}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codegraph.query import WorkspaceQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(query: WorkspaceQuery, name: str) -> None:
|
|
7
|
+
results = query.get_callees(name)
|
|
8
|
+
if not results:
|
|
9
|
+
print(f"No callees found for '{name}'")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
print(f"Callees of '{name}':")
|
|
13
|
+
print()
|
|
14
|
+
header = f"{'Callee':<30} {'Entry':<16} {'File':<40} {'Line'}"
|
|
15
|
+
print(header)
|
|
16
|
+
print("-" * len(header))
|
|
17
|
+
for callee_sym, edge in results:
|
|
18
|
+
callee_name = callee_sym.get("name", "") if callee_sym else edge.get("callee_raw", "")
|
|
19
|
+
entry = edge.get("entry_name", "")
|
|
20
|
+
file_path = edge.get("file", "")
|
|
21
|
+
line = edge.get("line", 0)
|
|
22
|
+
print(f"{callee_name:<30} {entry:<16} {file_path:<40} {line}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codegraph.query import WorkspaceQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(query: WorkspaceQuery, name: str) -> None:
|
|
7
|
+
results = query.get_callers(name)
|
|
8
|
+
if not results:
|
|
9
|
+
print(f"No callers found for '{name}'")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
print(f"Callers of '{name}':")
|
|
13
|
+
print()
|
|
14
|
+
header = f"{'Caller':<30} {'Entry':<16} {'File':<40} {'Line'}"
|
|
15
|
+
print(header)
|
|
16
|
+
print("-" * len(header))
|
|
17
|
+
for caller_sym, edge in results:
|
|
18
|
+
caller_name = caller_sym.get("name", "")
|
|
19
|
+
entry = caller_sym.get("entry_name", "")
|
|
20
|
+
file_path = caller_sym.get("file", "")
|
|
21
|
+
line = edge.get("line", 0)
|
|
22
|
+
print(f"{caller_name:<30} {entry:<16} {file_path:<40} {line}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run(root: str) -> None:
|
|
8
|
+
root_path = Path(root).resolve()
|
|
9
|
+
out_dir = root_path / ".codegraph"
|
|
10
|
+
|
|
11
|
+
if not out_dir.exists():
|
|
12
|
+
print("Nothing to clean — .codegraph/ does not exist.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
shutil.rmtree(out_dir)
|
|
16
|
+
print(f"Removed {out_dir}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codegraph.query import WorkspaceQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(query: WorkspaceQuery, name: str, show_source: bool = False) -> None:
|
|
7
|
+
ctx = query.get_context(name, include_source=show_source)
|
|
8
|
+
symbol = ctx.get("symbol")
|
|
9
|
+
|
|
10
|
+
if not symbol:
|
|
11
|
+
print(f"Symbol '{name}' not found")
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
print(f"Symbol: {symbol.get('name', '')}")
|
|
15
|
+
print(f" Kind: {symbol.get('kind', '')}")
|
|
16
|
+
print(f" Entry: {symbol.get('entry_name', '')}")
|
|
17
|
+
print(f" Language: {symbol.get('language', '')}")
|
|
18
|
+
print(f" Type: {symbol.get('type', '')}")
|
|
19
|
+
print(f" File: {symbol.get('file', '')}")
|
|
20
|
+
print(f" Line: {symbol.get('line', 0)}")
|
|
21
|
+
print(f" Exported: {symbol.get('is_exported', False)}")
|
|
22
|
+
print()
|
|
23
|
+
|
|
24
|
+
callers = ctx.get("callers", [])
|
|
25
|
+
print(f"Callers ({len(callers)}):")
|
|
26
|
+
if callers:
|
|
27
|
+
for c in callers[:10]:
|
|
28
|
+
entry = c.get("entry_name", "")
|
|
29
|
+
print(f" {c.get('caller', '')} — {c.get('file', '')}:{c.get('line', '')} [{entry}]")
|
|
30
|
+
print()
|
|
31
|
+
|
|
32
|
+
callees = ctx.get("callees", [])
|
|
33
|
+
print(f"Callees ({len(callees)}):")
|
|
34
|
+
if callees:
|
|
35
|
+
for c in callees[:10]:
|
|
36
|
+
entry = c.get("entry_name", "")
|
|
37
|
+
print(f" {c.get('callee', '')} — {c.get('file', '')}:{c.get('line', '')} [{entry}]")
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
tests = ctx.get("tests", [])
|
|
41
|
+
print(f"Tests ({len(tests)}):")
|
|
42
|
+
if tests:
|
|
43
|
+
for t in tests[:10]:
|
|
44
|
+
entry = t.get("entry_name", "")
|
|
45
|
+
print(f" {t.get('test_func', '')} — {t.get('file', '')}:{t.get('line', '')} [{entry}]")
|
|
46
|
+
|
|
47
|
+
source = ctx.get("source")
|
|
48
|
+
if source:
|
|
49
|
+
print()
|
|
50
|
+
print("Source:")
|
|
51
|
+
print(source)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codegraph.query import WorkspaceQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(
|
|
7
|
+
query: WorkspaceQuery,
|
|
8
|
+
source_entry: str | None = None,
|
|
9
|
+
target_entry: str | None = None,
|
|
10
|
+
) -> None:
|
|
11
|
+
edges = query.get_cross_service_edges(
|
|
12
|
+
source_entry=source_entry, target_entry=target_entry,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if not edges:
|
|
16
|
+
msg = "No cross-service edges found"
|
|
17
|
+
if source_entry:
|
|
18
|
+
msg += f" from '{source_entry}'"
|
|
19
|
+
if target_entry:
|
|
20
|
+
msg += f" to '{target_entry}'"
|
|
21
|
+
print(msg)
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
print(f"Cross-service edges ({len(edges)} total):")
|
|
25
|
+
print()
|
|
26
|
+
hdr = (
|
|
27
|
+
f"{'Source Entry':<16} {'Source Symbol':<24} "
|
|
28
|
+
f"{'Method':<8} {'Target Entry':<16} {'Target Route':<40} "
|
|
29
|
+
f"{'Confidence':<10}"
|
|
30
|
+
)
|
|
31
|
+
print(hdr)
|
|
32
|
+
print("-" * len(hdr))
|
|
33
|
+
for e in edges:
|
|
34
|
+
print(
|
|
35
|
+
f"{e.get('source_entry', ''):<16} "
|
|
36
|
+
f"{e.get('source_symbol', ''):<24} "
|
|
37
|
+
f"{e.get('method', ''):<8} "
|
|
38
|
+
f"{e.get('target_entry', ''):<16} "
|
|
39
|
+
f"{e.get('target_route_path', ''):<40} "
|
|
40
|
+
f"{e.get('confidence', ''):<10}"
|
|
41
|
+
)
|