monoco-toolkit 0.3.11__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 (132) hide show
  1. monoco/core/automation/__init__.py +40 -0
  2. monoco/core/automation/field_watcher.py +296 -0
  3. monoco/core/automation/handlers.py +805 -0
  4. monoco/core/config.py +29 -11
  5. monoco/core/daemon/__init__.py +5 -0
  6. monoco/core/daemon/pid.py +290 -0
  7. monoco/core/git.py +15 -0
  8. monoco/core/hooks/context.py +74 -13
  9. monoco/core/injection.py +86 -8
  10. monoco/core/integrations.py +0 -24
  11. monoco/core/router/__init__.py +17 -0
  12. monoco/core/router/action.py +202 -0
  13. monoco/core/scheduler/__init__.py +63 -0
  14. monoco/core/scheduler/base.py +152 -0
  15. monoco/core/scheduler/engines.py +175 -0
  16. monoco/core/scheduler/events.py +197 -0
  17. monoco/core/scheduler/local.py +377 -0
  18. monoco/core/setup.py +9 -0
  19. monoco/core/sync.py +199 -4
  20. monoco/core/watcher/__init__.py +63 -0
  21. monoco/core/watcher/base.py +382 -0
  22. monoco/core/watcher/dropzone.py +152 -0
  23. monoco/core/watcher/im.py +460 -0
  24. monoco/core/watcher/issue.py +303 -0
  25. monoco/core/watcher/memo.py +192 -0
  26. monoco/core/watcher/task.py +238 -0
  27. monoco/daemon/app.py +3 -60
  28. monoco/daemon/commands.py +459 -25
  29. monoco/daemon/events.py +34 -0
  30. monoco/daemon/scheduler.py +157 -201
  31. monoco/daemon/services.py +42 -243
  32. monoco/features/agent/__init__.py +25 -7
  33. monoco/features/agent/cli.py +91 -57
  34. monoco/features/agent/engines.py +31 -170
  35. monoco/features/agent/resources/en/AGENTS.md +14 -14
  36. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  37. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  38. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  39. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  40. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  41. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  42. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  43. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  44. monoco/features/agent/worker.py +1 -1
  45. monoco/features/hooks/__init__.py +61 -6
  46. monoco/features/hooks/commands.py +281 -271
  47. monoco/features/hooks/dispatchers/__init__.py +23 -0
  48. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  49. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  50. monoco/features/hooks/manager.py +357 -0
  51. monoco/features/hooks/models.py +262 -0
  52. monoco/features/hooks/parser.py +322 -0
  53. monoco/features/hooks/universal_interceptor.py +503 -0
  54. monoco/features/im/__init__.py +67 -0
  55. monoco/features/im/core.py +782 -0
  56. monoco/features/im/models.py +311 -0
  57. monoco/features/issue/commands.py +133 -60
  58. monoco/features/issue/core.py +385 -40
  59. monoco/features/issue/domain_commands.py +0 -19
  60. monoco/features/issue/resources/en/AGENTS.md +17 -122
  61. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  62. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  63. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  64. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  65. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  66. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  67. monoco/features/memo/cli.py +15 -64
  68. monoco/features/memo/core.py +6 -34
  69. monoco/features/memo/models.py +24 -15
  70. monoco/features/memo/resources/en/AGENTS.md +31 -0
  71. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  72. monoco/features/spike/commands.py +5 -3
  73. monoco/main.py +5 -3
  74. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  75. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  76. monoco/core/execution.py +0 -67
  77. monoco/features/agent/apoptosis.py +0 -44
  78. monoco/features/agent/manager.py +0 -127
  79. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  80. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  81. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  82. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  83. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  84. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  85. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  86. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  87. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  88. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  89. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  90. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  91. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  92. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  93. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  94. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  95. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  96. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  97. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  98. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  99. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  100. monoco/features/agent/session.py +0 -169
  101. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  102. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  103. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  104. monoco/features/hooks/adapter.py +0 -67
  105. monoco/features/hooks/core.py +0 -441
  106. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  107. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  108. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  109. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  110. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  111. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  112. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  113. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  114. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  115. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  116. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  117. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  118. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  119. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  120. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  121. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  122. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  123. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  124. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  125. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  126. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  127. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  128. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  129. monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
  130. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,66 @@
1
1
  """
2
- Git Hooks management feature for Monoco.
2
+ Universal Hooks management feature for Monoco.
3
3
 
4
- Provides distributed hooks architecture where each Feature can contribute
5
- its own hooks in resources/hooks/, aggregated by this feature.
4
+ Provides a unified hooks system for Git, IDE, and Agent integration
5
+ with Front Matter metadata support.
6
6
  """
7
7
 
8
- from .core import GitHooksManager, HookDeclaration, HookType
9
- from .adapter import HooksFeature
8
+ from .models import (
9
+ HookType,
10
+ HookMetadata,
11
+ GitEvent,
12
+ AgentEvent,
13
+ IDEEvent,
14
+ ParsedHook,
15
+ HookGroup,
16
+ )
17
+ from .parser import HookParser, ParseError
18
+ from .manager import UniversalHookManager, ValidationResult, HookDispatcher
19
+ from .dispatchers import (
20
+ GitHookDispatcher,
21
+ AgentHookDispatcher,
22
+ ClaudeCodeDispatcher,
23
+ GeminiDispatcher,
24
+ create_agent_dispatchers,
25
+ get_dispatcher_for_provider,
26
+ )
27
+ from .universal_interceptor import (
28
+ UniversalInterceptor,
29
+ AgentAdapter,
30
+ ClaudeAdapter,
31
+ GeminiAdapter,
32
+ UnifiedDecision,
33
+ UnifiedHookInput,
34
+ )
10
35
 
11
- __all__ = ["GitHooksManager", "HookDeclaration", "HookType", "HooksFeature"]
36
+ __all__ = [
37
+ # Models
38
+ "HookType",
39
+ "HookMetadata",
40
+ "GitEvent",
41
+ "AgentEvent",
42
+ "IDEEvent",
43
+ "ParsedHook",
44
+ "HookGroup",
45
+ # Parser
46
+ "HookParser",
47
+ "ParseError",
48
+ # Manager
49
+ "UniversalHookManager",
50
+ "ValidationResult",
51
+ "HookDispatcher",
52
+ # Dispatchers
53
+ "GitHookDispatcher",
54
+ "AgentHookDispatcher",
55
+ "ClaudeCodeDispatcher",
56
+ "GeminiDispatcher",
57
+ "create_agent_dispatchers",
58
+ "get_dispatcher_for_provider",
59
+ # Universal Interceptor (ACL)
60
+ "UniversalInterceptor",
61
+ "AgentAdapter",
62
+ "ClaudeAdapter",
63
+ "GeminiAdapter",
64
+ "UnifiedDecision",
65
+ "UnifiedHookInput",
66
+ ]
@@ -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']}")