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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp311-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,1051 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for CodeGraphContext.
|
|
3
|
+
Handles reading, writing, and validating configuration settings.
|
|
4
|
+
Also manages the context system (config.yaml) alongside the existing .env file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Dict, Any, List
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
import os
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
# Configuration file location
|
|
18
|
+
CONFIG_DIR = Path.home() / ".codegraphcontext"
|
|
19
|
+
CONFIG_FILE = CONFIG_DIR / ".env"
|
|
20
|
+
|
|
21
|
+
# Database credential keys (stored in same .env file but not managed as config)
|
|
22
|
+
DATABASE_CREDENTIAL_KEYS = {"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE"}
|
|
23
|
+
|
|
24
|
+
# Default configuration values
|
|
25
|
+
DEFAULT_CONFIG = {
|
|
26
|
+
"DEFAULT_DATABASE": "falkordb",
|
|
27
|
+
"FALKORDB_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb"),
|
|
28
|
+
"FALKORDB_SOCKET_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb.sock"),
|
|
29
|
+
"INDEX_VARIABLES": "true",
|
|
30
|
+
"ALLOW_DB_DELETION": "false",
|
|
31
|
+
"DEBUG_LOGS": "false",
|
|
32
|
+
"DEBUG_LOG_PATH": str(Path.home() / "mcp_debug.log"),
|
|
33
|
+
"ENABLE_APP_LOGS": "CRITICAL",
|
|
34
|
+
"LIBRARY_LOG_LEVEL": "WARNING",
|
|
35
|
+
"LOG_FILE_PATH": str(CONFIG_DIR / "logs" / "cgc.log"),
|
|
36
|
+
"MAX_FILE_SIZE_MB": "10",
|
|
37
|
+
"IGNORE_TEST_FILES": "false",
|
|
38
|
+
"IGNORE_HIDDEN_FILES": "true",
|
|
39
|
+
"ENABLE_AUTO_WATCH": "false",
|
|
40
|
+
"COMPLEXITY_THRESHOLD": "10",
|
|
41
|
+
"MAX_DEPTH": "unlimited",
|
|
42
|
+
"PARALLEL_WORKERS": "4",
|
|
43
|
+
"CACHE_ENABLED": "true",
|
|
44
|
+
"IGNORE_DIRS": "node_modules,venv,.venv,env,.env,dist,build,target,out,.git,.idea,.vscode,__pycache__",
|
|
45
|
+
"INDEX_SOURCE": "true",
|
|
46
|
+
# SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
|
|
47
|
+
"SCIP_INDEXER": "false",
|
|
48
|
+
"SCIP_LANGUAGES": "python,typescript,go,rust,java",
|
|
49
|
+
"SKIP_EXTERNAL_RESOLUTION": "false",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Configuration key descriptions
|
|
53
|
+
CONFIG_DESCRIPTIONS = {
|
|
54
|
+
"DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb)",
|
|
55
|
+
"FALKORDB_PATH": "Path to FalkorDB database file",
|
|
56
|
+
"FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
|
|
57
|
+
"INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
|
|
58
|
+
"ALLOW_DB_DELETION": "Allow full database deletion commands",
|
|
59
|
+
"DEBUG_LOGS": "Enable debug logging (for development/troubleshooting)",
|
|
60
|
+
"DEBUG_LOG_PATH": "Path to debug log file",
|
|
61
|
+
"ENABLE_APP_LOGS": "Application log level (DEBUG|INFO|WARNING|ERROR|CRITICAL|DISABLED)",
|
|
62
|
+
"LIBRARY_LOG_LEVEL": "Log level for third-party libraries (neo4j, asyncio, urllib3) (DEBUG|INFO|WARNING|ERROR|CRITICAL)",
|
|
63
|
+
"LOG_FILE_PATH": "Path to application log file",
|
|
64
|
+
"MAX_FILE_SIZE_MB": "Maximum file size to index (in MB)",
|
|
65
|
+
"IGNORE_TEST_FILES": "Skip test files during indexing",
|
|
66
|
+
"IGNORE_HIDDEN_FILES": "Skip hidden files/directories",
|
|
67
|
+
"ENABLE_AUTO_WATCH": "Automatically watch directory after indexing",
|
|
68
|
+
"COMPLEXITY_THRESHOLD": "Cyclomatic complexity warning threshold",
|
|
69
|
+
"MAX_DEPTH": "Maximum directory depth for indexing (unlimited or number)",
|
|
70
|
+
"PARALLEL_WORKERS": "Number of parallel indexing workers",
|
|
71
|
+
"CACHE_ENABLED": "Enable caching for faster re-indexing",
|
|
72
|
+
"IGNORE_DIRS": "Comma-separated list of directory names to ignore during indexing",
|
|
73
|
+
"INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
|
|
74
|
+
"SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
|
|
75
|
+
"SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,go,rust,java)",
|
|
76
|
+
"SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Valid values for each config key
|
|
80
|
+
CONFIG_VALIDATORS = {
|
|
81
|
+
"DEFAULT_DATABASE": ["neo4j", "falkordb", "falkordb-remote", "kuzudb"],
|
|
82
|
+
"INDEX_VARIABLES": ["true", "false"],
|
|
83
|
+
"ALLOW_DB_DELETION": ["true", "false"],
|
|
84
|
+
"DEBUG_LOGS": ["true", "false"],
|
|
85
|
+
"ENABLE_APP_LOGS": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "DISABLED"],
|
|
86
|
+
"LIBRARY_LOG_LEVEL": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
87
|
+
"IGNORE_TEST_FILES": ["true", "false"],
|
|
88
|
+
"IGNORE_HIDDEN_FILES": ["true", "false"],
|
|
89
|
+
"ENABLE_AUTO_WATCH": ["true", "false"],
|
|
90
|
+
"CACHE_ENABLED": ["true", "false"],
|
|
91
|
+
"INDEX_SOURCE": ["true", "false"],
|
|
92
|
+
"SCIP_INDEXER": ["true", "false"],
|
|
93
|
+
"SKIP_EXTERNAL_RESOLUTION": ["true", "false"],
|
|
94
|
+
}
|
|
95
|
+
DEFAULT_CGCIGNORE_PATTERNS = """\
|
|
96
|
+
# Default .cgcignore patterns
|
|
97
|
+
# Lines starting with # are comments; blank lines are ignored.
|
|
98
|
+
# Patterns follow .gitignore syntax.
|
|
99
|
+
|
|
100
|
+
node_modules/
|
|
101
|
+
venv/
|
|
102
|
+
.venv/
|
|
103
|
+
env/
|
|
104
|
+
.env/
|
|
105
|
+
dist/
|
|
106
|
+
build/
|
|
107
|
+
target/
|
|
108
|
+
out/
|
|
109
|
+
.git/
|
|
110
|
+
.idea/
|
|
111
|
+
.vscode/
|
|
112
|
+
__pycache__/
|
|
113
|
+
*.pyc
|
|
114
|
+
*.pyo
|
|
115
|
+
*.egg-info/
|
|
116
|
+
.tox/
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
.pytest_cache/
|
|
119
|
+
coverage/
|
|
120
|
+
.next/
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ensure_config_dir(path: Path = CONFIG_DIR):
|
|
125
|
+
"""
|
|
126
|
+
Ensure that the configuration directory exists.
|
|
127
|
+
Creates the directory and a logs subdirectory if they do not already exist.
|
|
128
|
+
"""
|
|
129
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
(path / "logs").mkdir(parents=True, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def ensure_global_cgcignore() -> bool:
|
|
134
|
+
"""Create ``~/.codegraphcontext/global/.cgcignore`` with sensible defaults
|
|
135
|
+
if it does not already exist. Returns True when a new file was created."""
|
|
136
|
+
cgcignore_path = CONFIG_DIR / "global" / ".cgcignore"
|
|
137
|
+
if cgcignore_path.exists():
|
|
138
|
+
return False
|
|
139
|
+
cgcignore_path.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
cgcignore_path.write_text(DEFAULT_CGCIGNORE_PATTERNS)
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_config() -> Dict[str, str]:
|
|
146
|
+
"""
|
|
147
|
+
Load configuration with priority support.
|
|
148
|
+
Priority order (highest to lowest):
|
|
149
|
+
1. Environment variables
|
|
150
|
+
2. Local .env file (in current or parent directories)
|
|
151
|
+
3. Global ~/.codegraphcontext/.env
|
|
152
|
+
|
|
153
|
+
Note: Does NOT create config directory - caller must call ensure_config_dir() first if needed.
|
|
154
|
+
"""
|
|
155
|
+
# Start with defaults
|
|
156
|
+
config = DEFAULT_CONFIG.copy()
|
|
157
|
+
|
|
158
|
+
# Load global config
|
|
159
|
+
if CONFIG_FILE.exists():
|
|
160
|
+
try:
|
|
161
|
+
with open(CONFIG_FILE, "r") as f:
|
|
162
|
+
for line in f:
|
|
163
|
+
line = line.strip()
|
|
164
|
+
if line and not line.startswith("#") and "=" in line:
|
|
165
|
+
key, value = line.split("=", 1)
|
|
166
|
+
config[key.strip()] = value.strip()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
console.print(f"[red]Error loading global config: {e}[/red]")
|
|
169
|
+
|
|
170
|
+
# Load local .env file if it exists (overrides global)
|
|
171
|
+
local_env = find_local_env()
|
|
172
|
+
if local_env and local_env.exists():
|
|
173
|
+
try:
|
|
174
|
+
with open(local_env, "r") as f:
|
|
175
|
+
for line in f:
|
|
176
|
+
line = line.strip()
|
|
177
|
+
if line and not line.startswith("#") and "=" in line:
|
|
178
|
+
key, value = line.split("=", 1)
|
|
179
|
+
key = key.strip()
|
|
180
|
+
# Only override if it's a config key (not database credentials in local file)
|
|
181
|
+
if key in DEFAULT_CONFIG or key in DATABASE_CREDENTIAL_KEYS:
|
|
182
|
+
config[key] = value.strip()
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f"[yellow]Warning: Error loading local .env: {e}[/yellow]")
|
|
185
|
+
|
|
186
|
+
# Environment variables have highest priority
|
|
187
|
+
for key in DEFAULT_CONFIG.keys():
|
|
188
|
+
env_value = os.getenv(key)
|
|
189
|
+
if env_value is not None:
|
|
190
|
+
config[key] = env_value
|
|
191
|
+
|
|
192
|
+
return config
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def find_local_env() -> Optional[Path]:
|
|
196
|
+
"""
|
|
197
|
+
Find a local .env file by searching current directory and parents.
|
|
198
|
+
Returns the first .env file found, or None.
|
|
199
|
+
"""
|
|
200
|
+
current = Path.cwd()
|
|
201
|
+
|
|
202
|
+
# Search up to 5 levels up
|
|
203
|
+
for _ in range(5):
|
|
204
|
+
# 1. Prefer .codegraphcontext/.env if it exists
|
|
205
|
+
cgc_env = current / ".codegraphcontext" / ".env"
|
|
206
|
+
if cgc_env.exists() and cgc_env != CONFIG_FILE:
|
|
207
|
+
return cgc_env
|
|
208
|
+
|
|
209
|
+
# 2. Fall back to root project .env
|
|
210
|
+
env_file = current / ".env"
|
|
211
|
+
if env_file.exists() and env_file != CONFIG_FILE:
|
|
212
|
+
return env_file
|
|
213
|
+
|
|
214
|
+
# Stop at root
|
|
215
|
+
if current.parent == current:
|
|
216
|
+
break
|
|
217
|
+
current = current.parent
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def codegraphcontext_dotenv_at_cwd(cwd: Optional[Path] = None) -> Optional[Path]:
|
|
223
|
+
"""
|
|
224
|
+
Return ``<cwd>/.codegraphcontext/.env`` if that file exists, else None.
|
|
225
|
+
|
|
226
|
+
*cwd* defaults to ``Path.cwd()``. Parent directories are **not** searched—same rule as
|
|
227
|
+
local context resolution (``find_local_cgc_dir``).
|
|
228
|
+
"""
|
|
229
|
+
root = (cwd or Path.cwd()).resolve()
|
|
230
|
+
candidate = root / ".codegraphcontext" / ".env"
|
|
231
|
+
return candidate if candidate.exists() else None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
|
|
235
|
+
"""
|
|
236
|
+
Save configuration to file.
|
|
237
|
+
If preserve_db_credentials is True, existing database credentials will be preserved.
|
|
238
|
+
If preserve_db_credentials is False, credentials from config dict will be written.
|
|
239
|
+
"""
|
|
240
|
+
ensure_config_dir()
|
|
241
|
+
|
|
242
|
+
# Determine which credentials to write
|
|
243
|
+
credentials_to_write = {}
|
|
244
|
+
|
|
245
|
+
if preserve_db_credentials and CONFIG_FILE.exists():
|
|
246
|
+
# Load existing credentials from file to preserve them
|
|
247
|
+
try:
|
|
248
|
+
with open(CONFIG_FILE, "r") as f:
|
|
249
|
+
for line in f:
|
|
250
|
+
line = line.strip()
|
|
251
|
+
if line and not line.startswith("#") and "=" in line:
|
|
252
|
+
key, value = line.split("=", 1)
|
|
253
|
+
key = key.strip()
|
|
254
|
+
if key in DATABASE_CREDENTIAL_KEYS:
|
|
255
|
+
credentials_to_write[key] = value.strip()
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
# Merge credentials from the config dict (handles both new and updated values)
|
|
259
|
+
for key in DATABASE_CREDENTIAL_KEYS:
|
|
260
|
+
if key in config:
|
|
261
|
+
credentials_to_write[key] = config[key]
|
|
262
|
+
else:
|
|
263
|
+
# Use credentials from the config dict being passed in
|
|
264
|
+
for key in DATABASE_CREDENTIAL_KEYS:
|
|
265
|
+
if key in config:
|
|
266
|
+
credentials_to_write[key] = config[key]
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
with open(CONFIG_FILE, "w") as f:
|
|
270
|
+
f.write("# CodeGraphContext Configuration\n")
|
|
271
|
+
f.write(f"# Location: {CONFIG_FILE}\n\n")
|
|
272
|
+
|
|
273
|
+
# Write database credentials first if they exist
|
|
274
|
+
if credentials_to_write:
|
|
275
|
+
f.write("# ===== Database Credentials =====\n")
|
|
276
|
+
for key in sorted(DATABASE_CREDENTIAL_KEYS):
|
|
277
|
+
if key in credentials_to_write:
|
|
278
|
+
f.write(f"{key}={credentials_to_write[key]}\n")
|
|
279
|
+
f.write("\n")
|
|
280
|
+
|
|
281
|
+
# Write configuration settings
|
|
282
|
+
f.write("# ===== Configuration Settings =====\n")
|
|
283
|
+
for key, value in sorted(config.items()):
|
|
284
|
+
# Skip database credentials (already written above)
|
|
285
|
+
if key in DATABASE_CREDENTIAL_KEYS:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
description = CONFIG_DESCRIPTIONS.get(key, "")
|
|
289
|
+
if description:
|
|
290
|
+
f.write(f"# {description}\n")
|
|
291
|
+
f.write(f"{key}={value}\n\n")
|
|
292
|
+
|
|
293
|
+
console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
|
|
294
|
+
except Exception as e:
|
|
295
|
+
console.print(f"[red]Error saving config: {e}[/red]")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
|
|
299
|
+
"""
|
|
300
|
+
Validate a configuration value.
|
|
301
|
+
Returns (is_valid, error_message)
|
|
302
|
+
"""
|
|
303
|
+
# Skip validation for database credentials (they have their own validation elsewhere)
|
|
304
|
+
if key in DATABASE_CREDENTIAL_KEYS:
|
|
305
|
+
return True, None
|
|
306
|
+
|
|
307
|
+
# Strip quotes that might be in the value
|
|
308
|
+
value = value.strip().strip("'\"")
|
|
309
|
+
|
|
310
|
+
# Check if key exists
|
|
311
|
+
if key not in DEFAULT_CONFIG:
|
|
312
|
+
available_keys = ", ".join(sorted(DEFAULT_CONFIG.keys()))
|
|
313
|
+
return False, f"Unknown config key: {key}. Available keys: {available_keys}"
|
|
314
|
+
|
|
315
|
+
# Validate against specific validators if they exist
|
|
316
|
+
if key in CONFIG_VALIDATORS:
|
|
317
|
+
valid_values = CONFIG_VALIDATORS[key]
|
|
318
|
+
if value.lower() not in [v.lower() for v in valid_values]:
|
|
319
|
+
return False, f"Invalid value for {key}. Must be one of: {', '.join(valid_values)}"
|
|
320
|
+
|
|
321
|
+
# Specific validation for numeric values
|
|
322
|
+
if key == "MAX_FILE_SIZE_MB":
|
|
323
|
+
try:
|
|
324
|
+
size = int(value)
|
|
325
|
+
if size <= 0:
|
|
326
|
+
return False, "MAX_FILE_SIZE_MB must be a positive number"
|
|
327
|
+
except ValueError:
|
|
328
|
+
return False, "MAX_FILE_SIZE_MB must be a number"
|
|
329
|
+
|
|
330
|
+
if key == "COMPLEXITY_THRESHOLD":
|
|
331
|
+
try:
|
|
332
|
+
threshold = int(value)
|
|
333
|
+
if threshold <= 0:
|
|
334
|
+
return False, "COMPLEXITY_THRESHOLD must be a positive number"
|
|
335
|
+
except ValueError:
|
|
336
|
+
return False, "COMPLEXITY_THRESHOLD must be a number"
|
|
337
|
+
|
|
338
|
+
if key == "PARALLEL_WORKERS":
|
|
339
|
+
try:
|
|
340
|
+
workers = int(value)
|
|
341
|
+
if workers <= 0 or workers > 32:
|
|
342
|
+
return False, "PARALLEL_WORKERS must be between 1 and 32"
|
|
343
|
+
except ValueError:
|
|
344
|
+
return False, "PARALLEL_WORKERS must be a number"
|
|
345
|
+
|
|
346
|
+
if key == "MAX_DEPTH":
|
|
347
|
+
if value.lower() != "unlimited":
|
|
348
|
+
try:
|
|
349
|
+
depth = int(value)
|
|
350
|
+
if depth <= 0:
|
|
351
|
+
return False, "MAX_DEPTH must be 'unlimited' or a positive number"
|
|
352
|
+
except ValueError:
|
|
353
|
+
return False, "MAX_DEPTH must be 'unlimited' or a number"
|
|
354
|
+
|
|
355
|
+
if key in ("LOG_FILE_PATH", "DEBUG_LOG_PATH"):
|
|
356
|
+
# Validate path is writable
|
|
357
|
+
log_path = Path(value)
|
|
358
|
+
try:
|
|
359
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
return False, f"Cannot create log directory: {e}"
|
|
362
|
+
|
|
363
|
+
if key in ("FALKORDB_PATH", "FALKORDB_SOCKET_PATH"):
|
|
364
|
+
# Validate path is writable
|
|
365
|
+
db_path = Path(value)
|
|
366
|
+
try:
|
|
367
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
return False, f"Cannot create directory for {key}: {e}"
|
|
370
|
+
|
|
371
|
+
# Check if parent directory is writable
|
|
372
|
+
if not os.access(db_path.parent, os.W_OK):
|
|
373
|
+
return False, f"Directory {db_path.parent} is not writable"
|
|
374
|
+
|
|
375
|
+
return True, None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_config_value(key: str) -> Optional[str]:
|
|
379
|
+
"""Get a specific configuration value."""
|
|
380
|
+
config = load_config()
|
|
381
|
+
return config.get(key)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def set_config_value(key: str, value: str) -> bool:
|
|
385
|
+
"""Set a configuration value. Returns True if successful.
|
|
386
|
+
|
|
387
|
+
The special key ``mode`` is delegated to :func:`set_context_mode` so
|
|
388
|
+
that ``cgc config set mode named`` works as expected.
|
|
389
|
+
"""
|
|
390
|
+
if key.lower() == "mode":
|
|
391
|
+
return set_context_mode(value.lower())
|
|
392
|
+
|
|
393
|
+
# Ensure config directory exists
|
|
394
|
+
ensure_config_dir()
|
|
395
|
+
|
|
396
|
+
# Validate
|
|
397
|
+
is_valid, error_msg = validate_config_value(key, value)
|
|
398
|
+
if not is_valid:
|
|
399
|
+
console.print(f"[red]❌ {error_msg}[/red]")
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
# Load, update, and save
|
|
403
|
+
config = load_config()
|
|
404
|
+
config[key] = value
|
|
405
|
+
save_config(config)
|
|
406
|
+
|
|
407
|
+
console.print(f"[green]✅ Set {key} = {value}[/green]")
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def reset_config():
|
|
412
|
+
"""Reset configuration to defaults (preserves database credentials)."""
|
|
413
|
+
ensure_config_dir()
|
|
414
|
+
save_config(DEFAULT_CONFIG.copy(), preserve_db_credentials=True)
|
|
415
|
+
console.print("[green]✅ Configuration reset to defaults[/green]")
|
|
416
|
+
console.print("[cyan]Note: Database credentials were preserved[/cyan]")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
_FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_done"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _print_welcome_banner() -> None:
|
|
424
|
+
"""Print a one-time welcome message explaining the context system."""
|
|
425
|
+
console.print()
|
|
426
|
+
console.print("[bold green]Welcome to CodeGraphContext![/bold green]")
|
|
427
|
+
console.print()
|
|
428
|
+
console.print("CGC organises your code graphs using [bold]contexts[/bold]:")
|
|
429
|
+
console.print(" [cyan]global[/cyan] - One shared graph for all projects (default)")
|
|
430
|
+
console.print(" [cyan]per-repo[/cyan] - Each repo gets its own .codegraphcontext/ folder")
|
|
431
|
+
console.print(" [cyan]named[/cyan] - Create named workspaces (e.g. cgc index . --context MyProject)")
|
|
432
|
+
console.print()
|
|
433
|
+
console.print("Switch modes anytime: [dim]cgc context mode <global|per-repo|named>[/dim]")
|
|
434
|
+
console.print("Or: [dim]cgc config set mode <global|per-repo|named>[/dim]")
|
|
435
|
+
console.print()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def ensure_first_run_bootstrap() -> bool:
|
|
439
|
+
"""Run one-time setup for brand-new installs.
|
|
440
|
+
|
|
441
|
+
Creates default config files, the global .cgcignore, and prints a
|
|
442
|
+
welcome banner. Returns True when bootstrap was performed.
|
|
443
|
+
"""
|
|
444
|
+
if _FIRST_RUN_MARKER.exists():
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
ensure_config_dir()
|
|
448
|
+
ensure_global_cgcignore()
|
|
449
|
+
# Ensure config.yaml exists (triggers creation with defaults)
|
|
450
|
+
load_context_config()
|
|
451
|
+
_print_welcome_banner()
|
|
452
|
+
|
|
453
|
+
# Stamp so we don't repeat
|
|
454
|
+
_FIRST_RUN_MARKER.parent.mkdir(parents=True, exist_ok=True)
|
|
455
|
+
_FIRST_RUN_MARKER.write_text("1")
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def ensure_config_file():
|
|
460
|
+
"""
|
|
461
|
+
Create default .env config file on first run if it does not exist.
|
|
462
|
+
"""
|
|
463
|
+
ensure_config_dir()
|
|
464
|
+
|
|
465
|
+
if CONFIG_FILE.exists():
|
|
466
|
+
return False # file already exists
|
|
467
|
+
|
|
468
|
+
save_config(DEFAULT_CONFIG.copy(), preserve_db_credentials=False)
|
|
469
|
+
return True # file was created
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def show_config():
|
|
475
|
+
"""Display current configuration in a nice table."""
|
|
476
|
+
created = ensure_config_file()
|
|
477
|
+
if created:
|
|
478
|
+
console.print(
|
|
479
|
+
f"[green]🆕 Created default configuration at {CONFIG_FILE}[/green]\n"
|
|
480
|
+
)
|
|
481
|
+
config = load_config()
|
|
482
|
+
|
|
483
|
+
# Separate database credentials from configuration
|
|
484
|
+
db_creds = {k: v for k, v in config.items() if k in DATABASE_CREDENTIAL_KEYS}
|
|
485
|
+
config_settings = {k: v for k, v in config.items() if k not in DATABASE_CREDENTIAL_KEYS}
|
|
486
|
+
|
|
487
|
+
# Show database credentials if they exist
|
|
488
|
+
if db_creds:
|
|
489
|
+
console.print("\n[bold cyan]Database Credentials[/bold cyan]")
|
|
490
|
+
db_table = Table(show_header=True, header_style="bold magenta")
|
|
491
|
+
db_table.add_column("Credential", style="cyan", width=20)
|
|
492
|
+
db_table.add_column("Value", style="green", width=30)
|
|
493
|
+
|
|
494
|
+
for key in sorted(db_creds.keys()):
|
|
495
|
+
value = db_creds[key]
|
|
496
|
+
# Mask password
|
|
497
|
+
if "PASSWORD" in key:
|
|
498
|
+
value = "********" if value else "(not set)"
|
|
499
|
+
db_table.add_row(key, value)
|
|
500
|
+
|
|
501
|
+
console.print(db_table)
|
|
502
|
+
|
|
503
|
+
# Show configuration settings
|
|
504
|
+
console.print("\n[bold cyan]Configuration Settings[/bold cyan]")
|
|
505
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
506
|
+
table.add_column("Setting", style="cyan", width=25)
|
|
507
|
+
table.add_column("Value", style="green", width=20)
|
|
508
|
+
table.add_column("Description", style="dim", width=50)
|
|
509
|
+
|
|
510
|
+
for key in sorted(config_settings.keys()):
|
|
511
|
+
value = config_settings[key]
|
|
512
|
+
description = CONFIG_DESCRIPTIONS.get(key, "")
|
|
513
|
+
|
|
514
|
+
# Highlight non-default values
|
|
515
|
+
if value != DEFAULT_CONFIG.get(key):
|
|
516
|
+
value_style = "[bold yellow]" + value + "[/bold yellow]"
|
|
517
|
+
else:
|
|
518
|
+
value_style = value
|
|
519
|
+
|
|
520
|
+
table.add_row(key, value_style, description)
|
|
521
|
+
|
|
522
|
+
console.print(table)
|
|
523
|
+
console.print(f"\n[cyan]Config file: {CONFIG_FILE}[/cyan]")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# =============================================================================
|
|
527
|
+
# CONTEXT SYSTEM (config.yaml)
|
|
528
|
+
# =============================================================================
|
|
529
|
+
|
|
530
|
+
CONTEXT_CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
531
|
+
_LEGACY_CONTEXT_CONFIG_FILE = CONFIG_DIR / "cgc_config.yaml"
|
|
532
|
+
|
|
533
|
+
# Valid mode values
|
|
534
|
+
VALID_MODES = ["global", "per-repo", "named"]
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@dataclass
|
|
538
|
+
class ContextInfo:
|
|
539
|
+
"""Metadata for a single named context."""
|
|
540
|
+
name: str
|
|
541
|
+
database: str = "falkordb" # neo4j | falkordb | kuzudb
|
|
542
|
+
db_path: str = "" # resolved at init if empty
|
|
543
|
+
repos: List[str] = field(default_factory=list)
|
|
544
|
+
cgcignore_path: str = "" # resolved at init if empty
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@dataclass
|
|
548
|
+
class ContextConfig:
|
|
549
|
+
"""Top-level structure stored in ~/.codegraphcontext/config.yaml."""
|
|
550
|
+
version: int = 1
|
|
551
|
+
mode: str = "global" # global | per-repo | named
|
|
552
|
+
default_context: str = "" # used when mode=named and no --context flag
|
|
553
|
+
contexts: Dict[str, ContextInfo] = field(default_factory=dict)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _default_db_path(context_name: str, database: str) -> str:
|
|
557
|
+
"""Return the canonical DB path for a named context."""
|
|
558
|
+
return str(CONFIG_DIR / "contexts" / context_name / "db" / database)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
_LEGACY_FALKORDB_PATH = CONFIG_DIR / "global" / "falkordb.db"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _default_global_db_path(database: str) -> str:
|
|
565
|
+
"""Return the canonical DB path for the global context.
|
|
566
|
+
|
|
567
|
+
New layout: ``~/.codegraphcontext/global/db/<backend>/``
|
|
568
|
+
For backward-compat, if the legacy flat path exists we keep using it.
|
|
569
|
+
"""
|
|
570
|
+
if database == "falkordb" and _LEGACY_FALKORDB_PATH.exists():
|
|
571
|
+
return str(_LEGACY_FALKORDB_PATH)
|
|
572
|
+
return str(CONFIG_DIR / "global" / "db" / database)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _migrate_legacy_config_yaml() -> None:
|
|
576
|
+
"""Rename cgc_config.yaml -> config.yaml if the old name exists and the new one does not."""
|
|
577
|
+
if _LEGACY_CONTEXT_CONFIG_FILE.exists() and not CONTEXT_CONFIG_FILE.exists():
|
|
578
|
+
import shutil
|
|
579
|
+
shutil.copy2(_LEGACY_CONTEXT_CONFIG_FILE, CONTEXT_CONFIG_FILE)
|
|
580
|
+
console.print(f"[dim]Migrated {_LEGACY_CONTEXT_CONFIG_FILE.name} -> {CONTEXT_CONFIG_FILE.name}[/dim]")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def load_context_config() -> ContextConfig:
|
|
584
|
+
"""
|
|
585
|
+
Load ~/.codegraphcontext/config.yaml.
|
|
586
|
+
Returns a ContextConfig with defaults if the file does not exist.
|
|
587
|
+
"""
|
|
588
|
+
_migrate_legacy_config_yaml()
|
|
589
|
+
|
|
590
|
+
if not CONTEXT_CONFIG_FILE.exists():
|
|
591
|
+
cfg = ContextConfig()
|
|
592
|
+
save_context_config(cfg)
|
|
593
|
+
return cfg
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
with open(CONTEXT_CONFIG_FILE, "r") as f:
|
|
597
|
+
raw = yaml.safe_load(f) or {}
|
|
598
|
+
|
|
599
|
+
contexts: Dict[str, ContextInfo] = {}
|
|
600
|
+
for name, meta in raw.get("contexts", {}).items():
|
|
601
|
+
meta = meta or {}
|
|
602
|
+
db = meta.get("database", "falkordb")
|
|
603
|
+
ctx = ContextInfo(
|
|
604
|
+
name=name,
|
|
605
|
+
database=db,
|
|
606
|
+
db_path=meta.get("db_path") or _default_db_path(name, db),
|
|
607
|
+
repos=[str(r) for r in meta.get("repos", [])],
|
|
608
|
+
cgcignore_path=meta.get("cgcignore_path") or str(
|
|
609
|
+
CONFIG_DIR / "contexts" / name / ".cgcignore"
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
contexts[name] = ctx
|
|
613
|
+
|
|
614
|
+
return ContextConfig(
|
|
615
|
+
version=raw.get("version", 1),
|
|
616
|
+
mode=raw.get("mode", "global"),
|
|
617
|
+
default_context=raw.get("default_context", ""),
|
|
618
|
+
contexts=contexts,
|
|
619
|
+
)
|
|
620
|
+
except Exception as e:
|
|
621
|
+
console.print(f"[yellow]Warning: Could not load config.yaml: {e}. Using defaults.[/yellow]")
|
|
622
|
+
return ContextConfig()
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def save_context_config(cfg: ContextConfig) -> None:
|
|
626
|
+
"""Persist ContextConfig to ~/.codegraphcontext/config.yaml."""
|
|
627
|
+
ensure_config_dir()
|
|
628
|
+
|
|
629
|
+
contexts_raw: Dict[str, Any] = {}
|
|
630
|
+
for name, ctx in cfg.contexts.items():
|
|
631
|
+
contexts_raw[name] = {
|
|
632
|
+
"database": ctx.database,
|
|
633
|
+
"db_path": ctx.db_path,
|
|
634
|
+
"repos": ctx.repos,
|
|
635
|
+
"cgcignore_path": ctx.cgcignore_path,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
raw = {
|
|
639
|
+
"version": cfg.version,
|
|
640
|
+
"mode": cfg.mode,
|
|
641
|
+
"default_context": cfg.default_context,
|
|
642
|
+
"contexts": contexts_raw,
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
with open(CONTEXT_CONFIG_FILE, "w") as f:
|
|
647
|
+
yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
console.print(f"[red]Error saving config.yaml: {e}[/red]")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# ---------------------------------------------------------------------------
|
|
653
|
+
# Context Resolution
|
|
654
|
+
# ---------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
@dataclass
|
|
657
|
+
class ResolvedContext:
|
|
658
|
+
"""Result of resolve_context() — everything needed to instantiate the DB."""
|
|
659
|
+
mode: str # global | per-repo | named
|
|
660
|
+
context_name: str # empty for global / per-repo
|
|
661
|
+
database: str # neo4j | falkordb | kuzudb
|
|
662
|
+
db_path: str # absolute path to the DB directory
|
|
663
|
+
cgcignore_path: str # path to the applicable .cgcignore
|
|
664
|
+
is_local: bool = False # True when a local .codegraphcontext/ was found
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def find_local_cgc_dir(start: Optional[Path] = None) -> Optional[Path]:
|
|
668
|
+
"""
|
|
669
|
+
Check *start* (default: cwd) for a ``.codegraphcontext/`` directory that
|
|
670
|
+
belongs to a repo (i.e. is NOT the global ``~/.codegraphcontext``).
|
|
671
|
+
Returns the directory path or None.
|
|
672
|
+
"""
|
|
673
|
+
current = start or Path.cwd()
|
|
674
|
+
global_dir = CONFIG_DIR.resolve()
|
|
675
|
+
|
|
676
|
+
candidate = current / ".codegraphcontext"
|
|
677
|
+
if candidate.exists() and candidate.resolve() != global_dir:
|
|
678
|
+
return candidate
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def resolve_context(
|
|
683
|
+
cli_context: Optional[str] = None,
|
|
684
|
+
cwd: Optional[Path] = None,
|
|
685
|
+
) -> ResolvedContext:
|
|
686
|
+
"""
|
|
687
|
+
Determine which context / DB to use.
|
|
688
|
+
|
|
689
|
+
Resolution order (highest priority first):
|
|
690
|
+
1. --context <name> CLI flag
|
|
691
|
+
2. Local .codegraphcontext/ directory (per-repo)
|
|
692
|
+
3. Global config.yaml: mode + default_context
|
|
693
|
+
4. Ultimate fallback: global mode, default DB
|
|
694
|
+
"""
|
|
695
|
+
cfg = load_context_config()
|
|
696
|
+
cwd = cwd or Path.cwd()
|
|
697
|
+
|
|
698
|
+
# --- 1. Explicit CLI flag ---
|
|
699
|
+
if cli_context:
|
|
700
|
+
ctx = cfg.contexts.get(cli_context)
|
|
701
|
+
db = ctx.database if ctx else "falkordb"
|
|
702
|
+
db_path = ctx.db_path if ctx else _default_db_path(cli_context, db)
|
|
703
|
+
cgcignore = (
|
|
704
|
+
ctx.cgcignore_path
|
|
705
|
+
if ctx
|
|
706
|
+
else str(CONFIG_DIR / "contexts" / cli_context / ".cgcignore")
|
|
707
|
+
)
|
|
708
|
+
return ResolvedContext(
|
|
709
|
+
mode="named",
|
|
710
|
+
context_name=cli_context,
|
|
711
|
+
database=db,
|
|
712
|
+
db_path=db_path,
|
|
713
|
+
cgcignore_path=cgcignore,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# --- 2. Local .codegraphcontext/ in repo ---
|
|
717
|
+
local_cgc = find_local_cgc_dir(cwd)
|
|
718
|
+
|
|
719
|
+
# If we are in per-repo mode and no local folder was found, create it in CWD
|
|
720
|
+
if local_cgc is None and cfg.mode == "per-repo":
|
|
721
|
+
local_cgc = cwd / ".codegraphcontext"
|
|
722
|
+
local_cgc.mkdir(parents=True, exist_ok=True)
|
|
723
|
+
(local_cgc / "db").mkdir(exist_ok=True)
|
|
724
|
+
|
|
725
|
+
# Copy global .env into local context for easy per-repo tweaking
|
|
726
|
+
import shutil
|
|
727
|
+
if CONFIG_FILE.exists():
|
|
728
|
+
shutil.copy2(CONFIG_FILE, local_cgc / ".env")
|
|
729
|
+
|
|
730
|
+
console.print(f"[dim]Auto-initialized per-repo context at {local_cgc}[/dim]")
|
|
731
|
+
|
|
732
|
+
if local_cgc is not None:
|
|
733
|
+
# Read local config.yaml if present
|
|
734
|
+
local_yaml = local_cgc / "config.yaml"
|
|
735
|
+
local_db = "falkordb"
|
|
736
|
+
if local_yaml.exists():
|
|
737
|
+
try:
|
|
738
|
+
with open(local_yaml) as f:
|
|
739
|
+
local_raw = yaml.safe_load(f) or {}
|
|
740
|
+
local_db = local_raw.get("database", "falkordb")
|
|
741
|
+
except Exception:
|
|
742
|
+
pass
|
|
743
|
+
db_path = str(local_cgc / "db" / local_db)
|
|
744
|
+
cgcignore = str(local_cgc / ".cgcignore")
|
|
745
|
+
return ResolvedContext(
|
|
746
|
+
mode="per-repo",
|
|
747
|
+
context_name="",
|
|
748
|
+
database=local_db,
|
|
749
|
+
db_path=db_path,
|
|
750
|
+
cgcignore_path=cgcignore,
|
|
751
|
+
is_local=True,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# --- 2b. Saved workspace mapping (CWD -> child .codegraphcontext/) ---
|
|
755
|
+
mapping = get_workspace_mapping(cwd)
|
|
756
|
+
if mapping:
|
|
757
|
+
mapped_ctx_path = Path(mapping["context_path"])
|
|
758
|
+
if mapped_ctx_path.exists() and mapped_ctx_path.is_dir():
|
|
759
|
+
mapped_db = mapping.get("database", "falkordb")
|
|
760
|
+
return ResolvedContext(
|
|
761
|
+
mode="per-repo",
|
|
762
|
+
context_name="",
|
|
763
|
+
database=mapped_db,
|
|
764
|
+
db_path=str(mapped_ctx_path / "db" / mapped_db),
|
|
765
|
+
cgcignore_path=str(mapped_ctx_path / ".cgcignore"),
|
|
766
|
+
is_local=True,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# --- 3. Global config.yaml ---
|
|
770
|
+
if cfg.mode == "named":
|
|
771
|
+
ctx_name = cfg.default_context
|
|
772
|
+
ctx = cfg.contexts.get(ctx_name) if ctx_name else None
|
|
773
|
+
db = ctx.database if ctx else "falkordb"
|
|
774
|
+
db_path = ctx.db_path if ctx else _default_db_path(ctx_name, db) if ctx_name else ""
|
|
775
|
+
if not db_path:
|
|
776
|
+
# No default context set — fall through to global
|
|
777
|
+
pass
|
|
778
|
+
else:
|
|
779
|
+
cgcignore = (
|
|
780
|
+
ctx.cgcignore_path
|
|
781
|
+
if ctx
|
|
782
|
+
else str(CONFIG_DIR / "contexts" / ctx_name / ".cgcignore")
|
|
783
|
+
)
|
|
784
|
+
return ResolvedContext(
|
|
785
|
+
mode="named",
|
|
786
|
+
context_name=ctx_name,
|
|
787
|
+
database=db,
|
|
788
|
+
db_path=db_path,
|
|
789
|
+
cgcignore_path=cgcignore,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
# --- 4. Global fallback ---
|
|
793
|
+
db = load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
794
|
+
return ResolvedContext(
|
|
795
|
+
mode="global",
|
|
796
|
+
context_name="",
|
|
797
|
+
database=db,
|
|
798
|
+
db_path=_default_global_db_path(db),
|
|
799
|
+
cgcignore_path=str(CONFIG_DIR / "global" / ".cgcignore"),
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# ---------------------------------------------------------------------------
|
|
804
|
+
# Context CRUD helpers
|
|
805
|
+
# ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
def create_context(
|
|
808
|
+
name: str,
|
|
809
|
+
database: str = "falkordb",
|
|
810
|
+
db_path: Optional[str] = None,
|
|
811
|
+
) -> bool:
|
|
812
|
+
"""Create a new named context. Returns True on success."""
|
|
813
|
+
cfg = load_context_config()
|
|
814
|
+
if name in cfg.contexts:
|
|
815
|
+
console.print(f"[yellow]Context '{name}' already exists.[/yellow]")
|
|
816
|
+
return False
|
|
817
|
+
|
|
818
|
+
resolved_db_path = db_path or _default_db_path(name, database)
|
|
819
|
+
cgcignore = str(CONFIG_DIR / "contexts" / name / ".cgcignore")
|
|
820
|
+
|
|
821
|
+
# Ensure the context directories exist (create parent of db_path so DBs can create their files/dirs)
|
|
822
|
+
Path(resolved_db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
823
|
+
Path(cgcignore).parent.mkdir(parents=True, exist_ok=True)
|
|
824
|
+
|
|
825
|
+
cfg.contexts[name] = ContextInfo(
|
|
826
|
+
name=name,
|
|
827
|
+
database=database,
|
|
828
|
+
db_path=resolved_db_path,
|
|
829
|
+
repos=[],
|
|
830
|
+
cgcignore_path=cgcignore,
|
|
831
|
+
)
|
|
832
|
+
save_context_config(cfg)
|
|
833
|
+
console.print(f"[green]✅ Created context '{name}' (DB: {database})[/green]")
|
|
834
|
+
console.print(f" [dim]DB path: {resolved_db_path}[/dim]")
|
|
835
|
+
return True
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def delete_context(name: str) -> bool:
|
|
839
|
+
"""Delete a named context from the registry. Returns True on success."""
|
|
840
|
+
cfg = load_context_config()
|
|
841
|
+
if name not in cfg.contexts:
|
|
842
|
+
console.print(f"[red]Context '{name}' not found.[/red]")
|
|
843
|
+
return False
|
|
844
|
+
del cfg.contexts[name]
|
|
845
|
+
if cfg.default_context == name:
|
|
846
|
+
cfg.default_context = ""
|
|
847
|
+
save_context_config(cfg)
|
|
848
|
+
console.print(f"[green]✅ Deleted context '{name}'[/green]")
|
|
849
|
+
console.print("[dim]Note: DB files were NOT deleted. Remove manually if needed.[/dim]")
|
|
850
|
+
return True
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def register_repo_in_context(context_name: str, repo_path: str, auto_create: bool = False) -> bool:
|
|
854
|
+
"""Add a repo path to a named context (idempotent).
|
|
855
|
+
|
|
856
|
+
When *auto_create* is True the context is silently created with default
|
|
857
|
+
settings if it does not yet exist, matching the UX of
|
|
858
|
+
``cgc index ./frontend --context ProjectAB`` "just working".
|
|
859
|
+
"""
|
|
860
|
+
cfg = load_context_config()
|
|
861
|
+
ctx = cfg.contexts.get(context_name)
|
|
862
|
+
if ctx is None:
|
|
863
|
+
if not auto_create:
|
|
864
|
+
console.print(f"[red]Context '{context_name}' not found. Create it first with 'cgc context create {context_name}'.[/red]")
|
|
865
|
+
return False
|
|
866
|
+
create_context(context_name)
|
|
867
|
+
cfg = load_context_config()
|
|
868
|
+
ctx = cfg.contexts.get(context_name)
|
|
869
|
+
if ctx is None:
|
|
870
|
+
return False
|
|
871
|
+
resolved = str(Path(repo_path).resolve())
|
|
872
|
+
if resolved not in ctx.repos:
|
|
873
|
+
ctx.repos.append(resolved)
|
|
874
|
+
save_context_config(cfg)
|
|
875
|
+
return True
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def set_context_mode(mode: str) -> bool:
|
|
879
|
+
"""Set the global CGC mode. Returns True on success."""
|
|
880
|
+
if mode not in VALID_MODES:
|
|
881
|
+
console.print(f"[red]Invalid mode '{mode}'. Must be one of: {', '.join(VALID_MODES)}[/red]")
|
|
882
|
+
return False
|
|
883
|
+
cfg = load_context_config()
|
|
884
|
+
cfg.mode = mode
|
|
885
|
+
save_context_config(cfg)
|
|
886
|
+
console.print(f"[green]✅ Mode set to '{mode}'[/green]")
|
|
887
|
+
return True
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def set_default_context(name: str) -> bool:
|
|
891
|
+
"""Set the default named context used when no --context flag is given."""
|
|
892
|
+
cfg = load_context_config()
|
|
893
|
+
if name and name not in cfg.contexts:
|
|
894
|
+
console.print(f"[red]Context '{name}' not found. Create it first.[/red]")
|
|
895
|
+
return False
|
|
896
|
+
cfg.default_context = name
|
|
897
|
+
save_context_config(cfg)
|
|
898
|
+
console.print(f"[green]✅ Default context set to '{name}'[/green]")
|
|
899
|
+
return True
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def list_contexts() -> List[ContextInfo]:
|
|
903
|
+
"""Return all named contexts."""
|
|
904
|
+
cfg = load_context_config()
|
|
905
|
+
return list(cfg.contexts.values())
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
# =============================================================================
|
|
909
|
+
# CHILD CONTEXT DISCOVERY
|
|
910
|
+
# =============================================================================
|
|
911
|
+
|
|
912
|
+
@dataclass
|
|
913
|
+
class DiscoveredContext:
|
|
914
|
+
"""A .codegraphcontext folder found in a child directory."""
|
|
915
|
+
path: str # absolute path to the parent repo directory
|
|
916
|
+
cgc_path: str # absolute path to the .codegraphcontext directory
|
|
917
|
+
repo_name: str # name of the parent directory
|
|
918
|
+
database: str # backend from local config.yaml, or default
|
|
919
|
+
db_path: str # resolved db path
|
|
920
|
+
cgcignore_path: str # path to .cgcignore if present
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def discover_child_contexts(
|
|
924
|
+
start: Optional[Path] = None,
|
|
925
|
+
max_depth: int = 1,
|
|
926
|
+
) -> List[DiscoveredContext]:
|
|
927
|
+
"""Walk child directories of *start* up to *max_depth* levels looking for
|
|
928
|
+
``.codegraphcontext/`` folders that represent per-repo databases.
|
|
929
|
+
|
|
930
|
+
Returns a list of :class:`DiscoveredContext` for each match found.
|
|
931
|
+
The global ``~/.codegraphcontext`` is always excluded.
|
|
932
|
+
"""
|
|
933
|
+
start = (start or Path.cwd()).resolve()
|
|
934
|
+
global_dir = CONFIG_DIR.resolve()
|
|
935
|
+
results: List[DiscoveredContext] = []
|
|
936
|
+
|
|
937
|
+
def _scan(directory: Path, depth: int) -> None:
|
|
938
|
+
if depth > max_depth:
|
|
939
|
+
return
|
|
940
|
+
try:
|
|
941
|
+
entries = sorted(directory.iterdir())
|
|
942
|
+
except PermissionError:
|
|
943
|
+
return
|
|
944
|
+
for entry in entries:
|
|
945
|
+
if not entry.is_dir() or entry.name.startswith("."):
|
|
946
|
+
continue
|
|
947
|
+
candidate = entry / ".codegraphcontext"
|
|
948
|
+
if candidate.exists() and candidate.is_dir() and candidate.resolve() != global_dir:
|
|
949
|
+
local_db = "falkordb"
|
|
950
|
+
local_yaml = candidate / "config.yaml"
|
|
951
|
+
if local_yaml.exists():
|
|
952
|
+
try:
|
|
953
|
+
with open(local_yaml) as f:
|
|
954
|
+
raw = yaml.safe_load(f) or {}
|
|
955
|
+
local_db = raw.get("database", "falkordb")
|
|
956
|
+
except Exception:
|
|
957
|
+
pass
|
|
958
|
+
results.append(DiscoveredContext(
|
|
959
|
+
path=str(entry),
|
|
960
|
+
cgc_path=str(candidate),
|
|
961
|
+
repo_name=entry.name,
|
|
962
|
+
database=local_db,
|
|
963
|
+
db_path=str(candidate / "db" / local_db),
|
|
964
|
+
cgcignore_path=str(candidate / ".cgcignore"),
|
|
965
|
+
))
|
|
966
|
+
if depth < max_depth:
|
|
967
|
+
_scan(entry, depth + 1)
|
|
968
|
+
|
|
969
|
+
_scan(start, 1)
|
|
970
|
+
return results
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
# =============================================================================
|
|
974
|
+
# WORKSPACE MAPPINGS (global persistence of CWD -> context path)
|
|
975
|
+
# =============================================================================
|
|
976
|
+
|
|
977
|
+
def _load_workspace_mappings() -> Dict[str, Dict[str, str]]:
|
|
978
|
+
"""Load the ``workspace_mappings`` section from config.yaml."""
|
|
979
|
+
if not CONTEXT_CONFIG_FILE.exists():
|
|
980
|
+
return {}
|
|
981
|
+
try:
|
|
982
|
+
with open(CONTEXT_CONFIG_FILE, "r") as f:
|
|
983
|
+
raw = yaml.safe_load(f) or {}
|
|
984
|
+
return raw.get("workspace_mappings", {}) or {}
|
|
985
|
+
except Exception:
|
|
986
|
+
return {}
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
|
|
990
|
+
"""Write *mappings* back into the ``workspace_mappings`` key of config.yaml,
|
|
991
|
+
preserving all other keys."""
|
|
992
|
+
ensure_config_dir()
|
|
993
|
+
raw: Dict[str, Any] = {}
|
|
994
|
+
if CONTEXT_CONFIG_FILE.exists():
|
|
995
|
+
try:
|
|
996
|
+
with open(CONTEXT_CONFIG_FILE, "r") as f:
|
|
997
|
+
raw = yaml.safe_load(f) or {}
|
|
998
|
+
except Exception:
|
|
999
|
+
raw = {}
|
|
1000
|
+
raw["workspace_mappings"] = mappings
|
|
1001
|
+
try:
|
|
1002
|
+
with open(CONTEXT_CONFIG_FILE, "w") as f:
|
|
1003
|
+
yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
console.print(f"[red]Error saving workspace mappings: {e}[/red]")
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def get_workspace_mapping(cwd: Path) -> Optional[Dict[str, str]]:
|
|
1009
|
+
"""Look up a saved workspace mapping for *cwd*.
|
|
1010
|
+
|
|
1011
|
+
Returns a dict with ``context_path`` and ``database`` keys, or None.
|
|
1012
|
+
"""
|
|
1013
|
+
mappings = _load_workspace_mappings()
|
|
1014
|
+
return mappings.get(str(cwd.resolve()))
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def save_workspace_mapping(cwd: Path, context_path: Path) -> None:
|
|
1018
|
+
"""Persist an association from *cwd* to a ``.codegraphcontext`` directory."""
|
|
1019
|
+
context_path = context_path.resolve()
|
|
1020
|
+
local_db = "falkordb"
|
|
1021
|
+
local_yaml = context_path / "config.yaml"
|
|
1022
|
+
if local_yaml.exists():
|
|
1023
|
+
try:
|
|
1024
|
+
with open(local_yaml) as f:
|
|
1025
|
+
raw = yaml.safe_load(f) or {}
|
|
1026
|
+
local_db = raw.get("database", "falkordb")
|
|
1027
|
+
except Exception:
|
|
1028
|
+
pass
|
|
1029
|
+
|
|
1030
|
+
mappings = _load_workspace_mappings()
|
|
1031
|
+
mappings[str(cwd.resolve())] = {
|
|
1032
|
+
"context_path": str(context_path),
|
|
1033
|
+
"database": local_db,
|
|
1034
|
+
}
|
|
1035
|
+
_save_workspace_mappings(mappings)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def remove_workspace_mapping(cwd: Path) -> bool:
|
|
1039
|
+
"""Delete a saved workspace mapping. Returns True if one was removed."""
|
|
1040
|
+
mappings = _load_workspace_mappings()
|
|
1041
|
+
key = str(cwd.resolve())
|
|
1042
|
+
if key in mappings:
|
|
1043
|
+
del mappings[key]
|
|
1044
|
+
_save_workspace_mappings(mappings)
|
|
1045
|
+
return True
|
|
1046
|
+
return False
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def list_workspace_mappings() -> Dict[str, Dict[str, str]]:
|
|
1050
|
+
"""Return all saved workspace mappings."""
|
|
1051
|
+
return _load_workspace_mappings()
|