osscodeiq 0.0.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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
osscodeiq/cli.py
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""CLI entry point for OSSCodeIQ."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from osscodeiq.config import Config
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="osscodeiq",
|
|
15
|
+
help="Intelligent code graph discovery and analysis CLI.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
_GRAPH_DIR_NAME = ".code-intelligence"
|
|
21
|
+
_KUZU_DB_NAME = "graph.kuzu"
|
|
22
|
+
_SQLITE_DB_NAME = "graph.db"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_version() -> str:
|
|
26
|
+
"""Get package version from metadata."""
|
|
27
|
+
try:
|
|
28
|
+
from importlib.metadata import version
|
|
29
|
+
return version("osscodeiq")
|
|
30
|
+
except Exception:
|
|
31
|
+
return "0.1.0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def version() -> None:
|
|
36
|
+
"""Show version information."""
|
|
37
|
+
ver = _get_version()
|
|
38
|
+
from osscodeiq.detectors.registry import DetectorRegistry
|
|
39
|
+
r = DetectorRegistry()
|
|
40
|
+
r.load_builtin_detectors()
|
|
41
|
+
console.print(f"osscodeiq v{ver}")
|
|
42
|
+
console.print(f" Detectors: {len(r.all_detectors())}")
|
|
43
|
+
langs = set()
|
|
44
|
+
for d in r.all_detectors():
|
|
45
|
+
for l in d.supported_languages:
|
|
46
|
+
langs.add(l)
|
|
47
|
+
console.print(f" Languages: {len(langs)}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_config(config: Path | None, project_path: Path | None = None) -> Config:
|
|
51
|
+
return Config.load(config, project_path=project_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def analyze(
|
|
56
|
+
path: Annotated[Path, typer.Argument(help="Path to the codebase to analyze")] = Path("."),
|
|
57
|
+
incremental: Annotated[bool, typer.Option("--incremental/--full", help="Use incremental analysis")] = True,
|
|
58
|
+
parallelism: Annotated[int, typer.Option("--parallelism", "-j", help="Number of parallel workers")] = 8,
|
|
59
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend (networkx, kuzu, sqlite)")] = "networkx",
|
|
60
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config file")] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Analyze a codebase and build the OSSCodeIQ graph."""
|
|
63
|
+
from osscodeiq.analyzer import Analyzer
|
|
64
|
+
|
|
65
|
+
cfg = _load_config(config)
|
|
66
|
+
cfg.analysis.parallelism = parallelism
|
|
67
|
+
cfg.analysis.incremental = incremental
|
|
68
|
+
cfg.graph.backend = backend
|
|
69
|
+
if backend in ("kuzu", "sqlite"):
|
|
70
|
+
graph_dir = path.resolve() / _GRAPH_DIR_NAME
|
|
71
|
+
if backend == "kuzu":
|
|
72
|
+
cfg.graph.path = str(graph_dir / _KUZU_DB_NAME)
|
|
73
|
+
elif backend == "sqlite":
|
|
74
|
+
cfg.graph.path = str(graph_dir / _SQLITE_DB_NAME)
|
|
75
|
+
|
|
76
|
+
console.print("🚀 Starting analysis…")
|
|
77
|
+
analyzer = Analyzer(cfg)
|
|
78
|
+
result = analyzer.run(
|
|
79
|
+
path.resolve(),
|
|
80
|
+
incremental=incremental,
|
|
81
|
+
on_progress=console.print,
|
|
82
|
+
)
|
|
83
|
+
console.print()
|
|
84
|
+
console.print(f"📊 [bold]Results:[/bold] {result.graph.node_count} nodes, {result.graph.edge_count} edges")
|
|
85
|
+
console.print(f" 📂 {result.total_files} total files — {result.files_cached} cached, {result.files_analyzed} analyzed")
|
|
86
|
+
|
|
87
|
+
# Language breakdown
|
|
88
|
+
if result.language_breakdown:
|
|
89
|
+
console.print()
|
|
90
|
+
console.print("📋 [bold]Language Breakdown:[/bold]")
|
|
91
|
+
|
|
92
|
+
# We need the registry to check detector support
|
|
93
|
+
from osscodeiq.detectors.registry import DetectorRegistry
|
|
94
|
+
from osscodeiq.analyzer import _TREESITTER_LANGUAGES, _STRUCTURED_LANGUAGES
|
|
95
|
+
|
|
96
|
+
registry = DetectorRegistry()
|
|
97
|
+
registry.load_builtin_detectors()
|
|
98
|
+
registry.load_plugin_detectors()
|
|
99
|
+
|
|
100
|
+
# Sort by file count descending
|
|
101
|
+
sorted_langs = sorted(result.language_breakdown.items(), key=lambda x: -x[1])
|
|
102
|
+
|
|
103
|
+
for lang, count in sorted_langs:
|
|
104
|
+
detectors = registry.detectors_for_language(lang)
|
|
105
|
+
if detectors:
|
|
106
|
+
det_count = len(detectors)
|
|
107
|
+
status = f"🟢 {det_count} detector{'s' if det_count != 1 else ''}"
|
|
108
|
+
elif lang in _TREESITTER_LANGUAGES or lang in _STRUCTURED_LANGUAGES:
|
|
109
|
+
status = "🟡 parsed"
|
|
110
|
+
else:
|
|
111
|
+
status = "🔴 discovered only"
|
|
112
|
+
console.print(f" {status} {lang:<16} {count:>6} files")
|
|
113
|
+
|
|
114
|
+
# Node breakdown
|
|
115
|
+
if result.node_breakdown:
|
|
116
|
+
console.print()
|
|
117
|
+
console.print("🏗️ [bold]Detection Summary:[/bold]")
|
|
118
|
+
sorted_kinds = sorted(result.node_breakdown.items(), key=lambda x: -x[1])
|
|
119
|
+
for kind, count in sorted_kinds:
|
|
120
|
+
console.print(f" {kind:<24} {count:>8,}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def graph(
|
|
125
|
+
path: Annotated[Path, typer.Argument(help="Path to the analyzed codebase")] = Path("."),
|
|
126
|
+
format: Annotated[str, typer.Option("--format", "-f", help="Output format: json, yaml, mermaid, dot")] = "json",
|
|
127
|
+
view: Annotated[str, typer.Option("--view", "-v", help="View level: developer, architect, domain")] = "developer",
|
|
128
|
+
module: Annotated[Optional[list[str]], typer.Option("--module", "-m", help="Filter by module")] = None,
|
|
129
|
+
node_type: Annotated[Optional[list[str]], typer.Option("--node-type", help="Filter by node type")] = None,
|
|
130
|
+
edge_type: Annotated[Optional[list[str]], typer.Option("--edge-type", help="Filter by edge type")] = None,
|
|
131
|
+
focus: Annotated[Optional[str], typer.Option("--focus", help="Center node for ego-graph")] = None,
|
|
132
|
+
hops: Annotated[int, typer.Option("--hops", help="Radius from focus node")] = 2,
|
|
133
|
+
output: Annotated[Optional[Path], typer.Option("--output", "-o", help="Output file path")] = None,
|
|
134
|
+
max_nodes: Annotated[int, typer.Option("--max-nodes", help="Maximum nodes before safety guard")] = 500,
|
|
135
|
+
cluster_by: Annotated[str, typer.Option("--cluster-by", help="Clustering: module, domain, node-type")] = "module",
|
|
136
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config file")] = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Export the OSSCodeIQ graph in various formats."""
|
|
139
|
+
from osscodeiq.graph.query import GraphQuery
|
|
140
|
+
from osscodeiq.graph.store import GraphStore
|
|
141
|
+
from osscodeiq.graph.views import ArchitectView, DomainView
|
|
142
|
+
from osscodeiq.models.graph import EdgeKind, NodeKind
|
|
143
|
+
from osscodeiq.output.safety import check_graph_size
|
|
144
|
+
from osscodeiq.output.serializers import JsonSerializer, YamlSerializer
|
|
145
|
+
from osscodeiq.output.mermaid import MermaidRenderer
|
|
146
|
+
from osscodeiq.output.dot import DotRenderer
|
|
147
|
+
|
|
148
|
+
cfg = _load_config(config)
|
|
149
|
+
cache_path = path.resolve() / cfg.cache.directory / cfg.cache.db_name
|
|
150
|
+
|
|
151
|
+
if not cache_path.exists():
|
|
152
|
+
console.print("❌ No analysis cache found. Run 'osscodeiq analyze' first.")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
console.print("💾 Loading analysis cache…")
|
|
156
|
+
from osscodeiq.cache.store import CacheStore
|
|
157
|
+
cache = CacheStore(cache_path)
|
|
158
|
+
store = cache.load_full_graph()
|
|
159
|
+
|
|
160
|
+
# Apply view transformation
|
|
161
|
+
if view == "architect":
|
|
162
|
+
console.print("🔭 Applying architect view…")
|
|
163
|
+
store = ArchitectView().roll_up(store)
|
|
164
|
+
elif view == "domain":
|
|
165
|
+
console.print("🔭 Applying domain view…")
|
|
166
|
+
store = DomainView(cfg.domains).roll_up(store)
|
|
167
|
+
|
|
168
|
+
# Apply filters via query builder
|
|
169
|
+
query = GraphQuery(store)
|
|
170
|
+
has_filters = any([module, node_type, edge_type, focus])
|
|
171
|
+
if has_filters:
|
|
172
|
+
console.print("🔍 Applying filters…")
|
|
173
|
+
if module:
|
|
174
|
+
query = query.filter_modules(module)
|
|
175
|
+
if node_type:
|
|
176
|
+
kinds = [NodeKind(t) for t in node_type]
|
|
177
|
+
query = query.filter_node_kinds(kinds)
|
|
178
|
+
if edge_type:
|
|
179
|
+
e_kinds = [EdgeKind(t) for t in edge_type]
|
|
180
|
+
query = query.filter_edge_kinds(e_kinds)
|
|
181
|
+
if focus:
|
|
182
|
+
query = query.focus(focus, hops)
|
|
183
|
+
|
|
184
|
+
result_store = query.execute()
|
|
185
|
+
|
|
186
|
+
# Safety check
|
|
187
|
+
check_graph_size(result_store, max_nodes, console)
|
|
188
|
+
|
|
189
|
+
# Render output
|
|
190
|
+
console.print(f"🎨 Rendering {format} output…")
|
|
191
|
+
model = result_store.to_model()
|
|
192
|
+
model.metadata["view"] = view
|
|
193
|
+
model.metadata["filters_applied"] = {
|
|
194
|
+
"modules": module,
|
|
195
|
+
"node_types": node_type,
|
|
196
|
+
"edge_types": edge_type,
|
|
197
|
+
"focus": focus,
|
|
198
|
+
"hops": hops if focus else None,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if format == "json":
|
|
202
|
+
content = JsonSerializer().serialize(model)
|
|
203
|
+
elif format == "yaml":
|
|
204
|
+
content = YamlSerializer().serialize(model)
|
|
205
|
+
elif format == "mermaid":
|
|
206
|
+
content = MermaidRenderer().render(result_store, cluster_by=cluster_by)
|
|
207
|
+
elif format == "dot":
|
|
208
|
+
content = DotRenderer().render(result_store, cluster_by=cluster_by)
|
|
209
|
+
else:
|
|
210
|
+
console.print(f"❌ Unknown format: {format}")
|
|
211
|
+
raise typer.Exit(1)
|
|
212
|
+
|
|
213
|
+
if output:
|
|
214
|
+
output.write_text(content)
|
|
215
|
+
console.print(f"✅ Graph written to {output}")
|
|
216
|
+
else:
|
|
217
|
+
console.print(content)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def query(
|
|
222
|
+
path: Annotated[Path, typer.Argument(help="Path to the analyzed codebase")] = Path("."),
|
|
223
|
+
consumers_of: Annotated[Optional[str], typer.Option("--consumers-of", help="Show consumers of topic/queue")] = None,
|
|
224
|
+
producers_of: Annotated[Optional[str], typer.Option("--producers-of", help="Show producers to topic/queue")] = None,
|
|
225
|
+
callers_of: Annotated[Optional[str], typer.Option("--callers-of", help="Show callers of endpoint/method")] = None,
|
|
226
|
+
dependencies_of: Annotated[Optional[str], typer.Option("--dependencies-of", help="Show dependencies of module")] = None,
|
|
227
|
+
dependents_of: Annotated[Optional[str], typer.Option("--dependents-of", help="Show dependents of module")] = None,
|
|
228
|
+
cycles: Annotated[bool, typer.Option("--cycles", help="Detect circular dependencies")] = False,
|
|
229
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config file")] = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Query the OSSCodeIQ graph."""
|
|
232
|
+
cfg = _load_config(config)
|
|
233
|
+
cache_path = path.resolve() / cfg.cache.directory / cfg.cache.db_name
|
|
234
|
+
|
|
235
|
+
if not cache_path.exists():
|
|
236
|
+
console.print("❌ No analysis cache found. Run 'osscodeiq analyze' first.")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
console.print("💾 Loading analysis cache…")
|
|
240
|
+
from osscodeiq.cache.store import CacheStore
|
|
241
|
+
from osscodeiq.graph.query import GraphQuery
|
|
242
|
+
|
|
243
|
+
cache = CacheStore(cache_path)
|
|
244
|
+
store = cache.load_full_graph()
|
|
245
|
+
q = GraphQuery(store)
|
|
246
|
+
|
|
247
|
+
if consumers_of:
|
|
248
|
+
console.print(f"🔍 Querying consumers of '{consumers_of}'…")
|
|
249
|
+
result = q.consumers_of(consumers_of).execute()
|
|
250
|
+
_print_query_result(result, f"Consumers of '{consumers_of}'")
|
|
251
|
+
elif producers_of:
|
|
252
|
+
console.print(f"🔍 Querying producers of '{producers_of}'…")
|
|
253
|
+
result = q.producers_of(producers_of).execute()
|
|
254
|
+
_print_query_result(result, f"Producers of '{producers_of}'")
|
|
255
|
+
elif callers_of:
|
|
256
|
+
console.print(f"🔍 Querying callers of '{callers_of}'…")
|
|
257
|
+
result = q.callers_of(callers_of).execute()
|
|
258
|
+
_print_query_result(result, f"Callers of '{callers_of}'")
|
|
259
|
+
elif dependencies_of:
|
|
260
|
+
console.print(f"🔍 Querying dependencies of '{dependencies_of}'…")
|
|
261
|
+
result = q.dependencies_of(dependencies_of).execute()
|
|
262
|
+
_print_query_result(result, f"Dependencies of '{dependencies_of}'")
|
|
263
|
+
elif dependents_of:
|
|
264
|
+
console.print(f"🔍 Querying dependents of '{dependents_of}'…")
|
|
265
|
+
result = q.dependents_of(dependents_of).execute()
|
|
266
|
+
_print_query_result(result, f"Dependents of '{dependents_of}'")
|
|
267
|
+
elif cycles:
|
|
268
|
+
console.print("🔄 Detecting circular dependencies…")
|
|
269
|
+
cycle_list = store.find_cycles()
|
|
270
|
+
if cycle_list:
|
|
271
|
+
console.print(f"⚠️ Found {len(cycle_list)} cycles:")
|
|
272
|
+
for i, cycle in enumerate(cycle_list[:20], 1):
|
|
273
|
+
console.print(f" {i}. {' → '.join(cycle)}")
|
|
274
|
+
if len(cycle_list) > 20:
|
|
275
|
+
console.print(f" … and {len(cycle_list) - 20} more")
|
|
276
|
+
else:
|
|
277
|
+
console.print("✅ No circular dependencies found!")
|
|
278
|
+
else:
|
|
279
|
+
console.print("⚠️ Specify a query option. Use --help for available queries.")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _print_query_result(store: "GraphStore", title: str) -> None: # noqa: F821
|
|
283
|
+
nodes = store.all_nodes()
|
|
284
|
+
console.print(f"📊 [bold]{title}[/bold] ({len(nodes)} results):")
|
|
285
|
+
for node in nodes:
|
|
286
|
+
loc = f" ({node.location.file_path}:{node.location.line_start})" if node.location else ""
|
|
287
|
+
console.print(f" [{node.kind.value}] {node.label}{loc}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@app.command()
|
|
291
|
+
def cache(
|
|
292
|
+
action: Annotated[str, typer.Argument(help="Action: stats, clear")],
|
|
293
|
+
path: Annotated[Path, typer.Argument(help="Path to the codebase")] = Path("."),
|
|
294
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Manage the analysis cache."""
|
|
297
|
+
cfg = _load_config(config)
|
|
298
|
+
cache_path = path.resolve() / cfg.cache.directory / cfg.cache.db_name
|
|
299
|
+
|
|
300
|
+
if action == "clear":
|
|
301
|
+
if cache_path.exists():
|
|
302
|
+
console.print("🗑️ Clearing cache…")
|
|
303
|
+
cache_path.unlink()
|
|
304
|
+
console.print("✅ Cache cleared!")
|
|
305
|
+
else:
|
|
306
|
+
console.print("⚠️ No cache found.")
|
|
307
|
+
elif action == "stats":
|
|
308
|
+
if not cache_path.exists():
|
|
309
|
+
console.print("⚠️ No cache found. Run 'osscodeiq analyze' first.")
|
|
310
|
+
return
|
|
311
|
+
console.print("📊 Loading cache statistics…")
|
|
312
|
+
from osscodeiq.cache.store import CacheStore
|
|
313
|
+
cs = CacheStore(cache_path)
|
|
314
|
+
stats = cs.get_stats()
|
|
315
|
+
console.print("📊 [bold]Cache Statistics:[/bold]")
|
|
316
|
+
for key, value in stats.items():
|
|
317
|
+
console.print(f" {key}: {value}")
|
|
318
|
+
else:
|
|
319
|
+
console.print(f"❌ Unknown action: {action}. Use 'stats' or 'clear'.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command()
|
|
323
|
+
def plugins(
|
|
324
|
+
action: Annotated[str, typer.Argument(help="Action: list, info")] = "list",
|
|
325
|
+
name: Annotated[Optional[str], typer.Argument(help="Plugin name (for info)")] = None,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Manage detector plugins."""
|
|
328
|
+
from osscodeiq.detectors.registry import DetectorRegistry
|
|
329
|
+
|
|
330
|
+
console.print("🔌 Loading detectors…")
|
|
331
|
+
registry = DetectorRegistry()
|
|
332
|
+
registry.load_builtin_detectors()
|
|
333
|
+
registry.load_plugin_detectors()
|
|
334
|
+
|
|
335
|
+
if action == "list":
|
|
336
|
+
detectors = registry.all_detectors()
|
|
337
|
+
console.print(f"📋 [bold]Registered detectors ({len(detectors)}):[/bold]")
|
|
338
|
+
for det in detectors:
|
|
339
|
+
langs = ", ".join(det.supported_languages)
|
|
340
|
+
console.print(f" 🔹 {det.name} [{langs}]")
|
|
341
|
+
elif action == "info" and name:
|
|
342
|
+
det = registry.get(name)
|
|
343
|
+
if det:
|
|
344
|
+
console.print(f"🔹 [bold]{det.name}[/bold]")
|
|
345
|
+
console.print(f" Languages: {', '.join(det.supported_languages)}")
|
|
346
|
+
else:
|
|
347
|
+
console.print(f"❌ Detector '{name}' not found.")
|
|
348
|
+
else:
|
|
349
|
+
console.print("⚠️ Use 'list' or 'info <name>'.")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command()
|
|
353
|
+
def bundle(
|
|
354
|
+
path: Annotated[Path, typer.Argument(help="Path to analyzed codebase")] = Path("."),
|
|
355
|
+
tag: Annotated[str, typer.Option("--tag", "-t", help="Version tag")] = "latest",
|
|
356
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "kuzu",
|
|
357
|
+
output: Annotated[Path | None, typer.Option("--output", "-o", help="Output zip path")] = None,
|
|
358
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Analyze and package graph into a distributable bundle."""
|
|
361
|
+
import json
|
|
362
|
+
import zipfile
|
|
363
|
+
from datetime import datetime, timezone
|
|
364
|
+
|
|
365
|
+
cfg = _load_config(config)
|
|
366
|
+
cfg.graph.backend = backend
|
|
367
|
+
|
|
368
|
+
# Set default path for file-based backends
|
|
369
|
+
graph_dir = path.resolve() / _GRAPH_DIR_NAME
|
|
370
|
+
if backend == "kuzu":
|
|
371
|
+
cfg.graph.path = str(graph_dir / _KUZU_DB_NAME)
|
|
372
|
+
elif backend == "sqlite":
|
|
373
|
+
cfg.graph.path = str(graph_dir / _SQLITE_DB_NAME)
|
|
374
|
+
|
|
375
|
+
# Run analysis
|
|
376
|
+
from osscodeiq.analyzer import Analyzer
|
|
377
|
+
analyzer = Analyzer(cfg)
|
|
378
|
+
result = analyzer.run(path.resolve(), incremental=False)
|
|
379
|
+
|
|
380
|
+
# Determine output path
|
|
381
|
+
project_name = path.resolve().name
|
|
382
|
+
if output is None:
|
|
383
|
+
output = Path(f"{project_name}-{tag}-codegraph.zip")
|
|
384
|
+
|
|
385
|
+
# Create bundle
|
|
386
|
+
manifest = {
|
|
387
|
+
"tag": tag,
|
|
388
|
+
"backend": backend,
|
|
389
|
+
"project": project_name,
|
|
390
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
391
|
+
"node_count": result.graph.node_count,
|
|
392
|
+
"edge_count": result.graph.edge_count,
|
|
393
|
+
"files_analyzed": result.total_files,
|
|
394
|
+
"osscodeiq_version": "0.1.0",
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
398
|
+
# Write manifest
|
|
399
|
+
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
|
|
400
|
+
|
|
401
|
+
# Bundle the graph database files
|
|
402
|
+
if backend == "kuzu" and cfg.graph.path:
|
|
403
|
+
graph_path = Path(cfg.graph.path)
|
|
404
|
+
if graph_path.exists():
|
|
405
|
+
for f in graph_path.rglob("*"):
|
|
406
|
+
if f.is_file():
|
|
407
|
+
zf.write(f, f"graph/{f.relative_to(graph_path)}")
|
|
408
|
+
elif backend == "sqlite" and cfg.graph.path:
|
|
409
|
+
graph_path = Path(cfg.graph.path)
|
|
410
|
+
if graph_path.exists():
|
|
411
|
+
zf.write(graph_path, "graph/graph.db")
|
|
412
|
+
else:
|
|
413
|
+
# NetworkX -- serialize to JSON
|
|
414
|
+
model = result.graph.to_model()
|
|
415
|
+
from osscodeiq.output.serializers import JsonSerializer
|
|
416
|
+
zf.writestr("graph/graph.json", JsonSerializer().serialize(model))
|
|
417
|
+
|
|
418
|
+
# Include interactive flow HTML
|
|
419
|
+
try:
|
|
420
|
+
from osscodeiq.flow.engine import FlowEngine
|
|
421
|
+
flow_html = FlowEngine(result.graph).render_interactive()
|
|
422
|
+
zf.writestr("flow.html", flow_html)
|
|
423
|
+
except Exception:
|
|
424
|
+
pass # Flow generation is optional in bundles
|
|
425
|
+
|
|
426
|
+
result.graph.close()
|
|
427
|
+
|
|
428
|
+
console.print(f"Bundle created: [bold]{output}[/bold]")
|
|
429
|
+
console.print(f" Tag: {tag}")
|
|
430
|
+
console.print(f" Backend: {backend}")
|
|
431
|
+
console.print(f" Nodes: {manifest['node_count']}, Edges: {manifest['edge_count']}")
|
|
432
|
+
console.print(f" Size: {output.stat().st_size / 1024 / 1024:.1f} MB")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _load_graph_backend(path: Path, backend: str, config: Path | None = None):
|
|
436
|
+
"""Load a graph backend from a previously analyzed project."""
|
|
437
|
+
from osscodeiq.graph.backends import create_backend
|
|
438
|
+
|
|
439
|
+
graph_dir = path.resolve() / _GRAPH_DIR_NAME
|
|
440
|
+
if backend == "kuzu":
|
|
441
|
+
db_path = str(graph_dir / _KUZU_DB_NAME)
|
|
442
|
+
elif backend == "sqlite":
|
|
443
|
+
db_path = str(graph_dir / _SQLITE_DB_NAME)
|
|
444
|
+
else:
|
|
445
|
+
# NetworkX ��� load from cache
|
|
446
|
+
cfg = _load_config(config)
|
|
447
|
+
cache_path = path.resolve() / cfg.cache.directory / cfg.cache.db_name
|
|
448
|
+
if not cache_path.exists():
|
|
449
|
+
console.print("No analysis cache found. Run 'osscodeiq analyze' first.")
|
|
450
|
+
raise typer.Exit(1)
|
|
451
|
+
from osscodeiq.cache.store import CacheStore
|
|
452
|
+
cache = CacheStore(cache_path)
|
|
453
|
+
store = cache.load_full_graph()
|
|
454
|
+
return store
|
|
455
|
+
|
|
456
|
+
from pathlib import Path as P
|
|
457
|
+
if not P(db_path).exists():
|
|
458
|
+
console.print(f"No graph database found at {db_path}. Run 'osscodeiq analyze --backend {backend}' first.")
|
|
459
|
+
raise typer.Exit(1)
|
|
460
|
+
|
|
461
|
+
from osscodeiq.graph.store import GraphStore
|
|
462
|
+
return GraphStore(backend=create_backend(backend, path=db_path))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@app.command()
|
|
466
|
+
def cypher(
|
|
467
|
+
query_str: Annotated[str, typer.Argument(help="Cypher query to execute")],
|
|
468
|
+
path: Annotated[Path, typer.Argument(help="Path to analyzed codebase")] = Path("."),
|
|
469
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "kuzu",
|
|
470
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Max rows")] = 50,
|
|
471
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Execute a raw Cypher query on the graph database."""
|
|
474
|
+
import time as _time
|
|
475
|
+
|
|
476
|
+
store = _load_graph_backend(path, backend, config)
|
|
477
|
+
|
|
478
|
+
if not store.supports_cypher:
|
|
479
|
+
console.print(f"Backend '{backend}' does not support Cypher. Use --backend kuzu.")
|
|
480
|
+
raise typer.Exit(1)
|
|
481
|
+
|
|
482
|
+
console.print(f"[dim]Executing: {query_str}[/dim]")
|
|
483
|
+
t0 = _time.perf_counter()
|
|
484
|
+
try:
|
|
485
|
+
results = store.query_cypher(query_str)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
console.print(f"Query failed: {e}")
|
|
488
|
+
raise typer.Exit(1)
|
|
489
|
+
elapsed = _time.perf_counter() - t0
|
|
490
|
+
|
|
491
|
+
if not results:
|
|
492
|
+
console.print(f"(no results) [{elapsed*1000:.1f}ms]")
|
|
493
|
+
store.close()
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# Display as table
|
|
497
|
+
from rich.table import Table
|
|
498
|
+
columns = list(results[0].keys())
|
|
499
|
+
table = Table(title=f"Results ({min(len(results), limit)} of {len(results)} rows, {elapsed*1000:.1f}ms)")
|
|
500
|
+
for col in columns:
|
|
501
|
+
table.add_column(col, overflow="fold")
|
|
502
|
+
|
|
503
|
+
for row in results[:limit]:
|
|
504
|
+
table.add_row(*[str(row.get(c, "")) for c in columns])
|
|
505
|
+
|
|
506
|
+
console.print(table)
|
|
507
|
+
store.close()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@app.command(name="find")
|
|
511
|
+
def find_cmd(
|
|
512
|
+
what: Annotated[str, typer.Argument(help="What to find: endpoints, guards, entities, components, unprotected, flow")],
|
|
513
|
+
path: Annotated[Path, typer.Argument(help="Path to analyzed codebase")] = Path("."),
|
|
514
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "kuzu",
|
|
515
|
+
node_id: Annotated[Optional[str], typer.Option("--from", "-f", help="Starting node ID (for flow)")] = None,
|
|
516
|
+
hops: Annotated[int, typer.Option("--hops", "-h", help="Traversal depth")] = 3,
|
|
517
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Run preset graph queries: endpoints, guards, entities, components, unprotected, flow."""
|
|
520
|
+
import time as _time
|
|
521
|
+
|
|
522
|
+
store = _load_graph_backend(path, backend, config)
|
|
523
|
+
|
|
524
|
+
_PRESETS = {
|
|
525
|
+
"endpoints": {
|
|
526
|
+
"cypher": "MATCH (e:CodeNode) WHERE e.kind = 'endpoint' RETURN e.id, e.label, e.properties ORDER BY e.label",
|
|
527
|
+
"fallback_kind": "endpoint",
|
|
528
|
+
"desc": "All API endpoints",
|
|
529
|
+
},
|
|
530
|
+
"guards": {
|
|
531
|
+
"cypher": "MATCH (g:CodeNode) WHERE g.kind = 'guard' RETURN g.id, g.label, g.properties ORDER BY g.label",
|
|
532
|
+
"fallback_kind": "guard",
|
|
533
|
+
"desc": "All auth guards",
|
|
534
|
+
},
|
|
535
|
+
"entities": {
|
|
536
|
+
"cypher": "MATCH (e:CodeNode) WHERE e.kind = 'entity' RETURN e.id, e.label, e.properties ORDER BY e.label",
|
|
537
|
+
"fallback_kind": "entity",
|
|
538
|
+
"desc": "All data entities",
|
|
539
|
+
},
|
|
540
|
+
"components": {
|
|
541
|
+
"cypher": "MATCH (c:CodeNode) WHERE c.kind = 'component' RETURN c.id, c.label, c.properties ORDER BY c.label",
|
|
542
|
+
"fallback_kind": "component",
|
|
543
|
+
"desc": "All frontend components",
|
|
544
|
+
},
|
|
545
|
+
"unprotected": {
|
|
546
|
+
"cypher": (
|
|
547
|
+
"MATCH (e:CodeNode) WHERE e.kind = 'endpoint' "
|
|
548
|
+
"AND NOT EXISTS { MATCH (g:CodeNode)-[:CODE_EDGE]->(e) WHERE g.kind = 'guard' } "
|
|
549
|
+
"RETURN e.id, e.label, e.properties ORDER BY e.label"
|
|
550
|
+
),
|
|
551
|
+
"desc": "Endpoints without auth guards",
|
|
552
|
+
},
|
|
553
|
+
"flow": {
|
|
554
|
+
"cypher_template": (
|
|
555
|
+
"MATCH (start:CodeNode {{id: $node_id}})-[e:CODE_EDGE*1..{hops}]->(target:CodeNode) "
|
|
556
|
+
"RETURN DISTINCT target.id, target.kind, target.label"
|
|
557
|
+
),
|
|
558
|
+
"desc": "Trace flow from a node",
|
|
559
|
+
},
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if what not in _PRESETS:
|
|
563
|
+
console.print(f"Unknown query: '{what}'. Available: {', '.join(_PRESETS.keys())}")
|
|
564
|
+
raise typer.Exit(1)
|
|
565
|
+
|
|
566
|
+
preset = _PRESETS[what]
|
|
567
|
+
console.print(f"[bold]{preset['desc']}[/bold]")
|
|
568
|
+
|
|
569
|
+
t0 = _time.perf_counter()
|
|
570
|
+
|
|
571
|
+
if store.supports_cypher:
|
|
572
|
+
# Use Cypher for graph DB backends
|
|
573
|
+
if what == "flow":
|
|
574
|
+
if not node_id:
|
|
575
|
+
console.print("--from/-f required for flow query. Pass a node ID.")
|
|
576
|
+
raise typer.Exit(1)
|
|
577
|
+
cypher_q = preset["cypher_template"].format(hops=hops)
|
|
578
|
+
try:
|
|
579
|
+
results = store.query_cypher(cypher_q, {"node_id": node_id})
|
|
580
|
+
except Exception:
|
|
581
|
+
# Fallback: use neighbors traversal
|
|
582
|
+
results = _flow_fallback(store, node_id, hops)
|
|
583
|
+
elif what == "unprotected":
|
|
584
|
+
try:
|
|
585
|
+
results = store.query_cypher(preset["cypher"])
|
|
586
|
+
except Exception:
|
|
587
|
+
results = _unprotected_fallback(store)
|
|
588
|
+
else:
|
|
589
|
+
results = store.query_cypher(preset["cypher"])
|
|
590
|
+
else:
|
|
591
|
+
# Fallback for non-Cypher backends (NetworkX, SQLite)
|
|
592
|
+
from osscodeiq.models.graph import NodeKind
|
|
593
|
+
if what == "flow":
|
|
594
|
+
results = _flow_fallback(store, node_id, hops)
|
|
595
|
+
elif what == "unprotected":
|
|
596
|
+
results = _unprotected_fallback(store)
|
|
597
|
+
else:
|
|
598
|
+
kind_str = preset.get("fallback_kind", what)
|
|
599
|
+
try:
|
|
600
|
+
kind = NodeKind(kind_str)
|
|
601
|
+
nodes = store.nodes_by_kind(kind)
|
|
602
|
+
results = [{"id": n.id, "label": n.label, "properties": str(n.properties)} for n in nodes]
|
|
603
|
+
except ValueError:
|
|
604
|
+
results = []
|
|
605
|
+
|
|
606
|
+
elapsed = _time.perf_counter() - t0
|
|
607
|
+
|
|
608
|
+
if not results:
|
|
609
|
+
console.print(f"(no results) [{elapsed*1000:.1f}ms]")
|
|
610
|
+
store.close()
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
from rich.table import Table
|
|
614
|
+
columns = list(results[0].keys())
|
|
615
|
+
table = Table(title=f"{len(results)} results ({elapsed*1000:.1f}ms)")
|
|
616
|
+
for col in columns:
|
|
617
|
+
table.add_column(col, overflow="fold")
|
|
618
|
+
for row in results[:100]:
|
|
619
|
+
table.add_row(*[str(row.get(c, "")) for c in columns])
|
|
620
|
+
console.print(table)
|
|
621
|
+
store.close()
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _flow_fallback(store, node_id: str | None, hops: int) -> list[dict]:
|
|
625
|
+
"""Trace flow using iterative neighbor traversal (non-Cypher fallback)."""
|
|
626
|
+
if not node_id:
|
|
627
|
+
return []
|
|
628
|
+
visited: set[str] = set()
|
|
629
|
+
frontier = {node_id}
|
|
630
|
+
results = []
|
|
631
|
+
for _ in range(1, hops + 1):
|
|
632
|
+
next_frontier: set[str] = set()
|
|
633
|
+
for nid in frontier:
|
|
634
|
+
for neighbor in store.neighbors(nid, direction="out"):
|
|
635
|
+
if neighbor not in visited:
|
|
636
|
+
visited.add(neighbor)
|
|
637
|
+
next_frontier.add(neighbor)
|
|
638
|
+
node = store.get_node(neighbor)
|
|
639
|
+
if node:
|
|
640
|
+
results.append({"id": node.id, "kind": node.kind.value, "label": node.label})
|
|
641
|
+
frontier = next_frontier
|
|
642
|
+
return results
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _unprotected_fallback(store) -> list[dict]:
|
|
646
|
+
"""Find unprotected endpoints using public API (non-Cypher fallback)."""
|
|
647
|
+
from osscodeiq.models.graph import NodeKind, EdgeKind
|
|
648
|
+
endpoints = store.nodes_by_kind(NodeKind.ENDPOINT)
|
|
649
|
+
guards = store.edges_by_kind(EdgeKind.PROTECTS)
|
|
650
|
+
protected_ids = {e.target for e in guards}
|
|
651
|
+
return [
|
|
652
|
+
{"id": e.id, "label": e.label, "properties": str(e.properties)}
|
|
653
|
+
for e in endpoints if e.id not in protected_ids
|
|
654
|
+
]
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@app.command()
|
|
658
|
+
def flow(
|
|
659
|
+
path: Annotated[Path, typer.Argument(help="Path to analyzed codebase")] = Path("."),
|
|
660
|
+
view: Annotated[str, typer.Option("--view", "-v", help="View: overview, ci, deploy, runtime, auth")] = "overview",
|
|
661
|
+
format: Annotated[str, typer.Option("--format", "-f", help="Format: mermaid, json, html")] = "mermaid",
|
|
662
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "networkx",
|
|
663
|
+
output: Annotated[Optional[Path], typer.Option("--output", "-o", help="Output file path")] = None,
|
|
664
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Generate architecture flow diagrams."""
|
|
667
|
+
store = _load_graph_backend(path, backend, config)
|
|
668
|
+
|
|
669
|
+
from osscodeiq.flow.engine import FlowEngine
|
|
670
|
+
engine = FlowEngine(store)
|
|
671
|
+
|
|
672
|
+
if format == "html":
|
|
673
|
+
content = engine.render_interactive(project_name=path.resolve().name)
|
|
674
|
+
out_path = output or Path("flow.html")
|
|
675
|
+
out_path.write_text(content, encoding="utf-8")
|
|
676
|
+
console.print(f"Interactive flow diagram saved to [bold]{out_path}[/bold]")
|
|
677
|
+
size_kb = out_path.stat().st_size / 1024
|
|
678
|
+
console.print(f" Size: {size_kb:.1f} KB — open in any browser, no server needed")
|
|
679
|
+
else:
|
|
680
|
+
diagram = engine.generate(view)
|
|
681
|
+
content = engine.render(diagram, format)
|
|
682
|
+
if output:
|
|
683
|
+
output.write_text(content)
|
|
684
|
+
console.print(f"Flow diagram ({view}) saved to [bold]{output}[/bold]")
|
|
685
|
+
else:
|
|
686
|
+
console.print(content)
|
|
687
|
+
|
|
688
|
+
store.close()
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@app.command()
|
|
692
|
+
def serve(
|
|
693
|
+
path: Annotated[Path, typer.Argument(help="Path to the codebase")] = Path("."),
|
|
694
|
+
port: Annotated[int, typer.Option("--port", "-p", help="Port to listen on")] = 8080,
|
|
695
|
+
host: Annotated[str, typer.Option("--host", help="Host to bind to")] = "0.0.0.0",
|
|
696
|
+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "networkx",
|
|
697
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
|
|
698
|
+
) -> None:
|
|
699
|
+
"""Start the OSSCodeIQ server (API + MCP on one port)."""
|
|
700
|
+
import warnings
|
|
701
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
|
|
702
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="uvicorn")
|
|
703
|
+
|
|
704
|
+
import uvicorn
|
|
705
|
+
from osscodeiq.server.app import create_app
|
|
706
|
+
|
|
707
|
+
console.print("[bold]OSSCodeIQ Server[/bold]")
|
|
708
|
+
console.print(f" Codebase: {path.resolve()}")
|
|
709
|
+
console.print(f" Backend: {backend}")
|
|
710
|
+
console.print(f" API docs: http://{host}:{port}/docs")
|
|
711
|
+
console.print(f" MCP: http://{host}:{port}/mcp")
|
|
712
|
+
console.print()
|
|
713
|
+
|
|
714
|
+
application = create_app(
|
|
715
|
+
codebase_path=path.resolve(), backend=backend, config_path=config
|
|
716
|
+
)
|
|
717
|
+
uvicorn.run(application, host=host, port=port)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
if __name__ == "__main__":
|
|
721
|
+
app()
|