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.
- monoco/core/automation/__init__.py +40 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +805 -0
- monoco/core/config.py +29 -11
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +17 -0
- monoco/core/router/action.py +202 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +197 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +63 -0
- monoco/core/watcher/base.py +382 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +192 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +157 -201
- monoco/daemon/services.py +42 -243
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/worker.py +1 -1
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +133 -60
- monoco/features/issue/core.py +385 -40
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/features/spike/commands.py +5 -3
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/execution.py +0 -67
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/agent/session.py +0 -169
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Hooks Dispatcher for Universal Hooks system.
|
|
3
|
+
|
|
4
|
+
Manages installation, execution, and uninstallation of Git hooks
|
|
5
|
+
to the `.git/hooks/` directory with non-destructive installation
|
|
6
|
+
and Glob matcher support for staged files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import fnmatch
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from ..manager import HookDispatcher
|
|
17
|
+
from ..models import GitEvent, HookType, ParsedHook
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GitHookDispatcher(HookDispatcher):
|
|
21
|
+
"""
|
|
22
|
+
Dispatcher for Git lifecycle hooks.
|
|
23
|
+
|
|
24
|
+
Responsible for:
|
|
25
|
+
- Installing hook proxy scripts to `.git/hooks/`
|
|
26
|
+
- Executing hooks with staged file filtering
|
|
27
|
+
- Non-destructive installation (coexisting with Husky/pre-commit)
|
|
28
|
+
- Uninstalling and restoring original hooks
|
|
29
|
+
|
|
30
|
+
Supported events:
|
|
31
|
+
- pre-commit
|
|
32
|
+
- prepare-commit-msg
|
|
33
|
+
- commit-msg
|
|
34
|
+
- post-merge
|
|
35
|
+
- pre-push
|
|
36
|
+
- post-checkout
|
|
37
|
+
- pre-rebase
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Marker used to identify Monoco-managed hooks
|
|
41
|
+
HOOK_MARKER = "MONOCO_HOOK_MARKER"
|
|
42
|
+
BACKUP_SUFFIX = ".monoco.backup"
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
"""Initialize the Git hook dispatcher."""
|
|
46
|
+
super().__init__(HookType.GIT, provider=None)
|
|
47
|
+
self._git_dir: Optional[Path] = None
|
|
48
|
+
self._hooks_dir: Optional[Path] = None
|
|
49
|
+
|
|
50
|
+
def _ensure_git_repo(self, project_root: Path) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Check if project_root is a git repository and set up paths.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project_root: The project root directory
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if valid git repository, False otherwise
|
|
59
|
+
"""
|
|
60
|
+
git_dir = project_root / ".git"
|
|
61
|
+
if not git_dir.exists():
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
self._git_dir = git_dir
|
|
65
|
+
self._hooks_dir = git_dir / "hooks"
|
|
66
|
+
self._hooks_dir.mkdir(exist_ok=True)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def can_execute(self, hook: ParsedHook) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Check if this dispatcher can execute the given hook.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
hook: The parsed hook to check
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if this is a git hook
|
|
78
|
+
"""
|
|
79
|
+
return hook.metadata.type == HookType.GIT
|
|
80
|
+
|
|
81
|
+
def execute(self, hook: ParsedHook, context: Optional[dict] = None) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Execute a git hook script.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
hook: The parsed hook to execute
|
|
87
|
+
context: Optional execution context with 'event' and 'git_root'
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if execution succeeded
|
|
91
|
+
"""
|
|
92
|
+
if not hook.script_path.exists():
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Check if we need to filter by staged files
|
|
96
|
+
if hook.metadata.matcher and context:
|
|
97
|
+
git_root = context.get("git_root")
|
|
98
|
+
if git_root and not self._should_trigger_for_staged_files(
|
|
99
|
+
git_root, hook.metadata.matcher
|
|
100
|
+
):
|
|
101
|
+
# No matching staged files, skip silently
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Execute the hook script
|
|
105
|
+
try:
|
|
106
|
+
env = os.environ.copy()
|
|
107
|
+
if context:
|
|
108
|
+
env["MONOCO_HOOK_EVENT"] = context.get("event", "")
|
|
109
|
+
env["MONOCO_HOOK_TYPE"] = "git"
|
|
110
|
+
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
[str(hook.script_path)],
|
|
113
|
+
cwd=hook.script_path.parent,
|
|
114
|
+
env=env,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=300, # 5 minute timeout
|
|
118
|
+
)
|
|
119
|
+
return result.returncode == 0
|
|
120
|
+
except Exception:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def _should_trigger_for_staged_files(
|
|
124
|
+
self, git_root: Path, matchers: list[str]
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
Check if any staged files match the given glob patterns.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
git_root: Path to the git repository root
|
|
131
|
+
matchers: List of glob patterns to match
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if any staged file matches any pattern
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
# Get staged files
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
|
|
140
|
+
cwd=git_root,
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
timeout=30,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if result.returncode != 0:
|
|
147
|
+
return True # If we can't get staged files, allow execution
|
|
148
|
+
|
|
149
|
+
staged_files = result.stdout.strip().split("\n")
|
|
150
|
+
staged_files = [f for f in staged_files if f]
|
|
151
|
+
|
|
152
|
+
if not staged_files:
|
|
153
|
+
return False # No staged files, don't trigger
|
|
154
|
+
|
|
155
|
+
# Check if any staged file matches any pattern
|
|
156
|
+
for file_path in staged_files:
|
|
157
|
+
for pattern in matchers:
|
|
158
|
+
if fnmatch.fnmatch(file_path, pattern):
|
|
159
|
+
return True
|
|
160
|
+
# Also try matching against just the filename
|
|
161
|
+
if fnmatch.fnmatch(Path(file_path).name, pattern):
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
except Exception:
|
|
167
|
+
# If anything goes wrong, allow execution
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def install(
|
|
171
|
+
self,
|
|
172
|
+
hook: ParsedHook,
|
|
173
|
+
project_root: Path,
|
|
174
|
+
hook_id: Optional[str] = None,
|
|
175
|
+
) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Install a hook proxy script to `.git/hooks/`.
|
|
178
|
+
|
|
179
|
+
The proxy script calls `monoco hook run git <event>` to execute
|
|
180
|
+
the actual hook logic.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
hook: The parsed hook to install
|
|
184
|
+
project_root: The project root directory
|
|
185
|
+
hook_id: Optional unique identifier for this hook
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if installation succeeded
|
|
189
|
+
"""
|
|
190
|
+
if not self._ensure_git_repo(project_root):
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
event = hook.metadata.event
|
|
194
|
+
if not event:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
hook_path = self._hooks_dir / event
|
|
198
|
+
hook_id = hook_id or hook.script_path.stem
|
|
199
|
+
|
|
200
|
+
# Generate proxy script content
|
|
201
|
+
proxy_content = self._generate_proxy_script(event, hook_id, hook.metadata.matcher)
|
|
202
|
+
|
|
203
|
+
if hook_path.exists():
|
|
204
|
+
# Check if it's already managed by us
|
|
205
|
+
existing_content = hook_path.read_text(encoding="utf-8")
|
|
206
|
+
if self.HOOK_MARKER in existing_content:
|
|
207
|
+
# Already our hook, update it
|
|
208
|
+
hook_path.write_text(proxy_content, encoding="utf-8")
|
|
209
|
+
os.chmod(hook_path, 0o755)
|
|
210
|
+
return True
|
|
211
|
+
else:
|
|
212
|
+
# Existing hook not managed by us - backup and merge
|
|
213
|
+
return self._install_merged(hook_path, proxy_content, event, hook_id)
|
|
214
|
+
else:
|
|
215
|
+
# Fresh install
|
|
216
|
+
hook_path.write_text(proxy_content, encoding="utf-8")
|
|
217
|
+
os.chmod(hook_path, 0o755)
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def _generate_proxy_script(
|
|
221
|
+
self,
|
|
222
|
+
event: str,
|
|
223
|
+
hook_id: str,
|
|
224
|
+
matchers: Optional[list[str]] = None,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""
|
|
227
|
+
Generate a proxy script for a git hook.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
event: The git hook event (e.g., "pre-commit")
|
|
231
|
+
hook_id: Unique identifier for this hook
|
|
232
|
+
matchers: Optional list of glob patterns for file filtering
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The proxy script content
|
|
236
|
+
"""
|
|
237
|
+
# Build staged files check if matchers are provided
|
|
238
|
+
staged_check = ""
|
|
239
|
+
if matchers:
|
|
240
|
+
patterns_str = " ".join(f'"{m}"' for m in matchers)
|
|
241
|
+
staged_check = f"""
|
|
242
|
+
# Check if staged files match patterns
|
|
243
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
|
|
244
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
245
|
+
exit 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
MATCHED=false
|
|
249
|
+
PATTERNS="{patterns_str}"
|
|
250
|
+
for file in $STAGED_FILES; do
|
|
251
|
+
for pattern in $PATTERNS; do
|
|
252
|
+
case "$file" in
|
|
253
|
+
$pattern | */$pattern)
|
|
254
|
+
MATCHED=true
|
|
255
|
+
break 2
|
|
256
|
+
;;
|
|
257
|
+
esac
|
|
258
|
+
done
|
|
259
|
+
done
|
|
260
|
+
|
|
261
|
+
if [ "$MATCHED" = "false" ]; then
|
|
262
|
+
exit 0
|
|
263
|
+
fi
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
return f"""#!/bin/sh
|
|
267
|
+
# {self.HOOK_MARKER}: {hook_id}
|
|
268
|
+
# Auto-generated by Monoco Toolkit. Do not edit manually.
|
|
269
|
+
|
|
270
|
+
{staged_check}
|
|
271
|
+
# Execute Monoco hook
|
|
272
|
+
exec uv run python3 -m monoco hook run git {event} "$@"
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def _install_merged(
|
|
276
|
+
self,
|
|
277
|
+
hook_path: Path,
|
|
278
|
+
proxy_content: str,
|
|
279
|
+
event: str,
|
|
280
|
+
hook_id: str,
|
|
281
|
+
) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Install by merging with an existing hook.
|
|
284
|
+
|
|
285
|
+
Creates a backup of the original and appends our proxy.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
hook_path: Path to the existing hook
|
|
289
|
+
proxy_content: Our proxy script content
|
|
290
|
+
event: The git hook event
|
|
291
|
+
hook_id: Unique identifier for this hook
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True if installation succeeded
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
# Backup original hook
|
|
298
|
+
backup_path = hook_path.with_suffix(self.BACKUP_SUFFIX)
|
|
299
|
+
shutil.copy2(hook_path, backup_path)
|
|
300
|
+
|
|
301
|
+
# Read original content
|
|
302
|
+
original_content = hook_path.read_text(encoding="utf-8")
|
|
303
|
+
|
|
304
|
+
# Create merged content - original runs first, then our proxy
|
|
305
|
+
# Extract just the execution part from our proxy (without shebang)
|
|
306
|
+
proxy_lines = proxy_content.split("\n")
|
|
307
|
+
exec_lines = []
|
|
308
|
+
in_staged_check = False
|
|
309
|
+
for line in proxy_lines:
|
|
310
|
+
if line.startswith("#!/"):
|
|
311
|
+
continue
|
|
312
|
+
if "exec monoco hook run" in line:
|
|
313
|
+
# Replace exec with direct call to allow continuation
|
|
314
|
+
line = line.replace("exec ", "")
|
|
315
|
+
exec_lines.append(line)
|
|
316
|
+
|
|
317
|
+
merged_content = f"""#!/bin/sh
|
|
318
|
+
# {self.HOOK_MARKER}: merged
|
|
319
|
+
# Original hook preserved by Monoco Toolkit
|
|
320
|
+
|
|
321
|
+
# Run original hook
|
|
322
|
+
ORIGINAL_EXIT=$?
|
|
323
|
+
|
|
324
|
+
# Run Monoco hook
|
|
325
|
+
{chr(10).join(exec_lines)}
|
|
326
|
+
MONOCO_EXIT=$?
|
|
327
|
+
|
|
328
|
+
# Return non-zero if either failed
|
|
329
|
+
if [ $ORIGINAL_EXIT -ne 0 ]; then
|
|
330
|
+
exit $ORIGINAL_EXIT
|
|
331
|
+
fi
|
|
332
|
+
exit $MONOCO_EXIT
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
# Insert original content before "Run original hook" comment
|
|
336
|
+
marker = "# Run original hook"
|
|
337
|
+
if marker in merged_content:
|
|
338
|
+
parts = merged_content.split(marker)
|
|
339
|
+
merged_content = parts[0] + original_content + "\n" + marker + parts[1]
|
|
340
|
+
|
|
341
|
+
hook_path.write_text(merged_content, encoding="utf-8")
|
|
342
|
+
os.chmod(hook_path, 0o755)
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
except Exception:
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
def uninstall(
|
|
349
|
+
self,
|
|
350
|
+
event: str,
|
|
351
|
+
project_root: Path,
|
|
352
|
+
hook_id: Optional[str] = None,
|
|
353
|
+
) -> bool:
|
|
354
|
+
"""
|
|
355
|
+
Uninstall a hook from `.git/hooks/`.
|
|
356
|
+
|
|
357
|
+
Restores the original hook if it was backed up.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
event: The git hook event (e.g., "pre-commit")
|
|
361
|
+
project_root: The project root directory
|
|
362
|
+
hook_id: Optional hook identifier (for selective uninstall)
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
True if uninstallation succeeded
|
|
366
|
+
"""
|
|
367
|
+
if not self._ensure_git_repo(project_root):
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
hook_path = self._hooks_dir / event
|
|
371
|
+
if not hook_path.exists():
|
|
372
|
+
return True # Already uninstalled
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
content = hook_path.read_text(encoding="utf-8")
|
|
376
|
+
|
|
377
|
+
# Check if it's our marker
|
|
378
|
+
if self.HOOK_MARKER not in content:
|
|
379
|
+
# Not our hook, skip
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
# Check for backup
|
|
383
|
+
backup_path = hook_path.with_suffix(self.BACKUP_SUFFIX)
|
|
384
|
+
if backup_path.exists():
|
|
385
|
+
# Restore original
|
|
386
|
+
shutil.move(backup_path, hook_path)
|
|
387
|
+
return True
|
|
388
|
+
else:
|
|
389
|
+
# No backup, just remove
|
|
390
|
+
hook_path.unlink()
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
except Exception:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
def list_installed(self, project_root: Path) -> list[dict]:
|
|
397
|
+
"""
|
|
398
|
+
List all Git hooks installed by Monoco.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
project_root: The project root directory
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List of installed hook information
|
|
405
|
+
"""
|
|
406
|
+
if not self._ensure_git_repo(project_root):
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
installed = []
|
|
410
|
+
|
|
411
|
+
for event in GitEvent:
|
|
412
|
+
hook_path = self._hooks_dir / event.value
|
|
413
|
+
if hook_path.exists():
|
|
414
|
+
try:
|
|
415
|
+
content = hook_path.read_text(encoding="utf-8")
|
|
416
|
+
if self.HOOK_MARKER in content:
|
|
417
|
+
# Extract hook ID from marker
|
|
418
|
+
hook_id = None
|
|
419
|
+
for line in content.split("\n"):
|
|
420
|
+
if self.HOOK_MARKER in line:
|
|
421
|
+
parts = line.split(":")
|
|
422
|
+
if len(parts) >= 2:
|
|
423
|
+
hook_id = parts[1].strip()
|
|
424
|
+
break
|
|
425
|
+
|
|
426
|
+
installed.append({
|
|
427
|
+
"event": event.value,
|
|
428
|
+
"hook_id": hook_id,
|
|
429
|
+
"path": str(hook_path),
|
|
430
|
+
"is_merged": "merged" in content,
|
|
431
|
+
})
|
|
432
|
+
except Exception:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
return installed
|
|
436
|
+
|
|
437
|
+
def sync(
|
|
438
|
+
self,
|
|
439
|
+
hooks: list[ParsedHook],
|
|
440
|
+
project_root: Path,
|
|
441
|
+
) -> dict[str, bool]:
|
|
442
|
+
"""
|
|
443
|
+
Synchronize all git hooks with the repository.
|
|
444
|
+
|
|
445
|
+
Installs new hooks, updates existing ones, and removes
|
|
446
|
+
hooks that are no longer in the list.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
hooks: List of parsed hooks to install
|
|
450
|
+
project_root: The project root directory
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Dictionary mapping hook events to success status
|
|
454
|
+
"""
|
|
455
|
+
results = {}
|
|
456
|
+
|
|
457
|
+
if not self._ensure_git_repo(project_root):
|
|
458
|
+
return results
|
|
459
|
+
|
|
460
|
+
# Get current installed hooks
|
|
461
|
+
current = self.list_installed(project_root)
|
|
462
|
+
current_events = {h["event"] for h in current}
|
|
463
|
+
|
|
464
|
+
# Install/update hooks
|
|
465
|
+
new_events = set()
|
|
466
|
+
for hook in hooks:
|
|
467
|
+
if hook.metadata.type != HookType.GIT:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
event = hook.metadata.event
|
|
471
|
+
new_events.add(event)
|
|
472
|
+
results[event] = self.install(hook, project_root)
|
|
473
|
+
|
|
474
|
+
# Remove hooks that are no longer needed
|
|
475
|
+
for old_event in current_events - new_events:
|
|
476
|
+
results[old_event] = self.uninstall(old_event, project_root)
|
|
477
|
+
|
|
478
|
+
return results
|