gobby 0.2.8__py3-none-any.whl → 0.2.11__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Copilot CLI installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing and uninstalling Gobby hooks for Copilot CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from shutil import copy2
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from gobby.cli.utils import get_install_dir
|
|
17
|
+
|
|
18
|
+
from .shared import install_shared_content
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def install_copilot(project_path: Path) -> dict[str, Any]:
|
|
24
|
+
"""Install Gobby integration for Copilot CLI (hooks, workflows).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_path: Path to the project root
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with installation results including success status and installed items
|
|
31
|
+
"""
|
|
32
|
+
hooks_installed: list[str] = []
|
|
33
|
+
result: dict[str, Any] = {
|
|
34
|
+
"success": False,
|
|
35
|
+
"hooks_installed": hooks_installed,
|
|
36
|
+
"workflows_installed": [],
|
|
37
|
+
"error": None,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
copilot_path = project_path / ".copilot"
|
|
41
|
+
hooks_file = copilot_path / "hooks.json"
|
|
42
|
+
|
|
43
|
+
# Ensure .copilot subdirectories exist
|
|
44
|
+
copilot_path.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
hooks_dir = copilot_path / "hooks"
|
|
46
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Get source files
|
|
49
|
+
install_dir = get_install_dir()
|
|
50
|
+
copilot_install_dir = install_dir / "copilot"
|
|
51
|
+
install_hooks_dir = copilot_install_dir / "hooks"
|
|
52
|
+
|
|
53
|
+
# Hook files to copy
|
|
54
|
+
hook_files = {
|
|
55
|
+
"hook_dispatcher.py": True, # Make executable
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
source_hooks_template = copilot_install_dir / "hooks-template.json"
|
|
59
|
+
|
|
60
|
+
# Verify all source files exist
|
|
61
|
+
missing_files = []
|
|
62
|
+
for filename in hook_files.keys():
|
|
63
|
+
source_file = install_hooks_dir / filename
|
|
64
|
+
if not source_file.exists():
|
|
65
|
+
missing_files.append(str(source_file))
|
|
66
|
+
|
|
67
|
+
if not source_hooks_template.exists():
|
|
68
|
+
missing_files.append(str(source_hooks_template))
|
|
69
|
+
|
|
70
|
+
if missing_files:
|
|
71
|
+
result["error"] = f"Missing source files: {missing_files}"
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
# Copy hook files
|
|
75
|
+
try:
|
|
76
|
+
for filename, make_executable in hook_files.items():
|
|
77
|
+
source_file = install_hooks_dir / filename
|
|
78
|
+
target_file = hooks_dir / filename
|
|
79
|
+
|
|
80
|
+
if target_file.exists():
|
|
81
|
+
target_file.unlink()
|
|
82
|
+
|
|
83
|
+
copy2(source_file, target_file)
|
|
84
|
+
if make_executable:
|
|
85
|
+
target_file.chmod(0o755)
|
|
86
|
+
except OSError as e:
|
|
87
|
+
logger.error(f"Failed to copy hook files: {e}")
|
|
88
|
+
result["error"] = f"Failed to copy hook files: {e}"
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
# Install shared content (workflows) to .gobby/
|
|
92
|
+
try:
|
|
93
|
+
gobby_path = project_path / ".gobby"
|
|
94
|
+
shared = install_shared_content(gobby_path, project_path)
|
|
95
|
+
result["workflows_installed"] = shared.get("workflows", [])
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Failed to install shared content: {e}")
|
|
98
|
+
# Non-fatal - continue with hooks installation
|
|
99
|
+
|
|
100
|
+
# Backup existing hooks.json if it exists
|
|
101
|
+
backup_file = None
|
|
102
|
+
if hooks_file.exists():
|
|
103
|
+
timestamp = int(time.time())
|
|
104
|
+
backup_file = copilot_path / f"hooks.json.{timestamp}.backup"
|
|
105
|
+
try:
|
|
106
|
+
copy2(hooks_file, backup_file)
|
|
107
|
+
except OSError as e:
|
|
108
|
+
logger.error(f"Failed to create backup of hooks.json: {e}")
|
|
109
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
# Load existing hooks or create empty
|
|
113
|
+
existing_hooks: dict[str, Any] = {"hooks": {}}
|
|
114
|
+
if hooks_file.exists():
|
|
115
|
+
try:
|
|
116
|
+
with open(hooks_file) as f:
|
|
117
|
+
existing_hooks = json.load(f)
|
|
118
|
+
except json.JSONDecodeError as e:
|
|
119
|
+
logger.warning(f"Failed to parse hooks.json, starting fresh: {e}")
|
|
120
|
+
except OSError as e:
|
|
121
|
+
logger.error(f"Failed to read hooks.json: {e}")
|
|
122
|
+
result["error"] = f"Failed to read hooks.json: {e}"
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
# Ensure structure
|
|
126
|
+
if "hooks" not in existing_hooks:
|
|
127
|
+
existing_hooks["hooks"] = {}
|
|
128
|
+
|
|
129
|
+
# Load Gobby hooks from template
|
|
130
|
+
try:
|
|
131
|
+
with open(source_hooks_template) as f:
|
|
132
|
+
gobby_hooks_str = f.read()
|
|
133
|
+
except OSError as e:
|
|
134
|
+
logger.error(f"Failed to read hooks template: {e}")
|
|
135
|
+
result["error"] = f"Failed to read hooks template: {e}"
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
# Replace $PROJECT_PATH with absolute project path
|
|
139
|
+
abs_project_path = str(project_path.resolve())
|
|
140
|
+
gobby_hooks_str = gobby_hooks_str.replace("$PROJECT_PATH", abs_project_path)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
gobby_hooks = json.loads(gobby_hooks_str)
|
|
144
|
+
except json.JSONDecodeError as e:
|
|
145
|
+
logger.error(f"Failed to parse hooks template: {e}")
|
|
146
|
+
result["error"] = f"Failed to parse hooks template: {e}"
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
# Merge Gobby hooks
|
|
150
|
+
new_hooks = gobby_hooks.get("hooks", {})
|
|
151
|
+
for hook_type, hook_config in new_hooks.items():
|
|
152
|
+
existing_hooks["hooks"][hook_type] = hook_config
|
|
153
|
+
hooks_installed.append(hook_type)
|
|
154
|
+
|
|
155
|
+
# Write merged hooks back using atomic write
|
|
156
|
+
try:
|
|
157
|
+
fd, temp_path = tempfile.mkstemp(dir=str(copilot_path), suffix=".tmp", prefix="hooks_")
|
|
158
|
+
try:
|
|
159
|
+
with os.fdopen(fd, "w") as f:
|
|
160
|
+
json.dump(existing_hooks, f, indent=2)
|
|
161
|
+
f.flush()
|
|
162
|
+
os.fsync(f.fileno())
|
|
163
|
+
# Atomic replace
|
|
164
|
+
os.replace(temp_path, hooks_file)
|
|
165
|
+
except Exception:
|
|
166
|
+
if os.path.exists(temp_path):
|
|
167
|
+
os.unlink(temp_path)
|
|
168
|
+
raise
|
|
169
|
+
except OSError as e:
|
|
170
|
+
logger.error(f"Failed to write hooks.json: {e}")
|
|
171
|
+
if backup_file and backup_file.exists():
|
|
172
|
+
try:
|
|
173
|
+
copy2(backup_file, hooks_file)
|
|
174
|
+
logger.info("Restored hooks.json from backup after write failure")
|
|
175
|
+
except OSError as restore_error:
|
|
176
|
+
logger.error(f"Failed to restore from backup: {restore_error}")
|
|
177
|
+
result["error"] = f"Failed to write hooks.json: {e}"
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
result["success"] = True
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def uninstall_copilot(project_path: Path) -> dict[str, Any]:
|
|
185
|
+
"""Uninstall Gobby integration from Copilot CLI.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
project_path: Path to the project root
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict with uninstallation results
|
|
192
|
+
"""
|
|
193
|
+
result: dict[str, Any] = {
|
|
194
|
+
"success": False,
|
|
195
|
+
"hooks_removed": [],
|
|
196
|
+
"files_removed": [],
|
|
197
|
+
"error": None,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
copilot_path = project_path / ".copilot"
|
|
201
|
+
hooks_file = copilot_path / "hooks.json"
|
|
202
|
+
hooks_dir = copilot_path / "hooks"
|
|
203
|
+
|
|
204
|
+
# Remove hook dispatcher
|
|
205
|
+
dispatcher = hooks_dir / "hook_dispatcher.py"
|
|
206
|
+
if dispatcher.exists():
|
|
207
|
+
try:
|
|
208
|
+
dispatcher.unlink()
|
|
209
|
+
result["files_removed"].append(str(dispatcher))
|
|
210
|
+
except OSError as e:
|
|
211
|
+
logger.warning(f"Failed to remove {dispatcher}: {e}")
|
|
212
|
+
|
|
213
|
+
# Remove hooks from hooks.json
|
|
214
|
+
if hooks_file.exists():
|
|
215
|
+
try:
|
|
216
|
+
with open(hooks_file) as f:
|
|
217
|
+
hooks_config = json.load(f)
|
|
218
|
+
|
|
219
|
+
# Remove all Gobby hooks (those that reference hook_dispatcher.py)
|
|
220
|
+
if "hooks" in hooks_config:
|
|
221
|
+
hooks_to_remove = []
|
|
222
|
+
for hook_type, hook_list in hooks_config["hooks"].items():
|
|
223
|
+
if isinstance(hook_list, list):
|
|
224
|
+
for hook in hook_list:
|
|
225
|
+
cmd = hook.get("command", "")
|
|
226
|
+
if "hook_dispatcher.py" in cmd:
|
|
227
|
+
hooks_to_remove.append(hook_type)
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
for hook_type in hooks_to_remove:
|
|
231
|
+
del hooks_config["hooks"][hook_type]
|
|
232
|
+
result["hooks_removed"].append(hook_type)
|
|
233
|
+
|
|
234
|
+
# Write back
|
|
235
|
+
with open(hooks_file, "w") as f:
|
|
236
|
+
json.dump(hooks_config, f, indent=2)
|
|
237
|
+
|
|
238
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
239
|
+
logger.warning(f"Failed to update hooks.json: {e}")
|
|
240
|
+
|
|
241
|
+
result["success"] = True
|
|
242
|
+
return result
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing and uninstalling Gobby hooks for Cursor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from shutil import copy2
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from gobby.cli.utils import get_install_dir
|
|
17
|
+
|
|
18
|
+
from .shared import install_shared_content
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def install_cursor(project_path: Path) -> dict[str, Any]:
|
|
24
|
+
"""Install Gobby integration for Cursor (hooks, workflows).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_path: Path to the project root
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with installation results including success status and installed items
|
|
31
|
+
"""
|
|
32
|
+
hooks_installed: list[str] = []
|
|
33
|
+
result: dict[str, Any] = {
|
|
34
|
+
"success": False,
|
|
35
|
+
"hooks_installed": hooks_installed,
|
|
36
|
+
"workflows_installed": [],
|
|
37
|
+
"error": None,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
cursor_path = project_path / ".cursor"
|
|
41
|
+
hooks_file = cursor_path / "hooks.json"
|
|
42
|
+
|
|
43
|
+
# Ensure .cursor subdirectories exist
|
|
44
|
+
cursor_path.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
hooks_dir = cursor_path / "hooks"
|
|
46
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Get source files
|
|
49
|
+
install_dir = get_install_dir()
|
|
50
|
+
cursor_install_dir = install_dir / "cursor"
|
|
51
|
+
install_hooks_dir = cursor_install_dir / "hooks"
|
|
52
|
+
|
|
53
|
+
# Hook files to copy
|
|
54
|
+
hook_files = {
|
|
55
|
+
"hook_dispatcher.py": True, # Make executable
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
source_hooks_template = cursor_install_dir / "hooks-template.json"
|
|
59
|
+
|
|
60
|
+
# Verify all source files exist
|
|
61
|
+
missing_files = []
|
|
62
|
+
for filename in hook_files.keys():
|
|
63
|
+
source_file = install_hooks_dir / filename
|
|
64
|
+
if not source_file.exists():
|
|
65
|
+
missing_files.append(str(source_file))
|
|
66
|
+
|
|
67
|
+
if not source_hooks_template.exists():
|
|
68
|
+
missing_files.append(str(source_hooks_template))
|
|
69
|
+
|
|
70
|
+
if missing_files:
|
|
71
|
+
result["error"] = f"Missing source files: {missing_files}"
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
# Copy hook files
|
|
75
|
+
try:
|
|
76
|
+
for filename, make_executable in hook_files.items():
|
|
77
|
+
source_file = install_hooks_dir / filename
|
|
78
|
+
target_file = hooks_dir / filename
|
|
79
|
+
|
|
80
|
+
if target_file.exists():
|
|
81
|
+
target_file.unlink()
|
|
82
|
+
|
|
83
|
+
copy2(source_file, target_file)
|
|
84
|
+
if make_executable:
|
|
85
|
+
target_file.chmod(0o755)
|
|
86
|
+
except OSError as e:
|
|
87
|
+
logger.error(f"Failed to copy hook files: {e}")
|
|
88
|
+
result["error"] = f"Failed to copy hook files: {e}"
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
# Install shared content (workflows) to .gobby/
|
|
92
|
+
try:
|
|
93
|
+
gobby_path = project_path / ".gobby"
|
|
94
|
+
shared = install_shared_content(gobby_path, project_path)
|
|
95
|
+
result["workflows_installed"] = shared.get("workflows", [])
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Failed to install shared content: {e}")
|
|
98
|
+
# Non-fatal - continue with hooks installation
|
|
99
|
+
|
|
100
|
+
# Backup existing hooks.json if it exists
|
|
101
|
+
backup_file = None
|
|
102
|
+
if hooks_file.exists():
|
|
103
|
+
timestamp = int(time.time())
|
|
104
|
+
backup_file = cursor_path / f"hooks.json.{timestamp}.backup"
|
|
105
|
+
try:
|
|
106
|
+
copy2(hooks_file, backup_file)
|
|
107
|
+
except OSError as e:
|
|
108
|
+
logger.error(f"Failed to create backup of hooks.json: {e}")
|
|
109
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
# Load existing hooks or create empty
|
|
113
|
+
existing_hooks: dict[str, Any] = {"version": 1, "hooks": {}}
|
|
114
|
+
if hooks_file.exists():
|
|
115
|
+
try:
|
|
116
|
+
with open(hooks_file) as f:
|
|
117
|
+
existing_hooks = json.load(f)
|
|
118
|
+
except json.JSONDecodeError as e:
|
|
119
|
+
logger.warning(f"Failed to parse hooks.json, starting fresh: {e}")
|
|
120
|
+
except OSError as e:
|
|
121
|
+
logger.error(f"Failed to read hooks.json: {e}")
|
|
122
|
+
result["error"] = f"Failed to read hooks.json: {e}"
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
# Ensure structure
|
|
126
|
+
if "version" not in existing_hooks:
|
|
127
|
+
existing_hooks["version"] = 1
|
|
128
|
+
if "hooks" not in existing_hooks:
|
|
129
|
+
existing_hooks["hooks"] = {}
|
|
130
|
+
|
|
131
|
+
# Load Gobby hooks from template
|
|
132
|
+
try:
|
|
133
|
+
with open(source_hooks_template) as f:
|
|
134
|
+
gobby_hooks_str = f.read()
|
|
135
|
+
except OSError as e:
|
|
136
|
+
logger.error(f"Failed to read hooks template: {e}")
|
|
137
|
+
result["error"] = f"Failed to read hooks template: {e}"
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
# Replace $PROJECT_PATH with absolute project path
|
|
141
|
+
abs_project_path = str(project_path.resolve())
|
|
142
|
+
gobby_hooks_str = gobby_hooks_str.replace("$PROJECT_PATH", abs_project_path)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
gobby_hooks = json.loads(gobby_hooks_str)
|
|
146
|
+
except json.JSONDecodeError as e:
|
|
147
|
+
logger.error(f"Failed to parse hooks template: {e}")
|
|
148
|
+
result["error"] = f"Failed to parse hooks template: {e}"
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
# Merge Gobby hooks
|
|
152
|
+
new_hooks = gobby_hooks.get("hooks", {})
|
|
153
|
+
for hook_type, hook_config in new_hooks.items():
|
|
154
|
+
existing_hooks["hooks"][hook_type] = hook_config
|
|
155
|
+
hooks_installed.append(hook_type)
|
|
156
|
+
|
|
157
|
+
# Write merged hooks back using atomic write
|
|
158
|
+
try:
|
|
159
|
+
fd, temp_path = tempfile.mkstemp(dir=str(cursor_path), suffix=".tmp", prefix="hooks_")
|
|
160
|
+
try:
|
|
161
|
+
with os.fdopen(fd, "w") as f:
|
|
162
|
+
json.dump(existing_hooks, f, indent=2)
|
|
163
|
+
f.flush()
|
|
164
|
+
os.fsync(f.fileno())
|
|
165
|
+
# Atomic replace
|
|
166
|
+
os.replace(temp_path, hooks_file)
|
|
167
|
+
except Exception:
|
|
168
|
+
if os.path.exists(temp_path):
|
|
169
|
+
os.unlink(temp_path)
|
|
170
|
+
raise
|
|
171
|
+
except OSError as e:
|
|
172
|
+
logger.error(f"Failed to write hooks.json: {e}")
|
|
173
|
+
if backup_file and backup_file.exists():
|
|
174
|
+
try:
|
|
175
|
+
copy2(backup_file, hooks_file)
|
|
176
|
+
logger.info("Restored hooks.json from backup after write failure")
|
|
177
|
+
except OSError as restore_error:
|
|
178
|
+
logger.error(f"Failed to restore from backup: {restore_error}")
|
|
179
|
+
result["error"] = f"Failed to write hooks.json: {e}"
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
result["success"] = True
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def uninstall_cursor(project_path: Path) -> dict[str, Any]:
|
|
187
|
+
"""Uninstall Gobby integration from Cursor.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
project_path: Path to the project root
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dict with uninstallation results
|
|
194
|
+
"""
|
|
195
|
+
result: dict[str, Any] = {
|
|
196
|
+
"success": False,
|
|
197
|
+
"hooks_removed": [],
|
|
198
|
+
"files_removed": [],
|
|
199
|
+
"error": None,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
cursor_path = project_path / ".cursor"
|
|
203
|
+
hooks_file = cursor_path / "hooks.json"
|
|
204
|
+
hooks_dir = cursor_path / "hooks"
|
|
205
|
+
|
|
206
|
+
# Remove hook dispatcher
|
|
207
|
+
dispatcher = hooks_dir / "hook_dispatcher.py"
|
|
208
|
+
if dispatcher.exists():
|
|
209
|
+
try:
|
|
210
|
+
dispatcher.unlink()
|
|
211
|
+
result["files_removed"].append(str(dispatcher))
|
|
212
|
+
except OSError as e:
|
|
213
|
+
logger.warning(f"Failed to remove {dispatcher}: {e}")
|
|
214
|
+
|
|
215
|
+
# Remove hooks from hooks.json
|
|
216
|
+
if hooks_file.exists():
|
|
217
|
+
try:
|
|
218
|
+
with open(hooks_file) as f:
|
|
219
|
+
hooks_config = json.load(f)
|
|
220
|
+
|
|
221
|
+
# Remove all Gobby hooks (those that reference hook_dispatcher.py)
|
|
222
|
+
if "hooks" in hooks_config:
|
|
223
|
+
hooks_to_remove = []
|
|
224
|
+
for hook_type, hook_list in hooks_config["hooks"].items():
|
|
225
|
+
if isinstance(hook_list, list):
|
|
226
|
+
for hook in hook_list:
|
|
227
|
+
cmd = hook.get("command", "")
|
|
228
|
+
if "hook_dispatcher.py" in cmd:
|
|
229
|
+
hooks_to_remove.append(hook_type)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
for hook_type in hooks_to_remove:
|
|
233
|
+
del hooks_config["hooks"][hook_type]
|
|
234
|
+
result["hooks_removed"].append(hook_type)
|
|
235
|
+
|
|
236
|
+
# Write back
|
|
237
|
+
with open(hooks_file, "w") as f:
|
|
238
|
+
json.dump(hooks_config, f, indent=2)
|
|
239
|
+
|
|
240
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
241
|
+
logger.warning(f"Failed to update hooks.json: {e}")
|
|
242
|
+
|
|
243
|
+
result["success"] = True
|
|
244
|
+
return result
|
gobby/cli/installers/shared.py
CHANGED
|
@@ -43,6 +43,9 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
43
43
|
target_workflows = project_path / ".gobby" / "workflows"
|
|
44
44
|
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
45
45
|
for item in shared_workflows.iterdir():
|
|
46
|
+
# Skip deprecated workflows - they are kept for reference only
|
|
47
|
+
if item.name == "deprecated":
|
|
48
|
+
continue
|
|
46
49
|
if item.is_file():
|
|
47
50
|
copy2(item, target_workflows / item.name)
|
|
48
51
|
installed["workflows"].append(item.name)
|