gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini CLI installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing and uninstalling Gobby hooks
|
|
5
|
+
and workflows for Gemini CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from shutil import copy2, which
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gobby.cli.utils import get_install_dir
|
|
16
|
+
|
|
17
|
+
from .shared import (
|
|
18
|
+
configure_mcp_server_json,
|
|
19
|
+
install_cli_content,
|
|
20
|
+
install_shared_content,
|
|
21
|
+
install_shared_skills,
|
|
22
|
+
remove_mcp_server_json,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def install_gemini(project_path: Path) -> dict[str, Any]:
|
|
29
|
+
"""Install Gobby integration for Gemini CLI (hooks, workflows).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_path: Path to the project root
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with installation results including success status and installed items
|
|
36
|
+
"""
|
|
37
|
+
hooks_installed: list[str] = []
|
|
38
|
+
result: dict[str, Any] = {
|
|
39
|
+
"success": False,
|
|
40
|
+
"hooks_installed": hooks_installed,
|
|
41
|
+
"workflows_installed": [],
|
|
42
|
+
"commands_installed": [],
|
|
43
|
+
"mcp_configured": False,
|
|
44
|
+
"mcp_already_configured": False,
|
|
45
|
+
"error": None,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
gemini_path = project_path / ".gemini"
|
|
49
|
+
settings_file = gemini_path / "settings.json"
|
|
50
|
+
|
|
51
|
+
# Ensure .gemini subdirectories exist
|
|
52
|
+
gemini_path.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
hooks_dir = gemini_path / "hooks"
|
|
54
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# Get source files
|
|
57
|
+
install_dir = get_install_dir()
|
|
58
|
+
gemini_install_dir = install_dir / "gemini"
|
|
59
|
+
install_hooks_dir = gemini_install_dir / "hooks"
|
|
60
|
+
source_hooks_template = gemini_install_dir / "hooks-template.json"
|
|
61
|
+
|
|
62
|
+
# Verify source files exist
|
|
63
|
+
dispatcher_file = install_hooks_dir / "hook_dispatcher.py"
|
|
64
|
+
if not dispatcher_file.exists():
|
|
65
|
+
result["error"] = f"Missing hook dispatcher: {dispatcher_file}"
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
if not source_hooks_template.exists():
|
|
69
|
+
result["error"] = f"Missing hooks template: {source_hooks_template}"
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
# Copy hook dispatcher
|
|
73
|
+
target_dispatcher = hooks_dir / "hook_dispatcher.py"
|
|
74
|
+
if target_dispatcher.exists():
|
|
75
|
+
target_dispatcher.unlink()
|
|
76
|
+
copy2(dispatcher_file, target_dispatcher)
|
|
77
|
+
target_dispatcher.chmod(0o755)
|
|
78
|
+
|
|
79
|
+
# Install shared content (workflows)
|
|
80
|
+
shared = install_shared_content(gemini_path, project_path)
|
|
81
|
+
# Install CLI-specific content (can override shared)
|
|
82
|
+
cli = install_cli_content("gemini", gemini_path)
|
|
83
|
+
|
|
84
|
+
# Install shared skills (SKILL.md)
|
|
85
|
+
try:
|
|
86
|
+
skills = install_shared_skills(gemini_path / "skills")
|
|
87
|
+
result["commands_installed"].extend([f"{s} (skill)" for s in skills])
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to install shared skills: {e}")
|
|
90
|
+
# Proceeding despite skill install failure
|
|
91
|
+
|
|
92
|
+
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
93
|
+
result["commands_installed"] = cli.get("commands", [])
|
|
94
|
+
result["plugins_installed"] = shared.get("plugins", [])
|
|
95
|
+
|
|
96
|
+
# Backup existing settings.json if it exists
|
|
97
|
+
if settings_file.exists():
|
|
98
|
+
timestamp = int(time.time())
|
|
99
|
+
backup_file = gemini_path / f"settings.json.{timestamp}.backup"
|
|
100
|
+
copy2(settings_file, backup_file)
|
|
101
|
+
|
|
102
|
+
# Load existing settings or create empty
|
|
103
|
+
if settings_file.exists():
|
|
104
|
+
try:
|
|
105
|
+
with open(settings_file) as f:
|
|
106
|
+
existing_settings = json.load(f)
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
# If invalid JSON, treat as empty but warn (backup already made)
|
|
109
|
+
existing_settings = {}
|
|
110
|
+
else:
|
|
111
|
+
existing_settings = {}
|
|
112
|
+
|
|
113
|
+
# Load Gobby hooks from template
|
|
114
|
+
with open(source_hooks_template) as f:
|
|
115
|
+
gobby_settings_str = f.read()
|
|
116
|
+
|
|
117
|
+
# Resolve uv path dynamically to avoid PATH issues in Gemini CLI
|
|
118
|
+
uv_path = which("uv")
|
|
119
|
+
if not uv_path:
|
|
120
|
+
uv_path = "uv" # Fallback
|
|
121
|
+
|
|
122
|
+
# Replace $PROJECT_PATH with absolute project path
|
|
123
|
+
abs_project_path = str(project_path.resolve())
|
|
124
|
+
|
|
125
|
+
# Replace variables in template
|
|
126
|
+
gobby_settings_str = gobby_settings_str.replace("$PROJECT_PATH", abs_project_path)
|
|
127
|
+
|
|
128
|
+
# Also replace "uv run python" with absolute path if found
|
|
129
|
+
# The template uses "uv run python" by default
|
|
130
|
+
if uv_path != "uv":
|
|
131
|
+
gobby_settings_str = gobby_settings_str.replace("uv run python", f"{uv_path} run python")
|
|
132
|
+
|
|
133
|
+
gobby_settings = json.loads(gobby_settings_str)
|
|
134
|
+
|
|
135
|
+
# Ensure hooks section exists
|
|
136
|
+
if "hooks" not in existing_settings:
|
|
137
|
+
existing_settings["hooks"] = {}
|
|
138
|
+
|
|
139
|
+
# Merge Gobby hooks (preserving any existing hooks)
|
|
140
|
+
gobby_hooks = gobby_settings.get("hooks", {})
|
|
141
|
+
for hook_type, hook_config in gobby_hooks.items():
|
|
142
|
+
existing_settings["hooks"][hook_type] = hook_config
|
|
143
|
+
hooks_installed.append(hook_type)
|
|
144
|
+
|
|
145
|
+
# Crucially, ensure hooks are enabled in Gemini CLI
|
|
146
|
+
if "general" not in existing_settings:
|
|
147
|
+
existing_settings["general"] = {}
|
|
148
|
+
existing_settings["general"]["enableHooks"] = True
|
|
149
|
+
|
|
150
|
+
# Write merged settings back
|
|
151
|
+
with open(settings_file, "w") as f:
|
|
152
|
+
json.dump(existing_settings, f, indent=2)
|
|
153
|
+
|
|
154
|
+
# Configure MCP server in global settings (~/.gemini/settings.json)
|
|
155
|
+
global_settings = Path.home() / ".gemini" / "settings.json"
|
|
156
|
+
mcp_result = configure_mcp_server_json(global_settings)
|
|
157
|
+
if mcp_result["success"]:
|
|
158
|
+
result["mcp_configured"] = mcp_result.get("added", False)
|
|
159
|
+
result["mcp_already_configured"] = mcp_result.get("already_configured", False)
|
|
160
|
+
else:
|
|
161
|
+
# MCP config failure is non-fatal, just log it
|
|
162
|
+
logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
|
|
163
|
+
|
|
164
|
+
# Install agent scripts (used by meeseeks workflow)
|
|
165
|
+
scripts_installed = _install_agent_scripts(install_dir)
|
|
166
|
+
result["scripts_installed"] = scripts_installed
|
|
167
|
+
|
|
168
|
+
result["success"] = True
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _install_agent_scripts(install_dir: Path) -> list[str]:
|
|
173
|
+
"""Install shared agent scripts to ~/.gobby/scripts/.
|
|
174
|
+
|
|
175
|
+
Installs scripts like agent_shutdown.sh used by workflows.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
install_dir: Path to the install source directory
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of installed script names
|
|
182
|
+
"""
|
|
183
|
+
scripts_installed: list[str] = []
|
|
184
|
+
source_scripts_dir = install_dir / "shared" / "scripts"
|
|
185
|
+
target_scripts_dir = Path.home() / ".gobby" / "scripts"
|
|
186
|
+
|
|
187
|
+
if not source_scripts_dir.exists():
|
|
188
|
+
logger.debug(f"No scripts directory found at {source_scripts_dir}")
|
|
189
|
+
return scripts_installed
|
|
190
|
+
|
|
191
|
+
# Ensure target directory exists
|
|
192
|
+
target_scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
|
|
194
|
+
# Copy all scripts
|
|
195
|
+
for script_file in source_scripts_dir.glob("*.sh"):
|
|
196
|
+
target_file = target_scripts_dir / script_file.name
|
|
197
|
+
copy2(script_file, target_file)
|
|
198
|
+
# Make executable
|
|
199
|
+
target_file.chmod(0o755)
|
|
200
|
+
scripts_installed.append(script_file.name)
|
|
201
|
+
logger.debug(f"Installed script: {script_file.name}")
|
|
202
|
+
|
|
203
|
+
return scripts_installed
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def uninstall_gemini(project_path: Path) -> dict[str, Any]:
|
|
207
|
+
"""Uninstall Gobby integration from Gemini CLI.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
project_path: Path to the project root
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict with uninstallation results including success status and removed items
|
|
214
|
+
"""
|
|
215
|
+
hooks_removed: list[str] = []
|
|
216
|
+
files_removed: list[str] = []
|
|
217
|
+
result: dict[str, Any] = {
|
|
218
|
+
"success": False,
|
|
219
|
+
"hooks_removed": hooks_removed,
|
|
220
|
+
"files_removed": files_removed,
|
|
221
|
+
"mcp_removed": False,
|
|
222
|
+
"error": None,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
gemini_path = project_path / ".gemini"
|
|
226
|
+
settings_file = gemini_path / "settings.json"
|
|
227
|
+
hooks_dir = gemini_path / "hooks"
|
|
228
|
+
|
|
229
|
+
if not settings_file.exists():
|
|
230
|
+
# No settings file means nothing to uninstall
|
|
231
|
+
result["success"] = True
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
# Backup settings.json
|
|
235
|
+
timestamp = int(time.time())
|
|
236
|
+
backup_file = gemini_path / f"settings.json.{timestamp}.backup"
|
|
237
|
+
copy2(settings_file, backup_file)
|
|
238
|
+
|
|
239
|
+
# Remove hooks from settings.json
|
|
240
|
+
with open(settings_file) as f:
|
|
241
|
+
settings = json.load(f)
|
|
242
|
+
|
|
243
|
+
if "hooks" in settings:
|
|
244
|
+
hook_types = [
|
|
245
|
+
"SessionStart",
|
|
246
|
+
"SessionEnd",
|
|
247
|
+
"BeforeAgent",
|
|
248
|
+
"AfterAgent",
|
|
249
|
+
"BeforeTool",
|
|
250
|
+
"AfterTool",
|
|
251
|
+
"BeforeToolSelection",
|
|
252
|
+
"BeforeModel",
|
|
253
|
+
"AfterModel",
|
|
254
|
+
"PreCompress",
|
|
255
|
+
"Notification",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
for hook_type in hook_types:
|
|
259
|
+
if hook_type in settings["hooks"]:
|
|
260
|
+
del settings["hooks"][hook_type]
|
|
261
|
+
hooks_removed.append(hook_type)
|
|
262
|
+
|
|
263
|
+
# Also remove the "general" section if "enableHooks" was the only entry
|
|
264
|
+
if "general" in settings and settings["general"].get("enableHooks") is True:
|
|
265
|
+
# Check if there are other entries in "general"
|
|
266
|
+
if len(settings["general"]) == 1:
|
|
267
|
+
del settings["general"]
|
|
268
|
+
else:
|
|
269
|
+
del settings["general"]["enableHooks"]
|
|
270
|
+
|
|
271
|
+
with open(settings_file, "w") as f:
|
|
272
|
+
json.dump(settings, f, indent=2)
|
|
273
|
+
|
|
274
|
+
# Remove hook dispatcher
|
|
275
|
+
dispatcher_file = hooks_dir / "hook_dispatcher.py"
|
|
276
|
+
if dispatcher_file.exists():
|
|
277
|
+
dispatcher_file.unlink()
|
|
278
|
+
files_removed.append("hook_dispatcher.py")
|
|
279
|
+
|
|
280
|
+
# Attempt to remove empty hooks directory
|
|
281
|
+
try:
|
|
282
|
+
if hooks_dir.exists() and not any(hooks_dir.iterdir()):
|
|
283
|
+
hooks_dir.rmdir()
|
|
284
|
+
except Exception:
|
|
285
|
+
pass # nosec B110 - best-effort cleanup
|
|
286
|
+
|
|
287
|
+
# Remove MCP server from global settings (~/.gemini/settings.json)
|
|
288
|
+
global_settings = Path.home() / ".gemini" / "settings.json"
|
|
289
|
+
mcp_result = remove_mcp_server_json(global_settings)
|
|
290
|
+
if mcp_result["success"]:
|
|
291
|
+
result["mcp_removed"] = mcp_result.get("removed", False)
|
|
292
|
+
|
|
293
|
+
result["success"] = True
|
|
294
|
+
return result
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git hooks installation for Gobby task sync.
|
|
3
|
+
|
|
4
|
+
This module handles installing git hooks for automatic task
|
|
5
|
+
synchronization on commit, merge, and checkout operations.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Backs up existing hooks before modification
|
|
9
|
+
- Chains with existing hooks (doesn't overwrite)
|
|
10
|
+
- Integrates with pre-commit framework when available
|
|
11
|
+
- Supports clean uninstallation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import shutil
|
|
16
|
+
import stat
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Markers for identifying Gobby hook sections
|
|
24
|
+
GOBBY_HOOK_START = "# >>> GOBBY HOOK START >>>"
|
|
25
|
+
GOBBY_HOOK_END = "# <<< GOBBY HOOK END <<<"
|
|
26
|
+
|
|
27
|
+
# Hook script templates - these get wrapped with markers
|
|
28
|
+
HOOK_TEMPLATES = {
|
|
29
|
+
"pre-commit": """
|
|
30
|
+
# Gobby smart pre-commit wrapper
|
|
31
|
+
# - Runs gobby verification commands (if configured)
|
|
32
|
+
# - Runs pre-commit framework if available
|
|
33
|
+
# - Auto-commits formatting fixes separately
|
|
34
|
+
# - Syncs tasks before commit
|
|
35
|
+
|
|
36
|
+
# Run Gobby verification commands for pre-commit stage
|
|
37
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
38
|
+
gobby hooks run pre-commit 2>/dev/null
|
|
39
|
+
GOBBY_EXIT=$?
|
|
40
|
+
if [ $GOBBY_EXIT -ne 0 ]; then
|
|
41
|
+
echo "Gobby pre-commit verification failed"
|
|
42
|
+
exit $GOBBY_EXIT
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Record which files have unstaged changes before pre-commit runs
|
|
47
|
+
UNSTAGED_BEFORE=$(git diff --name-only 2>/dev/null | sort)
|
|
48
|
+
|
|
49
|
+
# Run pre-commit if available and config exists
|
|
50
|
+
if command -v pre-commit >/dev/null 2>&1 && [ -f .pre-commit-config.yaml ]; then
|
|
51
|
+
pre-commit run --hook-stage pre-commit
|
|
52
|
+
PRECOMMIT_EXIT=$?
|
|
53
|
+
|
|
54
|
+
if [ $PRECOMMIT_EXIT -ne 0 ]; then
|
|
55
|
+
# Check if files were auto-fixed (new unstaged changes appeared)
|
|
56
|
+
UNSTAGED_AFTER=$(git diff --name-only 2>/dev/null | sort)
|
|
57
|
+
|
|
58
|
+
if [ "$UNSTAGED_BEFORE" != "$UNSTAGED_AFTER" ]; then
|
|
59
|
+
# Find files that were auto-fixed (newly unstaged)
|
|
60
|
+
AUTO_FIXED=$(comm -13 <(echo "$UNSTAGED_BEFORE") <(echo "$UNSTAGED_AFTER") 2>/dev/null)
|
|
61
|
+
|
|
62
|
+
if [ -n "$AUTO_FIXED" ]; then
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Pre-commit auto-fixed files. Creating separate commit..."
|
|
65
|
+
|
|
66
|
+
# Stage only the auto-fixed files (handle filenames with spaces/special chars)
|
|
67
|
+
echo "$AUTO_FIXED" | while IFS= read -r file; do
|
|
68
|
+
[ -n "$file" ] && git add -- "$file"
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
# Commit them with --no-verify to skip hooks
|
|
72
|
+
git commit --no-verify -m "style: auto-format (pre-commit)" >/dev/null
|
|
73
|
+
|
|
74
|
+
echo "Auto-format committed. Please run 'git commit' again for your changes."
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Pre-commit failed for other reasons
|
|
80
|
+
exit $PRECOMMIT_EXIT
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Gobby task sync - export tasks before commit
|
|
85
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
86
|
+
gobby tasks sync --export --quiet 2>/dev/null || true
|
|
87
|
+
fi
|
|
88
|
+
""",
|
|
89
|
+
"pre-push": """
|
|
90
|
+
# Gobby verification runner for pre-push
|
|
91
|
+
# Runs configured verification commands (type_check, unit_tests, security, etc.)
|
|
92
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
93
|
+
gobby hooks run pre-push 2>/dev/null
|
|
94
|
+
GOBBY_EXIT=$?
|
|
95
|
+
if [ $GOBBY_EXIT -ne 0 ]; then
|
|
96
|
+
echo "Gobby pre-push verification failed"
|
|
97
|
+
exit $GOBBY_EXIT
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
""",
|
|
101
|
+
"pre-merge-commit": """
|
|
102
|
+
# Gobby verification runner for pre-merge-commit
|
|
103
|
+
# Runs configured verification commands (code_review, integration tests, etc.)
|
|
104
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
105
|
+
gobby hooks run pre-merge-commit 2>/dev/null
|
|
106
|
+
GOBBY_EXIT=$?
|
|
107
|
+
if [ $GOBBY_EXIT -ne 0 ]; then
|
|
108
|
+
echo "Gobby pre-merge-commit verification failed"
|
|
109
|
+
exit $GOBBY_EXIT
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
""",
|
|
113
|
+
"post-merge": """
|
|
114
|
+
# Gobby task sync - import tasks after merge/pull
|
|
115
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
116
|
+
gobby tasks sync --import --quiet 2>/dev/null || true
|
|
117
|
+
fi
|
|
118
|
+
""",
|
|
119
|
+
"post-checkout": """
|
|
120
|
+
# Gobby task sync - import tasks on branch switch
|
|
121
|
+
# $3 is 1 if this was a branch checkout (vs file checkout)
|
|
122
|
+
if [ "$3" = "1" ]; then
|
|
123
|
+
if command -v gobby >/dev/null 2>&1; then
|
|
124
|
+
gobby tasks sync --import --quiet 2>/dev/null || true
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
""",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _backup_hook(hook_path: Path, hooks_dir: Path) -> str | None:
|
|
132
|
+
"""Create a timestamped backup of an existing hook.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
hook_path: Path to the hook file
|
|
136
|
+
hooks_dir: Directory containing hooks
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Backup path if created, None otherwise
|
|
140
|
+
"""
|
|
141
|
+
if not hook_path.exists():
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
timestamp = int(time.time())
|
|
145
|
+
backup_path = hooks_dir / f"{hook_path.name}.{timestamp}.backup"
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
shutil.copy2(hook_path, backup_path)
|
|
149
|
+
logger.debug(f"Backed up {hook_path.name} to {backup_path.name}")
|
|
150
|
+
return str(backup_path)
|
|
151
|
+
except OSError as e:
|
|
152
|
+
logger.warning(f"Failed to backup {hook_path.name}: {e}")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _has_gobby_hook(content: str) -> bool:
|
|
157
|
+
"""Check if content already contains Gobby hook markers."""
|
|
158
|
+
return GOBBY_HOOK_START in content
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_precommit_framework_hook(content: str) -> bool:
|
|
162
|
+
"""Check if this is a hook generated by the pre-commit framework."""
|
|
163
|
+
return "File generated by pre-commit" in content or "pre_commit" in content
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _wrap_gobby_section(script: str) -> str:
|
|
167
|
+
"""Wrap a script section with Gobby markers."""
|
|
168
|
+
return f"{GOBBY_HOOK_START}\n{script.strip()}\n{GOBBY_HOOK_END}\n"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _remove_gobby_section(content: str) -> str:
|
|
172
|
+
"""Remove Gobby hook section from content."""
|
|
173
|
+
lines = content.split("\n")
|
|
174
|
+
result = []
|
|
175
|
+
in_gobby_section = False
|
|
176
|
+
|
|
177
|
+
for line in lines:
|
|
178
|
+
if GOBBY_HOOK_START in line:
|
|
179
|
+
in_gobby_section = True
|
|
180
|
+
continue
|
|
181
|
+
if GOBBY_HOOK_END in line:
|
|
182
|
+
in_gobby_section = False
|
|
183
|
+
continue
|
|
184
|
+
if not in_gobby_section:
|
|
185
|
+
result.append(line)
|
|
186
|
+
|
|
187
|
+
# Clean up multiple blank lines
|
|
188
|
+
cleaned = "\n".join(result)
|
|
189
|
+
while "\n\n\n" in cleaned:
|
|
190
|
+
cleaned = cleaned.replace("\n\n\n", "\n\n")
|
|
191
|
+
|
|
192
|
+
return cleaned.strip() + "\n" if cleaned.strip() else ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_precommit_installed() -> bool:
|
|
196
|
+
"""Check if pre-commit framework is installed and configured."""
|
|
197
|
+
return shutil.which("pre-commit") is not None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _has_precommit_config(project_path: Path) -> bool:
|
|
201
|
+
"""Check if project has a .pre-commit-config.yaml."""
|
|
202
|
+
return (project_path / ".pre-commit-config.yaml").exists()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def install_git_hooks(
|
|
206
|
+
project_path: Path,
|
|
207
|
+
*,
|
|
208
|
+
force: bool = False,
|
|
209
|
+
setup_precommit: bool = True,
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""Install Gobby git hooks to the current repository.
|
|
212
|
+
|
|
213
|
+
Safely installs hooks by:
|
|
214
|
+
1. Backing up existing hooks
|
|
215
|
+
2. Chaining with existing hooks (appending Gobby section)
|
|
216
|
+
3. Optionally setting up pre-commit framework
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
project_path: Path to the project root
|
|
220
|
+
force: If True, reinstall even if already present
|
|
221
|
+
setup_precommit: If True, run `pre-commit install` if config exists
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Dict with installation results including:
|
|
225
|
+
- success: bool
|
|
226
|
+
- installed: list of installed hook names
|
|
227
|
+
- skipped: list of skipped hooks with reasons
|
|
228
|
+
- backups: list of backup file paths
|
|
229
|
+
- precommit_installed: bool if pre-commit was set up
|
|
230
|
+
- error: error message if failed
|
|
231
|
+
"""
|
|
232
|
+
result: dict[str, Any] = {
|
|
233
|
+
"success": False,
|
|
234
|
+
"installed": [],
|
|
235
|
+
"skipped": [],
|
|
236
|
+
"backups": [],
|
|
237
|
+
"precommit_installed": False,
|
|
238
|
+
"error": None,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
git_dir = project_path / ".git"
|
|
242
|
+
if not git_dir.exists():
|
|
243
|
+
result["error"] = "Not a git repository (no .git directory found)"
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
hooks_dir = git_dir / "hooks"
|
|
247
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# Install each hook
|
|
250
|
+
for hook_name, gobby_script in HOOK_TEMPLATES.items():
|
|
251
|
+
hook_path = hooks_dir / hook_name
|
|
252
|
+
gobby_section = _wrap_gobby_section(gobby_script)
|
|
253
|
+
|
|
254
|
+
if hook_path.exists():
|
|
255
|
+
content = hook_path.read_text()
|
|
256
|
+
|
|
257
|
+
# Check if already installed
|
|
258
|
+
if _has_gobby_hook(content) and not force:
|
|
259
|
+
result["skipped"].append(f"{hook_name} (already installed)")
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Backup existing hook
|
|
263
|
+
backup_path = _backup_hook(hook_path, hooks_dir)
|
|
264
|
+
if backup_path:
|
|
265
|
+
result["backups"].append(backup_path)
|
|
266
|
+
|
|
267
|
+
# If this is a pre-commit framework hook for pre-commit stage,
|
|
268
|
+
# replace it entirely with our wrapper (which calls pre-commit)
|
|
269
|
+
if hook_name == "pre-commit" and _is_precommit_framework_hook(content):
|
|
270
|
+
new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
|
|
271
|
+
hook_path.write_text(new_content)
|
|
272
|
+
logger.info("Replaced pre-commit framework hook with Gobby wrapper")
|
|
273
|
+
else:
|
|
274
|
+
# Remove old Gobby section if force reinstalling
|
|
275
|
+
if force and GOBBY_HOOK_START in content:
|
|
276
|
+
content = _remove_gobby_section(content)
|
|
277
|
+
|
|
278
|
+
# Append Gobby section to existing hook
|
|
279
|
+
if content.strip():
|
|
280
|
+
# Ensure shebang is preserved at top
|
|
281
|
+
if content.startswith("#!"):
|
|
282
|
+
lines = content.split("\n", 1)
|
|
283
|
+
shebang = lines[0]
|
|
284
|
+
rest = lines[1] if len(lines) > 1 else ""
|
|
285
|
+
new_content = f"{shebang}\n\n{gobby_section}\n{rest.strip()}\n"
|
|
286
|
+
else:
|
|
287
|
+
new_content = f"#!/usr/bin/env bash\n\n{gobby_section}\n{content}"
|
|
288
|
+
else:
|
|
289
|
+
new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
|
|
290
|
+
|
|
291
|
+
hook_path.write_text(new_content)
|
|
292
|
+
logger.info(f"Appended Gobby hook to existing {hook_name}")
|
|
293
|
+
|
|
294
|
+
else:
|
|
295
|
+
# Create new hook (use bash for pre-commit process substitution)
|
|
296
|
+
new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
|
|
297
|
+
hook_path.write_text(new_content)
|
|
298
|
+
logger.info(f"Created new {hook_name} hook")
|
|
299
|
+
|
|
300
|
+
# Ensure executable
|
|
301
|
+
hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
302
|
+
result["installed"].append(hook_name)
|
|
303
|
+
|
|
304
|
+
# Note: We intentionally DON'T run `pre-commit install` here.
|
|
305
|
+
# Our smart pre-commit hook wrapper calls `pre-commit run` directly,
|
|
306
|
+
# which allows us to handle auto-fixes by creating separate commits.
|
|
307
|
+
# Running `pre-commit install` would overwrite our wrapper.
|
|
308
|
+
#
|
|
309
|
+
# We also don't run `pre-commit install --hook-type pre-push` because
|
|
310
|
+
# our pre-push hook now runs gobby verification commands first, and
|
|
311
|
+
# the pre-commit framework's hook would overwrite ours.
|
|
312
|
+
if setup_precommit and _has_precommit_config(project_path) and _check_precommit_installed():
|
|
313
|
+
result["precommit_installed"] = True
|
|
314
|
+
logger.info(
|
|
315
|
+
"Pre-commit detected - gobby hooks will run verification first, then pre-commit framework"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
result["success"] = True
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def uninstall_git_hooks(project_path: Path) -> dict[str, Any]:
|
|
323
|
+
"""Remove Gobby sections from git hooks.
|
|
324
|
+
|
|
325
|
+
Safely removes only Gobby-added sections, preserving other hook functionality.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
project_path: Path to the project root
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Dict with uninstallation results
|
|
332
|
+
"""
|
|
333
|
+
result: dict[str, Any] = {
|
|
334
|
+
"success": False,
|
|
335
|
+
"removed": [],
|
|
336
|
+
"not_found": [],
|
|
337
|
+
"error": None,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
git_dir = project_path / ".git"
|
|
341
|
+
if not git_dir.exists():
|
|
342
|
+
result["error"] = "Not a git repository"
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
hooks_dir = git_dir / "hooks"
|
|
346
|
+
if not hooks_dir.exists():
|
|
347
|
+
result["success"] = True
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
for hook_name in HOOK_TEMPLATES:
|
|
351
|
+
hook_path = hooks_dir / hook_name
|
|
352
|
+
|
|
353
|
+
if not hook_path.exists():
|
|
354
|
+
result["not_found"].append(hook_name)
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
content = hook_path.read_text()
|
|
358
|
+
|
|
359
|
+
if not _has_gobby_hook(content):
|
|
360
|
+
result["not_found"].append(hook_name)
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# Remove Gobby section
|
|
364
|
+
new_content = _remove_gobby_section(content)
|
|
365
|
+
|
|
366
|
+
if new_content.strip():
|
|
367
|
+
# Hook still has content, keep it
|
|
368
|
+
hook_path.write_text(new_content)
|
|
369
|
+
else:
|
|
370
|
+
# Hook is now empty, remove it
|
|
371
|
+
hook_path.unlink()
|
|
372
|
+
|
|
373
|
+
result["removed"].append(hook_name)
|
|
374
|
+
logger.info(f"Removed Gobby section from {hook_name}")
|
|
375
|
+
|
|
376
|
+
result["success"] = True
|
|
377
|
+
return result
|