monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.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 (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,309 +1,319 @@
1
1
  """
2
- CLI commands for Git Hooks management.
2
+ CLI commands for Universal Hooks management.
3
3
 
4
- Provides:
5
- - monoco hooks install
6
- - monoco hooks uninstall
7
- - monoco hooks status
4
+ This module provides commands for managing Universal Hooks (Git/IDE/Agent).
8
5
  """
9
6
 
10
- import typer
11
7
  from pathlib import Path
12
8
  from typing import Optional
9
+
10
+ import typer
13
11
  from rich.console import Console
14
- from rich.table import Table
15
- from rich.panel import Panel
16
- from rich.tree import Tree
17
12
 
18
- from monoco.core.config import get_config
19
- from monoco.core.output import OutputManager
20
- from .core import GitHooksManager, HookConfig
13
+ from .manager import UniversalHookManager
14
+ from .models import HookType
15
+ from .dispatchers import GitHookDispatcher
21
16
 
22
- app = typer.Typer(help="Manage Git hooks for development workflow.")
17
+ app = typer.Typer(help="Universal Hooks management (Git/IDE/Agent).")
23
18
  console = Console()
24
19
 
25
20
 
26
- def _get_manager() -> GitHooksManager:
27
- """Get configured GitHooksManager instance."""
28
- config = get_config()
29
- project_root = Path(config.paths.root).resolve()
21
+ @app.command("scan")
22
+ def scan(
23
+ directory: Path = typer.Option(
24
+ ".monoco/hooks",
25
+ "--dir",
26
+ "-d",
27
+ help="Directory to scan for hook scripts",
28
+ ),
29
+ ) -> None:
30
+ """Scan for hook scripts with Front Matter metadata."""
31
+ manager = UniversalHookManager()
32
+ dir_path = Path(directory)
30
33
 
31
- hooks_config = HookConfig(
32
- enabled=config.hooks.enabled,
33
- enabled_features=config.hooks.features,
34
- enabled_hooks=config.hooks.hooks,
35
- )
34
+ if not dir_path.exists():
35
+ console.print(f"[yellow]Directory not found: {dir_path}[/yellow]")
36
+ raise typer.Exit(1)
36
37
 
37
- return GitHooksManager(project_root, hooks_config)
38
+ groups = manager.scan(dir_path)
38
39
 
40
+ if not groups:
41
+ console.print("[dim]No hooks found.[/dim]")
42
+ return
39
43
 
40
- @app.command("install")
41
- def install(
42
- hook_type: Optional[str] = typer.Argument(
43
- None,
44
- help="Specific hook type to install (pre-commit, pre-push, post-checkout). If not specified, installs all enabled hooks."
45
- ),
46
- force: bool = typer.Option(
47
- False,
48
- "--force",
49
- "-f",
50
- help="Force installation even if hook already exists",
44
+ for key, group in groups.items():
45
+ console.print(f"\n[bold]{key}[/bold] ({len(group.hooks)} hooks)")
46
+ for hook in group.get_prioritized_hooks():
47
+ console.print(f" • {hook.script_path.name}")
48
+ console.print(f" Event: {hook.metadata.event}")
49
+ if hook.metadata.matcher:
50
+ console.print(f" Matchers: {', '.join(hook.metadata.matcher)}")
51
+
52
+
53
+ @app.command("validate")
54
+ def validate(
55
+ directory: Path = typer.Option(
56
+ ".monoco/hooks",
57
+ "--dir",
58
+ "-d",
59
+ help="Directory to validate hook scripts",
51
60
  ),
52
- json: bool = typer.Option(
53
- False,
54
- "--json",
55
- help="Output in JSON format (for agent mode)",
61
+ ) -> None:
62
+ """Validate hook metadata."""
63
+ manager = UniversalHookManager()
64
+ dir_path = Path(directory)
65
+
66
+ if not dir_path.exists():
67
+ console.print(f"[yellow]Directory not found: {dir_path}[/yellow]")
68
+ raise typer.Exit(1)
69
+
70
+ groups = manager.scan(dir_path)
71
+ errors_found = False
72
+
73
+ for key, group in groups.items():
74
+ for hook in group.hooks:
75
+ result = manager.validate(hook)
76
+ if not result.is_valid:
77
+ errors_found = True
78
+ console.print(f"\n[red]✗ {hook.script_path}[/red]")
79
+ for error in result.errors:
80
+ console.print(f" Error: {error}")
81
+ for warning in result.warnings:
82
+ console.print(f" Warning: {warning}")
83
+ elif result.warnings:
84
+ console.print(f"\n[yellow]⚠ {hook.script_path}[/yellow]")
85
+ for warning in result.warnings:
86
+ console.print(f" Warning: {warning}")
87
+
88
+ if not errors_found:
89
+ console.print("[green]✓ All hooks are valid[/green]")
90
+ else:
91
+ raise typer.Exit(1)
92
+
93
+
94
+ @app.command("run")
95
+ def run(
96
+ hook_type: str = typer.Argument(..., help="Hook type (git, ide, agent)"),
97
+ event: str = typer.Argument(..., help="Hook event (e.g., pre-commit, before-tool)"),
98
+ project_root: Path = typer.Option(
99
+ ".",
100
+ "--project",
101
+ "-p",
102
+ help="Project root directory",
56
103
  ),
57
- ):
58
- """
59
- Install git hooks to .git/hooks/.
104
+ extra_args: Optional[list[str]] = typer.Argument(None),
105
+ ) -> None:
106
+ """Run hooks for a specific type and event."""
107
+ try:
108
+ hook_type_enum = HookType(hook_type.lower())
109
+ except ValueError:
110
+ console.print(f"[red]Invalid hook type: {hook_type}[/red]")
111
+ console.print(f"Valid types: git, ide, agent")
112
+ raise typer.Exit(1)
113
+
114
+ manager = UniversalHookManager()
60
115
 
61
- Discovers hooks from all Features and generates combined hook scripts.
62
- """
63
- manager = _get_manager()
116
+ # Discover hooks from all active features
117
+ from monoco.core.registry import FeatureRegistry
118
+ active_features = FeatureRegistry.get_features()
64
119
 
65
- if not manager.is_git_repo():
66
- OutputManager.error("Not a git repository. Cannot install hooks.")
67
- raise typer.Exit(code=1)
120
+ all_hooks_list = []
68
121
 
69
- if hook_type:
70
- # Install specific hook type
71
- from .core import HookType
122
+ # 1. Builtin hooks
123
+ try:
124
+ from monoco.features import hooks as hooks_module
125
+ hooks_feature_dir = Path(hooks_module.__file__).parent
126
+ builtin_hooks_dir = hooks_feature_dir / "resources" / "hooks"
127
+ if builtin_hooks_dir.exists():
128
+ groups = manager.scan(builtin_hooks_dir)
129
+ for group in groups.values():
130
+ all_hooks_list.extend(group.hooks)
131
+ except Exception:
132
+ pass
133
+
134
+ # 2. Feature hooks
135
+ for feature in active_features:
136
+ if feature.name == "hooks":
137
+ continue
72
138
  try:
73
- htype = HookType(hook_type)
74
- except ValueError:
75
- valid_types = [t.value for t in HookType]
76
- OutputManager.error(f"Invalid hook type: {hook_type}. Valid types: {', '.join(valid_types)}")
77
- raise typer.Exit(code=1)
78
-
79
- # Temporarily enable this hook type
80
- manager.config.enabled_hooks[hook_type] = True
81
-
82
- results = manager.install()
83
-
84
- if OutputManager.is_agent_mode() or json:
85
- OutputManager.print({
86
- "status": "installed",
87
- "hooks": {k.value: v for k, v in results.items()}
88
- })
139
+ import importlib
140
+ module_name = feature.__class__.__module__
141
+ module = importlib.import_module(module_name)
142
+ if hasattr(module, "__file__") and module.__file__:
143
+ feature_dir = Path(module.__file__).parent
144
+ hooks_resource_dir = feature_dir / "resources" / "hooks"
145
+ if hooks_resource_dir.exists():
146
+ groups = manager.scan(hooks_resource_dir)
147
+ for group in groups.values():
148
+ all_hooks_list.extend(group.hooks)
149
+ except Exception:
150
+ continue
151
+
152
+ # Find matching hooks
153
+ matching_hooks = [
154
+ h for h in all_hooks_list
155
+ if h.metadata.type == hook_type_enum and h.metadata.event == event
156
+ ]
157
+
158
+ if not matching_hooks:
159
+ raise typer.Exit(0)
160
+
161
+ # Execute matching hooks
162
+ if hook_type_enum == HookType.AGENT:
163
+ # Use Universal Interceptor for agent hooks
164
+ from .universal_interceptor import UniversalInterceptor
165
+
166
+ interceptor = UniversalInterceptor()
167
+ exit_code = 0
168
+
169
+ for hook in matching_hooks:
170
+ result = interceptor.run(str(hook.script_path))
171
+ if result != 0:
172
+ exit_code = result
173
+ console.print(f"[red]Hook failed: {hook.script_path.name}[/red]")
174
+
175
+ raise typer.Exit(exit_code)
89
176
  else:
90
- installed = [k.value for k, v in results.items() if v]
91
- skipped = [k.value for k, v in results.items() if not v]
92
-
93
- if installed:
94
- console.print(f"[green]✓ Installed hooks:[/green] {', '.join(installed)}")
95
- if skipped:
96
- console.print(f"[yellow]⚠ Skipped hooks:[/yellow] {', '.join(skipped)}")
177
+ # Use dispatcher for Git/IDE hooks
178
+ dispatcher = manager.get_dispatcher(hook_type_enum)
179
+ if not dispatcher:
180
+ # Register appropriate dispatcher
181
+ if hook_type_enum == HookType.GIT:
182
+ dispatcher = GitHookDispatcher()
183
+ manager.register_dispatcher(hook_type_enum, dispatcher)
184
+ else:
185
+ console.print(f"[red]No dispatcher available for type: {hook_type}[/red]")
186
+ raise typer.Exit(1)
187
+
188
+ exit_code = 0
189
+ for hook in matching_hooks:
190
+ context = {
191
+ "event": event,
192
+ "git_root": str(project_root),
193
+ "args": extra_args or [],
194
+ }
195
+ if not dispatcher.execute(hook, context):
196
+ exit_code = 1
197
+ console.print(f"[red]Hook failed: {hook.script_path.name}[/red]")
198
+
199
+ raise typer.Exit(exit_code)
97
200
 
98
201
 
99
- @app.command("uninstall")
100
- def uninstall(
101
- hook_type: Optional[str] = typer.Argument(
102
- None,
103
- help="Specific hook type to uninstall. If not specified, uninstalls all Monoco-managed hooks."
104
- ),
105
- json: bool = typer.Option(
106
- False,
107
- "--json",
108
- help="Output in JSON format (for agent mode)",
202
+ @app.command("install")
203
+ def install(
204
+ directory: Path = typer.Option(
205
+ ".monoco/hooks",
206
+ "--dir",
207
+ "-d",
208
+ help="Directory containing hook scripts",
109
209
  ),
110
- ):
111
- """
112
- Uninstall Monoco-managed git hooks from .git/hooks/.
113
-
114
- Only removes hooks that were installed by Monoco (identified by marker).
115
- """
116
- manager = _get_manager()
117
-
118
- if not manager.is_git_repo():
119
- OutputManager.error("Not a git repository.")
120
- raise typer.Exit(code=1)
121
-
122
- if hook_type:
123
- # Uninstall specific hook type
124
- hook_path = manager.hooks_dir / hook_type
125
- if hook_path.exists():
126
- try:
127
- content = hook_path.read_text(encoding="utf-8")
128
- if manager.MONOCO_MARKER in content:
129
- hook_path.unlink()
130
- results = {hook_type: True}
131
- else:
132
- results = {hook_type: False}
133
- except Exception as e:
134
- OutputManager.error(f"Failed to uninstall {hook_type}: {e}")
135
- raise typer.Exit(code=1)
136
- else:
137
- results = {hook_type: False}
138
- else:
139
- results = manager.uninstall()
140
-
141
- if OutputManager.is_agent_mode() or json:
142
- OutputManager.print({
143
- "status": "uninstalled",
144
- "hooks": {k.value if hasattr(k, 'value') else k: v for k, v in results.items()}
145
- })
146
- else:
147
- removed = [k.value if hasattr(k, 'value') else k for k, v in results.items() if v]
148
- skipped = [k.value if hasattr(k, 'value') else k for k, v in results.items() if not v]
149
-
150
- if removed:
151
- console.print(f"[green]✓ Removed hooks:[/green] {', '.join(removed)}")
152
- if skipped:
153
- console.print(f"[dim]- Skipped (not managed by Monoco):[/dim] {', '.join(skipped)}")
154
-
155
-
156
- @app.command("status")
157
- def status(
158
- json: bool = typer.Option(
159
- False,
160
- "--json",
161
- help="Output in JSON format (for agent mode)",
210
+ project_root: Path = typer.Option(
211
+ ".",
212
+ "--project",
213
+ "-p",
214
+ help="Project root directory",
162
215
  ),
163
- ):
164
- """
165
- Show current hooks installation status.
166
-
167
- Displays which hooks are installed, discovered, and their configuration.
168
- """
169
- manager = _get_manager()
170
- status_info = manager.get_status()
171
-
172
- if OutputManager.is_agent_mode() or json:
173
- OutputManager.print(status_info)
216
+ ) -> None:
217
+ """Install Git hooks to .git/hooks/."""
218
+ manager = UniversalHookManager()
219
+ dispatcher = GitHookDispatcher()
220
+ manager.register_dispatcher(HookType.GIT, dispatcher)
221
+
222
+ dir_path = Path(directory)
223
+ if not dir_path.exists():
224
+ console.print(f"[yellow]Hooks directory not found: {dir_path}[/yellow]")
225
+ raise typer.Exit(1)
226
+
227
+ # Scan for git hooks
228
+ groups = manager.scan(dir_path)
229
+
230
+ if "git" not in groups:
231
+ console.print("[dim]No Git hooks found.[/dim]")
174
232
  return
175
233
 
176
- # Human-readable output
177
- if not status_info["is_git_repo"]:
178
- console.print("[red]Not a git repository.[/red]")
179
- raise typer.Exit(code=1)
180
-
181
- console.print(Panel("[bold blue]Git Hooks Status[/bold blue]", expand=False))
182
-
183
- # Installed hooks table
184
- table = Table(title="Installed Hooks")
185
- table.add_column("Hook Type", style="cyan")
186
- table.add_column("Status", style="green")
187
- table.add_column("Managed By", style="dim")
234
+ # Install hooks
235
+ group = groups["git"]
236
+ installed = 0
237
+ failed = 0
188
238
 
189
- for hook_type, info in status_info["installed"].items():
190
- if info["exists"]:
191
- status_str = "[green]Installed[/green]"
192
- managed = "Monoco" if info.get("managed_by_monoco") else "External"
239
+ for hook in group.hooks:
240
+ if dispatcher.install(hook, project_root):
241
+ installed += 1
242
+ console.print(f"[green]✓ Installed {hook.metadata.event}[/green]")
193
243
  else:
194
- status_str = "[dim]Not installed[/dim]"
195
- managed = "-"
196
- table.add_row(hook_type, status_str, managed)
197
-
198
- console.print(table)
199
-
200
- # Discovered hooks
201
- if status_info["discovered"]:
202
- console.print("\n[bold]Discovered Hook Scripts:[/bold]")
203
- for hook_type, scripts in status_info["discovered"].items():
204
- tree = Tree(f"[cyan]{hook_type}[/cyan]")
205
- for script in scripts:
206
- tree.add(f"[dim]{script['feature']}[/dim] (priority: {script['priority']})")
207
- console.print(tree)
208
- else:
209
- console.print("\n[yellow]No hook scripts discovered from Features.[/yellow]")
210
-
211
- # Configuration
212
- config_tree = Tree("[bold]Configuration:[/bold]")
213
- config_tree.add(f"Hooks enabled: {status_info['config']['enabled']}")
214
-
215
- if status_info['config']['enabled_features']:
216
- features_branch = config_tree.add("Feature-specific settings:")
217
- for feature, enabled in status_info['config']['enabled_features'].items():
218
- status = "enabled" if enabled else "disabled"
219
- features_branch.add(f"{feature}: {status}")
244
+ failed += 1
245
+ console.print(f"[red]✗ Failed to install {hook.metadata.event}[/red]")
220
246
 
221
- if status_info['config']['enabled_hooks']:
222
- hooks_branch = config_tree.add("Hook-type settings:")
223
- for hook, enabled in status_info['config']['enabled_hooks'].items():
224
- status = "enabled" if enabled else "disabled"
225
- hooks_branch.add(f"{hook}: {status}")
247
+ if failed > 0:
248
+ raise typer.Exit(1)
226
249
 
227
- console.print(config_tree)
250
+ console.print(f"\n[green]Installed {installed} hook(s)[/green]")
228
251
 
229
252
 
230
- @app.command("enable")
231
- def enable(
232
- hook_type: str = typer.Argument(..., help="Hook type to enable (pre-commit, pre-push, post-checkout)"),
233
- ):
234
- """
235
- Enable a specific hook type in configuration.
253
+ @app.command("uninstall")
254
+ def uninstall(
255
+ event: Optional[str] = typer.Option(
256
+ None,
257
+ "--event",
258
+ "-e",
259
+ help="Specific event to uninstall (uninstalls all if not specified)",
260
+ ),
261
+ project_root: Path = typer.Option(
262
+ ".",
263
+ "--project",
264
+ "-p",
265
+ help="Project root directory",
266
+ ),
267
+ ) -> None:
268
+ """Uninstall Git hooks from .git/hooks/."""
269
+ dispatcher = GitHookDispatcher()
236
270
 
237
- This updates workspace.yaml to enable the hook for future installs.
238
- """
239
- from .core import HookType
240
- try:
241
- HookType(hook_type)
242
- except ValueError:
243
- valid_types = [t.value for t in HookType]
244
- OutputManager.error(f"Invalid hook type: {hook_type}. Valid types: {', '.join(valid_types)}")
245
- raise typer.Exit(code=1)
271
+ if event:
272
+ if dispatcher.uninstall(event, project_root):
273
+ console.print(f"[green]✓ Uninstalled {event}[/green]")
274
+ else:
275
+ console.print(f"[red]✗ Failed to uninstall {event}[/red]")
276
+ raise typer.Exit(1)
277
+ else:
278
+ # Uninstall all Monoco hooks
279
+ installed = dispatcher.list_installed(project_root)
280
+ uninstalled = 0
281
+ failed = 0
282
+
283
+ for hook_info in installed:
284
+ if dispatcher.uninstall(hook_info["event"], project_root):
285
+ uninstalled += 1
286
+ console.print(f"[green]✓ Uninstalled {hook_info['event']}[/green]")
287
+ else:
288
+ failed += 1
289
+ console.print(f"[red]✗ Failed to uninstall {hook_info['event']}[/red]")
290
+
291
+ if failed > 0:
292
+ raise typer.Exit(1)
293
+
294
+ console.print(f"\n[green]Uninstalled {uninstalled} hook(s)[/green]")
295
+
296
+
297
+ @app.command("list")
298
+ def list_hooks(
299
+ project_root: Path = typer.Option(
300
+ ".",
301
+ "--project",
302
+ "-p",
303
+ help="Project root directory",
304
+ ),
305
+ ) -> None:
306
+ """List installed Git hooks."""
307
+ dispatcher = GitHookDispatcher()
308
+ installed = dispatcher.list_installed(project_root)
246
309
 
247
- # Update workspace.yaml
248
- config = get_config()
249
- config_path = Path(config.paths.root) / ".monoco" / "workspace.yaml"
250
- try:
251
- import yaml
252
- with open(config_path, 'r') as f:
253
- data = yaml.safe_load(f) or {}
254
-
255
- if 'hooks' not in data:
256
- data['hooks'] = {}
257
- if 'hooks' not in data['hooks']:
258
- data['hooks']['hooks'] = {}
259
- data['hooks']['hooks'][hook_type] = True
260
-
261
- with open(config_path, 'w') as f:
262
- yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
263
-
264
- console.print(f"[green]✓ Enabled {hook_type} hook in configuration[/green]")
265
- console.print(f"[dim]Run 'monoco hooks install' to apply changes.[/dim]")
266
- except Exception as e:
267
- OutputManager.error(f"Failed to update configuration: {e}")
268
- raise typer.Exit(code=1)
269
-
270
-
271
- @app.command("disable")
272
- def disable(
273
- hook_type: str = typer.Argument(..., help="Hook type to disable (pre-commit, pre-push, post-checkout)"),
274
- ):
275
- """
276
- Disable a specific hook type in configuration.
277
-
278
- This updates workspace.yaml to disable the hook for future installs.
279
- """
280
- from .core import HookType
281
- try:
282
- HookType(hook_type)
283
- except ValueError:
284
- valid_types = [t.value for t in HookType]
285
- OutputManager.error(f"Invalid hook type: {hook_type}. Valid types: {', '.join(valid_types)}")
286
- raise typer.Exit(code=1)
310
+ if not installed:
311
+ console.print("[dim]No Monoco Git hooks installed.[/dim]")
312
+ return
287
313
 
288
- # Update workspace.yaml
289
- config = get_config()
290
- config_path = Path(config.paths.root) / ".monoco" / "workspace.yaml"
291
- try:
292
- import yaml
293
- with open(config_path, 'r') as f:
294
- data = yaml.safe_load(f) or {}
295
-
296
- if 'hooks' not in data:
297
- data['hooks'] = {}
298
- if 'hooks' not in data['hooks']:
299
- data['hooks']['hooks'] = {}
300
- data['hooks']['hooks'][hook_type] = False
301
-
302
- with open(config_path, 'w') as f:
303
- yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
304
-
305
- console.print(f"[green]✓ Disabled {hook_type} hook in configuration[/green]")
306
- console.print(f"[dim]Run 'monoco hooks uninstall {hook_type}' to remove existing hook.[/dim]")
307
- except Exception as e:
308
- OutputManager.error(f"Failed to update configuration: {e}")
309
- raise typer.Exit(code=1)
314
+ console.print("[bold]Installed Git Hooks:[/bold]\n")
315
+ for hook in installed:
316
+ merged_status = " (merged)" if hook.get("is_merged") else ""
317
+ console.print(f" • {hook['event']}{merged_status}")
318
+ if hook.get("hook_id"):
319
+ console.print(f" ID: {hook['hook_id']}")
@@ -0,0 +1,23 @@
1
+ """
2
+ Hook Dispatchers for Universal Hooks system.
3
+
4
+ Provides type-specific dispatchers for Git, IDE, and Agent hooks.
5
+ """
6
+
7
+ from .git_dispatcher import GitHookDispatcher
8
+ from .agent_dispatcher import (
9
+ AgentHookDispatcher,
10
+ ClaudeCodeDispatcher,
11
+ GeminiDispatcher,
12
+ create_agent_dispatchers,
13
+ get_dispatcher_for_provider,
14
+ )
15
+
16
+ __all__ = [
17
+ "GitHookDispatcher",
18
+ "AgentHookDispatcher",
19
+ "ClaudeCodeDispatcher",
20
+ "GeminiDispatcher",
21
+ "create_agent_dispatchers",
22
+ "get_dispatcher_for_provider",
23
+ ]