codegraph-engine 3.0.0__1-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 +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Canonical Runtime Observation & Static-Runtime Reconciliation Models (Phases 10-13, 16).
|
|
2
|
+
|
|
3
|
+
Architectural Rules:
|
|
4
|
+
1. Runtime mapping is an OPTIONAL observation layer.
|
|
5
|
+
2. Static and runtime evidence remain strictly distinguishable (`evidence_class="RUNTIME_OBSERVED"`).
|
|
6
|
+
3. Never convert runtime evidence into static proof (`AST_VERIFIED`).
|
|
7
|
+
4. Never convert static inference into runtime fact.
|
|
8
|
+
5. `NOT_OBSERVED_AT_RUNTIME` means no runtime observation was ingested for the static edge;
|
|
9
|
+
it NEVER means the path cannot happen.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from codegraph.security.redaction import normalize_and_redact_sql, redact_payload, redact_secrets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReconciliationStatus(StrEnum):
|
|
21
|
+
CONFIRMED_RUNTIME_PATH = "CONFIRMED_RUNTIME_PATH"
|
|
22
|
+
STATIC_RUNTIME_CONFLICT = "STATIC_RUNTIME_CONFLICT"
|
|
23
|
+
NOT_OBSERVED_AT_RUNTIME = "NOT_OBSERVED_AT_RUNTIME"
|
|
24
|
+
RUNTIME_ONLY_OBSERVED = "RUNTIME_ONLY_OBSERVED"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RuntimeEvent:
|
|
29
|
+
"""Normalized, privacy-safe runtime observation event."""
|
|
30
|
+
|
|
31
|
+
event_id: str
|
|
32
|
+
trace_id: str
|
|
33
|
+
span_id: str
|
|
34
|
+
parent_span_id: str = ""
|
|
35
|
+
timestamp: str = ""
|
|
36
|
+
source_format: str = "json" # otel | json | sql_log
|
|
37
|
+
http_method: str = ""
|
|
38
|
+
route_path: str = ""
|
|
39
|
+
handler_symbol: str = ""
|
|
40
|
+
caller_symbol: str = ""
|
|
41
|
+
callee_symbol: str = ""
|
|
42
|
+
service_name: str = ""
|
|
43
|
+
db_operation: str = "" # SELECT | INSERT | UPDATE | DELETE | ""
|
|
44
|
+
db_table: str = ""
|
|
45
|
+
db_columns: tuple[str, ...] = ()
|
|
46
|
+
normalized_sql: str = ""
|
|
47
|
+
status_code: int | None = None
|
|
48
|
+
exception_type: str = ""
|
|
49
|
+
duration_ms: float = 0.0
|
|
50
|
+
evidence_class: str = "RUNTIME_OBSERVED"
|
|
51
|
+
runtime_generation: int = 1
|
|
52
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
if self.normalized_sql:
|
|
56
|
+
object.__setattr__(self, "normalized_sql", normalize_and_redact_sql(self.normalized_sql))
|
|
57
|
+
if self.metadata:
|
|
58
|
+
object.__setattr__(self, "metadata", redact_payload(dict(self.metadata)))
|
|
59
|
+
|
|
60
|
+
def as_dict(self) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"event_id": self.event_id,
|
|
63
|
+
"trace_id": self.trace_id,
|
|
64
|
+
"span_id": self.span_id,
|
|
65
|
+
"parent_span_id": self.parent_span_id,
|
|
66
|
+
"timestamp": self.timestamp,
|
|
67
|
+
"source_format": self.source_format,
|
|
68
|
+
"http_method": self.http_method,
|
|
69
|
+
"route_path": redact_secrets(self.route_path),
|
|
70
|
+
"handler_symbol": self.handler_symbol,
|
|
71
|
+
"caller_symbol": self.caller_symbol,
|
|
72
|
+
"callee_symbol": self.callee_symbol,
|
|
73
|
+
"service_name": self.service_name,
|
|
74
|
+
"db_operation": self.db_operation,
|
|
75
|
+
"db_table": self.db_table,
|
|
76
|
+
"db_columns": list(self.db_columns),
|
|
77
|
+
"normalized_sql": self.normalized_sql,
|
|
78
|
+
"status_code": self.status_code,
|
|
79
|
+
"exception_type": redact_secrets(self.exception_type),
|
|
80
|
+
"duration_ms": self.duration_ms,
|
|
81
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
82
|
+
"runtime_generation": self.runtime_generation,
|
|
83
|
+
"metadata": dict(self.metadata),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class RuntimeAggregatedEdge:
|
|
89
|
+
"""Compact, deduplicated runtime edge stored in SQLite (Phase 16)."""
|
|
90
|
+
|
|
91
|
+
edge_id: str
|
|
92
|
+
source: str
|
|
93
|
+
target: str
|
|
94
|
+
relationship: str # HANDLED_BY | CALLS | READS_TABLE | WRITES_TABLE | READS_COLUMN | WRITES_COLUMN
|
|
95
|
+
operation: str = ""
|
|
96
|
+
evidence_class: str = "RUNTIME_OBSERVED"
|
|
97
|
+
observation_count: int = 1
|
|
98
|
+
first_seen: str = ""
|
|
99
|
+
last_seen: str = ""
|
|
100
|
+
avg_duration_ms: float = 0.0
|
|
101
|
+
max_duration_ms: float = 0.0
|
|
102
|
+
sample_trace_id: str = ""
|
|
103
|
+
sample_span_id: str = ""
|
|
104
|
+
normalized_sql: str = ""
|
|
105
|
+
status_code: int | None = None
|
|
106
|
+
exception_type: str = ""
|
|
107
|
+
runtime_generation: int = 1
|
|
108
|
+
|
|
109
|
+
def as_dict(self) -> dict[str, Any]:
|
|
110
|
+
return {
|
|
111
|
+
"edge_id": self.edge_id,
|
|
112
|
+
"source": self.source,
|
|
113
|
+
"target": self.target,
|
|
114
|
+
"relationship": self.relationship,
|
|
115
|
+
"operation": self.operation,
|
|
116
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
117
|
+
"confidence": "HIGH",
|
|
118
|
+
"status": "RUNTIME_OBSERVED",
|
|
119
|
+
"observation_count": self.observation_count,
|
|
120
|
+
"first_seen": self.first_seen,
|
|
121
|
+
"last_seen": self.last_seen,
|
|
122
|
+
"avg_duration_ms": round(self.avg_duration_ms, 3),
|
|
123
|
+
"max_duration_ms": round(self.max_duration_ms, 3),
|
|
124
|
+
"sample_trace_id": self.sample_trace_id,
|
|
125
|
+
"sample_span_id": self.sample_span_id,
|
|
126
|
+
"normalized_sql": self.normalized_sql,
|
|
127
|
+
"status_code": self.status_code,
|
|
128
|
+
"exception_type": self.exception_type,
|
|
129
|
+
"runtime_generation": self.runtime_generation,
|
|
130
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Localhost Port and Process Inspector for Multi-Service Architectures.
|
|
2
|
+
|
|
3
|
+
Deterministically detects, fingerprints, and attributes running localhost services:
|
|
4
|
+
1. Port <-> PID mapping via lsof / netstat / /proc.
|
|
5
|
+
2. Process CWD and command-line attribution to workspace packages.
|
|
6
|
+
3. Static port inference from docker-compose.yml, package.json, and .env.
|
|
7
|
+
4. Framework fingerprinting (Next.js, Vite, FastAPI, NestJS, Express, PostgreSQL, MongoDB).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ServiceEndpoint:
|
|
22
|
+
port: int
|
|
23
|
+
host: str = "localhost"
|
|
24
|
+
pid: int | None = None
|
|
25
|
+
service_name: str = "unknown"
|
|
26
|
+
framework: str = "unknown"
|
|
27
|
+
process_cwd: str = ""
|
|
28
|
+
relative_repo_path: str = ""
|
|
29
|
+
command: str = ""
|
|
30
|
+
is_live: bool = False
|
|
31
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def as_dict(self) -> dict[str, Any]:
|
|
34
|
+
return {
|
|
35
|
+
"port": self.port,
|
|
36
|
+
"host": self.host,
|
|
37
|
+
"pid": self.pid,
|
|
38
|
+
"service_name": self.service_name,
|
|
39
|
+
"framework": self.framework,
|
|
40
|
+
"process_cwd": self.process_cwd,
|
|
41
|
+
"relative_repo_path": self.relative_repo_path,
|
|
42
|
+
"command": self.command,
|
|
43
|
+
"is_live": self.is_live,
|
|
44
|
+
"attributes": dict(self.attributes),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _infer_framework_from_cmd(cmd: str) -> str:
|
|
49
|
+
cmd_lower = cmd.lower()
|
|
50
|
+
if "next" in cmd_lower:
|
|
51
|
+
return "Next.js"
|
|
52
|
+
if "vite" in cmd_lower:
|
|
53
|
+
return "Vite"
|
|
54
|
+
if "uvicorn" in cmd_lower or "fastapi" in cmd_lower:
|
|
55
|
+
return "FastAPI"
|
|
56
|
+
if "nest" in cmd_lower:
|
|
57
|
+
return "NestJS"
|
|
58
|
+
if "express" in cmd_lower or "node" in cmd_lower:
|
|
59
|
+
return "Node/Express"
|
|
60
|
+
if "postgres" in cmd_lower:
|
|
61
|
+
return "PostgreSQL"
|
|
62
|
+
if "mongod" in cmd_lower or "mongo" in cmd_lower:
|
|
63
|
+
return "MongoDB"
|
|
64
|
+
if "redis" in cmd_lower:
|
|
65
|
+
return "Redis"
|
|
66
|
+
if "celery" in cmd_lower:
|
|
67
|
+
return "Celery"
|
|
68
|
+
return "GenericService"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _infer_service_name_from_path(repo_root: Path, cwd_path: Path, framework: str) -> str:
|
|
72
|
+
try:
|
|
73
|
+
rel = cwd_path.relative_to(repo_root)
|
|
74
|
+
parts = rel.parts
|
|
75
|
+
if parts:
|
|
76
|
+
if parts[0] in ("apps", "packages", "services") and len(parts) > 1:
|
|
77
|
+
return parts[1]
|
|
78
|
+
return parts[-1]
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return framework.lower().replace(".", "").replace("/", "-")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def discover_static_configured_ports(repo_root: Path) -> dict[int, ServiceEndpoint]:
|
|
85
|
+
"""Scan docker-compose, package.json, and .env files with in-memory TTL caching."""
|
|
86
|
+
global _STATIC_PORT_CACHE
|
|
87
|
+
root_key = str(repo_root)
|
|
88
|
+
now = time.monotonic()
|
|
89
|
+
if root_key in _STATIC_PORT_CACHE:
|
|
90
|
+
last_t, cached_map = _STATIC_PORT_CACHE[root_key]
|
|
91
|
+
if now - last_t < _PORT_CACHE_TTL_SEC:
|
|
92
|
+
return cached_map
|
|
93
|
+
|
|
94
|
+
results: dict[int, ServiceEndpoint] = {}
|
|
95
|
+
|
|
96
|
+
# 1. docker-compose.yml / compose.yaml
|
|
97
|
+
for compose_name in ("docker-compose.yml", "docker-compose.yaml", "compose.yaml", "compose.yml"):
|
|
98
|
+
compose_file = repo_root / compose_name
|
|
99
|
+
if compose_file.exists():
|
|
100
|
+
try:
|
|
101
|
+
content = compose_file.read_text(encoding="utf-8", errors="replace")
|
|
102
|
+
# match ports: - "3000:3000" or - 8000:8000
|
|
103
|
+
for m in re.finditer(r"""['"]?(\d{2,5}):(\d{2,5})['"]?""", content):
|
|
104
|
+
host_port = int(m.group(1))
|
|
105
|
+
results[host_port] = ServiceEndpoint(
|
|
106
|
+
port=host_port,
|
|
107
|
+
service_name=f"docker-service-{host_port}",
|
|
108
|
+
framework="DockerContainer",
|
|
109
|
+
relative_repo_path=str(compose_file.relative_to(repo_root)),
|
|
110
|
+
is_live=False,
|
|
111
|
+
)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
# 2. package.json scripts (e.g. -p 3000, PORT=4000)
|
|
116
|
+
for pkg_file in repo_root.glob("**/package.json"):
|
|
117
|
+
if "node_modules" in pkg_file.parts:
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
content = pkg_file.read_text(encoding="utf-8", errors="replace")
|
|
121
|
+
for m in re.finditer(r"""(?:PORT\s*=\s*|-p\s+|--port\s+)(\d{2,5})""", content):
|
|
122
|
+
port_num = int(m.group(1))
|
|
123
|
+
rel_dir = pkg_file.parent.relative_to(repo_root)
|
|
124
|
+
s_name = pkg_file.parent.name or "web"
|
|
125
|
+
results[port_num] = ServiceEndpoint(
|
|
126
|
+
port=port_num,
|
|
127
|
+
service_name=s_name,
|
|
128
|
+
framework="Node/Web",
|
|
129
|
+
relative_repo_path=str(rel_dir),
|
|
130
|
+
is_live=False,
|
|
131
|
+
)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# 3. .env files
|
|
136
|
+
for env_file in (repo_root / ".env", repo_root / ".env.development", repo_root / ".env.local"):
|
|
137
|
+
if env_file.exists():
|
|
138
|
+
try:
|
|
139
|
+
content = env_file.read_text(encoding="utf-8", errors="replace")
|
|
140
|
+
for m in re.finditer(r"""^\s*(?:PORT|SERVER_PORT|API_PORT)\s*=\s*(\d{2,5})""", content, re.MULTILINE):
|
|
141
|
+
port_num = int(m.group(1))
|
|
142
|
+
results[port_num] = ServiceEndpoint(
|
|
143
|
+
port=port_num,
|
|
144
|
+
service_name="env-service",
|
|
145
|
+
framework="ConfiguredService",
|
|
146
|
+
relative_repo_path=str(env_file.relative_to(repo_root)),
|
|
147
|
+
is_live=False,
|
|
148
|
+
)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
_STATIC_PORT_CACHE[root_key] = (now, results)
|
|
153
|
+
return results
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_PORT_CACHE: tuple[float, list[ServiceEndpoint]] = (0.0, [])
|
|
157
|
+
_STATIC_PORT_CACHE: dict[str, tuple[float, dict[int, ServiceEndpoint]]] = {}
|
|
158
|
+
_PORT_CACHE_TTL_SEC = 5.0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def discover_live_listening_ports(repo_root: Path | None = None) -> list[ServiceEndpoint]:
|
|
162
|
+
"""Inspect active OS listening ports on localhost with 5-second TTL cache (Zero-CPU idle)."""
|
|
163
|
+
global _PORT_CACHE
|
|
164
|
+
now = time.monotonic()
|
|
165
|
+
last_time, cached_endpoints = _PORT_CACHE
|
|
166
|
+
if now - last_time < _PORT_CACHE_TTL_SEC and cached_endpoints:
|
|
167
|
+
return cached_endpoints
|
|
168
|
+
|
|
169
|
+
endpoints: list[ServiceEndpoint] = []
|
|
170
|
+
seen_ports: set[int] = set()
|
|
171
|
+
|
|
172
|
+
lsof_bin = shutil.which("lsof")
|
|
173
|
+
if lsof_bin:
|
|
174
|
+
try:
|
|
175
|
+
# lsof -iTCP -sTCP:LISTEN -n -P
|
|
176
|
+
proc = subprocess.run(
|
|
177
|
+
[lsof_bin, "-iTCP", "-sTCP:LISTEN", "-n", "-P"],
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
timeout=3,
|
|
181
|
+
check=False,
|
|
182
|
+
)
|
|
183
|
+
if proc.returncode == 0:
|
|
184
|
+
for line in proc.stdout.splitlines()[1:]:
|
|
185
|
+
parts = line.split()
|
|
186
|
+
if len(parts) >= 9:
|
|
187
|
+
cmd = parts[0]
|
|
188
|
+
pid_str = parts[1]
|
|
189
|
+
addr_str = parts[8]
|
|
190
|
+
# address format: *:3000, 127.0.0.1:8000, [::1]:5432
|
|
191
|
+
m = re.search(r":(\d+)$", addr_str)
|
|
192
|
+
if m:
|
|
193
|
+
port = int(m.group(1))
|
|
194
|
+
if port in seen_ports:
|
|
195
|
+
continue
|
|
196
|
+
seen_ports.add(port)
|
|
197
|
+
pid = int(pid_str) if pid_str.isdigit() else None
|
|
198
|
+
cwd_str = ""
|
|
199
|
+
rel_repo = ""
|
|
200
|
+
if pid:
|
|
201
|
+
cwd_str = _get_process_cwd(pid)
|
|
202
|
+
if repo_root and cwd_str:
|
|
203
|
+
try:
|
|
204
|
+
rel_repo = str(Path(cwd_str).relative_to(repo_root))
|
|
205
|
+
except Exception:
|
|
206
|
+
rel_repo = ""
|
|
207
|
+
framework = _infer_framework_from_cmd(cmd)
|
|
208
|
+
svc_name = (
|
|
209
|
+
_infer_service_name_from_path(repo_root, Path(cwd_str), framework)
|
|
210
|
+
if repo_root and cwd_str
|
|
211
|
+
else cmd
|
|
212
|
+
)
|
|
213
|
+
endpoints.append(
|
|
214
|
+
ServiceEndpoint(
|
|
215
|
+
port=port,
|
|
216
|
+
pid=pid,
|
|
217
|
+
service_name=svc_name,
|
|
218
|
+
framework=framework,
|
|
219
|
+
process_cwd=cwd_str,
|
|
220
|
+
relative_repo_path=rel_repo,
|
|
221
|
+
command=cmd,
|
|
222
|
+
is_live=True,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
_PORT_CACHE = (now, endpoints)
|
|
229
|
+
return endpoints
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _get_process_cwd(pid: int) -> str:
|
|
233
|
+
"""Get process working directory securely across macOS and Linux."""
|
|
234
|
+
# 1. Linux /proc/<pid>/cwd
|
|
235
|
+
proc_cwd = Path(f"/proc/{pid}/cwd")
|
|
236
|
+
if proc_cwd.exists():
|
|
237
|
+
try:
|
|
238
|
+
return str(proc_cwd.resolve())
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# 2. macOS lsof -p <pid> | grep cwd
|
|
243
|
+
lsof_bin = shutil.which("lsof")
|
|
244
|
+
if lsof_bin:
|
|
245
|
+
try:
|
|
246
|
+
proc = subprocess.run(
|
|
247
|
+
[lsof_bin, "-a", "-p", str(pid), "-d", "cwd", "-Fn"],
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
timeout=1,
|
|
251
|
+
check=False,
|
|
252
|
+
)
|
|
253
|
+
for line in proc.stdout.splitlines():
|
|
254
|
+
if line.startswith("n"):
|
|
255
|
+
return line[1:].strip()
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
return ""
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def resolve_port_to_service(
|
|
262
|
+
port: int,
|
|
263
|
+
repo_root: Path,
|
|
264
|
+
live_endpoints: list[ServiceEndpoint] | None = None,
|
|
265
|
+
) -> ServiceEndpoint:
|
|
266
|
+
"""Identify the exact service, package, and framework for a given localhost port."""
|
|
267
|
+
if live_endpoints is None:
|
|
268
|
+
live_endpoints = discover_live_listening_ports(repo_root)
|
|
269
|
+
|
|
270
|
+
for ep in live_endpoints:
|
|
271
|
+
if ep.port == port:
|
|
272
|
+
return ep
|
|
273
|
+
|
|
274
|
+
static_map = discover_static_configured_ports(repo_root)
|
|
275
|
+
if port in static_map:
|
|
276
|
+
return static_map[port]
|
|
277
|
+
|
|
278
|
+
# Common standard ports fallback
|
|
279
|
+
standard_defaults: dict[int, tuple[str, str]] = {
|
|
280
|
+
3000: ("frontend", "Next.js/React"),
|
|
281
|
+
5173: ("frontend", "Vite"),
|
|
282
|
+
8000: ("backend-api", "FastAPI/Django"),
|
|
283
|
+
4000: ("backend-api", "NestJS/Express"),
|
|
284
|
+
5000: ("backend-api", "Flask/Express"),
|
|
285
|
+
5432: ("database", "PostgreSQL"),
|
|
286
|
+
27017: ("database", "MongoDB"),
|
|
287
|
+
6379: ("cache", "Redis"),
|
|
288
|
+
3306: ("database", "MySQL"),
|
|
289
|
+
}
|
|
290
|
+
if port in standard_defaults:
|
|
291
|
+
s_name, fw = standard_defaults[port]
|
|
292
|
+
return ServiceEndpoint(
|
|
293
|
+
port=port,
|
|
294
|
+
service_name=s_name,
|
|
295
|
+
framework=fw,
|
|
296
|
+
is_live=False,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return ServiceEndpoint(
|
|
300
|
+
port=port,
|
|
301
|
+
service_name=f"service-{port}",
|
|
302
|
+
framework="Unknown",
|
|
303
|
+
is_live=False,
|
|
304
|
+
)
|