codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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.
Files changed (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,774 @@
1
+ import asyncio
2
+ import json
3
+ import uuid
4
+ import urllib.parse
5
+ from pathlib import Path
6
+ import time
7
+ import os
8
+ from typing import Optional, List, Dict, Any
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich.progress import (
12
+ Progress,
13
+ SpinnerColumn,
14
+ TextColumn,
15
+ BarColumn,
16
+ TaskProgressColumn,
17
+ TimeRemainingColumn,
18
+ MofNCompleteColumn,
19
+ )
20
+
21
+ from ..core import get_database_manager
22
+ from ..core.jobs import JobManager
23
+ from ..tools.code_finder import CodeFinder
24
+ from ..tools.graph_builder import GraphBuilder
25
+ from ..tools.package_resolver import get_local_package_path
26
+ from ..utils.debug_log import info_logger, warning_logger
27
+ from ..utils.repo_path import any_repo_matches_path
28
+ from .config_manager import resolve_context, ResolvedContext, register_repo_in_context, ensure_first_run_bootstrap
29
+
30
+ console = Console()
31
+
32
+
33
+ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, Any, Any, ResolvedContext]:
34
+ """
35
+ Initializes and returns core service managers based on the resolved context.
36
+ Returns (db_manager, graph_builder, code_finder, resolved_context).
37
+ """
38
+ ensure_first_run_bootstrap()
39
+ console.print("[dim]Resolving context...[/dim]")
40
+ ctx = resolve_context(cli_context_flag)
41
+
42
+ # Let the user know what context we're operating in
43
+ if ctx.mode == "named":
44
+ console.print(f"[cyan]Context:[/cyan] {ctx.context_name} (Database: {ctx.database})")
45
+ elif ctx.mode == "per-repo":
46
+ console.print(f"[cyan]Context:[/cyan] Per-repo local mode (Database: {ctx.database})")
47
+ else:
48
+ # Default global mode — silent to keep CLI clean for existing users
49
+ pass
50
+
51
+ console.print("[dim]Initializing services and database connection...[/dim]")
52
+ try:
53
+ # Override the database backend with the context's specific choice
54
+ if ctx.database:
55
+ os.environ['CGC_RUNTIME_DB_TYPE'] = ctx.database
56
+
57
+ # Pass the exact DB path resolved from the context
58
+ db_manager = get_database_manager(db_path=ctx.db_path)
59
+ except ValueError as e:
60
+ console.print(f"[bold red]Database Configuration Error:[/bold red] {e}")
61
+ return None, None, None, ctx
62
+
63
+ try:
64
+ db_manager.get_driver()
65
+ except Exception as e:
66
+ # Check if this is a FalkorDB failure that should trigger a KùzuDB fallback
67
+ from ..core.database_falkordb import FalkorDBUnavailableError
68
+ if isinstance(e, FalkorDBUnavailableError):
69
+ console.print(f"[yellow]⚠ FalkorDB Lite is not functional in this environment: {e}[/yellow]")
70
+ console.print("[cyan]Falling back to KùzuDB for a reliable experience...[/cyan]")
71
+
72
+ # Close the broken driver/socket
73
+ try:
74
+ db_manager.close_driver()
75
+ except Exception:
76
+ pass
77
+
78
+ # Re-initialize explicitly with KùzuDB
79
+ from ..core.database_kuzu import KuzuDBManager
80
+ db_manager = KuzuDBManager()
81
+ try:
82
+ db_manager.get_driver()
83
+ console.print("[green]✓[/green] Successfully switched to KùzuDB fallback")
84
+ except Exception as kuzu_e:
85
+ console.print(f"[bold red]Critical Error:[/bold red] Both FalkorDB and KùzuDB failed: {kuzu_e}")
86
+ return None, None, None, ctx
87
+ else:
88
+ console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
89
+ console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
90
+ return None, None, None, ctx
91
+
92
+ # The GraphBuilder requires an event loop, even for synchronous-style execution
93
+ try:
94
+ loop = asyncio.get_running_loop()
95
+ except RuntimeError:
96
+ loop = asyncio.new_event_loop()
97
+ asyncio.set_event_loop(loop)
98
+
99
+ graph_builder = GraphBuilder(db_manager, JobManager(), loop)
100
+ code_finder = CodeFinder(db_manager)
101
+ console.print("[dim]Services initialized.[/dim]")
102
+ return db_manager, graph_builder, code_finder, ctx
103
+
104
+
105
+ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path, is_dependency: bool = False, cgcignore_path: str = None):
106
+ """Internal helper to run indexing with a Live progress bar."""
107
+ job_id = graph_builder.job_manager.create_job(str(path_obj), is_dependency=is_dependency)
108
+
109
+ # Create the progress bar
110
+ with Progress(
111
+ SpinnerColumn(),
112
+ TextColumn("[progress.description]{task.description}"),
113
+ BarColumn(),
114
+ TaskProgressColumn(),
115
+ MofNCompleteColumn(),
116
+ TimeRemainingColumn(),
117
+ TextColumn("[dim]{task.fields[filename]}"),
118
+ console=console,
119
+ transient=True,
120
+ ) as progress:
121
+
122
+ task_id = progress.add_task(
123
+ "Indexing...",
124
+ total=None, # Will be updated once file discovery is done
125
+ filename=""
126
+ )
127
+
128
+ indexing_task = asyncio.create_task(
129
+ graph_builder.build_graph_from_path_async(path_obj, is_dependency=is_dependency, job_id=job_id, cgcignore_path=cgcignore_path)
130
+ )
131
+
132
+ from ..core.jobs import JobStatus
133
+
134
+ # Poll for updates
135
+ while not indexing_task.done():
136
+ job = graph_builder.job_manager.get_job(job_id)
137
+ if job:
138
+ if job.total_files > 0:
139
+ progress.update(task_id, total=job.total_files, completed=job.processed_files)
140
+
141
+ # Update the current filename in the UI
142
+ current_file = job.current_file or ""
143
+ if len(current_file) > 40:
144
+ current_file = "..." + current_file[-37:]
145
+ progress.update(task_id, filename=current_file)
146
+
147
+ if job.status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED]:
148
+ break
149
+
150
+ await asyncio.sleep(0.1)
151
+
152
+ # Wait for actual completion and handle final state
153
+ try:
154
+ await indexing_task
155
+ job = graph_builder.job_manager.get_job(job_id)
156
+ if job and job.status == JobStatus.FAILED:
157
+ error_msg = job.errors[0] if job.errors else "Unknown error"
158
+ raise RuntimeError(error_msg)
159
+ except Exception as e:
160
+ raise e
161
+
162
+
163
+ def index_helper(path: str, context: Optional[str] = None):
164
+ """Synchronously indexes a repository in a given context."""
165
+ time_start = time.time()
166
+ services = _initialize_services(context)
167
+ if not all(services[:3]):
168
+ return
169
+
170
+ db_manager, graph_builder, code_finder, ctx = services
171
+ path_obj = Path(path).resolve()
172
+
173
+ if not path_obj.exists():
174
+ console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
175
+ db_manager.close_driver()
176
+ return
177
+
178
+ indexed_repos = code_finder.list_indexed_repositories()
179
+ repo_exists = any_repo_matches_path(indexed_repos, path_obj)
180
+
181
+ if repo_exists:
182
+ # Check if the repository actually has files (not just an empty node from interrupted indexing)
183
+ # Use variable-length path to handle both flat (Repository->File) and
184
+ # hierarchical (Repository->Directory->...->File) graph structures
185
+ try:
186
+ with db_manager.get_driver().session() as session:
187
+ result = session.run(
188
+ "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(DISTINCT f) as file_count",
189
+ path=str(path_obj)
190
+ )
191
+ record = result.single()
192
+ file_count = record["file_count"] if record else 0
193
+
194
+ if file_count > 0:
195
+ console.print(f"[yellow]Repository '{path}' is already indexed with {file_count} files. Skipping.[/yellow]")
196
+ console.print("[dim]💡 Tip: Use 'cgc index --force' to re-index[/dim]")
197
+ db_manager.close_driver()
198
+ return
199
+ else:
200
+ console.print(f"[yellow]Repository '{path}' exists but has no files (likely interrupted). Re-indexing...[/yellow]")
201
+ except Exception as e:
202
+ console.print(f"[yellow]Warning: Could not check file count: {e}. Proceeding with indexing...[/yellow]")
203
+
204
+ # Auto-register the repo into the named context (auto-creates if needed)
205
+ if context and ctx.mode == "named":
206
+ register_repo_in_context(context, str(path_obj), auto_create=True)
207
+
208
+ console.print(f"Starting indexing for: {path_obj}")
209
+
210
+ try:
211
+ asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
212
+ time_end = time.time()
213
+ elapsed = time_end - time_start
214
+ console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
215
+
216
+ # Check if auto-watch is enabled
217
+ try:
218
+ from codegraphcontext.cli.config_manager import get_config_value
219
+ auto_watch = get_config_value('ENABLE_AUTO_WATCH')
220
+ if auto_watch and str(auto_watch).lower() == 'true':
221
+ console.print("\n[cyan]🔍 ENABLE_AUTO_WATCH is enabled. Starting watcher...[/cyan]")
222
+ db_manager.close_driver() # Close before starting watcher
223
+ watch_helper(path) # This will block the terminal
224
+ return # watch_helper handles its own cleanup
225
+ except Exception as e:
226
+ console.print(f"[yellow]Warning: Could not check ENABLE_AUTO_WATCH: {e}[/yellow]")
227
+
228
+ except Exception as e:
229
+ console.print(f"[bold red]An error occurred during indexing:[/bold red] {e}")
230
+ finally:
231
+ db_manager.close_driver()
232
+
233
+
234
+ def add_package_helper(package_name: str, language: str, context: Optional[str] = None):
235
+ """Synchronously indexes a package."""
236
+ services = _initialize_services(context)
237
+ if not all(services[:3]):
238
+ return
239
+
240
+ db_manager, graph_builder, code_finder, ctx = services
241
+
242
+ package_path_str = get_local_package_path(package_name, language)
243
+ if not package_path_str:
244
+ console.print(f"[red]Error: Could not find package '{package_name}' for language '{language}'.[/red]")
245
+ db_manager.close_driver()
246
+ return
247
+
248
+ package_path = Path(package_path_str)
249
+
250
+ indexed_repos = code_finder.list_indexed_repositories()
251
+ if any(repo.get("name") == package_name for repo in indexed_repos if repo.get("is_dependency")):
252
+ console.print(f"[yellow]Package '{package_name}' is already indexed. Skipping.[/yellow]")
253
+ db_manager.close_driver()
254
+ return
255
+
256
+ console.print(f"Starting indexing for package '{package_name}' at: {package_path}")
257
+
258
+ try:
259
+ asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
260
+ console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
261
+ except Exception as e:
262
+ console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
263
+ finally:
264
+ db_manager.close_driver()
265
+
266
+
267
+ def list_repos_helper(context: Optional[str] = None):
268
+ """Lists all indexed repositories."""
269
+ services = _initialize_services(context)
270
+ if not all(services[:3]):
271
+ return
272
+
273
+ db_manager, _, code_finder, ctx = services
274
+
275
+ try:
276
+ repos = code_finder.list_indexed_repositories()
277
+ if not repos:
278
+ console.print("[yellow]No repositories indexed yet.[/yellow]")
279
+ return
280
+
281
+ table = Table(show_header=True, header_style="bold magenta")
282
+ table.add_column("Name", style="dim")
283
+ table.add_column("Path")
284
+ table.add_column("Type")
285
+
286
+ for repo in repos:
287
+ repo_type = "Dependency" if repo.get("is_dependency") else "Project"
288
+ table.add_row(repo.get("name") or "", str(repo.get("path") or ""), repo_type)
289
+
290
+ console.print(table)
291
+ except Exception as e:
292
+ console.print(f"[bold red]An error occurred:[/bold red] {e}")
293
+ finally:
294
+ db_manager.close_driver()
295
+
296
+
297
+ def delete_helper(repo_path: str, context: Optional[str] = None):
298
+ """Deletes a repository from the graph."""
299
+ services = _initialize_services(context)
300
+ if not all(services[:3]):
301
+ return
302
+
303
+ db_manager, graph_builder, _, ctx = services
304
+
305
+ try:
306
+ if graph_builder.delete_repository_from_graph(repo_path):
307
+ console.print(f"[green]Successfully deleted repository: {repo_path}[/green]")
308
+ else:
309
+ console.print(f"[yellow]Repository not found in graph: {repo_path}[/yellow]")
310
+ console.print("[dim]Tip: Use 'cgc list' to see available repositories.[/dim]")
311
+ except Exception as e:
312
+ console.print(f"[bold red]An error occurred:[/bold red] {e}")
313
+ finally:
314
+ db_manager.close_driver()
315
+
316
+
317
+ def cypher_helper(query: str, context: Optional[str] = None):
318
+ """Executes a read-only Cypher query."""
319
+ services = _initialize_services(context)
320
+ if not all(services[:3]):
321
+ return
322
+
323
+ db_manager, _, _, ctx = services
324
+
325
+ # Replicating safety checks from MCPServer
326
+ forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
327
+ if any(keyword in query.upper() for keyword in forbidden_keywords):
328
+ console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
329
+ db_manager.close_driver()
330
+ return
331
+
332
+ try:
333
+ with db_manager.get_driver().session() as session:
334
+ result = session.run(query)
335
+ records = [record.data() for record in result]
336
+ console.print(json.dumps(records, indent=2))
337
+ except Exception as e:
338
+ console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
339
+ finally:
340
+ db_manager.close_driver()
341
+
342
+
343
+ def cypher_helper_visual(query: str, context: Optional[str] = None):
344
+ """Executes a read-only Cypher query and visualizes the results."""
345
+ from .visualizer import visualize_cypher_results
346
+
347
+ services = _initialize_services(context)
348
+ if not all(services[:3]):
349
+ return
350
+
351
+ db_manager, _, _, ctx = services
352
+
353
+ # Replicating safety checks from MCPServer
354
+ forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
355
+ if any(keyword in query.upper() for keyword in forbidden_keywords):
356
+ console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
357
+ db_manager.close_driver()
358
+ return
359
+
360
+ try:
361
+ with db_manager.get_driver().session() as session:
362
+ result = session.run(query)
363
+ records = [record.data() for record in result]
364
+
365
+ if not records:
366
+ console.print("[yellow]No results to visualize.[/yellow]")
367
+ return # finally block will close driver
368
+
369
+ visualize_cypher_results(records, query)
370
+ except Exception as e:
371
+ console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
372
+ finally:
373
+ db_manager.close_driver()
374
+
375
+
376
+ import uvicorn
377
+ import urllib.parse
378
+ from ..viz.server import run_server, set_db_manager
379
+
380
+ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context: Optional[str] = None):
381
+ """Generates an interactive visualization using the Playground UI."""
382
+ services = _initialize_services(context)
383
+ if not all(services[:3]):
384
+ return
385
+
386
+ db_manager, _, _, ctx = services
387
+
388
+ # Set the DB manager for the server
389
+ set_db_manager(db_manager)
390
+
391
+ # Determine the static directory (built React app)
392
+ # This points to src/codegraphcontext/viz/dist where we build the website
393
+ # (relative to src/codegraphcontext/cli/cli_helpers.py)
394
+ # Using .resolve() is more robust for path comparison and existence checks
395
+ this_file = Path(__file__).resolve()
396
+ package_root = this_file.parent.parent
397
+ static_dir = package_root / "viz" / "dist"
398
+
399
+ # Fallback for development if not yet built in viz/dist
400
+ if not static_dir.exists():
401
+ # Look for website/dist in the project root (3 levels up from cli/cli_helpers.py, 4 parents)
402
+ # 1: cli/, 2: codegraphcontext/, 3: src/, 4: project_root/
403
+ project_root = this_file.parent.parent.parent.parent
404
+ dev_static_dir = project_root / "website" / "dist"
405
+
406
+ # Also try one level up from package_root just in case of different layouts
407
+ alt_dev_dir = package_root.parent.parent / "website" / "dist"
408
+
409
+ if dev_static_dir.exists():
410
+ static_dir = dev_static_dir
411
+ elif alt_dev_dir.exists():
412
+ static_dir = alt_dev_dir
413
+ else:
414
+ # Last resort: try current working directory
415
+ cwd_static_dir = Path.cwd() / "website" / "dist"
416
+ if cwd_static_dir.exists():
417
+ static_dir = cwd_static_dir
418
+ else:
419
+ console.print("[bold red]Visualization assets not found.[/bold red]")
420
+ console.print("[dim]Checked paths:[/dim]")
421
+ console.print(f" [dim]- {package_root / 'viz' / 'dist'}[/dim]")
422
+ console.print(f" [dim]- {dev_static_dir}[/dim]")
423
+ console.print(f" [dim]- {alt_dev_dir}[/dim]")
424
+ console.print(f" [dim]- {cwd_static_dir}[/dim]")
425
+ console.print(
426
+ "[dim]If you installed from PyPI, upgrade after the next release "
427
+ "(wheels must bundle viz/dist). If you are developing from source, run:[/dim]"
428
+ )
429
+ console.print(" [cyan]./scripts/sync_viz_dist.sh[/cyan]")
430
+ console.print(
431
+ "[dim]or[/dim] [cyan]cd website && npm ci && npm run build[/cyan] "
432
+ "[dim]then sync[/dim] [cyan]website/dist[/cyan] [dim]→[/dim] "
433
+ "[cyan]src/codegraphcontext/viz/dist[/cyan][dim].[/dim]"
434
+ )
435
+ db_manager.close_driver()
436
+ raise SystemExit(1)
437
+
438
+ index_html = static_dir / "index.html"
439
+ if not index_html.is_file():
440
+ console.print(
441
+ f"[bold red]Invalid visualization bundle:[/bold red] missing {index_html}"
442
+ )
443
+ db_manager.close_driver()
444
+ raise SystemExit(1)
445
+
446
+ # Construct the URL
447
+ backend_url = f"http://localhost:{port}"
448
+ params = {"backend": backend_url}
449
+ if repo_path:
450
+ params["repo_path"] = str(Path(repo_path).resolve())
451
+
452
+ query_string = urllib.parse.urlencode(params)
453
+ visualization_url = f"{backend_url}/explore?{query_string}"
454
+
455
+ console.print(f"[green]Starting visualizer server on {backend_url}...[/green]")
456
+ console.print(f"[cyan]Opening Playground UI:[/cyan] {visualization_url}")
457
+
458
+ # Open browser in a separate thread/process if possible, or just before starting server
459
+ def open_browser():
460
+ import time
461
+ import webbrowser
462
+ time.sleep(1.5) # Give the server a moment to start
463
+ webbrowser.open(visualization_url)
464
+
465
+ import threading
466
+ threading.Thread(target=open_browser, daemon=True).start()
467
+
468
+ try:
469
+ run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
470
+ except Exception as e:
471
+ console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
472
+ finally:
473
+ db_manager.close_driver()
474
+
475
+
476
+ def reindex_helper(path: str, context: Optional[str] = None):
477
+ """Force re-index by deleting and rebuilding the repository."""
478
+ time_start = time.time()
479
+ services = _initialize_services(context)
480
+ if not all(services[:3]):
481
+ return
482
+
483
+ db_manager, graph_builder, code_finder, ctx = services
484
+ path_obj = Path(path).resolve()
485
+
486
+ if not path_obj.exists():
487
+ console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
488
+ db_manager.close_driver()
489
+ return
490
+
491
+ # Check if already indexed
492
+ indexed_repos = code_finder.list_indexed_repositories()
493
+ repo_exists = any_repo_matches_path(indexed_repos, path_obj)
494
+
495
+ if repo_exists:
496
+ console.print(f"[yellow]Deleting existing index for: {path_obj}[/yellow]")
497
+ try:
498
+ graph_builder.delete_repository_from_graph(str(path_obj))
499
+ console.print("[green]✓[/green] Deleted old index")
500
+ except Exception as e:
501
+ console.print(f"[red]Error deleting old index: {e}[/red]")
502
+ db_manager.close_driver()
503
+ return
504
+
505
+ console.print(f"[cyan]Re-indexing: {path_obj}[/cyan]")
506
+
507
+ try:
508
+ asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
509
+ time_end = time.time()
510
+ elapsed = time_end - time_start
511
+ console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
512
+ except Exception as e:
513
+ console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
514
+ finally:
515
+ db_manager.close_driver()
516
+
517
+
518
+ def update_helper(path: str, context: Optional[str] = None):
519
+ """Update/refresh index for a path (alias for reindex)."""
520
+ console.print("[cyan]Updating repository index...[/cyan]")
521
+ reindex_helper(path, context)
522
+
523
+
524
+ def clean_helper(context: Optional[str] = None):
525
+ """Remove orphaned nodes and relationships from the database."""
526
+ services = _initialize_services(context)
527
+ if not all(services[:3]):
528
+ return
529
+
530
+ db_manager, _, _, ctx = services
531
+
532
+ console.print("[cyan]🧹 Cleaning database (removing orphaned nodes)...[/cyan]")
533
+
534
+ try:
535
+ total_deleted = 0
536
+ batch_size = 500
537
+
538
+ with db_manager.get_driver().session() as session:
539
+ # Layer-by-layer deletion: iteratively delete nodes that lost
540
+ # their CONTAINS parent. Each pass peels one layer of the
541
+ # Repository → File → Class/Function → Variable hierarchy.
542
+ while True:
543
+ result = session.run("""
544
+ MATCH (n)
545
+ WHERE NOT n:Repository
546
+ AND NOT ()-[:CONTAINS]->(n)
547
+ WITH n LIMIT $batch_size
548
+ DETACH DELETE n
549
+ RETURN count(n) as deleted
550
+ """, batch_size=batch_size)
551
+ record = result.single()
552
+ deleted_count = record["deleted"] if record else 0
553
+ total_deleted += deleted_count
554
+
555
+ if deleted_count == 0:
556
+ break
557
+
558
+ console.print(f"[dim] Deleted {deleted_count} orphaned nodes (batch)...[/dim]")
559
+
560
+ if total_deleted > 0:
561
+ console.print(f"[green]✓[/green] Deleted {total_deleted} orphaned nodes total")
562
+ else:
563
+ console.print("[green]✓[/green] No orphaned nodes found")
564
+
565
+ console.print("[green]✅ Database cleanup complete![/green]")
566
+ except Exception as e:
567
+ console.print(f"[bold red]An error occurred during cleanup:[/bold red] {e}")
568
+ finally:
569
+ db_manager.close_driver()
570
+
571
+
572
+ def stats_helper(path: str = None, context: Optional[str] = None):
573
+ """Show indexing statistics for a repository or overall."""
574
+ services = _initialize_services(context)
575
+ if not all(services[:3]):
576
+ return
577
+
578
+ db_manager, _, code_finder, ctx = services
579
+
580
+ try:
581
+ if path:
582
+ # Stats for specific repository
583
+ path_obj = Path(path).resolve()
584
+ console.print(f"[cyan]📊 Statistics for: {path_obj}[/cyan]\n")
585
+
586
+ with db_manager.get_driver().session() as session:
587
+ # Get repository node
588
+ repo_query = """
589
+ MATCH (r:Repository {path: $path})
590
+ RETURN r
591
+ """
592
+ result = session.run(repo_query, path=str(path_obj))
593
+ if not result.single():
594
+ console.print(f"[red]Repository not found: {path_obj}[/red]")
595
+ return
596
+
597
+ # Get stats
598
+ # Get stats using separate queries to handle depth and avoid Cartesian products
599
+ # 1. Files
600
+ file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
601
+ file_count = session.run(file_query, path=str(path_obj)).single()["c"]
602
+
603
+ # 2. Functions (including methods in classes)
604
+ func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
605
+ func_count = session.run(func_query, path=str(path_obj)).single()["c"]
606
+
607
+ # 3. Classes
608
+ class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(c:Class) RETURN count(c) as c"
609
+ class_count = session.run(class_query, path=str(path_obj)).single()["c"]
610
+
611
+ # 4. Modules (imported) - Note: Module nodes are outside the repo structure usually, connected via IMPORTS
612
+ # We need to traverse from files to modules
613
+ module_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File)-[:IMPORTS]->(m:Module) RETURN count(DISTINCT m) as c"
614
+ module_count = session.run(module_query, path=str(path_obj)).single()["c"]
615
+
616
+ table = Table(show_header=True, header_style="bold magenta")
617
+ table.add_column("Metric", style="cyan")
618
+ table.add_column("Count", style="green", justify="right")
619
+
620
+ table.add_row("Files", str(file_count))
621
+ table.add_row("Functions", str(func_count))
622
+ table.add_row("Classes", str(class_count))
623
+ table.add_row("Imported Modules", str(module_count))
624
+
625
+ console.print(table)
626
+ else:
627
+ # Overall stats
628
+ console.print("[cyan]📊 Overall Database Statistics[/cyan]\n")
629
+
630
+ with db_manager.get_driver().session() as session:
631
+ # Get overall counts using separate O(1) queries
632
+ repo_count = session.run("MATCH (r:Repository) RETURN count(r) as c").single()["c"]
633
+
634
+ if repo_count > 0:
635
+ file_count = session.run("MATCH (f:File) RETURN count(f) as c").single()["c"]
636
+ func_count = session.run("MATCH (f:Function) RETURN count(f) as c").single()["c"]
637
+ class_count = session.run("MATCH (c:Class) RETURN count(c) as c").single()["c"]
638
+ module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
639
+
640
+ table = Table(show_header=True, header_style="bold magenta")
641
+ table.add_column("Metric", style="cyan")
642
+ table.add_column("Count", style="green", justify="right")
643
+
644
+ table.add_row("Repositories", str(repo_count))
645
+ table.add_row("Files", str(file_count))
646
+ table.add_row("Functions", str(func_count))
647
+ table.add_row("Classes", str(class_count))
648
+ table.add_row("Modules", str(module_count))
649
+
650
+ console.print(table)
651
+ else:
652
+ console.print("[yellow]No data indexed yet.[/yellow]")
653
+
654
+ except Exception as e:
655
+ console.print(f"[bold red]An error occurred:[/bold red] {e}")
656
+ finally:
657
+ db_manager.close_driver()
658
+
659
+
660
+ def watch_helper(path: str, context: Optional[str] = None):
661
+ """Watch a directory for changes and auto-update the graph (blocking mode)."""
662
+ import logging
663
+ from ..core.watcher import CodeWatcher
664
+
665
+ # Suppress verbose watchdog DEBUG logs
666
+ logging.getLogger('watchdog').setLevel(logging.WARNING)
667
+ logging.getLogger('watchdog.observers').setLevel(logging.WARNING)
668
+ logging.getLogger('watchdog.observers.inotify_buffer').setLevel(logging.WARNING)
669
+
670
+ services = _initialize_services(context)
671
+ if not all(services[:3]):
672
+ return
673
+
674
+ db_manager, graph_builder, code_finder, ctx = services
675
+ path_obj = Path(path).resolve()
676
+
677
+ if not path_obj.exists():
678
+ console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
679
+ db_manager.close_driver()
680
+ return
681
+
682
+ if not path_obj.is_dir():
683
+ console.print(f"[red]Error: Path must be a directory: {path_obj}[/red]")
684
+ db_manager.close_driver()
685
+ return
686
+
687
+ console.print(f"[bold cyan]🔍 Watching {path_obj} for changes...[/bold cyan]")
688
+
689
+ # Check if already indexed — use File node count as a robust fallback so a
690
+ # transient empty result from list_indexed_repositories never triggers a
691
+ # destructive full rescan of an already-populated graph.
692
+ indexed_repos = code_finder.list_indexed_repositories()
693
+ is_indexed = any_repo_matches_path(indexed_repos, path_obj)
694
+ if not is_indexed:
695
+ # Fallback: count File nodes whose path starts with this repo's path.
696
+ # If > 100 exist, the repo is clearly already indexed — skip the scan.
697
+ try:
698
+ with code_finder.driver.session() as _s:
699
+ _r = _s.run(
700
+ "MATCH (n:File) WHERE n.path STARTS WITH $p RETURN count(n) AS c",
701
+ p=str(path_obj) + "/"
702
+ )
703
+ _count = _r.single()["c"]
704
+ if _count > 100:
705
+ info_logger(
706
+ f"[watch] list_indexed_repositories returned no match for {path_obj} "
707
+ f"but {_count} File nodes exist — treating as already indexed."
708
+ )
709
+ is_indexed = True
710
+ except Exception as _e:
711
+ warning_logger(f"[watch] Fallback indexed check failed: {_e}")
712
+
713
+ # Create watcher instance
714
+ job_manager = JobManager()
715
+ watcher = CodeWatcher(graph_builder, job_manager)
716
+
717
+ try:
718
+ # Start the observer thread
719
+ watcher.start()
720
+
721
+ # Add the directory to watch
722
+ if is_indexed:
723
+ console.print("[green]✓[/green] Already indexed (no initial scan needed)")
724
+ watcher.watch_directory(str(path_obj), perform_initial_scan=False)
725
+ else:
726
+ console.print("[yellow]⚠[/yellow] Not indexed yet. Performing initial scan...")
727
+
728
+ # Index the repository first (like MCP does)
729
+ async def do_index():
730
+ await graph_builder.build_graph_from_path_async(path_obj, is_dependency=False)
731
+
732
+ asyncio.run(do_index())
733
+ console.print("[green]✓[/green] Initial scan complete")
734
+
735
+ # Now start watching (without another scan)
736
+ watcher.watch_directory(str(path_obj), perform_initial_scan=False)
737
+
738
+ console.print("[bold green]👀 Monitoring for file changes...[/bold green] (Press Ctrl+C to stop)")
739
+ console.print("[dim]💡 Tip: Open a new terminal window to continue working[/dim]\n")
740
+
741
+ # Block here and keep the watcher running
742
+ import threading
743
+ stop_event = threading.Event()
744
+
745
+ try:
746
+ stop_event.wait() # Wait indefinitely until interrupted
747
+ except KeyboardInterrupt:
748
+ console.print("\n[yellow]🛑 Stopping watcher...[/yellow]")
749
+
750
+ except KeyboardInterrupt:
751
+ console.print("\n[yellow]🛑 Stopping watcher...[/yellow]")
752
+ except Exception as e:
753
+ console.print(f"[bold red]An error occurred:[/bold red] {e}")
754
+ finally:
755
+ watcher.stop()
756
+ db_manager.close_driver()
757
+ console.print("[green]✓[/green] Watcher stopped. Graph is up to date.")
758
+
759
+
760
+
761
+ def unwatch_helper(path: str):
762
+ """Stop watching a directory."""
763
+ console.print(f"[yellow]⚠️ Note: 'cgc unwatch' only works when the watcher is running via MCP server.[/yellow]")
764
+ console.print(f"[dim]For CLI watch mode, simply press Ctrl+C in the watch terminal.[/dim]")
765
+ console.print(f"\n[cyan]Path specified:[/cyan] {Path(path).resolve()}")
766
+
767
+
768
+ def list_watching_helper():
769
+ """List all directories currently being watched."""
770
+ console.print(f"[yellow]⚠️ Note: 'cgc watching' only works when the watcher is running via MCP server.[/yellow]")
771
+ console.print(f"[dim]For CLI watch mode, check the terminal where you ran 'cgc watch'.[/dim]")
772
+ console.print(f"\n[cyan]To see watched directories in MCP mode:[/cyan]")
773
+ console.print(f" 1. Start the MCP server: cgc mcp start")
774
+ console.print(f" 2. Use the 'list_watched_paths' MCP tool from your IDE")