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.
Files changed (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. 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()