codeui-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. codeui/__init__.py +23 -0
  2. codeui/agent/__init__.py +15 -0
  3. codeui/agent/context.py +207 -0
  4. codeui/agent/contribution.py +224 -0
  5. codeui/agent/edit_tools.py +280 -0
  6. codeui/agent/integrations.py +243 -0
  7. codeui/agent/mcp_server.py +94 -0
  8. codeui/analysis/__init__.py +1 -0
  9. codeui/analysis/api_drift.py +79 -0
  10. codeui/analysis/base.py +42 -0
  11. codeui/analysis/circular.py +116 -0
  12. codeui/analysis/dead_code.py +63 -0
  13. codeui/analysis/duplicates.py +69 -0
  14. codeui/analysis/runner.py +57 -0
  15. codeui/analysis/shadowing.py +49 -0
  16. codeui/analysis/undefined.py +97 -0
  17. codeui/analysis/unreachable.py +28 -0
  18. codeui/analysis/unresolved_import.py +153 -0
  19. codeui/cli/__init__.py +1 -0
  20. codeui/cli/main.py +437 -0
  21. codeui/core/__init__.py +1 -0
  22. codeui/core/cache.py +135 -0
  23. codeui/core/graph.py +435 -0
  24. codeui/core/ir.py +218 -0
  25. codeui/core/override.py +230 -0
  26. codeui/core/repo.py +85 -0
  27. codeui/core/resolver.py +281 -0
  28. codeui/errors.py +98 -0
  29. codeui/lang/__init__.py +1 -0
  30. codeui/lang/base.py +117 -0
  31. codeui/lang/generic.py +90 -0
  32. codeui/lang/go.py +167 -0
  33. codeui/lang/python.py +294 -0
  34. codeui/lang/registry.py +83 -0
  35. codeui/lang/rust.py +185 -0
  36. codeui/lang/ts.py +396 -0
  37. codeui/plugins/__init__.py +1 -0
  38. codeui/plugins/registry.py +31 -0
  39. codeui/report/__init__.py +1 -0
  40. codeui/report/json_emitter.py +24 -0
  41. codeui/report/markdown_emitter.py +33 -0
  42. codeui/report/sarif_emitter.py +56 -0
  43. codeui/server/__init__.py +1 -0
  44. codeui/server/server.py +705 -0
  45. codeui/tracer/__init__.py +1 -0
  46. codeui/tracer/tracer.py +147 -0
  47. codeui_python-0.1.0.dist-info/METADATA +191 -0
  48. codeui_python-0.1.0.dist-info/RECORD +52 -0
  49. codeui_python-0.1.0.dist-info/WHEEL +5 -0
  50. codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
  51. codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  52. codeui_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,705 @@
