gobby 0.2.9__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 +2 -2
- 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 +5 -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/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/skills.py +23 -2
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/_base.py +6 -1
- gobby/hooks/event_handlers/_session.py +44 -130
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +25 -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 +217 -1
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +56 -9
- gobby/mcp_proxy/server.py +6 -2
- 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/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/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 +9 -2
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
- gobby/mcp_proxy/tools/workflows/_query.py +45 -26
- gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/context.py +5 -5
- gobby/runner.py +108 -6
- gobby/servers/http.py +7 -1
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +44 -0
- gobby/servers/routes/mcp/endpoints/execution.py +18 -25
- 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 +87 -1
- 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/storage/memories.py +4 -4
- gobby/storage/migrations.py +95 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +1 -1
- 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/workflows/actions.py +75 -0
- gobby/workflows/context_actions.py +246 -5
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +20 -1
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +57 -26
- 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.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/cli/install.py
CHANGED
|
@@ -3,6 +3,7 @@ Installation commands for hooks.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
6
7
|
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
@@ -15,12 +16,18 @@ from .installers import (
|
|
|
15
16
|
install_antigravity,
|
|
16
17
|
install_claude,
|
|
17
18
|
install_codex_notify,
|
|
19
|
+
install_copilot,
|
|
20
|
+
install_cursor,
|
|
18
21
|
install_default_mcp_servers,
|
|
19
22
|
install_gemini,
|
|
20
23
|
install_git_hooks,
|
|
24
|
+
install_windsurf,
|
|
21
25
|
uninstall_claude,
|
|
22
26
|
uninstall_codex_notify,
|
|
27
|
+
uninstall_copilot,
|
|
28
|
+
uninstall_cursor,
|
|
23
29
|
uninstall_gemini,
|
|
30
|
+
uninstall_windsurf,
|
|
24
31
|
)
|
|
25
32
|
from .utils import get_install_dir
|
|
26
33
|
|
|
@@ -75,6 +82,37 @@ def _is_codex_cli_installed() -> bool:
|
|
|
75
82
|
return shutil.which("codex") is not None
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
def _is_cursor_installed() -> bool:
|
|
86
|
+
"""Check if Cursor is installed."""
|
|
87
|
+
# Cursor is an IDE, check for common install locations
|
|
88
|
+
if sys.platform == "darwin":
|
|
89
|
+
return Path("/Applications/Cursor.app").exists()
|
|
90
|
+
elif sys.platform == "win32":
|
|
91
|
+
return Path(os.environ.get("LOCALAPPDATA", ""), "Programs", "cursor").exists()
|
|
92
|
+
else:
|
|
93
|
+
# Linux - check common locations
|
|
94
|
+
return (Path.home() / ".local" / "share" / "cursor").exists() or shutil.which(
|
|
95
|
+
"cursor"
|
|
96
|
+
) is not None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_windsurf_installed() -> bool:
|
|
100
|
+
"""Check if Windsurf (Codeium) is installed."""
|
|
101
|
+
# Windsurf is an IDE
|
|
102
|
+
if sys.platform == "darwin":
|
|
103
|
+
return Path("/Applications/Windsurf.app").exists()
|
|
104
|
+
elif sys.platform == "win32":
|
|
105
|
+
return Path(os.environ.get("LOCALAPPDATA", ""), "Programs", "windsurf").exists()
|
|
106
|
+
else:
|
|
107
|
+
return shutil.which("windsurf") is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_copilot_cli_installed() -> bool:
|
|
111
|
+
"""Check if GitHub Copilot CLI is installed."""
|
|
112
|
+
# Check for gh copilot extension or standalone CLI
|
|
113
|
+
return shutil.which("gh") is not None or shutil.which("github-copilot-cli") is not None
|
|
114
|
+
|
|
115
|
+
|
|
78
116
|
@click.command("install")
|
|
79
117
|
@click.option(
|
|
80
118
|
"--claude",
|
|
@@ -94,6 +132,24 @@ def _is_codex_cli_installed() -> bool:
|
|
|
94
132
|
is_flag=True,
|
|
95
133
|
help="Configure Codex notify integration (interactive Codex)",
|
|
96
134
|
)
|
|
135
|
+
@click.option(
|
|
136
|
+
"--cursor",
|
|
137
|
+
"cursor_flag",
|
|
138
|
+
is_flag=True,
|
|
139
|
+
help="Install Cursor hooks",
|
|
140
|
+
)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--windsurf",
|
|
143
|
+
"windsurf_flag",
|
|
144
|
+
is_flag=True,
|
|
145
|
+
help="Install Windsurf (Cascade) hooks",
|
|
146
|
+
)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--copilot",
|
|
149
|
+
"copilot_flag",
|
|
150
|
+
is_flag=True,
|
|
151
|
+
help="Install GitHub Copilot CLI hooks",
|
|
152
|
+
)
|
|
97
153
|
@click.option(
|
|
98
154
|
"--hooks",
|
|
99
155
|
"--git-hooks",
|
|
@@ -118,6 +174,9 @@ def install(
|
|
|
118
174
|
claude_flag: bool,
|
|
119
175
|
gemini_flag: bool,
|
|
120
176
|
codex_flag: bool,
|
|
177
|
+
cursor_flag: bool,
|
|
178
|
+
windsurf_flag: bool,
|
|
179
|
+
copilot_flag: bool,
|
|
121
180
|
hooks_flag: bool,
|
|
122
181
|
all_flag: bool,
|
|
123
182
|
antigravity_flag: bool,
|
|
@@ -143,6 +202,9 @@ def install(
|
|
|
143
202
|
not claude_flag
|
|
144
203
|
and not gemini_flag
|
|
145
204
|
and not codex_flag
|
|
205
|
+
and not cursor_flag
|
|
206
|
+
and not windsurf_flag
|
|
207
|
+
and not copilot_flag
|
|
146
208
|
and not hooks_flag
|
|
147
209
|
and not all_flag
|
|
148
210
|
and not antigravity_flag
|
|
@@ -162,6 +224,12 @@ def install(
|
|
|
162
224
|
clis_to_install.append("gemini")
|
|
163
225
|
if codex_detected:
|
|
164
226
|
clis_to_install.append("codex")
|
|
227
|
+
if _is_cursor_installed():
|
|
228
|
+
clis_to_install.append("cursor")
|
|
229
|
+
if _is_windsurf_installed():
|
|
230
|
+
clis_to_install.append("windsurf")
|
|
231
|
+
if _is_copilot_cli_installed():
|
|
232
|
+
clis_to_install.append("copilot")
|
|
165
233
|
|
|
166
234
|
# Check for git
|
|
167
235
|
if (project_path / ".git").exists():
|
|
@@ -174,8 +242,11 @@ def install(
|
|
|
174
242
|
click.echo(" - Claude Code: npm install -g @anthropic-ai/claude-code")
|
|
175
243
|
click.echo(" - Gemini CLI: npm install -g @google/gemini-cli")
|
|
176
244
|
click.echo(" - Codex CLI: npm install -g @openai/codex")
|
|
245
|
+
click.echo(" - Cursor: https://cursor.com")
|
|
246
|
+
click.echo(" - Windsurf: https://codeium.com/windsurf")
|
|
247
|
+
click.echo(" - Copilot CLI: gh extension install github/gh-copilot")
|
|
177
248
|
click.echo(
|
|
178
|
-
"\nYou can still install manually with --claude, --gemini, or --
|
|
249
|
+
"\nYou can still install manually with --claude, --gemini, --codex, --cursor, --windsurf, or --copilot flags."
|
|
179
250
|
)
|
|
180
251
|
sys.exit(1)
|
|
181
252
|
else:
|
|
@@ -185,6 +256,12 @@ def install(
|
|
|
185
256
|
clis_to_install.append("gemini")
|
|
186
257
|
if codex_flag:
|
|
187
258
|
clis_to_install.append("codex")
|
|
259
|
+
if cursor_flag:
|
|
260
|
+
clis_to_install.append("cursor")
|
|
261
|
+
if windsurf_flag:
|
|
262
|
+
clis_to_install.append("windsurf")
|
|
263
|
+
if copilot_flag:
|
|
264
|
+
clis_to_install.append("copilot")
|
|
188
265
|
if antigravity_flag:
|
|
189
266
|
clis_to_install.append("antigravity")
|
|
190
267
|
|
|
@@ -346,6 +423,66 @@ def install(
|
|
|
346
423
|
click.echo(f"Failed: {result['error']}", err=True)
|
|
347
424
|
click.echo("")
|
|
348
425
|
|
|
426
|
+
# Install Cursor hooks
|
|
427
|
+
if "cursor" in clis_to_install:
|
|
428
|
+
click.echo("-" * 40)
|
|
429
|
+
click.echo("Cursor")
|
|
430
|
+
click.echo("-" * 40)
|
|
431
|
+
|
|
432
|
+
result = install_cursor(project_path)
|
|
433
|
+
results["cursor"] = result
|
|
434
|
+
|
|
435
|
+
if result["success"]:
|
|
436
|
+
click.echo(f"Installed {len(result['hooks_installed'])} hooks")
|
|
437
|
+
for hook in result["hooks_installed"]:
|
|
438
|
+
click.echo(f" - {hook}")
|
|
439
|
+
if result.get("workflows_installed"):
|
|
440
|
+
click.echo(f"Installed {len(result['workflows_installed'])} workflows")
|
|
441
|
+
click.echo(f"Configuration: {project_path / '.cursor' / 'hooks.json'}")
|
|
442
|
+
else:
|
|
443
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
444
|
+
click.echo("")
|
|
445
|
+
|
|
446
|
+
# Install Windsurf hooks
|
|
447
|
+
if "windsurf" in clis_to_install:
|
|
448
|
+
click.echo("-" * 40)
|
|
449
|
+
click.echo("Windsurf (Cascade)")
|
|
450
|
+
click.echo("-" * 40)
|
|
451
|
+
|
|
452
|
+
result = install_windsurf(project_path)
|
|
453
|
+
results["windsurf"] = result
|
|
454
|
+
|
|
455
|
+
if result["success"]:
|
|
456
|
+
click.echo(f"Installed {len(result['hooks_installed'])} hooks")
|
|
457
|
+
for hook in result["hooks_installed"]:
|
|
458
|
+
click.echo(f" - {hook}")
|
|
459
|
+
if result.get("workflows_installed"):
|
|
460
|
+
click.echo(f"Installed {len(result['workflows_installed'])} workflows")
|
|
461
|
+
click.echo(f"Configuration: {project_path / '.windsurf' / 'hooks.json'}")
|
|
462
|
+
else:
|
|
463
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
464
|
+
click.echo("")
|
|
465
|
+
|
|
466
|
+
# Install Copilot CLI hooks
|
|
467
|
+
if "copilot" in clis_to_install:
|
|
468
|
+
click.echo("-" * 40)
|
|
469
|
+
click.echo("GitHub Copilot CLI")
|
|
470
|
+
click.echo("-" * 40)
|
|
471
|
+
|
|
472
|
+
result = install_copilot(project_path)
|
|
473
|
+
results["copilot"] = result
|
|
474
|
+
|
|
475
|
+
if result["success"]:
|
|
476
|
+
click.echo(f"Installed {len(result['hooks_installed'])} hooks")
|
|
477
|
+
for hook in result["hooks_installed"]:
|
|
478
|
+
click.echo(f" - {hook}")
|
|
479
|
+
if result.get("workflows_installed"):
|
|
480
|
+
click.echo(f"Installed {len(result['workflows_installed'])} workflows")
|
|
481
|
+
click.echo(f"Configuration: {project_path / '.copilot' / 'hooks.json'}")
|
|
482
|
+
else:
|
|
483
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
484
|
+
click.echo("")
|
|
485
|
+
|
|
349
486
|
# Install Git Hooks
|
|
350
487
|
if hooks_flag:
|
|
351
488
|
click.echo("-" * 40)
|
|
@@ -467,6 +604,24 @@ def install(
|
|
|
467
604
|
is_flag=True,
|
|
468
605
|
help="Uninstall Codex notify integration",
|
|
469
606
|
)
|
|
607
|
+
@click.option(
|
|
608
|
+
"--cursor",
|
|
609
|
+
"cursor_flag",
|
|
610
|
+
is_flag=True,
|
|
611
|
+
help="Uninstall Cursor hooks",
|
|
612
|
+
)
|
|
613
|
+
@click.option(
|
|
614
|
+
"--windsurf",
|
|
615
|
+
"windsurf_flag",
|
|
616
|
+
is_flag=True,
|
|
617
|
+
help="Uninstall Windsurf hooks",
|
|
618
|
+
)
|
|
619
|
+
@click.option(
|
|
620
|
+
"--copilot",
|
|
621
|
+
"copilot_flag",
|
|
622
|
+
is_flag=True,
|
|
623
|
+
help="Uninstall Copilot CLI hooks",
|
|
624
|
+
)
|
|
470
625
|
@click.option(
|
|
471
626
|
"--all",
|
|
472
627
|
"all_flag",
|
|
@@ -475,11 +630,19 @@ def install(
|
|
|
475
630
|
help="Uninstall hooks from all CLIs (default behavior when no flags specified)",
|
|
476
631
|
)
|
|
477
632
|
@click.confirmation_option(prompt="Are you sure you want to uninstall Gobby hooks?")
|
|
478
|
-
def uninstall(
|
|
633
|
+
def uninstall(
|
|
634
|
+
claude_flag: bool,
|
|
635
|
+
gemini_flag: bool,
|
|
636
|
+
codex_flag: bool,
|
|
637
|
+
cursor_flag: bool,
|
|
638
|
+
windsurf_flag: bool,
|
|
639
|
+
copilot_flag: bool,
|
|
640
|
+
all_flag: bool,
|
|
641
|
+
) -> None:
|
|
479
642
|
"""Uninstall Gobby hooks from AI coding CLIs.
|
|
480
643
|
|
|
481
644
|
By default (no flags), uninstalls from all CLIs that have hooks installed.
|
|
482
|
-
Use --claude, --gemini, or --
|
|
645
|
+
Use --claude, --gemini, --codex, --cursor, --windsurf, or --copilot to uninstall only from specific CLIs.
|
|
483
646
|
|
|
484
647
|
Uninstalls from project-level directories in current working directory.
|
|
485
648
|
"""
|
|
@@ -487,7 +650,15 @@ def uninstall(claude_flag: bool, gemini_flag: bool, codex_flag: bool, all_flag:
|
|
|
487
650
|
|
|
488
651
|
# Determine which CLIs to uninstall
|
|
489
652
|
# If no flags specified, act like --all
|
|
490
|
-
if
|
|
653
|
+
if (
|
|
654
|
+
not claude_flag
|
|
655
|
+
and not gemini_flag
|
|
656
|
+
and not codex_flag
|
|
657
|
+
and not cursor_flag
|
|
658
|
+
and not windsurf_flag
|
|
659
|
+
and not copilot_flag
|
|
660
|
+
and not all_flag
|
|
661
|
+
):
|
|
491
662
|
all_flag = True
|
|
492
663
|
|
|
493
664
|
# Build list of CLIs to uninstall
|
|
@@ -498,6 +669,9 @@ def uninstall(claude_flag: bool, gemini_flag: bool, codex_flag: bool, all_flag:
|
|
|
498
669
|
claude_settings = project_path / ".claude" / "settings.json"
|
|
499
670
|
gemini_settings = project_path / ".gemini" / "settings.json"
|
|
500
671
|
codex_notify = Path.home() / ".gobby" / "hooks" / "codex" / "hook_dispatcher.py"
|
|
672
|
+
cursor_hooks = project_path / ".cursor" / "hooks.json"
|
|
673
|
+
windsurf_hooks = project_path / ".windsurf" / "hooks.json"
|
|
674
|
+
copilot_hooks = project_path / ".copilot" / "hooks.json"
|
|
501
675
|
|
|
502
676
|
if claude_settings.exists():
|
|
503
677
|
clis_to_uninstall.append("claude")
|
|
@@ -505,11 +679,20 @@ def uninstall(claude_flag: bool, gemini_flag: bool, codex_flag: bool, all_flag:
|
|
|
505
679
|
clis_to_uninstall.append("gemini")
|
|
506
680
|
if codex_notify.exists():
|
|
507
681
|
clis_to_uninstall.append("codex")
|
|
682
|
+
if cursor_hooks.exists():
|
|
683
|
+
clis_to_uninstall.append("cursor")
|
|
684
|
+
if windsurf_hooks.exists():
|
|
685
|
+
clis_to_uninstall.append("windsurf")
|
|
686
|
+
if copilot_hooks.exists():
|
|
687
|
+
clis_to_uninstall.append("copilot")
|
|
508
688
|
|
|
509
689
|
if not clis_to_uninstall:
|
|
510
690
|
click.echo("No Gobby hooks found to uninstall.")
|
|
511
691
|
click.echo(f"\nChecked: {project_path / '.claude'}")
|
|
512
692
|
click.echo(f" {project_path / '.gemini'}")
|
|
693
|
+
click.echo(f" {project_path / '.cursor'}")
|
|
694
|
+
click.echo(f" {project_path / '.windsurf'}")
|
|
695
|
+
click.echo(f" {project_path / '.copilot'}")
|
|
513
696
|
click.echo(f" {codex_notify}")
|
|
514
697
|
sys.exit(0)
|
|
515
698
|
else:
|
|
@@ -519,6 +702,12 @@ def uninstall(claude_flag: bool, gemini_flag: bool, codex_flag: bool, all_flag:
|
|
|
519
702
|
clis_to_uninstall.append("gemini")
|
|
520
703
|
if codex_flag:
|
|
521
704
|
clis_to_uninstall.append("codex")
|
|
705
|
+
if cursor_flag:
|
|
706
|
+
clis_to_uninstall.append("cursor")
|
|
707
|
+
if windsurf_flag:
|
|
708
|
+
clis_to_uninstall.append("windsurf")
|
|
709
|
+
if copilot_flag:
|
|
710
|
+
clis_to_uninstall.append("copilot")
|
|
522
711
|
|
|
523
712
|
click.echo("=" * 60)
|
|
524
713
|
click.echo(" Gobby Hooks Uninstallation")
|
|
@@ -597,6 +786,72 @@ def uninstall(claude_flag: bool, gemini_flag: bool, codex_flag: bool, all_flag:
|
|
|
597
786
|
click.echo(f"Failed: {result['error']}", err=True)
|
|
598
787
|
click.echo("")
|
|
599
788
|
|
|
789
|
+
# Uninstall Cursor hooks
|
|
790
|
+
if "cursor" in clis_to_uninstall:
|
|
791
|
+
click.echo("-" * 40)
|
|
792
|
+
click.echo("Cursor")
|
|
793
|
+
click.echo("-" * 40)
|
|
794
|
+
|
|
795
|
+
result = uninstall_cursor(project_path)
|
|
796
|
+
results["cursor"] = result
|
|
797
|
+
|
|
798
|
+
if result["success"]:
|
|
799
|
+
if result["hooks_removed"]:
|
|
800
|
+
click.echo(f"Removed {len(result['hooks_removed'])} hooks from hooks.json")
|
|
801
|
+
for hook in result["hooks_removed"]:
|
|
802
|
+
click.echo(f" - {hook}")
|
|
803
|
+
if result["files_removed"]:
|
|
804
|
+
click.echo(f"Removed {len(result['files_removed'])} files")
|
|
805
|
+
if not result["hooks_removed"] and not result["files_removed"]:
|
|
806
|
+
click.echo(" (no hooks found to remove)")
|
|
807
|
+
else:
|
|
808
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
809
|
+
click.echo("")
|
|
810
|
+
|
|
811
|
+
# Uninstall Windsurf hooks
|
|
812
|
+
if "windsurf" in clis_to_uninstall:
|
|
813
|
+
click.echo("-" * 40)
|
|
814
|
+
click.echo("Windsurf")
|
|
815
|
+
click.echo("-" * 40)
|
|
816
|
+
|
|
817
|
+
result = uninstall_windsurf(project_path)
|
|
818
|
+
results["windsurf"] = result
|
|
819
|
+
|
|
820
|
+
if result["success"]:
|
|
821
|
+
if result["hooks_removed"]:
|
|
822
|
+
click.echo(f"Removed {len(result['hooks_removed'])} hooks from hooks.json")
|
|
823
|
+
for hook in result["hooks_removed"]:
|
|
824
|
+
click.echo(f" - {hook}")
|
|
825
|
+
if result["files_removed"]:
|
|
826
|
+
click.echo(f"Removed {len(result['files_removed'])} files")
|
|
827
|
+
if not result["hooks_removed"] and not result["files_removed"]:
|
|
828
|
+
click.echo(" (no hooks found to remove)")
|
|
829
|
+
else:
|
|
830
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
831
|
+
click.echo("")
|
|
832
|
+
|
|
833
|
+
# Uninstall Copilot hooks
|
|
834
|
+
if "copilot" in clis_to_uninstall:
|
|
835
|
+
click.echo("-" * 40)
|
|
836
|
+
click.echo("Copilot CLI")
|
|
837
|
+
click.echo("-" * 40)
|
|
838
|
+
|
|
839
|
+
result = uninstall_copilot(project_path)
|
|
840
|
+
results["copilot"] = result
|
|
841
|
+
|
|
842
|
+
if result["success"]:
|
|
843
|
+
if result["hooks_removed"]:
|
|
844
|
+
click.echo(f"Removed {len(result['hooks_removed'])} hooks from hooks.json")
|
|
845
|
+
for hook in result["hooks_removed"]:
|
|
846
|
+
click.echo(f" - {hook}")
|
|
847
|
+
if result["files_removed"]:
|
|
848
|
+
click.echo(f"Removed {len(result['files_removed'])} files")
|
|
849
|
+
if not result["hooks_removed"] and not result["files_removed"]:
|
|
850
|
+
click.echo(" (no hooks found to remove)")
|
|
851
|
+
else:
|
|
852
|
+
click.echo(f"Failed: {result['error']}", err=True)
|
|
853
|
+
click.echo("")
|
|
854
|
+
|
|
600
855
|
# Summary
|
|
601
856
|
click.echo("=" * 60)
|
|
602
857
|
click.echo(" Summary")
|
gobby/cli/installers/__init__.py
CHANGED
|
@@ -8,6 +8,8 @@ using the strangler fig pattern for incremental migration.
|
|
|
8
8
|
from .antigravity import install_antigravity
|
|
9
9
|
from .claude import install_claude, uninstall_claude
|
|
10
10
|
from .codex import install_codex_notify, uninstall_codex_notify
|
|
11
|
+
from .copilot import install_copilot, uninstall_copilot
|
|
12
|
+
from .cursor import install_cursor, uninstall_cursor
|
|
11
13
|
from .gemini import install_gemini, uninstall_gemini
|
|
12
14
|
from .git_hooks import install_git_hooks
|
|
13
15
|
from .shared import (
|
|
@@ -15,6 +17,7 @@ from .shared import (
|
|
|
15
17
|
install_default_mcp_servers,
|
|
16
18
|
install_shared_content,
|
|
17
19
|
)
|
|
20
|
+
from .windsurf import install_windsurf, uninstall_windsurf
|
|
18
21
|
|
|
19
22
|
__all__ = [
|
|
20
23
|
# Shared
|
|
@@ -30,6 +33,15 @@ __all__ = [
|
|
|
30
33
|
# Codex
|
|
31
34
|
"install_codex_notify",
|
|
32
35
|
"uninstall_codex_notify",
|
|
36
|
+
# Cursor
|
|
37
|
+
"install_cursor",
|
|
38
|
+
"uninstall_cursor",
|
|
39
|
+
# Windsurf
|
|
40
|
+
"install_windsurf",
|
|
41
|
+
"uninstall_windsurf",
|
|
42
|
+
# Copilot
|
|
43
|
+
"install_copilot",
|
|
44
|
+
"uninstall_copilot",
|
|
33
45
|
# Git Hooks
|
|
34
46
|
"install_git_hooks",
|
|
35
47
|
# Antigravity
|
|
@@ -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
|