codegraphcontext-rust 0.5.5__cp310-cp310-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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- 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")
|