htmlgraph 0.20.1__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 (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -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
+ )