superqode 0.1.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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Guard - Prevents Git Operations During QE Sessions.
|
|
3
|
+
|
|
4
|
+
Ensures the immutable repo guarantee by blocking all git operations
|
|
5
|
+
that could permanently alter the repository state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import List, Optional, Set, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitOperationType(Enum):
|
|
17
|
+
"""Types of git operations."""
|
|
18
|
+
|
|
19
|
+
READ = "read" # Safe: status, log, diff, show, branch -l
|
|
20
|
+
WRITE = "write" # Blocked: add, commit, push, merge, rebase
|
|
21
|
+
DESTRUCTIVE = "destructive" # Blocked: reset --hard, clean -f, checkout -f
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GitOperationBlocked(Exception):
|
|
25
|
+
"""Raised when a blocked git operation is attempted."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, command: str, reason: str, suggestion: str = ""):
|
|
28
|
+
self.command = command
|
|
29
|
+
self.reason = reason
|
|
30
|
+
self.suggestion = suggestion
|
|
31
|
+
super().__init__(f"Git operation blocked: {reason}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GitCommandAnalysis:
|
|
36
|
+
"""Analysis of a git command."""
|
|
37
|
+
|
|
38
|
+
command: str
|
|
39
|
+
operation_type: GitOperationType
|
|
40
|
+
is_blocked: bool
|
|
41
|
+
reason: str
|
|
42
|
+
suggestion: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GitGuard:
|
|
46
|
+
"""
|
|
47
|
+
Guards against git operations that would violate the immutable repo guarantee.
|
|
48
|
+
|
|
49
|
+
Rules:
|
|
50
|
+
- ❌ No commits
|
|
51
|
+
- ❌ No pushes
|
|
52
|
+
- ❌ No branching/merging/rebasing
|
|
53
|
+
- ❌ No checkout that overwrites changes
|
|
54
|
+
- ❌ No reset that loses changes
|
|
55
|
+
- ❌ No clean that removes files
|
|
56
|
+
- ✅ Read operations allowed (status, log, diff, show)
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
guard = GitGuard()
|
|
60
|
+
|
|
61
|
+
# Check before executing
|
|
62
|
+
if guard.is_blocked("git commit -m 'test'"):
|
|
63
|
+
raise guard.analyze("git commit -m 'test'").reason
|
|
64
|
+
|
|
65
|
+
# Or use the wrapper
|
|
66
|
+
guard.check_command("git push origin main") # Raises GitOperationBlocked
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Git commands that are always safe (read-only)
|
|
70
|
+
SAFE_COMMANDS: Set[str] = {
|
|
71
|
+
"status",
|
|
72
|
+
"log",
|
|
73
|
+
"diff",
|
|
74
|
+
"show",
|
|
75
|
+
"branch",
|
|
76
|
+
"tag",
|
|
77
|
+
"ls-files",
|
|
78
|
+
"ls-tree",
|
|
79
|
+
"cat-file",
|
|
80
|
+
"rev-parse",
|
|
81
|
+
"describe",
|
|
82
|
+
"name-rev",
|
|
83
|
+
"shortlog",
|
|
84
|
+
"whatchanged",
|
|
85
|
+
"blame",
|
|
86
|
+
"annotate",
|
|
87
|
+
"grep",
|
|
88
|
+
"log",
|
|
89
|
+
"reflog",
|
|
90
|
+
"remote",
|
|
91
|
+
"config",
|
|
92
|
+
"help",
|
|
93
|
+
"version",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Git commands that are blocked (write operations)
|
|
97
|
+
BLOCKED_COMMANDS: Set[str] = {
|
|
98
|
+
"commit",
|
|
99
|
+
"push",
|
|
100
|
+
"pull",
|
|
101
|
+
"fetch",
|
|
102
|
+
"merge",
|
|
103
|
+
"rebase",
|
|
104
|
+
"cherry-pick",
|
|
105
|
+
"revert",
|
|
106
|
+
"reset",
|
|
107
|
+
"checkout",
|
|
108
|
+
"switch",
|
|
109
|
+
"restore",
|
|
110
|
+
"add",
|
|
111
|
+
"rm",
|
|
112
|
+
"mv",
|
|
113
|
+
"clean",
|
|
114
|
+
"stash",
|
|
115
|
+
"tag",
|
|
116
|
+
"branch",
|
|
117
|
+
"remote",
|
|
118
|
+
"submodule",
|
|
119
|
+
"subtree",
|
|
120
|
+
"init",
|
|
121
|
+
"clone",
|
|
122
|
+
"gc",
|
|
123
|
+
"prune",
|
|
124
|
+
"fsck",
|
|
125
|
+
"reflog",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Patterns for safe variants of normally blocked commands
|
|
129
|
+
SAFE_PATTERNS: List[Tuple[str, re.Pattern]] = [
|
|
130
|
+
# git branch -l, --list, -a, -r (listing only)
|
|
131
|
+
("branch", re.compile(r"branch\s+(-[lar]+|--list|--all|--remotes)(\s|$)")),
|
|
132
|
+
# git remote -v, show, get-url (listing only)
|
|
133
|
+
("remote", re.compile(r"remote\s+(-v|--verbose|show|get-url)(\s|$)")),
|
|
134
|
+
# git tag -l, --list (listing only)
|
|
135
|
+
("tag", re.compile(r"tag\s+(-l|--list)(\s|$)")),
|
|
136
|
+
# git stash list, show (reading only)
|
|
137
|
+
("stash", re.compile(r"stash\s+(list|show)(\s|$)")),
|
|
138
|
+
# git config --get, --list (reading only)
|
|
139
|
+
("config", re.compile(r"config\s+(--get|--list|-l)(\s|$)")),
|
|
140
|
+
# git diff (always safe)
|
|
141
|
+
("diff", re.compile(r"diff(\s|$)")),
|
|
142
|
+
# git log (always safe)
|
|
143
|
+
("log", re.compile(r"log(\s|$)")),
|
|
144
|
+
# git status (always safe)
|
|
145
|
+
("status", re.compile(r"status(\s|$)")),
|
|
146
|
+
# git show (always safe)
|
|
147
|
+
("show", re.compile(r"show(\s|$)")),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# Human-readable reasons for blocking
|
|
151
|
+
BLOCK_REASONS = {
|
|
152
|
+
"commit": "Commits would permanently alter the repository history",
|
|
153
|
+
"push": "Push would send changes to remote repository",
|
|
154
|
+
"pull": "Pull could introduce external changes during QE session",
|
|
155
|
+
"fetch": "Fetch is unnecessary during ephemeral QE session",
|
|
156
|
+
"merge": "Merge would alter branch history",
|
|
157
|
+
"rebase": "Rebase would rewrite commit history",
|
|
158
|
+
"cherry-pick": "Cherry-pick would create new commits",
|
|
159
|
+
"revert": "Revert would create new commits",
|
|
160
|
+
"reset": "Reset could lose tracked changes",
|
|
161
|
+
"checkout": "Checkout could overwrite working changes",
|
|
162
|
+
"switch": "Branch switching is not allowed during QE",
|
|
163
|
+
"restore": "Restore could overwrite working changes",
|
|
164
|
+
"add": "Staging changes is not needed in ephemeral workspace",
|
|
165
|
+
"rm": "Git rm would stage deletions",
|
|
166
|
+
"mv": "Git mv would stage renames",
|
|
167
|
+
"clean": "Git clean could remove untracked files",
|
|
168
|
+
"stash": "Stashing is not needed in ephemeral workspace",
|
|
169
|
+
"tag": "Creating tags is not allowed during QE",
|
|
170
|
+
"branch": "Creating/deleting branches is not allowed during QE",
|
|
171
|
+
"init": "Repository initialization is not allowed",
|
|
172
|
+
"clone": "Cloning is not allowed during QE session",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
SUGGESTIONS = {
|
|
176
|
+
"commit": "Changes are automatically tracked and reverted. Use QIR to document findings.",
|
|
177
|
+
"push": "All findings are saved to .superqode/qe-artifacts/ for review.",
|
|
178
|
+
"add": "File tracking is automatic in ephemeral workspace.",
|
|
179
|
+
"checkout": "File modifications are tracked and will be reverted automatically.",
|
|
180
|
+
"reset": "Use 'superqode revert' to manually revert specific changes.",
|
|
181
|
+
"clean": "Ephemeral files are cleaned up automatically after QE session.",
|
|
182
|
+
"stash": "All changes are ephemeral - no need to stash.",
|
|
183
|
+
"branch": "QE runs in ephemeral mode - no branch needed.",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def __init__(self, enabled: bool = True):
|
|
187
|
+
"""
|
|
188
|
+
Initialize the Git Guard.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
enabled: If False, guard is disabled (all operations allowed).
|
|
192
|
+
"""
|
|
193
|
+
self.enabled = enabled
|
|
194
|
+
self._blocked_attempts: List[GitCommandAnalysis] = []
|
|
195
|
+
|
|
196
|
+
def is_git_command(self, command: str) -> bool:
|
|
197
|
+
"""Check if a command is a git command."""
|
|
198
|
+
cmd = command.strip().lower()
|
|
199
|
+
return cmd.startswith("git ") or cmd == "git"
|
|
200
|
+
|
|
201
|
+
def extract_git_subcommand(self, command: str) -> Optional[str]:
|
|
202
|
+
"""Extract the git subcommand from a full command."""
|
|
203
|
+
parts = command.strip().split()
|
|
204
|
+
if len(parts) < 2:
|
|
205
|
+
return None
|
|
206
|
+
if parts[0].lower() != "git":
|
|
207
|
+
return None
|
|
208
|
+
return parts[1].lower()
|
|
209
|
+
|
|
210
|
+
def is_safe_variant(self, command: str, subcommand: str) -> bool:
|
|
211
|
+
"""Check if this is a safe variant of a normally blocked command."""
|
|
212
|
+
# Remove 'git ' prefix for pattern matching
|
|
213
|
+
cmd_without_git = (
|
|
214
|
+
command.strip()[4:].strip() if command.strip().lower().startswith("git ") else command
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
for pattern_cmd, pattern in self.SAFE_PATTERNS:
|
|
218
|
+
if subcommand == pattern_cmd and pattern.search(cmd_without_git):
|
|
219
|
+
return True
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def analyze(self, command: str) -> GitCommandAnalysis:
|
|
223
|
+
"""
|
|
224
|
+
Analyze a git command and determine if it should be blocked.
|
|
225
|
+
|
|
226
|
+
Returns detailed analysis including reason and suggestion.
|
|
227
|
+
"""
|
|
228
|
+
if not self.is_git_command(command):
|
|
229
|
+
return GitCommandAnalysis(
|
|
230
|
+
command=command,
|
|
231
|
+
operation_type=GitOperationType.READ,
|
|
232
|
+
is_blocked=False,
|
|
233
|
+
reason="Not a git command",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
subcommand = self.extract_git_subcommand(command)
|
|
237
|
+
|
|
238
|
+
if not subcommand:
|
|
239
|
+
return GitCommandAnalysis(
|
|
240
|
+
command=command,
|
|
241
|
+
operation_type=GitOperationType.READ,
|
|
242
|
+
is_blocked=False,
|
|
243
|
+
reason="Bare git command",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Check if it's a known safe command
|
|
247
|
+
if subcommand in self.SAFE_COMMANDS:
|
|
248
|
+
return GitCommandAnalysis(
|
|
249
|
+
command=command,
|
|
250
|
+
operation_type=GitOperationType.READ,
|
|
251
|
+
is_blocked=False,
|
|
252
|
+
reason=f"'{subcommand}' is a read-only operation",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Check if it's a safe variant of a blocked command
|
|
256
|
+
if subcommand in self.BLOCKED_COMMANDS and self.is_safe_variant(command, subcommand):
|
|
257
|
+
return GitCommandAnalysis(
|
|
258
|
+
command=command,
|
|
259
|
+
operation_type=GitOperationType.READ,
|
|
260
|
+
is_blocked=False,
|
|
261
|
+
reason=f"'{subcommand}' in read-only mode",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Check if it's a blocked command
|
|
265
|
+
if subcommand in self.BLOCKED_COMMANDS:
|
|
266
|
+
reason = self.BLOCK_REASONS.get(
|
|
267
|
+
subcommand, f"'{subcommand}' could modify repository state"
|
|
268
|
+
)
|
|
269
|
+
suggestion = self.SUGGESTIONS.get(subcommand, "")
|
|
270
|
+
|
|
271
|
+
# Determine operation type
|
|
272
|
+
if subcommand in {"reset", "clean", "checkout"}:
|
|
273
|
+
op_type = GitOperationType.DESTRUCTIVE
|
|
274
|
+
else:
|
|
275
|
+
op_type = GitOperationType.WRITE
|
|
276
|
+
|
|
277
|
+
return GitCommandAnalysis(
|
|
278
|
+
command=command,
|
|
279
|
+
operation_type=op_type,
|
|
280
|
+
is_blocked=True,
|
|
281
|
+
reason=reason,
|
|
282
|
+
suggestion=suggestion,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Unknown git command - block by default for safety
|
|
286
|
+
return GitCommandAnalysis(
|
|
287
|
+
command=command,
|
|
288
|
+
operation_type=GitOperationType.WRITE,
|
|
289
|
+
is_blocked=True,
|
|
290
|
+
reason=f"Unknown git subcommand '{subcommand}' - blocked for safety",
|
|
291
|
+
suggestion="Only read operations (status, log, diff, show) are allowed during QE.",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def is_blocked(self, command: str) -> bool:
|
|
295
|
+
"""Quick check if a command is blocked."""
|
|
296
|
+
if not self.enabled:
|
|
297
|
+
return False
|
|
298
|
+
if not self.is_git_command(command):
|
|
299
|
+
return False
|
|
300
|
+
return self.analyze(command).is_blocked
|
|
301
|
+
|
|
302
|
+
def check_command(self, command: str) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Check a command and raise GitOperationBlocked if blocked.
|
|
305
|
+
|
|
306
|
+
Use this as a guard before executing commands.
|
|
307
|
+
"""
|
|
308
|
+
if not self.enabled:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
analysis = self.analyze(command)
|
|
312
|
+
|
|
313
|
+
if analysis.is_blocked:
|
|
314
|
+
self._blocked_attempts.append(analysis)
|
|
315
|
+
raise GitOperationBlocked(
|
|
316
|
+
command=command,
|
|
317
|
+
reason=analysis.reason,
|
|
318
|
+
suggestion=analysis.suggestion,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def get_blocked_attempts(self) -> List[GitCommandAnalysis]:
|
|
322
|
+
"""Get list of all blocked command attempts."""
|
|
323
|
+
return self._blocked_attempts.copy()
|
|
324
|
+
|
|
325
|
+
def clear_blocked_attempts(self) -> None:
|
|
326
|
+
"""Clear the blocked attempts log."""
|
|
327
|
+
self._blocked_attempts.clear()
|
|
328
|
+
|
|
329
|
+
def format_block_message(self, analysis: GitCommandAnalysis) -> str:
|
|
330
|
+
"""Format a user-friendly block message."""
|
|
331
|
+
lines = [
|
|
332
|
+
"🛡️ Git Operation Blocked",
|
|
333
|
+
"━" * 40,
|
|
334
|
+
f"Command: {analysis.command}",
|
|
335
|
+
f"Reason: {analysis.reason}",
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
if analysis.suggestion:
|
|
339
|
+
lines.append(f"💡 Tip: {analysis.suggestion}")
|
|
340
|
+
|
|
341
|
+
lines.extend(
|
|
342
|
+
[
|
|
343
|
+
"",
|
|
344
|
+
"SuperQode runs in ephemeral mode - all changes are",
|
|
345
|
+
"automatically tracked and reverted after QE completes.",
|
|
346
|
+
"Findings are preserved in .superqode/qe-artifacts/",
|
|
347
|
+
]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return "\n".join(lines)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# Singleton instance for easy access
|
|
354
|
+
_default_guard: Optional[GitGuard] = None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_git_guard() -> GitGuard:
|
|
358
|
+
"""Get the default Git Guard instance."""
|
|
359
|
+
global _default_guard
|
|
360
|
+
if _default_guard is None:
|
|
361
|
+
_default_guard = GitGuard()
|
|
362
|
+
return _default_guard
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def set_git_guard(guard: GitGuard) -> None:
|
|
366
|
+
"""Set the default Git Guard instance."""
|
|
367
|
+
global _default_guard
|
|
368
|
+
_default_guard = guard
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def check_git_command(command: str) -> None:
|
|
372
|
+
"""Convenience function to check a command against the default guard."""
|
|
373
|
+
get_git_guard().check_command(command)
|