1
+ """Localhost HTTP server for codeui interactive UI and REST API."""
2
+ import io
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ import urllib.parse
8
+ import zipfile
9
+ from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, Iterable, List, Optional
12
+ from codeui.core.graph import Graph
13
+ from codeui.core.ir import SymbolKind
14
+ from codeui.core.override import OverrideStore, OverrideKind
15
+ from codeui.errors import SecurityError, SymbolNotFoundError
16
+ from codeui.report.json_emitter import JSONEmitter
17
+ from codeui.report.sarif_emitter import SARIFEmitter
18
+
19
+ class CodeUIHTTPRequestHandler(BaseHTTPRequestHandler):
20
+ """HTTP Request Handler for codeui REST endpoints and web UI.
21
+ Example:
22
+ >>> handler = CodeUIHTTPRequestHandler
23
+ >>> handler.server_version
24
+ 'BaseHTTP/0.6'
25
+ """
26
+ CSRF_TOKEN = "codeui-local-csrf-token"
27
+ graph: Graph = None
28
+ override_store: OverrideStore = None
29
+ project_root: Path = None
30
+ rescan_fn: Optional[Callable[..., Graph]] = None
31
+ entry_points: Optional[List[str]] = None
32
+ layer_rules: Optional[Dict[str, Any]] = None
33
+
34
+ @classmethod
35
+ def execute_rescan(cls) -> None:
36
+ """Safely execute project rescan callback preserving entry points and layer rules."""
37
+ fn = getattr(cls, "rescan_fn", None)
38
+ if fn is None:
39
+ return
40
+ if isinstance(fn, staticmethod):
41
+ fn = fn.__func__
42
+ if cls.project_root:
43
+ existing_entries = list(cls.entry_points) if cls.entry_points else (list(cls.graph._entry_points) if cls.graph and hasattr(cls.graph, "_entry_points") else None)
44
+ existing_rules = dict(cls.layer_rules) if cls.layer_rules else (dict(cls.graph._layer_rules) if cls.graph and hasattr(cls.graph, "_layer_rules") else None)
45
+ try:
46
+ new_graph = fn(cls.project_root, entry_points=existing_entries, layer_rules=existing_rules)
47
+ except TypeError:
48
+ try:
49
+ new_graph = fn(cls.project_root, entry_points=existing_entries)
50
+ except TypeError:
51
+ new_graph = fn(cls.project_root)
52
+ if new_graph is not None:
53
+ if existing_entries:
54
+ new_graph.set_entry_points(existing_entries)
55
+ if existing_rules:
56
+ new_graph.set_layer_rules(existing_rules)
57
+ cls.graph = new_graph
58
+
59
+ def do_GET(self) -> None:
60
+ """Handle GET requests for UI static assets and REST endpoints.
61
+ Example:
62
+ >>> handler = CodeUIHTTPRequestHandler
63
+ """
64
+ try:
65
+ self._handle_get()
66
+ except Exception as e:
67
+ self._send_error(500, f"Internal server error: {e}")
68
+
69
+ def _handle_get(self) -> None:
70
+ parsed_url = urllib.parse.urlparse(self.path)
71
+ path = parsed_url.path
72
+ query = urllib.parse.parse_qs(parsed_url.query)
73
+
74
+ if path in ("/", "/index.html"):
75
+ self._serve_static_ui()
76
+ return
77
+
78
+ if path == "/health":
79
+ self._send_json({"status": "ok"})
80
+ return
81
+
82
+ if path.startswith("/static/"):
83
+ rel_path = path[len("/static/"):].split("?", 1)[0]
84
+ static_root = (Path(__file__).parent / "static").resolve()
85
+ try:
86
+ safe_path = self._safe_rel_path(rel_path)
87
+ if not safe_path:
88
+ raise SecurityError("Invalid static asset path")
89
+ full_path = (static_root / safe_path).resolve()
90
+ if not str(full_path).startswith(str(static_root)):
91
+ raise SecurityError("Asset path traversal outside static root blocked")
92
+ if not full_path.exists() or not full_path.is_file():
93
+ self._send_error(404, f"Static asset not found: {rel_path}")
94
+ return
95
+ data = full_path.read_bytes()
96
+ ctype = "application/octet-stream"
97
+ if safe_path.endswith(".css"):
98
+ ctype = "text/css; charset=utf-8"
99
+ elif safe_path.endswith(".js"):
100
+ ctype = "application/javascript; charset=utf-8"
101
+ elif safe_path.endswith(".html"):
102
+ ctype = "text/html; charset=utf-8"
103
+ elif safe_path.endswith(".json"):
104
+ ctype = "application/json; charset=utf-8"
105
+ elif safe_path.endswith(".svg"):
106
+ ctype = "image/svg+xml"
107
+ elif safe_path.endswith(".png"):
108
+ ctype = "image/png"
109
+ self._send_bytes(200, ctype, data)
110
+ except SecurityError as se:
111
+ self._send_error(403, str(se))
112
+ except Exception as e:
113
+ self._send_error(400, str(e))
114
+ return
115
+
116
+ if path.startswith("/asset/"):
117
+ rel_path = path[len("/asset/"):].split("?", 1)[0]
118
+ try:
119
+ safe_path = self._safe_rel_path(rel_path)
120
+ if not safe_path:
121
+ raise SecurityError("Invalid asset path")
122
+ full_path = (self.project_root / safe_path).resolve()
123
+ if not str(full_path).startswith(str(self.project_root.resolve())):
124
+ raise SecurityError("Asset path traversal outside project root blocked")
125
+ if not full_path.exists() or not full_path.is_file():
126
+ self._send_error(404, f"Asset not found: {rel_path}")
127
+ return
128
+ data = full_path.read_bytes()
129
+ ctype = "application/octet-stream"
130
+ if safe_path.endswith(".png"):
131
+ ctype = "image/png"
132
+ elif safe_path.endswith((".jpg", ".jpeg")):
133
+ ctype = "image/jpeg"
134
+ elif safe_path.endswith(".svg"):
135
+ ctype = "image/svg+xml"
136
+ elif safe_path.endswith(".json"):
137
+ ctype = "application/json"
138
+ elif safe_path.endswith(".css"):
139
+ ctype = "text/css"
140
+ elif safe_path.endswith(".js"):
141
+ ctype = "application/javascript"
142
+ self._send_bytes(200, ctype, data)
143
+ except SecurityError as se:
144
+ self._send_error(403, str(se))
145
+ except Exception as e:
146
+ self._send_error(400, str(e))
147
+ return
148
+
149
+ if path == "/api/v1/graph":
150
+ if query.get("refresh", ["0"])[0] in ("1", "true"):
151
+ try:
152
+ self.execute_rescan()
153
+ except Exception as e:
154
+ self._send_error(500, f"Rescan failed: {e}")
155
+ return
156
+ self._send_json(self.graph.to_dict() if self.graph else {})
157
+ return
158
+
159
+ if path == "/api/v1/subgraph":
160
+ file_id = query.get("file", [""])[0] or query.get("path", [""])[0]
161
+ symbol_id = query.get("symbol_id", [""])[0]
162
+ if not file_id and symbol_id and "::" in symbol_id:
163
+ file_id = symbol_id.split("::")[0]
164
+ if not file_id and symbol_id:
165
+ file_id = symbol_id
166
+ if "::" in file_id:
167
+ if not symbol_id:
168
+ symbol_id = file_id
169
+ file_id = file_id.split("::")[0]
170
+ if file_id.startswith("module::"):
171
+ file_id = file_id[len("module::"):]
172
+ if not file_id:
173
+ self._send_error(400, "Parameter 'file' or 'symbol_id' is required")
174
+ return
175
+ try:
176
+ raw_symbols = self.graph.get_symbols_by_file(file_id) if self.graph else []
177
+ excluded_kinds = {SymbolKind.VARIABLE}
178
+ symbols = [s for s in raw_symbols if s.kind not in excluded_kinds]
179
+ cls = self.graph.get_file_classification(file_id) if self.graph else classify_file(file_id)
180
+ sym_ids = {s.id for s in symbols}
181
+ intra_edges = [
182
+ e.to_dict() for e in (self.graph.get_edges() if self.graph else [])
183
+ if (e.source_id in sym_ids) and (e.target_id in sym_ids)
184
+ ]
185
+ ext_outgoing = []
186
+ for s_id in sym_ids:
187
+ for e in (self.graph.get_outgoing_edges(s_id) if self.graph else []):
188
+ if e.target_id not in sym_ids:
189
+ ext_outgoing.append(e.to_dict())
190
+ ext_incoming = []
191
+ for s_id in sym_ids:
192
+ for e in (self.graph.get_incoming_edges(s_id) if self.graph else []):
193
+ if e.source_id not in sym_ids:
194
+ ext_incoming.append(e.to_dict())
195
+ ext_edges = ext_outgoing + ext_incoming
196
+ related_symbols = {}
197
+ for edge in ext_edges:
198
+ for endpoint_id in (edge["source_id"], edge["target_id"]):
199
+ if endpoint_id not in sym_ids and endpoint_id not in related_symbols:
200
+ if self.graph and self.graph.has_symbol(endpoint_id):
201
+ other_sym = self.graph.get_symbol(endpoint_id)
202
+ other_file = other_sym.location.file_id if other_sym.location else ""
203
+ other_cls = self.graph.get_file_classification(other_file) if other_file else None
204
+ related_symbols[endpoint_id] = {
205
+ "id": other_sym.id,
206
+ "name": other_sym.name,
207
+ "qualified_name": other_sym.qualified_name,
208
+ "kind": str(other_sym.kind),
209
+ "file_id": other_file,
210
+ "layer": str(other_cls.layer) if other_cls else "shared",
211
+ "feature": other_cls.feature if other_cls else "external",
212
+ "is_external": True,
213
+ "location": other_sym.location.to_dict() if other_sym.location else None,
214
+ "signature": other_sym.signature,
215
+ }
216
+ else:
217
+ raw_clean = endpoint_id.replace("module::", "")
218
+ other_file = raw_clean.split("::")[0] if "::" in raw_clean else raw_clean
219
+ other_name = raw_clean.split("::")[1] if "::" in raw_clean else (other_file.split("/")[-1] if "/" in other_file else other_file)
220
+ other_cls = self.graph.get_file_classification(other_file) if (self.graph and other_file) else None
221
+ related_symbols[endpoint_id] = {
222
+ "id": endpoint_id,
223
+ "name": other_name,
224
+ "qualified_name": endpoint_id,
225
+ "kind": "module" if endpoint_id.startswith("module::") else "symbol",
226
+ "file_id": other_file,
227
+ "layer": str(other_cls.layer) if other_cls else "shared",
228
+ "feature": other_cls.feature if other_cls else "external",
229
+ "is_external": True,
230
+ "location": {"file_id": other_file, "start_line": 1, "end_line": 1} if other_file else None,
231
+ "signature": other_name,
232
+ }
233
+ self._send_json({
234
+ "file_id": file_id,
235
+ "classification": cls.to_dict(),
236
+ "symbols": [s.to_dict() for s in symbols],
237
+ "related_symbols": list(related_symbols.values()),
238
+ "edges": intra_edges + ext_outgoing + ext_incoming,
239
+ })
240
+ except Exception as e:
241
+ self._send_error(400, str(e))
242
+ return
243
+
244
+ if path == "/api/v1/defects":
245
+ findings = [f.to_dict() for f in self.graph.get_findings()] if self.graph else []
246
+ self._send_json({"findings": findings, "count": len(findings)})
247
+ return
248
+
249
+ if path == "/api/v1/file":
250
+ rel_path = query.get("path", [""])[0]
251
+ if "::" in rel_path:
252
+ rel_path = rel_path.split("::")[0]
253
+ if rel_path.startswith("module::"):
254
+ rel_path = rel_path[len("module::"):]
255
+ try:
256
+ content = self._read_project_file(rel_path)
257
+ overridden_content = self.override_store.get_file_content(rel_path) if self.override_store else None
258
+ self._send_json({"path": rel_path, "content": overridden_content if overridden_content is not None else content})
259
+ except Exception as e:
260
+ self._send_error(400, str(e))
261
+ return
262
+
263
+ if path in ("/api/v1/export", "/__export_zip__"):
264
+ export_type = query.get("type", ["zip" if path == "/__export_zip__" else "json"])[0]
265
+ if export_type == "diff":
266
+ diff = self.override_store.to_diff()
267
+ self._send_response(200, "text/plain", diff)
268
+ elif export_type == "sarif":
269
+ sarif = SARIFEmitter().emit(self.graph)
270
+ self._send_response(200, "application/json", sarif)
271
+ elif export_type == "zip":
272
+ self._handle_zip_export()
273
+ else:
274
+ json_str = JSONEmitter().emit(self.graph)
275
+ self._send_response(200, "application/json", json_str)
276
+ return
277
+
278
+ if path in ("/api/v1/repo/info", "/api/v1/library/repo"):
279
+ from codeui.core.repo import get_library_repo_url
280
+ self._send_json({
281
+ "repo_url": get_library_repo_url(),
282
+ "version": "0.1.0",
283
+ })
284
+ return
285
+
286
+ self._send_error(404, "Endpoint Not Found")
287
+
288
+ def _get_allowed_origin(self) -> str:
289
+ """Derive safe CORS allowed origin restricted to local origins."""
290
+ origin = self.headers.get("Origin", "") if self.headers else ""
291
+ host_hdr = self.headers.get("Host", "") if self.headers else ""
292
+ if origin:
293
+ parsed = urllib.parse.urlparse(origin)
294
+ if parsed.hostname in ("localhost", "127.0.0.1", "0.0.0.0") or (host_hdr and parsed.netloc == host_hdr):
295
+ return origin
296
+ return f"http://{host_hdr}" if host_hdr else "http://127.0.0.1:3000"
297
+
298
+ def do_OPTIONS(self) -> None:
299
+ """Handle OPTIONS preflight CORS requests.
300
+ Example:
301
+ >>> handler = CodeUIHTTPRequestHandler
302
+ """
303
+ self.send_response(204)
304
+ self.send_header("Access-Control-Allow-Origin", self._get_allowed_origin())
305
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
306
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
307
+ self.end_headers()
308
+
309
+ def do_POST(self) -> None:
310
+ """Handle POST requests for state overrides and edits.
311
+ Example:
312
+ >>> handler = CodeUIHTTPRequestHandler
313
+ """
314
+ parsed_url = urllib.parse.urlparse(self.path)
315
+ path = parsed_url.path
316
+
317
+ if path == "/__export_zip__":
318
+ self._handle_zip_export()
319
+ return
320
+
321
+ csrf_header = self.headers.get("X-CSRF-Token")
322
+ if csrf_header != self.CSRF_TOKEN:
323
+ self._send_error(403, "CSRF Token Validation Failed")
324
+ return
325
+
326
+ content_len = int(self.headers.get("Content-Length", 0))
327
+ body = self.rfile.read(content_len).decode("utf-8") if content_len > 0 else "{}"
328
+ try:
329
+ payload = json.loads(body)
330
+ except Exception:
331
+ self._send_error(400, "Invalid JSON payload")
332
+ return
333
+
334
+ if path == "/api/v1/clone":
335
+ repo = payload.get("repo", "").strip()
336
+ if not repo or repo.startswith("-"):
337
+ self._send_error(400, "Parameter 'repo' is invalid")
338
+ return
339
+ if not repo.startswith("http://") and not repo.startswith("https://") and not repo.startswith("git@"):
340
+ if not re.match(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$", repo):
341
+ self._send_error(400, "Invalid repository name format")
342
+ return
343
+ repo_url = f"https://github.com/{repo}.git"
344
+ else:
345
+ if not (repo.startswith("https://") or repo.startswith("http://") or repo.startswith("git@")):
346
+ self._send_error(400, "Unsupported repository protocol")
347
+ return
348
+ repo_url = repo
349
+ active_dir = Path("/tmp/codeui_repos/active_repo")
350
+ if active_dir.exists():
351
+ import shutil
352
+ shutil.rmtree(active_dir, ignore_errors=True)
353
+ active_dir.parent.mkdir(parents=True, exist_ok=True)
354
+ import subprocess
355
+ res = subprocess.run(["git", "clone", "--depth", "1", "--", repo_url, str(active_dir)], capture_output=True, text=True)
356
+ if res.returncode != 0:
357
+ self._send_error(500, f"Git clone failed: {res.stderr}")
358
+ return
359
+ self.__class__.project_root = active_dir
360
+ if self.override_store:
361
+ self.override_store.revert_all()
362
+ self.execute_rescan()
363
+ self._send_json({
364
+ "status": "ok",
365
+ "repo": repo,
366
+ "files_count": len(self.graph.get_all_files()) if self.graph else 0
367
+ })
368
+ return
369
+
370
+ if path == "/api/v1/refresh":
371
+ try:
372
+ self.execute_rescan()
373
+ except Exception as e:
374
+ self._send_error(500, f"Rescan failed: {e}")
375
+ return
376
+ self._send_json({"status": "ok", "files_count": len(self.graph.get_all_files()) if self.graph else 0})
377
+ return
378
+
379
+ if path == "/api/v1/edit":
380
+ kind = payload.get("kind", "replace_file")
381
+ target_id = payload.get("target_id")
382
+ new_val = payload.get("new_value", "")
383
+ dd_raw = payload.get("direct_disk", payload.get("directDisk", payload.get("write_to_disk", payload.get("is_direct", True))))
384
+ if isinstance(dd_raw, str):
385
+ direct_disk = dd_raw.lower() not in ("false", "0", "no", "off")
386
+ else:
387
+ direct_disk = bool(dd_raw)
388
+ if not target_id:
389
+ self._send_error(400, "target_id is required")
390
+ return
391
+ if kind == "replace_file":
392
+ full_path = self._resolve_disk_path(target_id)
393
+ print(f"[DISK_EDIT] Request target_id='{target_id}' resolved to full_path='{full_path}' (direct_disk={direct_disk})", file=sys.stderr, flush=True)
394
+ if not full_path:
395
+ print(f"[DISK_EDIT] ERROR: Could not resolve file path for target_id='{target_id}'", file=sys.stderr, flush=True)
396
+ self._send_error(404, f"Could not resolve file path: {target_id}")
397
+ return
398
+
399
+ try:
400
+ rel_for_graph = str(full_path.relative_to(self.project_root.resolve())).replace("\\", "/")
401
+ except Exception:
402
+ rel_for_graph = str(full_path.name)
403
+
404
+ if direct_disk:
405
+ print(f"[DISK_EDIT] Attempting direct disk write to '{full_path}'...", file=sys.stderr, flush=True)
406
+ try:
407
+ full_path.parent.mkdir(parents=True, exist_ok=True)
408
+ with open(full_path, "w", encoding="utf-8") as f:
409
+ f.write(new_val)
410
+ f.flush()
411
+ os.fsync(f.fileno())
412
+ print(f"[DISK_EDIT] SUCCESS: Direct disk write completed for '{full_path}' ({len(new_val)} bytes)", file=sys.stderr, flush=True)
413
+ except Exception as err:
414
+ print(f"[DISK_EDIT] ERROR: Direct disk write failed for '{full_path}': {err}", file=sys.stderr, flush=True)
415
+ self._send_error(500, f"Disk write failed for {full_path}: {err}")
416
+ return
417
+
418
+ if self.override_store:
419
+ self.override_store.revert(rel_for_graph)
420
+ self.override_store.revert(target_id)
421
+ clean_norm = target_id.split("::")[0].replace("module::", "").lstrip("./")
422
+ self.override_store.revert(clean_norm)
423
+ self.execute_rescan()
424
+ self._send_json({
425
+ "status": "ok",
426
+ "target_id": target_id,
427
+ "direct_disk": True,
428
+ "saved_path": str(full_path),
429
+ "relative_path": rel_for_graph,
430
+ })
431
+ return
432
+ elif self.override_store:
433
+ orig_content = ""
434
+ try:
435
+ orig_content = full_path.read_text(encoding="utf-8", errors="replace") if full_path.exists() else ""
436
+ except Exception:
437
+ pass
438
+ self.override_store.set_file_override(
439
+ file_path=rel_for_graph,
440
+ original_content=orig_content,
441
+ new_content=new_val,
442
+ author="ui_user",
443
+ )
444
+ self._send_json({
445
+ "status": "ok",
446
+ "target_id": target_id,
447
+ "direct_disk": False,
448
+ "relative_path": rel_for_graph,
449
+ })
450
+ return
451
+
452
+ if path == "/api/v1/revert":
453
+ target_id = payload.get("target_id")
454
+ if target_id == "all":
455
+ self.override_store.revert_all()
456
+ elif target_id:
457
+ self.override_store.revert(target_id)
458
+ self._send_json({"status": "ok"})
459
+ return
460
+
461
+ if path in ("/api/v1/agent/contribute", "/api/v1/contribute"):
462
+ title = payload.get("title", "AI Agent Contribution")
463
+ description = payload.get("description", "")
464
+ file_changes = payload.get("file_changes", {})
465
+ branch_name = payload.get("branch_name")
466
+ token = payload.get("token")
467
+ push = payload.get("push", True)
468
+ try:
469
+ from codeui.agent.contribution import ContributionManager
470
+ mgr = ContributionManager(token=token)
471
+ res = mgr.submit_contribution(
472
+ title=title,
473
+ description=description,
474
+ file_changes=file_changes,
475
+ branch_name=branch_name,
476
+ token=token,
477
+ push=push,
478
+ )
479
+ self._send_json(res)
480
+ except Exception as e:
481
+ self._send_error(500, f"Contribution failed: {e}")
482
+ return
483
+
484
+ self._send_error(404, "Endpoint Not Found")
485
+
486
+ def _handle_zip_export(self) -> None:
487
+ """Generate in-memory ZIP package of current project files, overrides, and metadata."""
488
+ buf = io.BytesIO()
489
+ written: set[str] = set()
490
+ ignored_dirs = {"node_modules", ".venv", "dist", ".git", ".codeui", "__pycache__", ".pytest_cache"}
491
+ with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
492
+ for path in self.project_root.rglob("*"):
493
+ if path.is_file() and path.suffix.lower() not in (".pyc", ".pyo", ".pyd") and not any(part in path.parts for part in ignored_dirs):
494
+ try:
495
+ rel = str(path.relative_to(self.project_root))
496
+ safe = self._safe_rel_path(rel)
497
+ if not safe or safe in written:
498
+ continue
499
+ overridden = self.override_store.get_file_content(safe)
500
+ if overridden is not None:
501
+ zf.writestr(safe, overridden)
502
+ else:
503
+ zf.writestr(safe, path.read_bytes())
504
+ written.add(safe)
505
+ except Exception:
506
+ pass
507
+ for file_path in self.graph.get_all_files():
508
+ safe = self._safe_rel_path(file_path)
509
+ if not safe or safe in written:
510
+ continue
511
+ overridden = self.override_store.get_file_content(file_path)
512
+ if overridden is not None:
513
+ zf.writestr(safe, overridden)
514
+ written.add(safe)
515
+ overrides = [o.to_dict() for o in self.override_store.list()]
516
+ if overrides:
517
+ zf.writestr("_overrides.json", json.dumps({"version": 1, "overrides": overrides}, indent=2))
518
+ zf.writestr("project_full.json", json.dumps(self.graph.to_dict(), indent=2))
519
+ buf.seek(0)
520
+ self._send_bytes(
521
+ 200,
522
+ "application/zip",
523
+ buf.read(),
524
+ {"Content-Disposition": 'attachment; filename="codeui_export.zip"'}
525
+ )
526
+
527
+ def _safe_rel_path(self, rel_path: str) -> Optional[str]:
528
+ """Validate and sanitize relative path."""
529
+ if not rel_path:
530
+ return None
531
+ safe = rel_path.replace("\\", "/").lstrip("/")
532
+ parts = [p for p in safe.split("/") if p]
533
+ if any(p == ".." for p in parts):
534
+ return None
535
+ return "/".join(parts)
536
+
537
+ def _serve_static_ui(self) -> None:
538
+ """Serve prebuilt embedded index.html visualizer asset."""
539
+ static_dir = Path(__file__).parent / "static" / "index.html"
540
+ if static_dir.exists():
541
+ html_content = static_dir.read_text(encoding="utf-8")
542
+ self._send_response(200, "text/html; charset=utf-8", html_content)
543
+ else:
544
+ self._send_error(500, "Static UI asset missing")
545
+
546
+ def _resolve_disk_path(self, rel_path: str) -> Optional[Path]:
547
+ """Resolve a relative or symbol file path to the absolute Path on disk.
548
+ Safely locates project files across various directory layouts and prevents directory traversal.
549
+ """
550
+ if not rel_path or not self.project_root:
551
+ return None
552
+ clean_rel = rel_path.split("::")[0].replace("module::", "").replace("\\", "/").strip().lstrip("./")
553
+ if not clean_rel:
554
+ return None
555
+
556
+ proj_root = self.project_root.resolve()
557
+ proj_str = str(proj_root)
558
+
559
+ if clean_rel.startswith(proj_str):
560
+ clean_rel = clean_rel[len(proj_str):].lstrip("/")
561
+
562
+ safe = self._safe_rel_path(clean_rel)
563
+ if safe:
564
+ cand = (proj_root / safe).resolve()
565
+ if str(cand).startswith(proj_str) and cand.exists() and cand.is_file():
566
+ return cand
567
+
568
+ if safe and proj_root.name and (safe.startswith(proj_root.name + "/") or safe == proj_root.name):
569
+ sub_safe = safe[len(proj_root.name):].lstrip("/")
570
+ if sub_safe:
571
+ cand = (proj_root / sub_safe).resolve()
572
+ if str(cand).startswith(proj_str) and cand.exists() and cand.is_file():
573
+ return cand
574
+
575
+ if self.graph:
576
+ for known_file in self.graph.get_all_files():
577
+ k_clean = known_file.replace("\\", "/").lstrip("./")
578
+ if (k_clean == safe or
579
+ (safe and (k_clean.endswith("/" + safe) or safe.endswith("/" + k_clean))) or
580
+ (safe and Path(k_clean).name == Path(safe).name)):
581
+ cand = (proj_root / k_clean).resolve()
582
+ if str(cand).startswith(proj_str) and cand.exists() and cand.is_file():
583
+ return cand
584
+
585
+ from codeui.lang.registry import LanguageRegistry
586
+ supported_exts = LanguageRegistry().get_supported_extensions()
587
+ if safe:
588
+ candidates = []
589
+ for ext in supported_exts:
590
+ candidates.append(safe + ext)
591
+ candidates.append(safe.replace(".", "/") + ext)
592
+ candidates.extend([
593
+ safe + "/index.ts",
594
+ safe + "/index.tsx",
595
+ safe + "/index.js",
596
+ safe + "/index.jsx",
597
+ safe + "/__init__.py",
598
+ safe + "/mod.rs",
599
+ safe + "/lib.rs",
600
+ safe.replace(".", "/") + "/__init__.py",
601
+ ])
602
+ for c in candidates:
603
+ cand = (proj_root / c).resolve()
604
+ if str(cand).startswith(proj_str) and cand.exists() and cand.is_file():
605
+ return cand
606
+
607
+ parent_root = proj_root.parent.resolve()
608
+ if safe and parent_root != proj_root:
609
+ cand = (parent_root / safe).resolve()
610
+ if str(cand).startswith(str(parent_root)) and cand.exists() and cand.is_file():
611
+ return cand
612
+
613
+ if safe:
614
+ cand = (proj_root / safe).resolve()
615
+ if str(cand).startswith(proj_str):
616
+ return cand
617
+ return None
618
+
619
+ def _read_project_file(self, rel_path: str) -> str:
620
+ """Read project file safely with path traversal protection and flexible extension resolution."""
621
+ full_path = self._resolve_disk_path(rel_path)
622
+ if full_path and full_path.exists() and full_path.is_file():
623
+ return full_path.read_text(encoding="utf-8", errors="replace")
624
+ raise FileNotFoundError(f"File not found: {rel_path}")
625
+
626
+ def _send_json(self, data: Dict[str, Any]) -> None:
627
+ """Send JSON HTTP 200 response with security headers."""
628
+ body = json.dumps(data, sort_keys=True, indent=2)
629
+ self._send_response(200, "application/json; charset=utf-8", body)
630
+
631
+ def _send_bytes(self, code: int, content_type: str, body: bytes, extra_headers: Optional[Dict[str, str]] = None) -> None:
632
+ """Helper to send raw binary HTTP response with security headers."""
633
+ self.send_response(code)
634
+ self.send_header("Content-Type", content_type)
635
+ self.send_header("Content-Length", str(len(body)))
636
+ self.send_header("X-Content-Type-Options", "nosniff")
637
+ self.send_header("Content-Security-Policy", "default-src 'self' 'unsafe-inline' data: blob:; object-src 'none'; frame-ancestors *;")
638
+ self.send_header("Access-Control-Allow-Origin", self._get_allowed_origin())
639
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
640
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
641
+ if extra_headers:
642
+ for k, v in extra_headers.items():
643
+ self.send_header(k, v)
644
+ self.end_headers()
645
+ self.wfile.write(body)
646
+
647
+ def _send_response(self, code: int, content_type: str, body: str) -> None:
648
+ """Helper to send HTTP text response with strict CSP and security headers."""
649
+ self._send_bytes(code, content_type, body.encode("utf-8"))
650
+
651
+ def _send_error(self, code: int, message: str) -> None:
652
+ """Send HTTP error payload."""
653
+ self._send_response(code, "application/json", json.dumps({"error": message, "code": code}))
654
+
655
+ def log_message(self, format: str, *args: Any) -> None:
656
+ """Suppress standard access logs under 400 to prevent false positive error triggers."""
657
+ if args and len(args) > 1:
658
+ try:
659
+ code = int(args[1])
660
+ if code < 400:
661
+ return
662
+ except (ValueError, TypeError):
663
+ pass
664
+ sys.stderr.write(f"{self.address_string()} - - [{self.log_date_time_string()}] {format % args}\n")
665
+ sys.stderr.flush()
666
+
667
+ class ReusableThreadingHTTPServer(ThreadingHTTPServer):
668
+ allow_reuse_address = True
669
+
670
+ def create_server(
671
+ graph: Graph,
672
+ override_store: OverrideStore,
673
+ project_root: Path,
674
+ host: str = "127.0.0.1",
675
+ port: int = 3000,
676
+ rescan_fn: Optional[Callable[..., Graph]] = None,
677
+ entry_points: Optional[Iterable[str]] = None,
678
+ layer_rules: Optional[Dict[str, Any]] = None,
679
+ ) -> ThreadingHTTPServer:
680
+ """Instantiate and configure codeui localhost HTTP server.
681
+ Example:
682
+ >>> g = Graph()
683
+ >>> store = OverrideStore()
684
+ >>> srv = create_server(g, store, Path("."), port=0)
685
+ >>> srv.server_port > 0
686
+ True
687
+ >>> srv.server_close()
688
+ """
689
+ CodeUIHTTPRequestHandler.graph = graph
690
+ CodeUIHTTPRequestHandler.override_store = override_store
691
+ CodeUIHTTPRequestHandler.project_root = project_root.resolve()
692
+ CodeUIHTTPRequestHandler.rescan_fn = staticmethod(rescan_fn) if rescan_fn is not None else None
693
+ if entry_points is not None:
694
+ CodeUIHTTPRequestHandler.entry_points = list(entry_points)
695
+ elif graph and hasattr(graph, "_entry_points") and graph._entry_points:
696
+ CodeUIHTTPRequestHandler.entry_points = list(graph._entry_points)
697
+ else:
698
+ CodeUIHTTPRequestHandler.entry_points = None
699
+ if layer_rules is not None:
700
+ CodeUIHTTPRequestHandler.layer_rules = dict(layer_rules)
701
+ elif graph and hasattr(graph, "_layer_rules") and graph._layer_rules:
702
+ CodeUIHTTPRequestHandler.layer_rules = dict(graph._layer_rules)
703
+ else:
704
+ CodeUIHTTPRequestHandler.layer_rules = None
705
+ return ReusableThreadingHTTPServer((host, port), CodeUIHTTPRequestHandler)