codexa 0.4.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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- semantic_code_intelligence/workspace/__init__.py +427 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""CLI command: plugin — manage and scaffold plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
import textwrap
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from semantic_code_intelligence.utils.logging import (
|
|
12
|
+
console,
|
|
13
|
+
get_logger,
|
|
14
|
+
print_error,
|
|
15
|
+
print_info,
|
|
16
|
+
print_success,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = get_logger("cli.plugin")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PLUGIN_TEMPLATE = textwrap.dedent('''\
|
|
23
|
+
"""CodexA plugin: {name}
|
|
24
|
+
|
|
25
|
+
{description}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from semantic_code_intelligence.plugins import PluginBase, PluginHook, PluginMetadata
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class {class_name}(PluginBase):
|
|
36
|
+
"""Plugin implementation for {name}."""
|
|
37
|
+
|
|
38
|
+
def metadata(self) -> PluginMetadata:
|
|
39
|
+
return PluginMetadata(
|
|
40
|
+
name="{name}",
|
|
41
|
+
version="0.1.0",
|
|
42
|
+
description="{description}",
|
|
43
|
+
author="{author}",
|
|
44
|
+
hooks=[{hooks}],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
48
|
+
"""Called when the plugin is activated."""
|
|
49
|
+
|
|
50
|
+
def deactivate(self) -> None:
|
|
51
|
+
"""Called when the plugin is deactivated."""
|
|
52
|
+
|
|
53
|
+
def on_hook(self, hook: PluginHook, data: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
"""Process hook events.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
hook: The hook that fired.
|
|
58
|
+
data: Hook-specific data. Modify and return.
|
|
59
|
+
"""
|
|
60
|
+
# Add your logic here
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_plugin() -> {class_name}:
|
|
65
|
+
"""Factory function for plugin discovery."""
|
|
66
|
+
return {class_name}()
|
|
67
|
+
''')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.group("plugin")
|
|
71
|
+
def plugin_cmd() -> None:
|
|
72
|
+
"""Manage CodexA plugins."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@plugin_cmd.command("new")
|
|
76
|
+
@click.argument("name")
|
|
77
|
+
@click.option(
|
|
78
|
+
"--description",
|
|
79
|
+
"-d",
|
|
80
|
+
default="A CodexA plugin",
|
|
81
|
+
help="Plugin description.",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--author",
|
|
85
|
+
"-a",
|
|
86
|
+
default="",
|
|
87
|
+
help="Plugin author name.",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--hooks",
|
|
91
|
+
"-H",
|
|
92
|
+
default="POST_SEARCH",
|
|
93
|
+
help="Comma-separated hook names (e.g. POST_SEARCH,POST_AI).",
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--output",
|
|
97
|
+
"-o",
|
|
98
|
+
default=None,
|
|
99
|
+
type=click.Path(file_okay=False),
|
|
100
|
+
help="Output directory (default: .codexa/plugins/).",
|
|
101
|
+
)
|
|
102
|
+
def plugin_new(name: str, description: str, author: str, hooks: str, output: str | None) -> None:
|
|
103
|
+
"""Scaffold a new plugin from template.
|
|
104
|
+
|
|
105
|
+
Creates a ready-to-use plugin file with the correct structure.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
|
|
109
|
+
codexa plugin new my-formatter
|
|
110
|
+
|
|
111
|
+
codexa plugin new lint-checker --hooks CUSTOM_VALIDATION,POST_AI
|
|
112
|
+
|
|
113
|
+
codexa plugin new metrics -o ./plugins/ -a "Your Name"
|
|
114
|
+
"""
|
|
115
|
+
# Validate hook names
|
|
116
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
117
|
+
|
|
118
|
+
hook_names = [h.strip().upper() for h in hooks.split(",") if h.strip()]
|
|
119
|
+
valid_hooks = {h.name for h in PluginHook}
|
|
120
|
+
for h in hook_names:
|
|
121
|
+
if h not in valid_hooks:
|
|
122
|
+
print_error(f"Unknown hook: {h}. Valid hooks: {', '.join(sorted(valid_hooks))}")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
hook_refs = ", ".join(f"PluginHook.{h}" for h in hook_names)
|
|
126
|
+
|
|
127
|
+
# Build class name from plugin name
|
|
128
|
+
class_name = "".join(part.capitalize() for part in name.replace("-", "_").split("_")) + "Plugin"
|
|
129
|
+
|
|
130
|
+
content = PLUGIN_TEMPLATE.format(
|
|
131
|
+
name=name,
|
|
132
|
+
description=description,
|
|
133
|
+
author=author,
|
|
134
|
+
class_name=class_name,
|
|
135
|
+
hooks=hook_refs,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Determine output path
|
|
139
|
+
if output:
|
|
140
|
+
out_dir = Path(output).resolve()
|
|
141
|
+
else:
|
|
142
|
+
out_dir = Path(".codexa/plugins").resolve()
|
|
143
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
filename = name.replace("-", "_") + ".py"
|
|
146
|
+
filepath = out_dir / filename
|
|
147
|
+
|
|
148
|
+
if filepath.exists():
|
|
149
|
+
print_error(f"File already exists: {filepath}")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
filepath.write_text(content, encoding="utf-8")
|
|
153
|
+
print_success(f"Created plugin: {filepath}")
|
|
154
|
+
print_info(f"Register it by placing it in .codexa/plugins/ or calling PluginManager.register()")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@plugin_cmd.command("list")
|
|
158
|
+
@click.option(
|
|
159
|
+
"--path",
|
|
160
|
+
"-p",
|
|
161
|
+
default=".",
|
|
162
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
163
|
+
help="Project root path.",
|
|
164
|
+
)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--json-output",
|
|
167
|
+
"--json",
|
|
168
|
+
"json_mode",
|
|
169
|
+
is_flag=True,
|
|
170
|
+
default=False,
|
|
171
|
+
help="Output results in JSON format.",
|
|
172
|
+
)
|
|
173
|
+
def plugin_list(path: str, json_mode: bool) -> None:
|
|
174
|
+
"""List available plugins in the project.
|
|
175
|
+
|
|
176
|
+
Scans .codexa/plugins/ for discoverable plugin files.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
|
|
180
|
+
codexa plugin list
|
|
181
|
+
|
|
182
|
+
codexa plugin list --json
|
|
183
|
+
"""
|
|
184
|
+
from semantic_code_intelligence.plugins import PluginManager
|
|
185
|
+
|
|
186
|
+
plugin_dir = Path(path) / ".codexa" / "plugins"
|
|
187
|
+
mgr = PluginManager()
|
|
188
|
+
|
|
189
|
+
if plugin_dir.is_dir():
|
|
190
|
+
count = mgr.discover_from_directory(plugin_dir)
|
|
191
|
+
else:
|
|
192
|
+
count = 0
|
|
193
|
+
|
|
194
|
+
plugins = []
|
|
195
|
+
for name in mgr.registered_plugins:
|
|
196
|
+
info = mgr.get_plugin_info(name)
|
|
197
|
+
if info:
|
|
198
|
+
plugins.append(info)
|
|
199
|
+
|
|
200
|
+
if json_mode:
|
|
201
|
+
click.echo(json_mod.dumps({"plugins": plugins, "count": len(plugins)}, indent=2))
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if not plugins:
|
|
205
|
+
print_info(f"No plugins found in {plugin_dir}/")
|
|
206
|
+
print_info("Create one with: codexa plugin new <name>")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
from rich.table import Table
|
|
210
|
+
|
|
211
|
+
table = Table(title="Discovered Plugins")
|
|
212
|
+
table.add_column("Name", style="bold")
|
|
213
|
+
table.add_column("Version")
|
|
214
|
+
table.add_column("Description")
|
|
215
|
+
table.add_column("Hooks")
|
|
216
|
+
|
|
217
|
+
for p in plugins:
|
|
218
|
+
table.add_row(
|
|
219
|
+
p["name"],
|
|
220
|
+
p["version"],
|
|
221
|
+
p["description"],
|
|
222
|
+
", ".join(p.get("hooks", [])),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
console.print(table)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@plugin_cmd.command("info")
|
|
229
|
+
@click.argument("name")
|
|
230
|
+
@click.option(
|
|
231
|
+
"--path",
|
|
232
|
+
"-p",
|
|
233
|
+
default=".",
|
|
234
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
235
|
+
help="Project root path.",
|
|
236
|
+
)
|
|
237
|
+
@click.option(
|
|
238
|
+
"--json-output",
|
|
239
|
+
"--json",
|
|
240
|
+
"json_mode",
|
|
241
|
+
is_flag=True,
|
|
242
|
+
default=False,
|
|
243
|
+
help="Output in JSON format.",
|
|
244
|
+
)
|
|
245
|
+
def plugin_info(name: str, path: str, json_mode: bool) -> None:
|
|
246
|
+
"""Show details about a specific plugin.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
|
|
250
|
+
codexa plugin info my-formatter
|
|
251
|
+
"""
|
|
252
|
+
from semantic_code_intelligence.plugins import PluginManager
|
|
253
|
+
|
|
254
|
+
plugin_dir = Path(path) / ".codexa" / "plugins"
|
|
255
|
+
mgr = PluginManager()
|
|
256
|
+
|
|
257
|
+
if plugin_dir.is_dir():
|
|
258
|
+
mgr.discover_from_directory(plugin_dir)
|
|
259
|
+
|
|
260
|
+
info = mgr.get_plugin_info(name)
|
|
261
|
+
if info is None:
|
|
262
|
+
print_error(f"Plugin '{name}' not found.")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
if json_mode:
|
|
266
|
+
click.echo(json_mod.dumps(info, indent=2))
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
console.print(f"[bold]{info['name']}[/bold] v{info['version']}")
|
|
270
|
+
if info.get("description"):
|
|
271
|
+
console.print(f" {info['description']}")
|
|
272
|
+
if info.get("author"):
|
|
273
|
+
console.print(f" Author: {info['author']}")
|
|
274
|
+
console.print(f" Hooks: {', '.join(info.get('hooks', []))}")
|
|
275
|
+
console.print(f" Active: {info.get('active', False)}")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""CLI command: pr-summary — generate PR intelligence report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.utils.logging import (
|
|
11
|
+
console,
|
|
12
|
+
get_logger,
|
|
13
|
+
print_error,
|
|
14
|
+
print_info,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = get_logger("cli.pr_summary")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find_changed_files(root: Path) -> list[str]:
|
|
21
|
+
"""Discover changed files using simple heuristics.
|
|
22
|
+
|
|
23
|
+
Checks for:
|
|
24
|
+
1. Git diff (if inside a git repo).
|
|
25
|
+
2. Falls back to all supported source files.
|
|
26
|
+
"""
|
|
27
|
+
import subprocess
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
["git", "diff", "--name-only", "HEAD~1", "HEAD"],
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
cwd=str(root),
|
|
35
|
+
timeout=10,
|
|
36
|
+
)
|
|
37
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
38
|
+
files = []
|
|
39
|
+
for line in result.stdout.strip().splitlines():
|
|
40
|
+
fpath = root / line.strip()
|
|
41
|
+
if fpath.exists():
|
|
42
|
+
files.append(str(fpath))
|
|
43
|
+
if files:
|
|
44
|
+
return files
|
|
45
|
+
except Exception:
|
|
46
|
+
logger.debug("git diff-tree failed; falling back to full file scan")
|
|
47
|
+
|
|
48
|
+
# Fallback: list all supported source files
|
|
49
|
+
from semantic_code_intelligence.parsing.parser import EXTENSION_TO_LANGUAGE
|
|
50
|
+
files = []
|
|
51
|
+
for f in root.rglob("*"):
|
|
52
|
+
if f.is_file() and f.suffix in EXTENSION_TO_LANGUAGE:
|
|
53
|
+
parts = f.relative_to(root).parts
|
|
54
|
+
if any(p.startswith(".") or p in ("__pycache__", "node_modules") for p in parts):
|
|
55
|
+
continue
|
|
56
|
+
files.append(str(f))
|
|
57
|
+
return files
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.command("pr-summary")
|
|
61
|
+
@click.option(
|
|
62
|
+
"--path",
|
|
63
|
+
"-p",
|
|
64
|
+
default=".",
|
|
65
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
66
|
+
help="Project root path.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--json-output",
|
|
70
|
+
"--json",
|
|
71
|
+
"json_mode",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
default=False,
|
|
74
|
+
help="Output in JSON format.",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--files",
|
|
78
|
+
"-f",
|
|
79
|
+
multiple=True,
|
|
80
|
+
help="Specific files to analyze (can be repeated).",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--pipe",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
default=False,
|
|
86
|
+
help="Plain text output for piping / CI.",
|
|
87
|
+
)
|
|
88
|
+
@click.pass_context
|
|
89
|
+
def pr_summary_cmd(
|
|
90
|
+
ctx: click.Context,
|
|
91
|
+
path: str,
|
|
92
|
+
json_mode: bool,
|
|
93
|
+
files: tuple[str, ...],
|
|
94
|
+
pipe: bool,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Generate a Pull Request intelligence report.
|
|
97
|
+
|
|
98
|
+
Analyzes changed files to produce a change summary, semantic impact
|
|
99
|
+
analysis, suggested reviewer domains, and risk scoring.
|
|
100
|
+
|
|
101
|
+
All output is advisory — CodexA never modifies repository code.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
|
|
105
|
+
codexa pr-summary
|
|
106
|
+
|
|
107
|
+
codexa pr-summary --json
|
|
108
|
+
|
|
109
|
+
codexa pr-summary -f src/main.py -f src/utils.py
|
|
110
|
+
"""
|
|
111
|
+
from semantic_code_intelligence.ci.pr import generate_pr_report
|
|
112
|
+
|
|
113
|
+
root = Path(path).resolve()
|
|
114
|
+
changed = list(files) if files else _find_changed_files(root)
|
|
115
|
+
|
|
116
|
+
if not changed:
|
|
117
|
+
if json_mode:
|
|
118
|
+
click.echo(json_mod.dumps({"error": "No changed files found"}, indent=2))
|
|
119
|
+
else:
|
|
120
|
+
print_info("No changed files found")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
report = generate_pr_report(changed, root)
|
|
124
|
+
|
|
125
|
+
if json_mode:
|
|
126
|
+
click.echo(json_mod.dumps(report.to_dict(), indent=2))
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
cs = report.change_summary
|
|
130
|
+
if pipe:
|
|
131
|
+
click.echo(f"Changed: {cs.files_changed} files Languages: {', '.join(cs.languages) or 'none'}")
|
|
132
|
+
click.echo(f"Symbols: +{cs.total_symbols_added} -{cs.total_symbols_removed} ~{cs.total_symbols_modified}")
|
|
133
|
+
if report.risk:
|
|
134
|
+
click.echo(f"Risk: {report.risk.level} ({report.risk.score}/100)")
|
|
135
|
+
for f in report.risk.factors:
|
|
136
|
+
click.echo(f" - {f}")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Rich output
|
|
140
|
+
console.print(f"\n[bold cyan]PR Summary[/bold cyan]\n")
|
|
141
|
+
console.print(f" Files changed: {cs.files_changed}")
|
|
142
|
+
console.print(f" Languages: {', '.join(cs.languages) or 'none'}")
|
|
143
|
+
console.print(f" Symbols: [green]+{cs.total_symbols_added}[/green] [red]-{cs.total_symbols_removed}[/red] [yellow]~{cs.total_symbols_modified}[/yellow]\n")
|
|
144
|
+
|
|
145
|
+
if cs.file_details:
|
|
146
|
+
console.print("[bold]File Details:[/bold]")
|
|
147
|
+
for fd in cs.file_details:
|
|
148
|
+
lang_tag = f" [{fd.language}]" if fd.language else ""
|
|
149
|
+
console.print(f" {fd.path}{lang_tag}")
|
|
150
|
+
if fd.symbols_added:
|
|
151
|
+
console.print(f" [green]+ {', '.join(fd.symbols_added)}[/green]")
|
|
152
|
+
if fd.symbols_removed:
|
|
153
|
+
console.print(f" [red]- {', '.join(fd.symbols_removed)}[/red]")
|
|
154
|
+
if fd.symbols_modified:
|
|
155
|
+
console.print(f" [yellow]~ {', '.join(fd.symbols_modified)}[/yellow]")
|
|
156
|
+
console.print()
|
|
157
|
+
|
|
158
|
+
if report.impact:
|
|
159
|
+
imp = report.impact
|
|
160
|
+
if imp.affected_symbols:
|
|
161
|
+
console.print("[bold]Impact — Affected Symbols:[/bold]")
|
|
162
|
+
for s in imp.affected_symbols[:20]:
|
|
163
|
+
console.print(f" {s}")
|
|
164
|
+
console.print()
|
|
165
|
+
|
|
166
|
+
if report.reviewers:
|
|
167
|
+
console.print("[bold]Suggested Reviewer Domains:[/bold]")
|
|
168
|
+
for r in report.reviewers[:10]:
|
|
169
|
+
console.print(f" {r['domain']} ({r['file_count']} file(s))")
|
|
170
|
+
console.print()
|
|
171
|
+
|
|
172
|
+
if report.risk:
|
|
173
|
+
color = {"low": "green", "medium": "yellow", "high": "red", "critical": "bold red"}
|
|
174
|
+
c = color.get(report.risk.level, "white")
|
|
175
|
+
console.print(f"[{c}]Risk: {report.risk.level.upper()} ({report.risk.score}/100)[/{c}]")
|
|
176
|
+
for f in report.risk.factors:
|
|
177
|
+
console.print(f" - {f}")
|
|
178
|
+
console.print()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""CLI command: quality — run code quality analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from semantic_code_intelligence.utils.logging import (
|
|
12
|
+
console,
|
|
13
|
+
get_logger,
|
|
14
|
+
print_error,
|
|
15
|
+
print_success,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from semantic_code_intelligence.ci.quality import QualityReport
|
|
20
|
+
from semantic_code_intelligence.llm.safety import SafetyResult
|
|
21
|
+
|
|
22
|
+
logger = get_logger("cli.quality")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Output helpers — each handles one output format
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _output_safety(safety: "SafetyResult", count: int, *, json_mode: bool, pipe: bool) -> None:
|
|
31
|
+
"""Emit safety-only results in the requested format."""
|
|
32
|
+
if json_mode:
|
|
33
|
+
click.echo(json_mod.dumps({
|
|
34
|
+
"files_analyzed": count,
|
|
35
|
+
"safety": safety.to_dict(),
|
|
36
|
+
}, indent=2))
|
|
37
|
+
elif pipe:
|
|
38
|
+
if safety.safe:
|
|
39
|
+
click.echo(f"PASS {count} files scanned, no safety issues")
|
|
40
|
+
else:
|
|
41
|
+
click.echo(f"FAIL {count} files scanned, {len(safety.issues)} safety issue(s)")
|
|
42
|
+
for issue in safety.issues:
|
|
43
|
+
click.echo(f" L{issue.line_number}: {issue.description}")
|
|
44
|
+
else:
|
|
45
|
+
if safety.safe:
|
|
46
|
+
print_success(f"Safety check passed — {count} files scanned")
|
|
47
|
+
else:
|
|
48
|
+
print_error(f"{len(safety.issues)} safety issue(s) found in {count} files")
|
|
49
|
+
for issue in safety.issues:
|
|
50
|
+
console.print(f" [yellow]L{issue.line_number}[/yellow]: {issue.description}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _output_report_pipe(report: "QualityReport") -> None:
|
|
54
|
+
"""Emit full quality report in pipe-friendly plain text."""
|
|
55
|
+
mi_str = f" MI: {report.maintainability_index:.1f}" if report.maintainability_index is not None else ""
|
|
56
|
+
click.echo(f"Files: {report.files_analyzed} Symbols: {report.symbol_count} Issues: {report.issue_count}{mi_str}")
|
|
57
|
+
for c in report.complexity_issues:
|
|
58
|
+
click.echo(f" COMPLEXITY {c.symbol_name} ({c.file_path}:{c.start_line}) score={c.complexity} [{c.rating}]")
|
|
59
|
+
for d in report.dead_code:
|
|
60
|
+
click.echo(f" DEAD_CODE {d.symbol_name} ({d.file_path}:{d.start_line}) kind={d.kind}")
|
|
61
|
+
for dup in report.duplicates:
|
|
62
|
+
click.echo(f" DUPLICATE {dup.symbol_a} ↔ {dup.symbol_b} sim={dup.similarity:.2f}")
|
|
63
|
+
for b in report.bandit_issues:
|
|
64
|
+
click.echo(f" SECURITY [{b.test_id}] {b.text} ({b.file_path}:{b.line}) sev={b.severity}")
|
|
65
|
+
if report.safety and not report.safety.safe:
|
|
66
|
+
for i in report.safety.issues:
|
|
67
|
+
click.echo(f" SAFETY L{i.line_number}: {i.description}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _output_report_rich(report: "QualityReport", root: Path) -> None:
|
|
71
|
+
"""Emit full quality report with Rich formatting."""
|
|
72
|
+
console.print(f"\n[bold cyan]Quality Report[/bold cyan] — {root}\n")
|
|
73
|
+
console.print(f" Files analyzed: {report.files_analyzed}")
|
|
74
|
+
console.print(f" Symbols: {report.symbol_count}")
|
|
75
|
+
console.print(f" Issues: {report.issue_count}")
|
|
76
|
+
if report.maintainability_index is not None:
|
|
77
|
+
console.print(f" Maintainability Index: {report.maintainability_index:.1f}")
|
|
78
|
+
console.print()
|
|
79
|
+
|
|
80
|
+
if report.complexity_issues:
|
|
81
|
+
console.print("[bold yellow]High Complexity Functions:[/bold yellow]")
|
|
82
|
+
for c in report.complexity_issues:
|
|
83
|
+
console.print(f" {c.symbol_name} ({c.file_path}:{c.start_line}) — score {c.complexity} [{c.rating}]")
|
|
84
|
+
console.print()
|
|
85
|
+
|
|
86
|
+
if report.dead_code:
|
|
87
|
+
console.print("[bold yellow]Potentially Dead Code:[/bold yellow]")
|
|
88
|
+
for d in report.dead_code:
|
|
89
|
+
console.print(f" {d.symbol_name} ({d.file_path}:{d.start_line}) — {d.kind}")
|
|
90
|
+
console.print()
|
|
91
|
+
|
|
92
|
+
if report.duplicates:
|
|
93
|
+
console.print("[bold yellow]Duplicate Logic:[/bold yellow]")
|
|
94
|
+
for dup in report.duplicates:
|
|
95
|
+
console.print(f" {dup.symbol_a} ↔ {dup.symbol_b} — {dup.similarity:.0%} similar")
|
|
96
|
+
console.print()
|
|
97
|
+
|
|
98
|
+
if report.bandit_issues:
|
|
99
|
+
console.print("[bold red]Security Issues (Bandit):[/bold red]")
|
|
100
|
+
for b in report.bandit_issues:
|
|
101
|
+
console.print(f" [{b.test_id}] {b.text} ({b.file_path}:{b.line}) — {b.severity}")
|
|
102
|
+
console.print()
|
|
103
|
+
|
|
104
|
+
if report.safety and not report.safety.safe:
|
|
105
|
+
console.print("[bold red]Safety Issues:[/bold red]")
|
|
106
|
+
for i in report.safety.issues:
|
|
107
|
+
console.print(f" L{i.line_number}: {i.description}")
|
|
108
|
+
console.print()
|
|
109
|
+
|
|
110
|
+
if report.issue_count == 0:
|
|
111
|
+
print_success("No quality issues found")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@click.command("quality")
|
|
115
|
+
@click.option(
|
|
116
|
+
"--path",
|
|
117
|
+
"-p",
|
|
118
|
+
default=".",
|
|
119
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
120
|
+
help="Project root path.",
|
|
121
|
+
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--json-output",
|
|
124
|
+
"--json",
|
|
125
|
+
"json_mode",
|
|
126
|
+
is_flag=True,
|
|
127
|
+
default=False,
|
|
128
|
+
help="Output in JSON format.",
|
|
129
|
+
)
|
|
130
|
+
@click.option(
|
|
131
|
+
"--complexity-threshold",
|
|
132
|
+
type=int,
|
|
133
|
+
default=10,
|
|
134
|
+
help="Minimum cyclomatic complexity to report (default: 10).",
|
|
135
|
+
)
|
|
136
|
+
@click.option(
|
|
137
|
+
"--safety-only",
|
|
138
|
+
is_flag=True,
|
|
139
|
+
default=False,
|
|
140
|
+
help="Run only the safety validator (fast mode).",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"--pipe",
|
|
144
|
+
is_flag=True,
|
|
145
|
+
default=False,
|
|
146
|
+
help="Plain text output for piping / CI.",
|
|
147
|
+
)
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def quality_cmd(
|
|
150
|
+
ctx: click.Context,
|
|
151
|
+
path: str,
|
|
152
|
+
json_mode: bool,
|
|
153
|
+
complexity_threshold: int,
|
|
154
|
+
safety_only: bool,
|
|
155
|
+
pipe: bool,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Analyze code quality — complexity, dead code, duplicates, security.
|
|
158
|
+
|
|
159
|
+
Scans the project for quality issues and produces a human-readable or
|
|
160
|
+
JSON report. Useful for CI pipelines and local development.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
|
|
164
|
+
codexa quality
|
|
165
|
+
|
|
166
|
+
codexa quality --json
|
|
167
|
+
|
|
168
|
+
codexa quality --safety-only --pipe
|
|
169
|
+
|
|
170
|
+
codexa quality --complexity-threshold 15
|
|
171
|
+
"""
|
|
172
|
+
from semantic_code_intelligence.ci.quality import analyze_project, QualityReport
|
|
173
|
+
from semantic_code_intelligence.llm.safety import SafetyValidator
|
|
174
|
+
|
|
175
|
+
root = Path(path).resolve()
|
|
176
|
+
|
|
177
|
+
if safety_only:
|
|
178
|
+
# Fast path: only safety scan
|
|
179
|
+
from semantic_code_intelligence.parsing.parser import EXTENSION_TO_LANGUAGE
|
|
180
|
+
code = ""
|
|
181
|
+
count = 0
|
|
182
|
+
for f in root.rglob("*"):
|
|
183
|
+
if f.is_file() and f.suffix in EXTENSION_TO_LANGUAGE:
|
|
184
|
+
parts = f.relative_to(root).parts
|
|
185
|
+
if any(p.startswith(".") or p in ("__pycache__", "node_modules") for p in parts):
|
|
186
|
+
continue
|
|
187
|
+
try:
|
|
188
|
+
code += f.read_text(encoding="utf-8", errors="replace") + "\n"
|
|
189
|
+
count += 1
|
|
190
|
+
except Exception:
|
|
191
|
+
logger.debug("Skipping unreadable file: %s", f)
|
|
192
|
+
validator = SafetyValidator()
|
|
193
|
+
safety = validator.validate(code)
|
|
194
|
+
_output_safety(safety, count, json_mode=json_mode, pipe=pipe)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Full quality analysis
|
|
198
|
+
report = analyze_project(
|
|
199
|
+
root,
|
|
200
|
+
complexity_threshold=complexity_threshold,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if json_mode:
|
|
204
|
+
click.echo(json_mod.dumps(report.to_dict(), indent=2))
|
|
205
|
+
elif pipe:
|
|
206
|
+
_output_report_pipe(report)
|
|
207
|
+
else:
|
|
208
|
+
_output_report_rich(report, root)
|