htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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 (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ """FastAPI-based server for HtmlGraph dashboard with real-time observability."""
4
+
5
+
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from htmlgraph.mcp_server import _resolve_project_dir
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FastAPIServerHandle:
18
+ """Handle to a running FastAPI server."""
19
+
20
+ url: str
21
+ port: int
22
+ host: str
23
+ server: Any | None = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FastAPIServerStartResult:
28
+ """Result of starting FastAPI server."""
29
+
30
+ handle: FastAPIServerHandle
31
+ warnings: list[str]
32
+ config_used: dict[str, Any]
33
+
34
+
35
+ class FastAPIServerError(RuntimeError):
36
+ """FastAPI server error."""
37
+
38
+ pass
39
+
40
+
41
+ class PortInUseError(FastAPIServerError):
42
+ """Requested port is already in use."""
43
+
44
+ pass
45
+
46
+
47
+ def start_fastapi_server(
48
+ *,
49
+ port: int = 8000,
50
+ host: str = "127.0.0.1",
51
+ db_path: str | None = None,
52
+ auto_port: bool = False,
53
+ reload: bool = False,
54
+ ) -> FastAPIServerStartResult:
55
+ """
56
+ Start FastAPI-based HtmlGraph dashboard server.
57
+
58
+ Args:
59
+ port: Port to listen on (default: 8000)
60
+ host: Host to bind to (default: 127.0.0.1)
61
+ db_path: Path to SQLite database file
62
+ auto_port: Automatically find available port if in use
63
+ reload: Enable auto-reload on file changes (development mode)
64
+
65
+ Returns:
66
+ FastAPIServerStartResult with handle, warnings, and config used
67
+
68
+ Raises:
69
+ PortInUseError: If port is in use and auto_port=False
70
+ FastAPIServerError: If server fails to start
71
+ """
72
+ import uvicorn
73
+
74
+ from htmlgraph.api.main import create_app
75
+
76
+ warnings: list[str] = []
77
+ original_port = port
78
+
79
+ # Default database path - prefer project-local database if available
80
+ if db_path is None:
81
+ # Check for project-local database first
82
+ project_dir = _resolve_project_dir()
83
+ project_db = Path(project_dir) / ".htmlgraph" / "htmlgraph.db"
84
+ if project_db.exists():
85
+ db_path = str(project_db) # Use project-local database
86
+ else:
87
+ db_path = str(
88
+ Path.home() / ".htmlgraph" / "htmlgraph.db"
89
+ ) # Fall back to home
90
+
91
+ # Ensure database exists
92
+ db_path_obj = Path(db_path)
93
+ db_path_obj.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+ # Handle auto-port selection
96
+ if auto_port and _check_port_in_use(port, host):
97
+ port = _find_available_port(port + 1)
98
+ warnings.append(f"Port {original_port} is in use, using {port} instead")
99
+
100
+ # Check if port is in use
101
+ if not auto_port and _check_port_in_use(port, host):
102
+ raise PortInUseError(
103
+ f"Port {port} is already in use. Use auto_port=True or choose a different port."
104
+ )
105
+
106
+ # Create FastAPI app
107
+ app = create_app(db_path=db_path)
108
+
109
+ # Create server config
110
+ config = uvicorn.Config(
111
+ app,
112
+ host=host,
113
+ port=port,
114
+ log_level="info",
115
+ reload=reload,
116
+ reload_dirs=None, # Disable file watching for now
117
+ )
118
+
119
+ # Create server instance
120
+ server = uvicorn.Server(config)
121
+
122
+ # Create handle
123
+ handle = FastAPIServerHandle(
124
+ url=f"http://{host}:{port}",
125
+ port=port,
126
+ host=host,
127
+ server=server,
128
+ )
129
+
130
+ # Configuration used
131
+ config_used = {
132
+ "port": port,
133
+ "original_port": original_port,
134
+ "host": host,
135
+ "db_path": db_path,
136
+ "auto_port": auto_port,
137
+ "reload": reload,
138
+ }
139
+
140
+ return FastAPIServerStartResult(
141
+ handle=handle,
142
+ warnings=warnings,
143
+ config_used=config_used,
144
+ )
145
+
146
+
147
+ async def run_fastapi_server(handle: FastAPIServerHandle) -> None:
148
+ """
149
+ Run FastAPI server (async).
150
+
151
+ Args:
152
+ handle: FastAPIServerHandle from start_fastapi_server()
153
+
154
+ Raises:
155
+ FastAPIServerError: If server fails
156
+ """
157
+ if handle.server is None:
158
+ raise FastAPIServerError("Invalid server handle")
159
+
160
+ try:
161
+ await handle.server.serve()
162
+ except Exception as e:
163
+ raise FastAPIServerError(f"Server error: {e}") from e
164
+
165
+
166
+ def stop_fastapi_server(handle: FastAPIServerHandle) -> None:
167
+ """
168
+ Stop FastAPI server.
169
+
170
+ Args:
171
+ handle: FastAPIServerHandle from start_fastapi_server()
172
+
173
+ Raises:
174
+ FastAPIServerError: If shutdown fails
175
+ """
176
+ if handle.server is None:
177
+ return
178
+
179
+ try:
180
+ handle.server.should_exit = True
181
+ except Exception as e:
182
+ raise FastAPIServerError(f"Failed to stop server: {e}") from e
183
+
184
+
185
+ def _check_port_in_use(port: int, host: str = "localhost") -> bool:
186
+ """
187
+ Check if a port is already in use.
188
+
189
+ Args:
190
+ port: Port number to check
191
+ host: Host to check on
192
+
193
+ Returns:
194
+ True if port is in use, False otherwise
195
+ """
196
+ import socket
197
+
198
+ try:
199
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
200
+ s.bind((host, port))
201
+ return False
202
+ except OSError:
203
+ return True
204
+
205
+
206
+ def _find_available_port(start_port: int = 8000, max_attempts: int = 10) -> int:
207
+ """
208
+ Find an available port starting from start_port.
209
+
210
+ Args:
211
+ start_port: Port to start searching from
212
+ max_attempts: Maximum number of ports to try
213
+
214
+ Returns:
215
+ Available port number
216
+
217
+ Raises:
218
+ FastAPIServerError: If no available port found
219
+ """
220
+ import socket
221
+
222
+ for port in range(start_port, start_port + max_attempts):
223
+ try:
224
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
225
+ s.bind(("", port))
226
+ return port
227
+ except OSError:
228
+ continue
229
+ raise FastAPIServerError(
230
+ f"No available ports found in range {start_port}-{start_port + max_attempts}"
231
+ )
@@ -0,0 +1,350 @@
1
+ from __future__ import annotations
2
+
3
+ """Git hook operations for HtmlGraph."""
4
+
5
+
6
+ import json
7
+ import shutil
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class HookInstallResult:
15
+ installed: list[str]
16
+ skipped: list[str]
17
+ warnings: list[str]
18
+ config_used: dict[str, Any]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class HookListResult:
23
+ enabled: list[str]
24
+ disabled: list[str]
25
+ missing: list[str]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class HookValidationResult:
30
+ valid: bool
31
+ errors: list[str]
32
+ warnings: list[str]
33
+
34
+
35
+ class HookConfigError(ValueError):
36
+ """Invalid hook configuration."""
37
+
38
+
39
+ class HookInstallError(RuntimeError):
40
+ """Hook installation failed."""
41
+
42
+
43
+ def _load_hook_config(project_dir: Path) -> dict[str, Any]:
44
+ """Load hook configuration from project."""
45
+ config_path = project_dir / ".htmlgraph" / "hooks-config.json"
46
+
47
+ # Default configuration
48
+ default_config: dict[str, Any] = {
49
+ "enabled_hooks": [
50
+ "post-commit",
51
+ "post-checkout",
52
+ "post-merge",
53
+ "pre-push",
54
+ ],
55
+ "use_symlinks": True,
56
+ "backup_existing": True,
57
+ "chain_existing": True,
58
+ }
59
+
60
+ if not config_path.exists():
61
+ return default_config
62
+
63
+ try:
64
+ with open(config_path, encoding="utf-8") as f:
65
+ user_config = json.load(f)
66
+ # Merge with defaults
67
+ config = default_config.copy()
68
+ config.update(user_config)
69
+ return config
70
+ except Exception as e:
71
+ raise HookConfigError(f"Failed to load hook config: {e}") from e
72
+
73
+
74
+ def _validate_environment(project_dir: Path) -> None:
75
+ """Validate that environment is ready for hook installation."""
76
+ git_dir = project_dir / ".git"
77
+ htmlgraph_dir = project_dir / ".htmlgraph"
78
+
79
+ if not git_dir.exists():
80
+ raise HookInstallError("Not a git repository (no .git directory found)")
81
+
82
+ if not htmlgraph_dir.exists():
83
+ raise HookInstallError("HtmlGraph not initialized (run 'htmlgraph init' first)")
84
+
85
+ git_hooks_dir = git_dir / "hooks"
86
+ if not git_hooks_dir.exists():
87
+ try:
88
+ git_hooks_dir.mkdir(parents=True)
89
+ except Exception as e:
90
+ raise HookInstallError(f"Cannot create .git/hooks directory: {e}") from e
91
+
92
+
93
+ def _install_single_hook(
94
+ hook_name: str,
95
+ *,
96
+ project_dir: Path,
97
+ config: dict[str, Any],
98
+ force: bool = False,
99
+ ) -> tuple[bool, str]:
100
+ """Install a single git hook."""
101
+ from htmlgraph.hooks import HOOKS_DIR
102
+
103
+ # Check if hook is enabled
104
+ if hook_name not in config.get("enabled_hooks", []):
105
+ return False, f"Hook '{hook_name}' is disabled in configuration"
106
+
107
+ # Source hook file
108
+ hook_source = HOOKS_DIR / f"{hook_name}.sh"
109
+ if not hook_source.exists():
110
+ return False, f"Hook template not found: {hook_source}"
111
+
112
+ # Destination in .htmlgraph/hooks/ (versioned)
113
+ htmlgraph_dir = project_dir / ".htmlgraph"
114
+ versioned_hooks_dir = htmlgraph_dir / "hooks"
115
+ versioned_hooks_dir.mkdir(exist_ok=True)
116
+ hook_dest = versioned_hooks_dir / f"{hook_name}.sh"
117
+
118
+ # Git hooks directory
119
+ git_dir = project_dir / ".git"
120
+ git_hooks_dir = git_dir / "hooks"
121
+ git_hook_path = git_hooks_dir / hook_name
122
+
123
+ # Copy hook to .htmlgraph/hooks/ (versioned)
124
+ try:
125
+ shutil.copy(hook_source, hook_dest)
126
+ hook_dest.chmod(0o755)
127
+ except Exception as e:
128
+ raise HookInstallError(f"Failed to copy hook to {hook_dest}: {e}") from e
129
+
130
+ # Handle existing git hook
131
+ if git_hook_path.exists() and not force:
132
+ if config.get("backup_existing", True):
133
+ backup_path = git_hook_path.with_suffix(".backup")
134
+ if not backup_path.exists():
135
+ shutil.copy(git_hook_path, backup_path)
136
+
137
+ if config.get("chain_existing", True):
138
+ # Create chained hook
139
+ return _create_chained_hook(
140
+ hook_name, hook_dest, git_hook_path, backup_path
141
+ )
142
+ else:
143
+ return False, (
144
+ f"Hook {hook_name} already exists. "
145
+ f"Backed up to {backup_path}. "
146
+ f"Use force=True to overwrite."
147
+ )
148
+
149
+ # Install hook (symlink or copy)
150
+ use_symlinks = not force and config.get("use_symlinks", True)
151
+
152
+ try:
153
+ if use_symlinks:
154
+ # Remove existing symlink if present
155
+ if git_hook_path.is_symlink():
156
+ git_hook_path.unlink()
157
+
158
+ git_hook_path.symlink_to(hook_dest.resolve())
159
+ return True, f"Installed {hook_name} (symlink)"
160
+ else:
161
+ shutil.copy(hook_dest, git_hook_path)
162
+ git_hook_path.chmod(0o755)
163
+ return True, f"Installed {hook_name} (copy)"
164
+ except Exception as e:
165
+ raise HookInstallError(f"Failed to install {hook_name}: {e}") from e
166
+
167
+
168
+ def _create_chained_hook(
169
+ hook_name: str, htmlgraph_hook: Path, git_hook: Path, backup_hook: Path
170
+ ) -> tuple[bool, str]:
171
+ """Create a chained hook that runs both existing and HtmlGraph hooks."""
172
+ chain_content = f'''#!/bin/bash
173
+ # Chained hook - runs existing hook then HtmlGraph hook
174
+
175
+ # Run existing hook
176
+ if [ -f "{backup_hook}" ]; then
177
+ "{backup_hook}" || exit $?
178
+ fi
179
+
180
+ # Run HtmlGraph hook
181
+ if [ -f "{htmlgraph_hook}" ]; then
182
+ "{htmlgraph_hook}" || true
183
+ fi
184
+ '''
185
+
186
+ try:
187
+ git_hook.write_text(chain_content, encoding="utf-8")
188
+ git_hook.chmod(0o755)
189
+ return True, f"Installed {hook_name} (chained)"
190
+ except Exception as e:
191
+ raise HookInstallError(f"Failed to create chained hook: {e}") from e
192
+
193
+
194
+ def install_hooks(*, project_dir: Path, use_copy: bool = False) -> HookInstallResult:
195
+ """
196
+ Install HtmlGraph git hooks into the project.
197
+
198
+ Args:
199
+ project_dir: Project root directory
200
+ use_copy: Force copy instead of symlink
201
+
202
+ Returns:
203
+ HookInstallResult with installation details
204
+
205
+ Raises:
206
+ HookInstallError: If installation fails
207
+ HookConfigError: If configuration is invalid
208
+ """
209
+ project_dir = Path(project_dir).resolve()
210
+
211
+ # Validate environment
212
+ _validate_environment(project_dir)
213
+
214
+ # Load configuration
215
+ config = _load_hook_config(project_dir)
216
+
217
+ # Override use_symlinks if use_copy is requested
218
+ if use_copy:
219
+ config["use_symlinks"] = False
220
+
221
+ installed: list[str] = []
222
+ skipped: list[str] = []
223
+ warnings: list[str] = []
224
+
225
+ # Install each enabled hook
226
+ for hook_name in config.get("enabled_hooks", []):
227
+ try:
228
+ success, message = _install_single_hook(
229
+ hook_name,
230
+ project_dir=project_dir,
231
+ config=config,
232
+ force=False,
233
+ )
234
+
235
+ if success:
236
+ installed.append(hook_name)
237
+ else:
238
+ skipped.append(hook_name)
239
+ warnings.append(message)
240
+ except Exception as e:
241
+ skipped.append(hook_name)
242
+ warnings.append(f"{hook_name}: {e}")
243
+
244
+ return HookInstallResult(
245
+ installed=installed,
246
+ skipped=skipped,
247
+ warnings=warnings,
248
+ config_used=config,
249
+ )
250
+
251
+
252
+ def list_hooks(*, project_dir: Path) -> HookListResult:
253
+ """
254
+ Return enabled/disabled/missing hooks for a project.
255
+
256
+ Args:
257
+ project_dir: Project root directory
258
+
259
+ Returns:
260
+ HookListResult with hook status
261
+ """
262
+ from htmlgraph.hooks import AVAILABLE_HOOKS
263
+
264
+ project_dir = Path(project_dir).resolve()
265
+ git_dir = project_dir / ".git"
266
+
267
+ # Load configuration
268
+ try:
269
+ config = _load_hook_config(project_dir)
270
+ except HookConfigError:
271
+ config = {"enabled_hooks": []}
272
+
273
+ enabled_hooks = config.get("enabled_hooks", [])
274
+
275
+ enabled: list[str] = []
276
+ disabled: list[str] = []
277
+ missing: list[str] = []
278
+
279
+ for hook_name in AVAILABLE_HOOKS:
280
+ git_hook_path = git_dir / "hooks" / hook_name
281
+ is_enabled = hook_name in enabled_hooks
282
+
283
+ if is_enabled:
284
+ if git_hook_path.exists():
285
+ enabled.append(hook_name)
286
+ else:
287
+ missing.append(hook_name)
288
+ else:
289
+ disabled.append(hook_name)
290
+
291
+ return HookListResult(
292
+ enabled=enabled,
293
+ disabled=disabled,
294
+ missing=missing,
295
+ )
296
+
297
+
298
+ def validate_hook_config(*, project_dir: Path) -> HookValidationResult:
299
+ """
300
+ Validate hook configuration for a project.
301
+
302
+ Args:
303
+ project_dir: Project root directory
304
+
305
+ Returns:
306
+ HookValidationResult with validation status
307
+ """
308
+ from htmlgraph.hooks import AVAILABLE_HOOKS
309
+
310
+ project_dir = Path(project_dir).resolve()
311
+ errors: list[str] = []
312
+ warnings: list[str] = []
313
+
314
+ # Check git repository
315
+ git_dir = project_dir / ".git"
316
+ if not git_dir.exists():
317
+ errors.append("Not a git repository (no .git directory found)")
318
+
319
+ # Check HtmlGraph initialization
320
+ htmlgraph_dir = project_dir / ".htmlgraph"
321
+ if not htmlgraph_dir.exists():
322
+ errors.append("HtmlGraph not initialized (run 'htmlgraph init' first)")
323
+
324
+ # Load and validate configuration
325
+ try:
326
+ config = _load_hook_config(project_dir)
327
+
328
+ # Validate enabled_hooks
329
+ enabled_hooks = config.get("enabled_hooks", [])
330
+ if not isinstance(enabled_hooks, list):
331
+ errors.append("enabled_hooks must be a list")
332
+ else:
333
+ for hook_name in enabled_hooks:
334
+ if hook_name not in AVAILABLE_HOOKS:
335
+ warnings.append(f"Unknown hook '{hook_name}' in configuration")
336
+
337
+ # Validate boolean options
338
+ for key in ["use_symlinks", "backup_existing", "chain_existing"]:
339
+ value = config.get(key)
340
+ if value is not None and not isinstance(value, bool):
341
+ errors.append(f"{key} must be a boolean")
342
+
343
+ except HookConfigError as e:
344
+ errors.append(str(e))
345
+
346
+ return HookValidationResult(
347
+ valid=len(errors) == 0,
348
+ errors=errors,
349
+ warnings=warnings,
350
+ )