codeui-python 0.1.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.
- codeui/__init__.py +23 -0
- codeui/agent/__init__.py +15 -0
- codeui/agent/context.py +207 -0
- codeui/agent/contribution.py +224 -0
- codeui/agent/edit_tools.py +280 -0
- codeui/agent/integrations.py +243 -0
- codeui/agent/mcp_server.py +94 -0
- codeui/analysis/__init__.py +1 -0
- codeui/analysis/api_drift.py +79 -0
- codeui/analysis/base.py +42 -0
- codeui/analysis/circular.py +116 -0
- codeui/analysis/dead_code.py +63 -0
- codeui/analysis/duplicates.py +69 -0
- codeui/analysis/runner.py +57 -0
- codeui/analysis/shadowing.py +49 -0
- codeui/analysis/undefined.py +97 -0
- codeui/analysis/unreachable.py +28 -0
- codeui/analysis/unresolved_import.py +153 -0
- codeui/cli/__init__.py +1 -0
- codeui/cli/main.py +437 -0
- codeui/core/__init__.py +1 -0
- codeui/core/cache.py +135 -0
- codeui/core/graph.py +435 -0
- codeui/core/ir.py +218 -0
- codeui/core/override.py +230 -0
- codeui/core/repo.py +85 -0
- codeui/core/resolver.py +281 -0
- codeui/errors.py +98 -0
- codeui/lang/__init__.py +1 -0
- codeui/lang/base.py +117 -0
- codeui/lang/generic.py +90 -0
- codeui/lang/go.py +167 -0
- codeui/lang/python.py +294 -0
- codeui/lang/registry.py +83 -0
- codeui/lang/rust.py +185 -0
- codeui/lang/ts.py +396 -0
- codeui/plugins/__init__.py +1 -0
- codeui/plugins/registry.py +31 -0
- codeui/report/__init__.py +1 -0
- codeui/report/json_emitter.py +24 -0
- codeui/report/markdown_emitter.py +33 -0
- codeui/report/sarif_emitter.py +56 -0
- codeui/server/__init__.py +1 -0
- codeui/server/server.py +705 -0
- codeui/tracer/__init__.py +1 -0
- codeui/tracer/tracer.py +147 -0
- codeui_python-0.1.0.dist-info/METADATA +191 -0
- codeui_python-0.1.0.dist-info/RECORD +52 -0
- codeui_python-0.1.0.dist-info/WHEEL +5 -0
- codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
- codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeui_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Unresolved import defect analyzer."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import ClassVar, Iterable, List, Set
|
|
7
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
8
|
+
from codeui.core.graph import Graph
|
|
9
|
+
from codeui.core.ir import EdgeKind, Finding, Location, Severity
|
|
10
|
+
|
|
11
|
+
class UnresolvedImportAnalyzer(Analyzer):
|
|
12
|
+
"""Detects imports targeting non-existent modules or files.
|
|
13
|
+
Example:
|
|
14
|
+
>>> analyzer = UnresolvedImportAnalyzer()
|
|
15
|
+
>>> g = Graph()
|
|
16
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
17
|
+
>>> list(analyzer.run(g, ctx))
|
|
18
|
+
[]
|
|
19
|
+
"""
|
|
20
|
+
id: ClassVar[str] = "unresolved_import"
|
|
21
|
+
severity_default: ClassVar[Severity] = Severity.ERROR
|
|
22
|
+
|
|
23
|
+
STD_MODULES: Set[str] = (
|
|
24
|
+
set(getattr(sys, "stdlib_module_names", set()))
|
|
25
|
+
| set(getattr(sys, "builtin_module_names", set()))
|
|
26
|
+
| {
|
|
27
|
+
"fs", "path", "http", "https", "os", "sys", "re", "json", "ast", "hashlib",
|
|
28
|
+
"sqlite3", "math", "fmt", "io", "net", "std", "core", "alloc", "url", "events",
|
|
29
|
+
"util", "stream", "buffer", "crypto", "child_process", "cluster", "dgram",
|
|
30
|
+
"dns", "tls", "readline", "zlib", "codeui", "react", "react-dom", "lucide-react",
|
|
31
|
+
"vite", "d3", "recharts", "motion", "clsx", "tailwind-merge", "express",
|
|
32
|
+
"typing", "dataclasses", "enum", "argparse", "shutil", "tempfile", "unittest",
|
|
33
|
+
"subprocess", "urllib", "concurrent", "threading", "time", "datetime", "collections",
|
|
34
|
+
"itertools", "functools", "inspect", "traceback", "copy", "glob", "fnmatch",
|
|
35
|
+
"typing_extensions", "pydantic", "pytest", "fastapi", "flask", "requests", "aiohttp",
|
|
36
|
+
"jinja2", "yaml", "toml", "dotenv", "sync", "strings", "bytes", "context", "errors",
|
|
37
|
+
"testing", "encoding", "log", "builtin", "runtime", "syscall"
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
42
|
+
"""Find unresolved import edges.
|
|
43
|
+
Example:
|
|
44
|
+
>>> analyzer = UnresolvedImportAnalyzer()
|
|
45
|
+
>>> g = Graph()
|
|
46
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
47
|
+
[]
|
|
48
|
+
"""
|
|
49
|
+
findings: List[Finding] = []
|
|
50
|
+
all_files = set(graph.get_all_files())
|
|
51
|
+
known_deps = set(self.STD_MODULES)
|
|
52
|
+
|
|
53
|
+
for f in all_files:
|
|
54
|
+
parts = f.replace("\\", "/").split("/")
|
|
55
|
+
for p in parts:
|
|
56
|
+
stem = p.split(".")[0]
|
|
57
|
+
if stem:
|
|
58
|
+
known_deps.add(stem)
|
|
59
|
+
known_deps.add(p)
|
|
60
|
+
|
|
61
|
+
if ctx and ctx.root_dir:
|
|
62
|
+
root = ctx.root_dir
|
|
63
|
+
for manifest_name in ("package.json", "requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "go.mod", "Cargo.toml"):
|
|
64
|
+
mpath = root / manifest_name
|
|
65
|
+
if mpath.exists():
|
|
66
|
+
try:
|
|
67
|
+
content = mpath.read_text(encoding="utf-8")
|
|
68
|
+
if manifest_name == "package.json":
|
|
69
|
+
pkg_data = json.loads(content)
|
|
70
|
+
deps = pkg_data.get("dependencies", {})
|
|
71
|
+
dev_deps = pkg_data.get("devDependencies", {})
|
|
72
|
+
known_deps.update(deps.keys())
|
|
73
|
+
known_deps.update(dev_deps.keys())
|
|
74
|
+
elif manifest_name == "requirements.txt":
|
|
75
|
+
for line in content.splitlines():
|
|
76
|
+
line = line.strip().split("#")[0].strip()
|
|
77
|
+
if line:
|
|
78
|
+
pkg = re.split(r"[=<>!~]", line)[0].strip()
|
|
79
|
+
if pkg:
|
|
80
|
+
known_deps.add(pkg)
|
|
81
|
+
elif manifest_name in ("pyproject.toml", "setup.py", "Cargo.toml", "go.mod"):
|
|
82
|
+
for match in re.finditer(r'["\']([a-zA-Z0-9_-]+)["\']', content):
|
|
83
|
+
known_deps.add(match.group(1))
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
for edge in graph.get_edges():
|
|
88
|
+
if edge.kind == EdgeKind.IMPORTS:
|
|
89
|
+
raw_mod = edge.target_id.replace("module::", "").strip()
|
|
90
|
+
if not raw_mod or raw_mod.startswith("."):
|
|
91
|
+
continue
|
|
92
|
+
if raw_mod.startswith("node:"):
|
|
93
|
+
continue
|
|
94
|
+
if raw_mod.startswith("@/") or raw_mod == "@":
|
|
95
|
+
base_mod = "@"
|
|
96
|
+
elif raw_mod.startswith("@"):
|
|
97
|
+
base_mod = "/".join(raw_mod.split("/")[:2])
|
|
98
|
+
else:
|
|
99
|
+
base_mod = raw_mod.split("/")[0].split(".")[0]
|
|
100
|
+
|
|
101
|
+
is_known = (
|
|
102
|
+
raw_mod in known_deps
|
|
103
|
+
or base_mod in known_deps
|
|
104
|
+
or any(raw_mod in f or base_mod in f for f in all_files)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not is_known and raw_mod.startswith("@/"):
|
|
108
|
+
as_src = "src/" + raw_mod[2:]
|
|
109
|
+
as_root = raw_mod[2:]
|
|
110
|
+
if any(as_src in f or as_root in f for f in all_files):
|
|
111
|
+
is_known = True
|
|
112
|
+
|
|
113
|
+
if not is_known and hasattr(ctx, "resolve_path"):
|
|
114
|
+
src_file = edge.source_id.split("::")[0]
|
|
115
|
+
resolved = ctx.resolve_path(src_file, raw_mod)
|
|
116
|
+
if resolved:
|
|
117
|
+
is_known = True
|
|
118
|
+
|
|
119
|
+
if not is_known and hasattr(ctx, "alias_mappings"):
|
|
120
|
+
for alias, target in getattr(ctx, "alias_mappings", {}).items():
|
|
121
|
+
if raw_mod.startswith(alias):
|
|
122
|
+
sub = raw_mod[len(alias):].lstrip("/")
|
|
123
|
+
target_path = Path(target) / sub
|
|
124
|
+
if target_path.exists() or any(target_path.with_suffix(ext).exists() for ext in (".ts", ".tsx", ".js", ".jsx", ".py")):
|
|
125
|
+
is_known = True
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if not is_known and ctx and ctx.root_dir:
|
|
129
|
+
candidate_rel = ctx.root_dir / raw_mod.replace(".", "/")
|
|
130
|
+
candidate_norm = ctx.root_dir / raw_mod
|
|
131
|
+
if (
|
|
132
|
+
candidate_rel.exists()
|
|
133
|
+
or candidate_norm.exists()
|
|
134
|
+
or any((candidate_rel.parent / f"{candidate_rel.name}{ext}").exists() for ext in (".ts", ".tsx", ".js", ".jsx", ".py"))
|
|
135
|
+
or any((candidate_norm.parent / f"{candidate_norm.name}{ext}").exists() for ext in (".ts", ".tsx", ".js", ".jsx", ".py"))
|
|
136
|
+
or any((candidate_rel / idx).exists() for idx in ("index.ts", "index.tsx", "index.js", "__init__.py"))
|
|
137
|
+
):
|
|
138
|
+
is_known = True
|
|
139
|
+
|
|
140
|
+
if not is_known:
|
|
141
|
+
loc = edge.location or Location(edge.source_id, 1, 0, 1, 0)
|
|
142
|
+
finding = Finding(
|
|
143
|
+
id=f"unresolved_import::{edge.source_id}::{raw_mod}",
|
|
144
|
+
rule_id="unresolved_import",
|
|
145
|
+
message=f"Unresolved import module '{raw_mod}'",
|
|
146
|
+
severity=self.severity_default,
|
|
147
|
+
location=loc,
|
|
148
|
+
symbol_id=edge.source_id,
|
|
149
|
+
fix_hint=f"Check import path '{raw_mod}' or install required dependency",
|
|
150
|
+
confidence=0.9,
|
|
151
|
+
)
|
|
152
|
+
findings.append(finding)
|
|
153
|
+
return findings
|
codeui/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI interface package for codeui."""
|
codeui/cli/main.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Command line interface entrypoint for codeui."""
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Sequence, Set, Union
|
|
10
|
+
from codeui.agent.context import ContextBuilder, TaskSpec
|
|
11
|
+
from codeui.analysis.base import AnalysisContext
|
|
12
|
+
from codeui.analysis.runner import AnalysisRunner
|
|
13
|
+
from codeui.core.cache import AnalysisCache
|
|
14
|
+
from codeui.core.graph import Graph
|
|
15
|
+
from codeui.core.ir import Edge, EdgeKind, Location
|
|
16
|
+
from codeui.core.override import Override, OverrideKind, OverrideStore
|
|
17
|
+
from codeui.core.repo import clone_git_repo, is_git_url
|
|
18
|
+
from codeui.core.resolver import ResolveContext
|
|
19
|
+
from codeui.lang.registry import LanguageRegistry
|
|
20
|
+
from codeui.report.json_emitter import JSONEmitter
|
|
21
|
+
from codeui.report.markdown_emitter import MarkdownEmitter
|
|
22
|
+
from codeui.report.sarif_emitter import SARIFEmitter
|
|
23
|
+
from codeui.server.server import create_server
|
|
24
|
+
from codeui.tracer.tracer import Tracer
|
|
25
|
+
|
|
26
|
+
def scan_project(
|
|
27
|
+
project_path: Union[str, Path],
|
|
28
|
+
entry_points: Optional[Sequence[str]] = None,
|
|
29
|
+
layer_rules: Optional[Dict[str, Any]] = None,
|
|
30
|
+
) -> Graph:
|
|
31
|
+
"""Scan a project directory or remote Git repository and build the universal graph.
|
|
32
|
+
Example:
|
|
33
|
+
>>> g = scan_project(Path("."))
|
|
34
|
+
>>> isinstance(g, Graph)
|
|
35
|
+
True
|
|
36
|
+
"""
|
|
37
|
+
path_str = str(project_path)
|
|
38
|
+
temp_clone_obj = None
|
|
39
|
+
if is_git_url(path_str):
|
|
40
|
+
resolved_project_path, temp_clone_obj = clone_git_repo(path_str)
|
|
41
|
+
else:
|
|
42
|
+
resolved_project_path = Path(project_path).resolve()
|
|
43
|
+
|
|
44
|
+
cache = None
|
|
45
|
+
try:
|
|
46
|
+
graph = Graph()
|
|
47
|
+
registry = LanguageRegistry()
|
|
48
|
+
ctx = ResolveContext(resolved_project_path)
|
|
49
|
+
cache_db_path = resolved_project_path / ".codeui" / "cache.db"
|
|
50
|
+
try:
|
|
51
|
+
cache = AnalysisCache(cache_db_path)
|
|
52
|
+
except Exception:
|
|
53
|
+
cache = None
|
|
54
|
+
|
|
55
|
+
all_entries: List[str] = list(ctx.entry_points)
|
|
56
|
+
if entry_points:
|
|
57
|
+
all_entries.extend(entry_points)
|
|
58
|
+
all_rules: Dict[str, Any] = dict(ctx.layer_rules)
|
|
59
|
+
if layer_rules:
|
|
60
|
+
all_rules.update(layer_rules)
|
|
61
|
+
|
|
62
|
+
if all_entries:
|
|
63
|
+
graph.set_entry_points(all_entries)
|
|
64
|
+
if all_rules:
|
|
65
|
+
graph.set_layer_rules(all_rules)
|
|
66
|
+
|
|
67
|
+
ignored_dir_names = {
|
|
68
|
+
"node_modules", ".venv", "venv", "env", "dist", "build", ".git", ".codeui",
|
|
69
|
+
"__pycache__", ".pytest_cache", ".egg-info", ".mypy_cache", ".tox", ".idea",
|
|
70
|
+
".vscode", "target", "out", "vendor", ".next", ".nuxt", "coverage", "site-packages"
|
|
71
|
+
}
|
|
72
|
+
ignored_exts = {
|
|
73
|
+
".pyc", ".pyo", ".pyd", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg",
|
|
74
|
+
".zip", ".tar", ".gz", ".lock", ".md", ".markdown", ".txt", ".rst",
|
|
75
|
+
".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".map", ".log"
|
|
76
|
+
}
|
|
77
|
+
valid_code_exts = registry.get_supported_extensions()
|
|
78
|
+
|
|
79
|
+
all_files: List[Path] = []
|
|
80
|
+
for root, dirs, files in os.walk(resolved_project_path):
|
|
81
|
+
dirs[:] = [
|
|
82
|
+
d for d in dirs
|
|
83
|
+
if d not in ignored_dir_names and not (d.startswith(".") and d not in (".", ".."))
|
|
84
|
+
]
|
|
85
|
+
for f in files:
|
|
86
|
+
p = Path(root) / f
|
|
87
|
+
try:
|
|
88
|
+
resolved_p = p.resolve()
|
|
89
|
+
resolved_p.relative_to(resolved_project_path)
|
|
90
|
+
except (ValueError, RuntimeError):
|
|
91
|
+
continue
|
|
92
|
+
ext = p.suffix.lower()
|
|
93
|
+
if ext in ignored_exts or any(p.name.endswith(e) for e in ignored_exts):
|
|
94
|
+
continue
|
|
95
|
+
if ext not in valid_code_exts and p.name.lower() not in ("dockerfile", "makefile"):
|
|
96
|
+
continue
|
|
97
|
+
all_files.append(p)
|
|
98
|
+
|
|
99
|
+
parsed_files: Set[str] = set()
|
|
100
|
+
raw_edges: List[Edge] = []
|
|
101
|
+
for p in all_files:
|
|
102
|
+
analyzer = registry.get_analyzer(p)
|
|
103
|
+
if not getattr(analyzer, "has_ast_support", True):
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
rel_path = str(p.relative_to(resolved_project_path)).replace("\\", "/")
|
|
107
|
+
source = p.read_text(encoding="utf-8", errors="replace")
|
|
108
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
109
|
+
cached_data = cache.get(rel_path, content_hash) if cache else None
|
|
110
|
+
if cached_data is not None:
|
|
111
|
+
symbols = cache.get_symbols(rel_path, content_hash) or []
|
|
112
|
+
edges = [
|
|
113
|
+
Edge(
|
|
114
|
+
source_id=e["source_id"],
|
|
115
|
+
target_id=e["target_id"],
|
|
116
|
+
kind=EdgeKind(e["kind"]),
|
|
117
|
+
weight=e.get("weight", 1.0),
|
|
118
|
+
confidence=e.get("confidence", 1.0),
|
|
119
|
+
location=Location(
|
|
120
|
+
file_id=e["location"]["file_id"],
|
|
121
|
+
start_line=e["location"]["start_line"],
|
|
122
|
+
start_col=e["location"]["start_col"],
|
|
123
|
+
end_line=e["location"]["end_line"],
|
|
124
|
+
end_col=e["location"]["end_col"],
|
|
125
|
+
) if e.get("location") else None,
|
|
126
|
+
)
|
|
127
|
+
for e in cached_data.get("edges", [])
|
|
128
|
+
]
|
|
129
|
+
else:
|
|
130
|
+
parse_res = analyzer.parse(Path(rel_path), source)
|
|
131
|
+
symbols = list(analyzer.extract_symbols(parse_res))
|
|
132
|
+
edges = list(analyzer.extract_edges(parse_res, symbols))
|
|
133
|
+
if cache:
|
|
134
|
+
cache.put(rel_path, content_hash, {
|
|
135
|
+
"symbols": [s.to_dict() for s in symbols],
|
|
136
|
+
"edges": [e.to_dict() for e in edges],
|
|
137
|
+
})
|
|
138
|
+
for s in symbols:
|
|
139
|
+
graph.add_symbol(s)
|
|
140
|
+
for e in edges:
|
|
141
|
+
graph.add_edge(e)
|
|
142
|
+
raw_edges.append(e)
|
|
143
|
+
parsed_files.add(rel_path)
|
|
144
|
+
except Exception as ex:
|
|
145
|
+
logging.warning(f"Error analyzing {p}: {ex}")
|
|
146
|
+
|
|
147
|
+
ctx.set_known_files(parsed_files)
|
|
148
|
+
|
|
149
|
+
for edge in raw_edges:
|
|
150
|
+
if edge.kind == EdgeKind.CONTAINS:
|
|
151
|
+
continue
|
|
152
|
+
src_file = edge.source_id.split("::")[0]
|
|
153
|
+
target_raw = edge.target_id.replace("module::", "").replace("import:", "").strip()
|
|
154
|
+
target_file_cand = target_raw.split("::")[0]
|
|
155
|
+
target_sym_name = target_raw.split("::")[1] if "::" in target_raw else (target_raw.split(".")[-1] if "." in target_raw else "")
|
|
156
|
+
|
|
157
|
+
if not (target_file_cand.startswith(".") or "/" in target_file_cand or "." in target_file_cand or edge.kind == EdgeKind.IMPORTS or target_file_cand in parsed_files):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
resolved_targets = ctx.resolve_path(src_file, target_file_cand)
|
|
161
|
+
if not resolved_targets and not target_file_cand.startswith("."):
|
|
162
|
+
dot_candidates = ctx.resolve_path(src_file, f"./{target_file_cand}")
|
|
163
|
+
if dot_candidates:
|
|
164
|
+
resolved_targets = dot_candidates
|
|
165
|
+
|
|
166
|
+
for rt in resolved_targets:
|
|
167
|
+
if rt != src_file and rt in parsed_files:
|
|
168
|
+
file_edge = Edge(
|
|
169
|
+
source_id=src_file,
|
|
170
|
+
target_id=rt,
|
|
171
|
+
kind=edge.kind if edge.kind != EdgeKind.CONTAINS else EdgeKind.IMPORTS,
|
|
172
|
+
weight=edge.weight,
|
|
173
|
+
confidence=edge.confidence,
|
|
174
|
+
location=edge.location,
|
|
175
|
+
)
|
|
176
|
+
graph.add_edge(file_edge)
|
|
177
|
+
if target_sym_name:
|
|
178
|
+
cand_sym_id = f"{rt}::{target_sym_name}"
|
|
179
|
+
if graph.has_symbol(cand_sym_id):
|
|
180
|
+
graph.add_edge(Edge(
|
|
181
|
+
source_id=edge.source_id,
|
|
182
|
+
target_id=cand_sym_id,
|
|
183
|
+
kind=edge.kind,
|
|
184
|
+
weight=edge.weight,
|
|
185
|
+
confidence=edge.confidence,
|
|
186
|
+
location=edge.location,
|
|
187
|
+
))
|
|
188
|
+
|
|
189
|
+
runner = AnalysisRunner()
|
|
190
|
+
actx = AnalysisContext(root_dir=resolved_project_path, resolve_context=ctx)
|
|
191
|
+
runner.run_all(graph, ctx=actx)
|
|
192
|
+
return graph
|
|
193
|
+
finally:
|
|
194
|
+
if cache:
|
|
195
|
+
cache.close()
|
|
196
|
+
if temp_clone_obj:
|
|
197
|
+
temp_clone_obj.cleanup()
|
|
198
|
+
|
|
199
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
200
|
+
"""Main CLI entrypoint.
|
|
201
|
+
Example:
|
|
202
|
+
>>> main(["langs", "--json"]) # doctest: +ELLIPSIS
|
|
203
|
+
{...}
|
|
204
|
+
0
|
|
205
|
+
"""
|
|
206
|
+
parent_parser = argparse.ArgumentParser(add_help=False)
|
|
207
|
+
parent_parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
208
|
+
|
|
209
|
+
parser = argparse.ArgumentParser(prog="codeui", description="Universal code-intelligence engine", parents=[parent_parser])
|
|
210
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
211
|
+
|
|
212
|
+
subparsers.add_parser("init", parents=[parent_parser], help="Initialize .codeui configuration")
|
|
213
|
+
|
|
214
|
+
scan_p = subparsers.add_parser("scan", parents=[parent_parser], help="Scan project and build code graph")
|
|
215
|
+
scan_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
216
|
+
scan_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
|
|
217
|
+
scan_p.add_argument("--out", default=".codeui", help="Output cache directory")
|
|
218
|
+
|
|
219
|
+
serve_p = subparsers.add_parser("serve", parents=[parent_parser], help="Start localhost web UI server")
|
|
220
|
+
serve_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
221
|
+
serve_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
|
|
222
|
+
serve_p.add_argument("--port", type=int, default=3000, help="Port number")
|
|
223
|
+
|
|
224
|
+
trace_p = subparsers.add_parser("trace", parents=[parent_parser], help="Trace symbol logic")
|
|
225
|
+
trace_p.add_argument("symbol", help="Target symbol ID")
|
|
226
|
+
trace_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
227
|
+
trace_p.add_argument("--direction", choices=["forward", "backward"], default="forward")
|
|
228
|
+
trace_p.add_argument("--depth", type=int, default=5)
|
|
229
|
+
|
|
230
|
+
context_p = subparsers.add_parser("context", parents=[parent_parser], help="Build dynamic AI agent context bundle")
|
|
231
|
+
context_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
232
|
+
context_p.add_argument("--symbol", help="Target symbol ID")
|
|
233
|
+
context_p.add_argument("--file", help="Target file ID")
|
|
234
|
+
context_p.add_argument("--query", default="Analyze code", help="Task query description")
|
|
235
|
+
context_p.add_argument("--defect", help="Target defect finding ID")
|
|
236
|
+
context_p.add_argument("--depth", type=int, default=1)
|
|
237
|
+
|
|
238
|
+
diff_p = subparsers.add_parser("diff", parents=[parent_parser], help="Display unified diff of active session overrides")
|
|
239
|
+
diff_p.add_argument("--overrides", default=".codeui/overrides.json", help="Path to overrides.json file")
|
|
240
|
+
|
|
241
|
+
apply_p = subparsers.add_parser("apply", parents=[parent_parser], help="Apply file edits directly to project files on disk")
|
|
242
|
+
apply_p.add_argument("file", help="Target file path to edit")
|
|
243
|
+
apply_p.add_argument("--content", help="New content to write to file")
|
|
244
|
+
apply_p.add_argument("--from-file", help="Path to source file containing new content")
|
|
245
|
+
apply_p.add_argument("--project", default=".", help="Project root directory")
|
|
246
|
+
apply_p.add_argument("--disk", action="store_true", default=True, help="Write directly to project file on disk (default)")
|
|
247
|
+
apply_p.add_argument("--no-disk", action="store_false", dest="disk", help="Store only in overrides without mutating disk")
|
|
248
|
+
|
|
249
|
+
defects_p = subparsers.add_parser("defects", parents=[parent_parser], help="List defect findings")
|
|
250
|
+
defects_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
251
|
+
defects_p.add_argument("--severity", choices=["error", "warning", "info"])
|
|
252
|
+
defects_p.add_argument("--format", choices=["json", "sarif", "markdown"], default="json")
|
|
253
|
+
|
|
254
|
+
subparsers.add_parser("langs", parents=[parent_parser], help="List supported programming languages")
|
|
255
|
+
subparsers.add_parser("plugins", parents=[parent_parser], help="List loaded plugins")
|
|
256
|
+
subparsers.add_parser("repo", parents=[parent_parser], help="Display official codeui GitHub repository URL")
|
|
257
|
+
|
|
258
|
+
contrib_p = subparsers.add_parser("contribute", parents=[parent_parser], help="Clone codeui, apply modifications, and push contribution to GitHub")
|
|
259
|
+
contrib_p.add_argument("--title", required=True, help="Contribution or bug fix title")
|
|
260
|
+
contrib_p.add_argument("--description", default="", help="Detailed description of the change")
|
|
261
|
+
contrib_p.add_argument("--file", action="append", nargs=2, metavar=("PATH", "CONTENT"), help="File relative path and new content")
|
|
262
|
+
contrib_p.add_argument("--branch", help="Target git branch name")
|
|
263
|
+
contrib_p.add_argument("--token", help="GitHub authentication token")
|
|
264
|
+
contrib_p.add_argument("--no-push", action="store_false", dest="push", help="Commit locally without pushing to GitHub")
|
|
265
|
+
|
|
266
|
+
parsed = parser.parse_args(args)
|
|
267
|
+
|
|
268
|
+
if parsed.command == "init":
|
|
269
|
+
Path(".codeui").mkdir(exist_ok=True)
|
|
270
|
+
if parsed.json:
|
|
271
|
+
print(json.dumps({"status": "initialized", "dir": ".codeui"}))
|
|
272
|
+
else:
|
|
273
|
+
print("Initialized .codeui workspace directory")
|
|
274
|
+
return 0
|
|
275
|
+
elif parsed.command == "scan":
|
|
276
|
+
p = Path(parsed.project).resolve()
|
|
277
|
+
g = scan_project(p, entry_points=parsed.entry)
|
|
278
|
+
out_dir = Path(parsed.out)
|
|
279
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
(out_dir / "graph.json").write_text(JSONEmitter().emit(g), encoding="utf-8")
|
|
281
|
+
if parsed.json:
|
|
282
|
+
print(JSONEmitter().emit(g))
|
|
283
|
+
else:
|
|
284
|
+
print(f"Scanned {len(g.get_all_files())} files, {len(g.get_all_symbols())} symbols, {len(g.get_findings())} defects.")
|
|
285
|
+
return 0
|
|
286
|
+
elif parsed.command == "serve":
|
|
287
|
+
p = Path(parsed.project).resolve()
|
|
288
|
+
g = scan_project(p, entry_points=parsed.entry)
|
|
289
|
+
store = OverrideStore()
|
|
290
|
+
server = create_server(g, store, p, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
|
|
291
|
+
print(f"codeui UI server running on http://127.0.0.1:{parsed.port}")
|
|
292
|
+
try:
|
|
293
|
+
server.serve_forever()
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
server.server_close()
|
|
296
|
+
return 0
|
|
297
|
+
elif parsed.command == "trace":
|
|
298
|
+
p = Path(parsed.project).resolve()
|
|
299
|
+
g = scan_project(p)
|
|
300
|
+
tracer = Tracer(g)
|
|
301
|
+
results = []
|
|
302
|
+
if parsed.direction == "forward":
|
|
303
|
+
results = list(tracer.forward(parsed.symbol, depth=parsed.depth))
|
|
304
|
+
else:
|
|
305
|
+
results = list(tracer.backward(parsed.symbol, depth=parsed.depth))
|
|
306
|
+
if parsed.json:
|
|
307
|
+
print(json.dumps([s.to_dict() for s in results], sort_keys=True, indent=2))
|
|
308
|
+
else:
|
|
309
|
+
print(f"Trace {parsed.direction} for {parsed.symbol} ({len(results)} symbols found):")
|
|
310
|
+
for sym in results:
|
|
311
|
+
print(f" - {sym.kind} {sym.id} ({sym.location.file_id}:{sym.location.start_line})")
|
|
312
|
+
return 0
|
|
313
|
+
elif parsed.command == "context":
|
|
314
|
+
p = Path(parsed.project).resolve()
|
|
315
|
+
g = scan_project(p)
|
|
316
|
+
builder = ContextBuilder(g)
|
|
317
|
+
if parsed.defect:
|
|
318
|
+
bundle = builder.for_defect(parsed.defect)
|
|
319
|
+
elif parsed.symbol:
|
|
320
|
+
bundle = builder.for_symbol(parsed.symbol, depth=parsed.depth)
|
|
321
|
+
elif parsed.file:
|
|
322
|
+
bundle = builder.for_file(parsed.file, depth=parsed.depth)
|
|
323
|
+
else:
|
|
324
|
+
task = TaskSpec(query=parsed.query, target_symbols=[s for s in [parsed.symbol] if s], target_files=[f for f in [parsed.file] if f])
|
|
325
|
+
bundle = builder.for_task(task, depth=parsed.depth)
|
|
326
|
+
print(bundle.to_json())
|
|
327
|
+
return 0
|
|
328
|
+
elif parsed.command == "diff":
|
|
329
|
+
store = OverrideStore()
|
|
330
|
+
overrides_path = Path(parsed.overrides)
|
|
331
|
+
if overrides_path.exists():
|
|
332
|
+
try:
|
|
333
|
+
data = json.loads(overrides_path.read_text(encoding="utf-8"))
|
|
334
|
+
items = data if isinstance(data, list) else data.get("overrides", [])
|
|
335
|
+
for item in items:
|
|
336
|
+
if item.get("kind") == "replace_file":
|
|
337
|
+
store.set_file_override(
|
|
338
|
+
file_path=item["target_id"],
|
|
339
|
+
original_content=item.get("original_content", ""),
|
|
340
|
+
new_content=item.get("new_value", ""),
|
|
341
|
+
author=item.get("author", "user"),
|
|
342
|
+
note=item.get("note"),
|
|
343
|
+
)
|
|
344
|
+
except Exception as ex:
|
|
345
|
+
logging.warning(f"Error loading overrides from {overrides_path}: {ex}")
|
|
346
|
+
diff_text = store.to_diff()
|
|
347
|
+
if parsed.json:
|
|
348
|
+
print(json.dumps({"diff": diff_text, "overrides_count": len(list(store.list()))}))
|
|
349
|
+
else:
|
|
350
|
+
if diff_text:
|
|
351
|
+
print(diff_text)
|
|
352
|
+
else:
|
|
353
|
+
print("No active file differences in overrides.")
|
|
354
|
+
return 0
|
|
355
|
+
elif parsed.command == "apply":
|
|
356
|
+
proj_path = Path(parsed.project).resolve()
|
|
357
|
+
new_content = parsed.content
|
|
358
|
+
if new_content is None and parsed.from_file:
|
|
359
|
+
new_content = Path(parsed.from_file).read_text(encoding="utf-8")
|
|
360
|
+
if new_content is None:
|
|
361
|
+
new_content = sys.stdin.read()
|
|
362
|
+
g = Graph()
|
|
363
|
+
store = OverrideStore()
|
|
364
|
+
from codeui.agent.edit_tools import EditTools
|
|
365
|
+
tools = EditTools(g, store, project_root=proj_path, direct_disk=parsed.disk)
|
|
366
|
+
prop = tools.replace_file(parsed.file, new_content)
|
|
367
|
+
if parsed.disk:
|
|
368
|
+
saved_path = tools.apply_to_disk(prop)
|
|
369
|
+
if parsed.json:
|
|
370
|
+
print(json.dumps({"status": "written_to_disk", "file": parsed.file, "path": str(saved_path)}))
|
|
371
|
+
else:
|
|
372
|
+
print(f"Applied changes directly to file on disk: {saved_path}")
|
|
373
|
+
else:
|
|
374
|
+
tools.apply(prop, write_to_disk=False)
|
|
375
|
+
if parsed.json:
|
|
376
|
+
print(json.dumps({"status": "stored_override", "file": parsed.file}))
|
|
377
|
+
else:
|
|
378
|
+
print(f"Stored non-destructive override for: {parsed.file}")
|
|
379
|
+
return 0
|
|
380
|
+
elif parsed.command == "langs":
|
|
381
|
+
langs = LanguageRegistry().supported_languages()
|
|
382
|
+
if parsed.json:
|
|
383
|
+
print(json.dumps({"languages": langs}))
|
|
384
|
+
else:
|
|
385
|
+
print("Supported languages: " + ", ".join(langs))
|
|
386
|
+
return 0
|
|
387
|
+
elif parsed.command == "defects":
|
|
388
|
+
p = Path(parsed.project).resolve()
|
|
389
|
+
g = scan_project(p)
|
|
390
|
+
if parsed.format == "sarif":
|
|
391
|
+
print(SARIFEmitter().emit(g))
|
|
392
|
+
elif parsed.format == "markdown":
|
|
393
|
+
print(MarkdownEmitter().emit(g))
|
|
394
|
+
else:
|
|
395
|
+
print(JSONEmitter().emit(g))
|
|
396
|
+
return 0
|
|
397
|
+
elif parsed.command == "plugins":
|
|
398
|
+
if parsed.json:
|
|
399
|
+
print(json.dumps({"plugins": []}))
|
|
400
|
+
else:
|
|
401
|
+
print("Loaded plugins: none")
|
|
402
|
+
return 0
|
|
403
|
+
elif parsed.command == "repo":
|
|
404
|
+
from codeui.core.repo import get_library_repo_url
|
|
405
|
+
repo_url = get_library_repo_url()
|
|
406
|
+
if parsed.json:
|
|
407
|
+
print(json.dumps({"repo_url": repo_url}))
|
|
408
|
+
else:
|
|
409
|
+
print(f"codeui GitHub Repository: {repo_url}")
|
|
410
|
+
return 0
|
|
411
|
+
elif parsed.command == "contribute":
|
|
412
|
+
from codeui.agent.contribution import ContributionManager
|
|
413
|
+
changes = {f[0]: f[1] for f in (parsed.file or [])}
|
|
414
|
+
mgr = ContributionManager(token=parsed.token)
|
|
415
|
+
res = mgr.submit_contribution(
|
|
416
|
+
title=parsed.title,
|
|
417
|
+
description=parsed.description,
|
|
418
|
+
file_changes=changes,
|
|
419
|
+
branch_name=parsed.branch,
|
|
420
|
+
token=parsed.token,
|
|
421
|
+
push=parsed.push,
|
|
422
|
+
)
|
|
423
|
+
if parsed.json:
|
|
424
|
+
print(json.dumps(res, indent=2))
|
|
425
|
+
else:
|
|
426
|
+
print(f"Contribution status: {res['status']}")
|
|
427
|
+
print(f"Target Branch: {res['branch']}")
|
|
428
|
+
print(f"Repository: {res['repo_url']}")
|
|
429
|
+
if res.get("changed_files"):
|
|
430
|
+
print("Changed files: " + ", ".join(res["changed_files"]))
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
parser.print_help()
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
sys.exit(main())
|
codeui/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core components for codeui."""
|