codegraph-engine 2.1.7__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +1 -1
- codegraph/cli.py +177 -54
- codegraph/cli_output.py +190 -0
- codegraph/context.py +119 -3
- codegraph/database/interrogation.py +30 -7
- codegraph/freshness.py +59 -21
- codegraph/indexing/indexer.py +11 -4
- codegraph/installer.py +72 -22
- codegraph/interrogation.py +19 -0
- codegraph/mcp/server.py +97 -6
- codegraph/mcp_diagnostics.py +5 -2
- codegraph/monorepo.py +1 -1
- codegraph/process_lifecycle.py +1115 -0
- codegraph/runtime/runner.py +204 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/METADATA +48 -23
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/RECORD +20 -17
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.2.0.dist-info}/top_level.txt +0 -0
codegraph/context.py
CHANGED
|
@@ -594,6 +594,7 @@ def _get_context_impl(
|
|
|
594
594
|
if path:
|
|
595
595
|
c = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
596
596
|
c.row_factory = sqlite3.Row
|
|
597
|
+
c.execute("PRAGMA busy_timeout = 15000")
|
|
597
598
|
try:
|
|
598
599
|
for query in queries:
|
|
599
600
|
for item in search(
|
|
@@ -629,6 +630,7 @@ def _get_context_impl(
|
|
|
629
630
|
if path:
|
|
630
631
|
c = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
631
632
|
c.row_factory = sqlite3.Row
|
|
633
|
+
c.execute("PRAGMA busy_timeout = 15000")
|
|
632
634
|
try:
|
|
633
635
|
return c.execute(
|
|
634
636
|
"SELECT route_path, http_method, handler_name, file_path, line, endpoint_id FROM framework_routes"
|
|
@@ -665,6 +667,7 @@ def _get_context_impl(
|
|
|
665
667
|
if path:
|
|
666
668
|
c = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
667
669
|
c.row_factory = sqlite3.Row
|
|
670
|
+
c.execute("PRAGMA busy_timeout = 15000")
|
|
668
671
|
try:
|
|
669
672
|
archs.append(get_architecture(c, repo))
|
|
670
673
|
finally:
|
|
@@ -708,6 +711,9 @@ def _get_context_impl(
|
|
|
708
711
|
|
|
709
712
|
stage1_candidate_count = len(candidate_dicts)
|
|
710
713
|
|
|
714
|
+
# Instantiate ConstraintGuard for hard safety invariants
|
|
715
|
+
_guard = ConstraintGuard.from_task_spec(task_spec)
|
|
716
|
+
|
|
711
717
|
# Process framework routes
|
|
712
718
|
route_relationships: list[Relationship] = []
|
|
713
719
|
entry_points_out: list[dict[str, object]] = []
|
|
@@ -717,16 +723,26 @@ def _get_context_impl(
|
|
|
717
723
|
r_path = r["route_path"]
|
|
718
724
|
r_method = r["http_method"]
|
|
719
725
|
ep_id = r["endpoint_id"] or f"{r_method} {r_path}"
|
|
726
|
+
r_segments = [
|
|
727
|
+
seg.lower()
|
|
728
|
+
for seg in str(r_path).split("/")
|
|
729
|
+
if len(seg) >= 4
|
|
730
|
+
and not seg.startswith("{")
|
|
731
|
+
and not seg.startswith("<")
|
|
732
|
+
and seg.lower() not in {"api", "v1", "v2"}
|
|
733
|
+
]
|
|
720
734
|
matches_task = (
|
|
721
735
|
r_path in task_display_str
|
|
722
736
|
or ep_id.lower() in task_display_str.lower()
|
|
723
737
|
or (r_method in task_display_str.upper() and r_path in task_display_str)
|
|
738
|
+
or task_spec.intent == "ARCHITECTURE"
|
|
739
|
+
or any(seg in task_display_str.lower() for seg in r_segments)
|
|
724
740
|
or any(
|
|
725
741
|
(len(tgt) >= 3 and (tgt.lower() == r["handler_name"].lower() or tgt.lower() in r_path.lower()))
|
|
726
742
|
for tgt in task_spec.targets
|
|
727
743
|
)
|
|
728
744
|
)
|
|
729
|
-
if matches_task:
|
|
745
|
+
if matches_task and _guard.allows_symbol(str(r["handler_name"])) and _guard.allows_file(str(r["file_path"])):
|
|
730
746
|
target_symbols.add(r["handler_name"])
|
|
731
747
|
r_line = r["line"] or 1
|
|
732
748
|
candidate_dicts.append(
|
|
@@ -799,8 +815,108 @@ def _get_context_impl(
|
|
|
799
815
|
except Exception:
|
|
800
816
|
pass
|
|
801
817
|
|
|
802
|
-
#
|
|
803
|
-
|
|
818
|
+
# Seed architectural boundary classes and gateway methods for ARCHITECTURE intent
|
|
819
|
+
if task_spec.intent == "ARCHITECTURE":
|
|
820
|
+
try:
|
|
821
|
+
arch_rows = con.execute(
|
|
822
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
823
|
+
"FROM symbols WHERE kind = 'class' OR name = 'app'"
|
|
824
|
+
).fetchall()
|
|
825
|
+
for a_row in arch_rows:
|
|
826
|
+
a_name = str(a_row["name"])
|
|
827
|
+
a_sym = str(a_row["qualified_name"] or a_name)
|
|
828
|
+
a_cid = str(a_row["canonical_id"] or "")
|
|
829
|
+
a_file = str(a_row["path"] or "")
|
|
830
|
+
if (
|
|
831
|
+
_guard.allows_symbol(a_sym)
|
|
832
|
+
and _guard.allows_symbol(a_name)
|
|
833
|
+
and (not a_cid or _guard.allows_canonical_id(a_cid))
|
|
834
|
+
and _guard.allows_file(a_file)
|
|
835
|
+
):
|
|
836
|
+
target_symbols.add(a_name)
|
|
837
|
+
target_symbols.add(a_sym)
|
|
838
|
+
candidate_dicts.append(
|
|
839
|
+
{
|
|
840
|
+
"file": a_file,
|
|
841
|
+
"symbol": a_name,
|
|
842
|
+
"canonical_id": a_cid,
|
|
843
|
+
"start_line": int(a_row["start_line"] or 1),
|
|
844
|
+
"end_line": int(a_row["end_line"] or 1),
|
|
845
|
+
"score": 0.90,
|
|
846
|
+
"relationship": "DEFINES",
|
|
847
|
+
"snippet": f"Architectural symbol {a_sym} ({a_row['kind']}) in {a_file}",
|
|
848
|
+
}
|
|
849
|
+
)
|
|
850
|
+
if "gateway" in task_display_str.lower():
|
|
851
|
+
gw_rows = con.execute(
|
|
852
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
853
|
+
"FROM symbols WHERE qualified_name LIKE '%Gateway.%'"
|
|
854
|
+
).fetchall()
|
|
855
|
+
for g_row in gw_rows:
|
|
856
|
+
g_name = str(g_row["name"])
|
|
857
|
+
g_sym = str(g_row["qualified_name"] or g_name)
|
|
858
|
+
g_cid = str(g_row["canonical_id"] or "")
|
|
859
|
+
g_file = str(g_row["path"] or "")
|
|
860
|
+
if (
|
|
861
|
+
_guard.allows_symbol(g_sym)
|
|
862
|
+
and _guard.allows_symbol(g_name)
|
|
863
|
+
and (not g_cid or _guard.allows_canonical_id(g_cid))
|
|
864
|
+
and _guard.allows_file(g_file)
|
|
865
|
+
):
|
|
866
|
+
target_symbols.add(g_name)
|
|
867
|
+
target_symbols.add(g_sym)
|
|
868
|
+
candidate_dicts.append(
|
|
869
|
+
{
|
|
870
|
+
"file": g_file,
|
|
871
|
+
"symbol": g_name,
|
|
872
|
+
"canonical_id": g_cid,
|
|
873
|
+
"start_line": int(g_row["start_line"] or 1),
|
|
874
|
+
"end_line": int(g_row["end_line"] or 1),
|
|
875
|
+
"score": 0.88,
|
|
876
|
+
"relationship": "DEFINES",
|
|
877
|
+
"snippet": f"Gateway method {g_sym} in {g_file}",
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
except Exception:
|
|
881
|
+
pass
|
|
882
|
+
|
|
883
|
+
# Seed direct methods of resolved class targets
|
|
884
|
+
for res in _target_resolutions.values():
|
|
885
|
+
cls_qname = res.qualified_name
|
|
886
|
+
if cls_qname and res.target_type.value == "CLASS":
|
|
887
|
+
try:
|
|
888
|
+
m_rows = con.execute(
|
|
889
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
890
|
+
"FROM symbols WHERE qualified_name LIKE ?",
|
|
891
|
+
(f"{cls_qname}.%",),
|
|
892
|
+
).fetchall()
|
|
893
|
+
for m_row in m_rows:
|
|
894
|
+
m_name = str(m_row["name"])
|
|
895
|
+
m_sym = str(m_row["qualified_name"] or m_name)
|
|
896
|
+
m_cid = str(m_row["canonical_id"] or "")
|
|
897
|
+
m_file = str(m_row["path"] or "")
|
|
898
|
+
if (
|
|
899
|
+
_guard.allows_symbol(m_sym)
|
|
900
|
+
and _guard.allows_symbol(m_name)
|
|
901
|
+
and (not m_cid or _guard.allows_canonical_id(m_cid))
|
|
902
|
+
and _guard.allows_file(m_file)
|
|
903
|
+
):
|
|
904
|
+
target_symbols.add(m_name)
|
|
905
|
+
target_symbols.add(m_sym)
|
|
906
|
+
candidate_dicts.append(
|
|
907
|
+
{
|
|
908
|
+
"file": m_file,
|
|
909
|
+
"symbol": m_name,
|
|
910
|
+
"canonical_id": m_cid,
|
|
911
|
+
"start_line": int(m_row["start_line"] or 1),
|
|
912
|
+
"end_line": int(m_row["end_line"] or 1),
|
|
913
|
+
"score": 0.88,
|
|
914
|
+
"relationship": "DEFINES",
|
|
915
|
+
"snippet": f"Method {m_sym} of target class {cls_qname}",
|
|
916
|
+
}
|
|
917
|
+
)
|
|
918
|
+
except Exception:
|
|
919
|
+
pass
|
|
804
920
|
|
|
805
921
|
# 5. Graph exploration (callers/callees) & Tests with deterministic seeds
|
|
806
922
|
t_graph_start = time.perf_counter()
|
|
@@ -748,6 +748,8 @@ def get_db_schema(
|
|
|
748
748
|
schema: str | None = None,
|
|
749
749
|
*,
|
|
750
750
|
dialect: str | None = None,
|
|
751
|
+
limit: int | None = None,
|
|
752
|
+
offset: int = 0,
|
|
751
753
|
) -> dict[str, Any]:
|
|
752
754
|
"""Return the complete repository database schema overview with safe credential redaction."""
|
|
753
755
|
if isinstance(repo_or_dialect, str) and not dialect:
|
|
@@ -800,21 +802,42 @@ def get_db_schema(
|
|
|
800
802
|
|
|
801
803
|
dialects = sorted({t["dialect"] for t in tables if t.get("dialect")}) or ["UNKNOWN"]
|
|
802
804
|
|
|
803
|
-
|
|
805
|
+
safe_tables = tables
|
|
806
|
+
safe_models = models
|
|
807
|
+
safe_migrations = migrations
|
|
808
|
+
safe_cols = columns
|
|
809
|
+
safe_rels = rels
|
|
810
|
+
|
|
811
|
+
if limit is not None:
|
|
812
|
+
safe_offset = max(0, offset)
|
|
813
|
+
safe_limit = max(0, limit)
|
|
814
|
+
safe_tables = tables[safe_offset : safe_offset + safe_limit]
|
|
815
|
+
safe_models = models[safe_offset : safe_offset + safe_limit]
|
|
816
|
+
safe_migrations = migrations[safe_offset : safe_offset + safe_limit]
|
|
817
|
+
safe_cols = columns[: max(safe_limit * 5, 20)]
|
|
818
|
+
safe_rels = rels[: max(safe_limit * 5, 20)]
|
|
819
|
+
|
|
820
|
+
result_dict: dict[str, Any] = {
|
|
804
821
|
"status": "ok",
|
|
805
822
|
"dialects": dialects,
|
|
806
823
|
"table_count": len(tables),
|
|
807
824
|
"column_count": len(columns),
|
|
808
825
|
"model_count": len(models),
|
|
809
826
|
"migration_count": len(migrations),
|
|
810
|
-
"tables":
|
|
811
|
-
"columns":
|
|
812
|
-
"models":
|
|
813
|
-
"relationships":
|
|
814
|
-
"migrations":
|
|
827
|
+
"tables": safe_tables,
|
|
828
|
+
"columns": safe_cols,
|
|
829
|
+
"models": safe_models,
|
|
830
|
+
"relationships": safe_rels,
|
|
831
|
+
"migrations": safe_migrations,
|
|
815
832
|
"env_variables": env_vars,
|
|
816
833
|
"database_credentials": "REDACTED",
|
|
817
|
-
}
|
|
834
|
+
}
|
|
835
|
+
if limit is not None:
|
|
836
|
+
result_dict["limit"] = safe_limit
|
|
837
|
+
result_dict["offset"] = safe_offset
|
|
838
|
+
result_dict["has_more"] = (safe_offset + len(safe_tables)) < len(tables)
|
|
839
|
+
|
|
840
|
+
return redact_payload(result_dict)
|
|
818
841
|
|
|
819
842
|
|
|
820
843
|
def get_db_impact(
|
codegraph/freshness.py
CHANGED
|
@@ -235,30 +235,68 @@ def check_freshness(
|
|
|
235
235
|
deleted: list[str] = []
|
|
236
236
|
parse_failed: list[str] = []
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
238
|
+
# Fast path 1: Git commit diff (branch switch or commit advance)
|
|
239
|
+
git_diff_handled = False
|
|
240
|
+
if cur_commit and idx_commit and cur_commit != idx_commit:
|
|
241
|
+
diff_out = _run_git(repository, "diff", "--name-status", idx_commit, cur_commit)
|
|
242
|
+
if diff_out is not None:
|
|
243
|
+
git_diff_handled = True
|
|
244
|
+
for line in diff_out.splitlines():
|
|
245
|
+
parts = line.strip().split(maxsplit=1)
|
|
246
|
+
if len(parts) == 2:
|
|
247
|
+
action, fpath = parts[0], parts[1]
|
|
248
|
+
if action.startswith("D"):
|
|
249
|
+
deleted.append(fpath)
|
|
250
|
+
else:
|
|
251
|
+
modified.append(fpath)
|
|
252
|
+
|
|
253
|
+
# Fast path 2: Git status when HEAD commit is unchanged
|
|
254
|
+
if not git_diff_handled and cur_commit and idx_commit and cur_commit == idx_commit:
|
|
255
|
+
status_out = _run_git(repository, "status", "--porcelain", "--untracked-files=no")
|
|
256
|
+
if status_out is not None and not status_out.strip():
|
|
257
|
+
# Working tree has zero tracked changes against HEAD
|
|
258
|
+
git_diff_handled = True
|
|
253
259
|
try:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
pf_rows = con.execute("SELECT path FROM files WHERE status='parse_failed'").fetchall()
|
|
261
|
+
for r in pf_rows:
|
|
262
|
+
parse_failed.append(str(r[0]))
|
|
263
|
+
modified.append(str(r[0]))
|
|
264
|
+
except sqlite3.OperationalError:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
if not git_diff_handled:
|
|
268
|
+
try:
|
|
269
|
+
rows = con.execute("SELECT path, hash, status, indexed_at FROM files").fetchall()
|
|
270
|
+
except sqlite3.OperationalError:
|
|
271
|
+
try:
|
|
272
|
+
rows = con.execute("SELECT path, hash, status, 0 AS indexed_at FROM files").fetchall()
|
|
273
|
+
except sqlite3.OperationalError:
|
|
274
|
+
rows = con.execute("SELECT path, hash, 'ok' AS status, 0 AS indexed_at FROM files").fetchall()
|
|
275
|
+
|
|
276
|
+
for row in rows:
|
|
277
|
+
rel = str(row["path"])
|
|
278
|
+
if row["status"] == "parse_failed":
|
|
279
|
+
parse_failed.append(rel)
|
|
260
280
|
if rel not in modified:
|
|
261
281
|
modified.append(rel)
|
|
282
|
+
abs_path = repository / rel
|
|
283
|
+
if not abs_path.exists():
|
|
284
|
+
deleted.append(rel)
|
|
285
|
+
else:
|
|
286
|
+
try:
|
|
287
|
+
st = abs_path.stat()
|
|
288
|
+
# Mtime check: skip reading & hashing files strictly older than indexing time
|
|
289
|
+
idx_at = int(row["indexed_at"] or 0)
|
|
290
|
+
if idx_at > 0 and int(st.st_mtime) < idx_at and row["status"] != "parse_failed":
|
|
291
|
+
continue
|
|
292
|
+
digest = hashlib.sha256(
|
|
293
|
+
abs_path.read_text(encoding="utf-8", errors="replace").encode()
|
|
294
|
+
).hexdigest()
|
|
295
|
+
if digest != row["hash"] and rel not in modified:
|
|
296
|
+
modified.append(rel)
|
|
297
|
+
except OSError:
|
|
298
|
+
if rel not in modified:
|
|
299
|
+
modified.append(rel)
|
|
262
300
|
|
|
263
301
|
changed = len(modified) + len(deleted)
|
|
264
302
|
if changed == 0 and not parse_failed and (idx_commit == cur_commit or cur_commit is None):
|
codegraph/indexing/indexer.py
CHANGED
|
@@ -493,8 +493,11 @@ class Indexer:
|
|
|
493
493
|
con = sqlite3.connect(self.db_path)
|
|
494
494
|
con.row_factory = make_compat_row
|
|
495
495
|
con.execute("PRAGMA journal_mode = WAL")
|
|
496
|
+
con.execute("PRAGMA synchronous = NORMAL")
|
|
496
497
|
con.execute("PRAGMA foreign_keys = ON")
|
|
497
|
-
con.execute("PRAGMA busy_timeout =
|
|
498
|
+
con.execute("PRAGMA busy_timeout = 15000")
|
|
499
|
+
con.execute("PRAGMA cache_size = -64000")
|
|
500
|
+
con.execute("PRAGMA temp_store = MEMORY")
|
|
498
501
|
self._ensure_schema(con)
|
|
499
502
|
return con
|
|
500
503
|
|
|
@@ -538,9 +541,13 @@ class Indexer:
|
|
|
538
541
|
pass
|
|
539
542
|
con.commit()
|
|
540
543
|
else:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
+
existing_core = con.execute(
|
|
545
|
+
"SELECT count(*) FROM sqlite_master WHERE type='table' "
|
|
546
|
+
"AND name IN ('files', 'symbols', 'graph_edges', 'db_tables', 'runtime_traces')"
|
|
547
|
+
).fetchone()[0]
|
|
548
|
+
if existing_core < 5:
|
|
549
|
+
con.executescript(SCHEMA)
|
|
550
|
+
con.commit()
|
|
544
551
|
|
|
545
552
|
def _migrate_schema(
|
|
546
553
|
self, con: sqlite3.Connection, from_version: int, to_version: int
|
codegraph/installer.py
CHANGED
|
@@ -201,12 +201,15 @@ class InstallerReport:
|
|
|
201
201
|
out["error"] = self.error
|
|
202
202
|
return out
|
|
203
203
|
|
|
204
|
-
def format_preview_human(self) -> str:
|
|
204
|
+
def format_preview_human(self, *, unicode_override: bool | None = None) -> str:
|
|
205
205
|
"""Render the interactive preview banner before confirmation."""
|
|
206
|
+
from codegraph.cli_output import get_cli_symbols
|
|
207
|
+
|
|
208
|
+
sym = get_cli_symbols(unicode_override=unicode_override)
|
|
206
209
|
lines: list[str] = ["CodeGraph Installer", ""]
|
|
207
210
|
lines.append("Detected agents:")
|
|
208
211
|
for da in self.detected_agents:
|
|
209
|
-
mark =
|
|
212
|
+
mark = sym.ok if da.detected else sym.error
|
|
210
213
|
lines.append(f" {mark} {da.display_name}")
|
|
211
214
|
|
|
212
215
|
lines.append("")
|
|
@@ -221,11 +224,11 @@ class InstallerReport:
|
|
|
221
224
|
lines.append("")
|
|
222
225
|
lines.append("Installation scope:")
|
|
223
226
|
if self.location == "global":
|
|
224
|
-
lines.append("
|
|
225
|
-
lines.append("
|
|
227
|
+
lines.append(f" {sym.radio_on} Global")
|
|
228
|
+
lines.append(f" {sym.radio_off} Current project")
|
|
226
229
|
else:
|
|
227
|
-
lines.append("
|
|
228
|
-
lines.append("
|
|
230
|
+
lines.append(f" {sym.radio_off} Global")
|
|
231
|
+
lines.append(f" {sym.radio_on} Current project")
|
|
229
232
|
|
|
230
233
|
lines.append("")
|
|
231
234
|
lines.append("Changes to be made:")
|
|
@@ -234,8 +237,11 @@ class InstallerReport:
|
|
|
234
237
|
lines.append(" + CodeGraph skill/rules")
|
|
235
238
|
return "\n".join(lines)
|
|
236
239
|
|
|
237
|
-
def format_human(self) -> str:
|
|
240
|
+
def format_human(self, *, unicode_override: bool | None = None) -> str:
|
|
238
241
|
"""Render concise, professional human-readable output."""
|
|
242
|
+
from codegraph.cli_output import get_cli_symbols
|
|
243
|
+
|
|
244
|
+
sym = get_cli_symbols(unicode_override=unicode_override)
|
|
239
245
|
if self.error is not None:
|
|
240
246
|
err_lines = [
|
|
241
247
|
f"Error [{self.error.get('code', 'ERROR')}]: {self.error.get('message', '')}",
|
|
@@ -256,10 +262,14 @@ class InstallerReport:
|
|
|
256
262
|
|
|
257
263
|
if self.status == "dry_run":
|
|
258
264
|
if self.operation == "install":
|
|
259
|
-
lines.append(self.format_preview_human())
|
|
265
|
+
lines.append(self.format_preview_human(unicode_override=unicode_override))
|
|
260
266
|
lines.append("")
|
|
261
267
|
else:
|
|
262
|
-
title =
|
|
268
|
+
title = (
|
|
269
|
+
"CodeGraph Uninstaller (Dry Run)"
|
|
270
|
+
if self.operation == "uninstall"
|
|
271
|
+
else "CodeGraph Uninit (Dry Run)"
|
|
272
|
+
)
|
|
263
273
|
lines.append(title)
|
|
264
274
|
lines.append("")
|
|
265
275
|
|
|
@@ -295,15 +305,15 @@ class InstallerReport:
|
|
|
295
305
|
|
|
296
306
|
if self.operation == "install":
|
|
297
307
|
for r_msg in self.repairs:
|
|
298
|
-
lines.append(f"
|
|
308
|
+
lines.append(f"{sym.repair} Repaired: {r_msg}")
|
|
299
309
|
for w_msg in self.warnings:
|
|
300
|
-
lines.append(f"
|
|
310
|
+
lines.append(f"{sym.warn} Warning: {w_msg}")
|
|
301
311
|
|
|
302
|
-
lines.append("
|
|
312
|
+
lines.append(f"{sym.ok} CodeGraph installed")
|
|
303
313
|
if self.verification is not None:
|
|
304
|
-
v_mcp =
|
|
305
|
-
v_inst =
|
|
306
|
-
v_srv =
|
|
314
|
+
v_mcp = sym.ok if self.verification.mcp_config_verified else sym.error
|
|
315
|
+
v_inst = sym.ok if self.verification.instructions_verified else sym.error
|
|
316
|
+
v_srv = sym.ok if self.verification.mcp_server_health_verified else sym.error
|
|
307
317
|
lines.append(f"{v_mcp} MCP configuration verified")
|
|
308
318
|
lines.append(f"{v_inst} Agent instructions verified")
|
|
309
319
|
lines.append(f"{v_srv} MCP server health verified")
|
|
@@ -319,23 +329,23 @@ class InstallerReport:
|
|
|
319
329
|
|
|
320
330
|
if self.operation == "uninstall":
|
|
321
331
|
for w_msg in self.warnings:
|
|
322
|
-
lines.append(f"
|
|
332
|
+
lines.append(f"{sym.warn} Warning: {w_msg}")
|
|
323
333
|
if removed or modified:
|
|
324
334
|
for c in [*removed, *modified]:
|
|
325
335
|
verb = "Removed" if c.action == "remove" else "Updated"
|
|
326
|
-
lines.append(f"
|
|
336
|
+
lines.append(f"{sym.ok} {verb} {c.display_path}")
|
|
327
337
|
else:
|
|
328
|
-
lines.append("
|
|
329
|
-
lines.append("
|
|
338
|
+
lines.append(f"{sym.ok} No CodeGraph agent integrations needed removal")
|
|
339
|
+
lines.append(f"{sym.ok} CodeGraph agent integration uninstalled")
|
|
330
340
|
return "\n".join(lines)
|
|
331
341
|
|
|
332
342
|
# uninit
|
|
333
343
|
if removed:
|
|
334
344
|
for c in removed:
|
|
335
|
-
lines.append(f"
|
|
345
|
+
lines.append(f"{sym.ok} Removed {c.display_path}")
|
|
336
346
|
else:
|
|
337
|
-
lines.append("
|
|
338
|
-
lines.append("
|
|
347
|
+
lines.append(f"{sym.ok} No CodeGraph index files present")
|
|
348
|
+
lines.append(f"{sym.ok} Project CodeGraph index removed")
|
|
339
349
|
return "\n".join(lines)
|
|
340
350
|
|
|
341
351
|
|
|
@@ -1351,6 +1361,33 @@ def run_install(
|
|
|
1351
1361
|
allow_malformed_repair=allow_malformed_repair,
|
|
1352
1362
|
)
|
|
1353
1363
|
|
|
1364
|
+
from codegraph.process_lifecycle import (
|
|
1365
|
+
discover_codegraph_processes,
|
|
1366
|
+
stop_codegraph_processes,
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
proc_discovery = discover_codegraph_processes(clean_stale=not dry_run)
|
|
1370
|
+
current_pid = os.getpid()
|
|
1371
|
+
other_active = [
|
|
1372
|
+
p for p in proc_discovery.active_processes if p.record.pid != current_pid
|
|
1373
|
+
]
|
|
1374
|
+
orphaned_procs = [p for p in other_active if p.orphaned]
|
|
1375
|
+
live_procs = [p for p in other_active if not p.orphaned]
|
|
1376
|
+
|
|
1377
|
+
if orphaned_procs and dry_run:
|
|
1378
|
+
pids_str = ", ".join(str(p.record.pid) for p in orphaned_procs)
|
|
1379
|
+
warnings.append(
|
|
1380
|
+
f"Orphaned CodeGraph process(es) detected (PID: {pids_str}); "
|
|
1381
|
+
"run 'codegraph stop' or 'codegraph install' to clean up."
|
|
1382
|
+
)
|
|
1383
|
+
if live_procs:
|
|
1384
|
+
pids_str = ", ".join(str(p.record.pid) for p in live_procs)
|
|
1385
|
+
warnings.append(
|
|
1386
|
+
f"Active CodeGraph process(es) running (PID: {pids_str}). "
|
|
1387
|
+
"Before upgrading ('pip install --upgrade codegraph-engine'), run 'codegraph stop' "
|
|
1388
|
+
"to prevent executable file lock errors (WinError 32)."
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1354
1391
|
preview_report = InstallerReport(
|
|
1355
1392
|
status="dry_run" if dry_run else "ok",
|
|
1356
1393
|
operation="install",
|
|
@@ -1383,6 +1420,12 @@ def run_install(
|
|
|
1383
1420
|
files_changed=False,
|
|
1384
1421
|
)
|
|
1385
1422
|
|
|
1423
|
+
if orphaned_procs:
|
|
1424
|
+
stop_rep = stop_codegraph_processes(only_orphaned=True)
|
|
1425
|
+
stopped_cnt = sum(1 for item in stop_rep.stopped_processes if item.stopped)
|
|
1426
|
+
if stopped_cnt > 0:
|
|
1427
|
+
repairs.append(f"Stopped {stopped_cnt} orphaned CodeGraph background process(es).")
|
|
1428
|
+
|
|
1386
1429
|
applied = _apply_planned_changes_atomically(changes)
|
|
1387
1430
|
ver = verify_installation(
|
|
1388
1431
|
base_root=base_root,
|
|
@@ -1545,6 +1588,13 @@ def run_uninstall(
|
|
|
1545
1588
|
files_changed=False,
|
|
1546
1589
|
)
|
|
1547
1590
|
|
|
1591
|
+
from codegraph.process_lifecycle import stop_codegraph_processes
|
|
1592
|
+
|
|
1593
|
+
stop_codegraph_processes(
|
|
1594
|
+
repository=None if scope == "global" else effective_ws,
|
|
1595
|
+
stop_all=(scope == "global"),
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1548
1598
|
applied = _apply_planned_changes_atomically(planned)
|
|
1549
1599
|
return InstallerReport(
|
|
1550
1600
|
status="ok",
|
codegraph/interrogation.py
CHANGED
|
@@ -1979,6 +1979,8 @@ def list_routes(
|
|
|
1979
1979
|
framework: str | None = None,
|
|
1980
1980
|
method: str | None = None,
|
|
1981
1981
|
path: str | None = None,
|
|
1982
|
+
limit: int | None = None,
|
|
1983
|
+
offset: int = 0,
|
|
1982
1984
|
) -> dict[str, Any]:
|
|
1983
1985
|
"""Expose application routes discovered from the repository."""
|
|
1984
1986
|
unindexed = check_index_available(con, repository)
|
|
@@ -2028,10 +2030,27 @@ def list_routes(
|
|
|
2028
2030
|
for r in rows
|
|
2029
2031
|
]
|
|
2030
2032
|
|
|
2033
|
+
total_count = len(routes)
|
|
2034
|
+
if limit is not None:
|
|
2035
|
+
safe_offset = max(0, offset)
|
|
2036
|
+
safe_limit = max(0, limit)
|
|
2037
|
+
paginated_routes = routes[safe_offset : safe_offset + safe_limit]
|
|
2038
|
+
return {
|
|
2039
|
+
"status": "ok",
|
|
2040
|
+
**meta,
|
|
2041
|
+
"count": len(paginated_routes),
|
|
2042
|
+
"total_count": total_count,
|
|
2043
|
+
"limit": safe_limit,
|
|
2044
|
+
"offset": safe_offset,
|
|
2045
|
+
"has_more": (safe_offset + len(paginated_routes)) < total_count,
|
|
2046
|
+
"routes": paginated_routes,
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2031
2049
|
return {
|
|
2032
2050
|
"status": "ok",
|
|
2033
2051
|
**meta,
|
|
2034
2052
|
"count": len(routes),
|
|
2053
|
+
"total_count": total_count,
|
|
2035
2054
|
"routes": routes,
|
|
2036
2055
|
}
|
|
2037
2056
|
|