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
superqode/approval.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Approval System - Accept/Reject File Changes
|
|
3
|
+
|
|
4
|
+
A beautiful approval UI for coding agent file modifications.
|
|
5
|
+
Features gradient styling and keyboard shortcuts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import List, Optional, Callable, Any
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.box import ROUNDED, HEAVY, DOUBLE
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApprovalAction(Enum):
|
|
23
|
+
"""Possible approval actions."""
|
|
24
|
+
|
|
25
|
+
PENDING = "pending"
|
|
26
|
+
APPROVED = "approved"
|
|
27
|
+
APPROVED_ALWAYS = "approved_always"
|
|
28
|
+
REJECTED = "rejected"
|
|
29
|
+
REJECTED_ALWAYS = "rejected_always"
|
|
30
|
+
SKIPPED = "skipped"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ApprovalRequest:
|
|
35
|
+
"""A request for user approval."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
title: str
|
|
39
|
+
description: str
|
|
40
|
+
file_path: Optional[str] = None
|
|
41
|
+
old_content: Optional[str] = None
|
|
42
|
+
new_content: Optional[str] = None
|
|
43
|
+
command: Optional[str] = None
|
|
44
|
+
danger_level: int = 0 # 0=safe, 1=unknown, 2=dangerous, 3=destructive
|
|
45
|
+
action: ApprovalAction = ApprovalAction.PENDING
|
|
46
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
47
|
+
metadata: dict = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# SuperQode approval colors
|
|
51
|
+
APPROVAL_COLORS = {
|
|
52
|
+
"approve": "#22c55e",
|
|
53
|
+
"approve_bg": "#22c55e20",
|
|
54
|
+
"reject": "#ef4444",
|
|
55
|
+
"reject_bg": "#ef444420",
|
|
56
|
+
"pending": "#f59e0b",
|
|
57
|
+
"pending_bg": "#f59e0b20",
|
|
58
|
+
"header": "#a855f7",
|
|
59
|
+
"border": "#2a2a2a",
|
|
60
|
+
"muted": "#71717a",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Icons for approval UI
|
|
64
|
+
APPROVAL_ICONS = {
|
|
65
|
+
"pending": "⏳",
|
|
66
|
+
"approved": "✅",
|
|
67
|
+
"rejected": "❌",
|
|
68
|
+
"file": "📄",
|
|
69
|
+
"command": "💻",
|
|
70
|
+
"warning": "⚠️",
|
|
71
|
+
"danger": "🚨",
|
|
72
|
+
"question": "❓",
|
|
73
|
+
"approve": "👍",
|
|
74
|
+
"reject": "👎",
|
|
75
|
+
"skip": "⏭️",
|
|
76
|
+
"view": "👁️",
|
|
77
|
+
"diff": "📊",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ApprovalManager:
|
|
82
|
+
"""Manages approval requests and user decisions."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, console: Console):
|
|
85
|
+
self.console = console
|
|
86
|
+
self.requests: List[ApprovalRequest] = []
|
|
87
|
+
self.always_approve: set = set() # Patterns to always approve
|
|
88
|
+
self.always_reject: set = set() # Patterns to always reject
|
|
89
|
+
self.history: List[ApprovalRequest] = []
|
|
90
|
+
|
|
91
|
+
def add_request(self, request: ApprovalRequest) -> None:
|
|
92
|
+
"""Add a new approval request."""
|
|
93
|
+
# Check if auto-approve/reject applies
|
|
94
|
+
if request.file_path:
|
|
95
|
+
if any(p in request.file_path for p in self.always_approve):
|
|
96
|
+
request.action = ApprovalAction.APPROVED_ALWAYS
|
|
97
|
+
self.history.append(request)
|
|
98
|
+
return
|
|
99
|
+
if any(p in request.file_path for p in self.always_reject):
|
|
100
|
+
request.action = ApprovalAction.REJECTED_ALWAYS
|
|
101
|
+
self.history.append(request)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
self.requests.append(request)
|
|
105
|
+
|
|
106
|
+
def get_pending(self) -> List[ApprovalRequest]:
|
|
107
|
+
"""Get all pending requests."""
|
|
108
|
+
return [r for r in self.requests if r.action == ApprovalAction.PENDING]
|
|
109
|
+
|
|
110
|
+
def approve(self, request_id: str, always: bool = False) -> bool:
|
|
111
|
+
"""Approve a request."""
|
|
112
|
+
for req in self.requests:
|
|
113
|
+
if req.id == request_id:
|
|
114
|
+
req.action = ApprovalAction.APPROVED_ALWAYS if always else ApprovalAction.APPROVED
|
|
115
|
+
if always and req.file_path:
|
|
116
|
+
# Add pattern for future auto-approve
|
|
117
|
+
self.always_approve.add(req.file_path)
|
|
118
|
+
self.history.append(req)
|
|
119
|
+
self.requests.remove(req)
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def reject(self, request_id: str, always: bool = False) -> bool:
|
|
124
|
+
"""Reject a request."""
|
|
125
|
+
for req in self.requests:
|
|
126
|
+
if req.id == request_id:
|
|
127
|
+
req.action = ApprovalAction.REJECTED_ALWAYS if always else ApprovalAction.REJECTED
|
|
128
|
+
if always and req.file_path:
|
|
129
|
+
self.always_reject.add(req.file_path)
|
|
130
|
+
self.history.append(req)
|
|
131
|
+
self.requests.remove(req)
|
|
132
|
+
return True
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def skip(self, request_id: str) -> bool:
|
|
136
|
+
"""Skip a request (defer decision)."""
|
|
137
|
+
for req in self.requests:
|
|
138
|
+
if req.id == request_id:
|
|
139
|
+
req.action = ApprovalAction.SKIPPED
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def approve_all(self) -> int:
|
|
144
|
+
"""Approve all pending requests."""
|
|
145
|
+
count = 0
|
|
146
|
+
for req in list(self.requests):
|
|
147
|
+
if req.action == ApprovalAction.PENDING:
|
|
148
|
+
req.action = ApprovalAction.APPROVED
|
|
149
|
+
self.history.append(req)
|
|
150
|
+
self.requests.remove(req)
|
|
151
|
+
count += 1
|
|
152
|
+
return count
|
|
153
|
+
|
|
154
|
+
def reject_all(self) -> int:
|
|
155
|
+
"""Reject all pending requests."""
|
|
156
|
+
count = 0
|
|
157
|
+
for req in list(self.requests):
|
|
158
|
+
if req.action == ApprovalAction.PENDING:
|
|
159
|
+
req.action = ApprovalAction.REJECTED
|
|
160
|
+
self.history.append(req)
|
|
161
|
+
self.requests.remove(req)
|
|
162
|
+
count += 1
|
|
163
|
+
return count
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def render_approval_request(
|
|
167
|
+
request: ApprovalRequest, console: Console, show_diff: bool = False
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Render a single approval request."""
|
|
170
|
+
# Header with gradient effect
|
|
171
|
+
header = Text()
|
|
172
|
+
|
|
173
|
+
# Icon based on type
|
|
174
|
+
if request.command:
|
|
175
|
+
icon = APPROVAL_ICONS["command"]
|
|
176
|
+
type_label = "Command"
|
|
177
|
+
else:
|
|
178
|
+
icon = APPROVAL_ICONS["file"]
|
|
179
|
+
type_label = "File Change"
|
|
180
|
+
|
|
181
|
+
# Danger indicator
|
|
182
|
+
danger_icons = ["", "", APPROVAL_ICONS["warning"], APPROVAL_ICONS["danger"]]
|
|
183
|
+
danger_icon = danger_icons[min(request.danger_level, 3)]
|
|
184
|
+
|
|
185
|
+
header.append(f" {icon} ", style="bold")
|
|
186
|
+
header.append(type_label, style="bold white")
|
|
187
|
+
if danger_icon:
|
|
188
|
+
header.append(f" {danger_icon}", style="")
|
|
189
|
+
|
|
190
|
+
# Status badge
|
|
191
|
+
status_colors = {
|
|
192
|
+
ApprovalAction.PENDING: ("⏳ Pending", APPROVAL_COLORS["pending"]),
|
|
193
|
+
ApprovalAction.APPROVED: ("✅ Approved", APPROVAL_COLORS["approve"]),
|
|
194
|
+
ApprovalAction.REJECTED: ("❌ Rejected", APPROVAL_COLORS["reject"]),
|
|
195
|
+
}
|
|
196
|
+
status_text, status_color = status_colors.get(
|
|
197
|
+
request.action, ("❓ Unknown", APPROVAL_COLORS["muted"])
|
|
198
|
+
)
|
|
199
|
+
header.append(" ", style="")
|
|
200
|
+
header.append(f"[{status_text}]", style=f"bold {status_color}")
|
|
201
|
+
|
|
202
|
+
# Build content
|
|
203
|
+
content = Text()
|
|
204
|
+
content.append(f"\n{request.title}\n", style="bold white")
|
|
205
|
+
|
|
206
|
+
if request.description:
|
|
207
|
+
content.append(f"{request.description}\n", style=APPROVAL_COLORS["muted"])
|
|
208
|
+
|
|
209
|
+
if request.file_path:
|
|
210
|
+
content.append(f"\n📁 ", style="")
|
|
211
|
+
content.append(request.file_path, style="bold cyan")
|
|
212
|
+
content.append("\n", style="")
|
|
213
|
+
|
|
214
|
+
if request.command:
|
|
215
|
+
content.append(f"\n💻 ", style="")
|
|
216
|
+
content.append(request.command, style="bold yellow")
|
|
217
|
+
content.append("\n", style="")
|
|
218
|
+
|
|
219
|
+
# Action hints
|
|
220
|
+
content.append("\n", style="")
|
|
221
|
+
content.append(" [A]", style=f"bold {APPROVAL_COLORS['approve']}")
|
|
222
|
+
content.append(" Approve ", style=APPROVAL_COLORS["approve"])
|
|
223
|
+
content.append("[Shift+A]", style=f"bold {APPROVAL_COLORS['approve']}")
|
|
224
|
+
content.append(" Always ", style="dim")
|
|
225
|
+
content.append("[R]", style=f"bold {APPROVAL_COLORS['reject']}")
|
|
226
|
+
content.append(" Reject ", style=APPROVAL_COLORS["reject"])
|
|
227
|
+
content.append("[Shift+R]", style=f"bold {APPROVAL_COLORS['reject']}")
|
|
228
|
+
content.append(" Never ", style="dim")
|
|
229
|
+
content.append("[V]", style="bold cyan")
|
|
230
|
+
content.append(" View Diff", style="cyan")
|
|
231
|
+
|
|
232
|
+
# Determine border color based on danger level
|
|
233
|
+
border_colors = ["#22c55e", "#f59e0b", "#f97316", "#ef4444"]
|
|
234
|
+
border_color = border_colors[min(request.danger_level, 3)]
|
|
235
|
+
|
|
236
|
+
console.print(
|
|
237
|
+
Panel(
|
|
238
|
+
content,
|
|
239
|
+
title=header,
|
|
240
|
+
title_align="left",
|
|
241
|
+
border_style=border_color,
|
|
242
|
+
box=ROUNDED,
|
|
243
|
+
padding=(1, 2),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def render_approval_list(requests: List[ApprovalRequest], console: Console) -> None:
|
|
249
|
+
"""Render a list of approval requests."""
|
|
250
|
+
if not requests:
|
|
251
|
+
console.print(" [dim]No pending approvals[/dim]")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Summary header
|
|
255
|
+
pending = sum(1 for r in requests if r.action == ApprovalAction.PENDING)
|
|
256
|
+
|
|
257
|
+
header = Text()
|
|
258
|
+
header.append(f" {APPROVAL_ICONS['pending']} ", style="bold")
|
|
259
|
+
header.append(f"{pending} Pending Approval(s)", style="bold white")
|
|
260
|
+
|
|
261
|
+
console.print(
|
|
262
|
+
Panel(header, border_style=APPROVAL_COLORS["header"], box=ROUNDED, padding=(0, 1))
|
|
263
|
+
)
|
|
264
|
+
console.print()
|
|
265
|
+
|
|
266
|
+
# Render each request
|
|
267
|
+
for i, request in enumerate(requests):
|
|
268
|
+
if request.action == ApprovalAction.PENDING:
|
|
269
|
+
console.print(f" [dim]#{i + 1}[/dim]")
|
|
270
|
+
render_approval_request(request, console)
|
|
271
|
+
console.print()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def render_approval_summary(manager: ApprovalManager, console: Console) -> None:
|
|
275
|
+
"""Render a summary of approval history."""
|
|
276
|
+
if not manager.history:
|
|
277
|
+
console.print(" [dim]No approval history[/dim]")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
table = Table(
|
|
281
|
+
show_header=True,
|
|
282
|
+
header_style="bold magenta",
|
|
283
|
+
box=ROUNDED,
|
|
284
|
+
border_style=APPROVAL_COLORS["border"],
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
table.add_column("Status", width=10)
|
|
288
|
+
table.add_column("Type", width=8)
|
|
289
|
+
table.add_column("Target", style="cyan")
|
|
290
|
+
table.add_column("Time", style="dim", width=10)
|
|
291
|
+
|
|
292
|
+
for req in manager.history[-10:]: # Last 10
|
|
293
|
+
# Status
|
|
294
|
+
if req.action in (ApprovalAction.APPROVED, ApprovalAction.APPROVED_ALWAYS):
|
|
295
|
+
status = f"[green]✅ Yes[/green]"
|
|
296
|
+
else:
|
|
297
|
+
status = f"[red]❌ No[/red]"
|
|
298
|
+
|
|
299
|
+
# Type
|
|
300
|
+
req_type = "Cmd" if req.command else "File"
|
|
301
|
+
|
|
302
|
+
# Target
|
|
303
|
+
target = req.command[:30] if req.command else (req.file_path or "Unknown")
|
|
304
|
+
if len(target) > 30:
|
|
305
|
+
target = target[:27] + "..."
|
|
306
|
+
|
|
307
|
+
# Time
|
|
308
|
+
time_str = req.timestamp.strftime("%H:%M:%S")
|
|
309
|
+
|
|
310
|
+
table.add_row(status, req_type, target, time_str)
|
|
311
|
+
|
|
312
|
+
console.print(table)
|
superqode/atomic.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Atomic File Operations - Safe File Writing
|
|
3
|
+
|
|
4
|
+
Provides atomic file operations to prevent data corruption:
|
|
5
|
+
- Writes to temp file first, then renames
|
|
6
|
+
- Supports undo/rollback
|
|
7
|
+
- Tracks file history for recovery
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import tempfile
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, List, Dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AtomicWriteError(Exception):
|
|
22
|
+
"""An atomic write operation failed."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AtomicReadError(Exception):
|
|
28
|
+
"""An atomic read operation failed."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FileVersion:
|
|
35
|
+
"""A version of a file for history tracking."""
|
|
36
|
+
|
|
37
|
+
path: str
|
|
38
|
+
content: str
|
|
39
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
40
|
+
operation: str = "write" # "write", "create", "delete", "modify"
|
|
41
|
+
backup_path: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class FileChange:
|
|
46
|
+
"""A pending file change."""
|
|
47
|
+
|
|
48
|
+
path: str
|
|
49
|
+
old_content: Optional[str]
|
|
50
|
+
new_content: str
|
|
51
|
+
operation: str = "write"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AtomicFileManager:
|
|
55
|
+
"""Manages atomic file operations with history and undo support."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, project_dir: str = "."):
|
|
58
|
+
self.project_dir = Path(project_dir).resolve()
|
|
59
|
+
self.history: List[FileVersion] = []
|
|
60
|
+
self.pending_changes: List[FileChange] = []
|
|
61
|
+
self.max_history = 50
|
|
62
|
+
self._backup_dir: Optional[Path] = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def backup_dir(self) -> Path:
|
|
66
|
+
"""Get or create the backup directory."""
|
|
67
|
+
if self._backup_dir is None:
|
|
68
|
+
self._backup_dir = self.project_dir / ".superqode" / "backups"
|
|
69
|
+
self._backup_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
return self._backup_dir
|
|
71
|
+
|
|
72
|
+
def read(self, path: str) -> str:
|
|
73
|
+
"""Read a file safely."""
|
|
74
|
+
file_path = self._resolve_path(path)
|
|
75
|
+
try:
|
|
76
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
77
|
+
return f.read()
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
raise AtomicReadError(f"File not found: {path}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise AtomicReadError(f"Failed to read {path}: {e}")
|
|
82
|
+
|
|
83
|
+
def write(self, path: str, content: str, create_backup: bool = True) -> FileVersion:
|
|
84
|
+
"""Write a file atomically with optional backup."""
|
|
85
|
+
file_path = self._resolve_path(path)
|
|
86
|
+
dir_path = file_path.parent
|
|
87
|
+
|
|
88
|
+
# Read existing content for backup
|
|
89
|
+
old_content = None
|
|
90
|
+
if file_path.exists() and create_backup:
|
|
91
|
+
try:
|
|
92
|
+
old_content = self.read(path)
|
|
93
|
+
except AtomicReadError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Create directory if needed
|
|
97
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# Create backup if file exists
|
|
100
|
+
backup_path = None
|
|
101
|
+
if old_content is not None:
|
|
102
|
+
backup_path = self._create_backup(path, old_content)
|
|
103
|
+
|
|
104
|
+
# Write to temp file first
|
|
105
|
+
try:
|
|
106
|
+
with tempfile.NamedTemporaryFile(
|
|
107
|
+
mode="w",
|
|
108
|
+
encoding="utf-8",
|
|
109
|
+
delete=False,
|
|
110
|
+
dir=str(dir_path),
|
|
111
|
+
prefix=f".{file_path.name}_tmp_",
|
|
112
|
+
suffix=".tmp",
|
|
113
|
+
) as tmp_file:
|
|
114
|
+
tmp_file.write(content)
|
|
115
|
+
temp_path = tmp_file.name
|
|
116
|
+
except (OSError, IOError) as e:
|
|
117
|
+
raise AtomicWriteError(f"Failed to create temp file for {path}: {e}")
|
|
118
|
+
|
|
119
|
+
# Atomic rename
|
|
120
|
+
try:
|
|
121
|
+
os.replace(temp_path, str(file_path))
|
|
122
|
+
except OSError as e:
|
|
123
|
+
# Clean up temp file
|
|
124
|
+
try:
|
|
125
|
+
os.unlink(temp_path)
|
|
126
|
+
except OSError:
|
|
127
|
+
pass # Best effort cleanup
|
|
128
|
+
raise AtomicWriteError(f"Failed to write {path}: {e}")
|
|
129
|
+
|
|
130
|
+
# Record in history
|
|
131
|
+
operation = "create" if old_content is None else "modify"
|
|
132
|
+
version = FileVersion(
|
|
133
|
+
path=path, content=old_content or "", operation=operation, backup_path=backup_path
|
|
134
|
+
)
|
|
135
|
+
self._add_to_history(version)
|
|
136
|
+
|
|
137
|
+
return version
|
|
138
|
+
|
|
139
|
+
def delete(self, path: str, create_backup: bool = True) -> FileVersion:
|
|
140
|
+
"""Delete a file with backup."""
|
|
141
|
+
file_path = self._resolve_path(path)
|
|
142
|
+
|
|
143
|
+
if not file_path.exists():
|
|
144
|
+
raise AtomicWriteError(f"File not found: {path}")
|
|
145
|
+
|
|
146
|
+
# Read content for backup
|
|
147
|
+
old_content = self.read(path)
|
|
148
|
+
|
|
149
|
+
# Create backup
|
|
150
|
+
backup_path = None
|
|
151
|
+
if create_backup:
|
|
152
|
+
backup_path = self._create_backup(path, old_content)
|
|
153
|
+
|
|
154
|
+
# Delete file
|
|
155
|
+
try:
|
|
156
|
+
os.unlink(str(file_path))
|
|
157
|
+
except Exception as e:
|
|
158
|
+
raise AtomicWriteError(f"Failed to delete {path}: {e}")
|
|
159
|
+
|
|
160
|
+
# Record in history
|
|
161
|
+
version = FileVersion(
|
|
162
|
+
path=path, content=old_content, operation="delete", backup_path=backup_path
|
|
163
|
+
)
|
|
164
|
+
self._add_to_history(version)
|
|
165
|
+
|
|
166
|
+
return version
|
|
167
|
+
|
|
168
|
+
def undo(self) -> Optional[FileVersion]:
|
|
169
|
+
"""Undo the last file operation."""
|
|
170
|
+
if not self.history:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
version = self.history.pop()
|
|
174
|
+
file_path = self._resolve_path(version.path)
|
|
175
|
+
|
|
176
|
+
if version.operation == "create":
|
|
177
|
+
# Undo create = delete the file
|
|
178
|
+
if file_path.exists():
|
|
179
|
+
os.unlink(str(file_path))
|
|
180
|
+
elif version.operation == "delete":
|
|
181
|
+
# Undo delete = restore from backup
|
|
182
|
+
if version.backup_path and Path(version.backup_path).exists():
|
|
183
|
+
shutil.copy2(version.backup_path, str(file_path))
|
|
184
|
+
elif version.content:
|
|
185
|
+
self.write(version.path, version.content, create_backup=False)
|
|
186
|
+
elif version.operation == "modify":
|
|
187
|
+
# Undo modify = restore previous content
|
|
188
|
+
if version.backup_path and Path(version.backup_path).exists():
|
|
189
|
+
shutil.copy2(version.backup_path, str(file_path))
|
|
190
|
+
elif version.content:
|
|
191
|
+
self.write(version.path, version.content, create_backup=False)
|
|
192
|
+
|
|
193
|
+
return version
|
|
194
|
+
|
|
195
|
+
def stage_change(self, path: str, new_content: str) -> FileChange:
|
|
196
|
+
"""Stage a file change without applying it."""
|
|
197
|
+
old_content = None
|
|
198
|
+
try:
|
|
199
|
+
old_content = self.read(path)
|
|
200
|
+
except AtomicReadError:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
change = FileChange(
|
|
204
|
+
path=path,
|
|
205
|
+
old_content=old_content,
|
|
206
|
+
new_content=new_content,
|
|
207
|
+
operation="create" if old_content is None else "modify",
|
|
208
|
+
)
|
|
209
|
+
self.pending_changes.append(change)
|
|
210
|
+
return change
|
|
211
|
+
|
|
212
|
+
def apply_staged(self) -> List[FileVersion]:
|
|
213
|
+
"""Apply all staged changes."""
|
|
214
|
+
versions = []
|
|
215
|
+
for change in self.pending_changes:
|
|
216
|
+
version = self.write(change.path, change.new_content)
|
|
217
|
+
versions.append(version)
|
|
218
|
+
self.pending_changes.clear()
|
|
219
|
+
return versions
|
|
220
|
+
|
|
221
|
+
def discard_staged(self) -> int:
|
|
222
|
+
"""Discard all staged changes."""
|
|
223
|
+
count = len(self.pending_changes)
|
|
224
|
+
self.pending_changes.clear()
|
|
225
|
+
return count
|
|
226
|
+
|
|
227
|
+
def get_history(self, limit: int = 10) -> List[FileVersion]:
|
|
228
|
+
"""Get recent file history."""
|
|
229
|
+
return self.history[-limit:]
|
|
230
|
+
|
|
231
|
+
def _resolve_path(self, path: str) -> Path:
|
|
232
|
+
"""Resolve a path relative to project directory."""
|
|
233
|
+
p = Path(path)
|
|
234
|
+
if p.is_absolute():
|
|
235
|
+
return p
|
|
236
|
+
return self.project_dir / p
|
|
237
|
+
|
|
238
|
+
def _create_backup(self, path: str, content: str) -> str:
|
|
239
|
+
"""Create a backup of file content."""
|
|
240
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
241
|
+
safe_name = Path(path).name.replace("/", "_").replace("\\", "_")
|
|
242
|
+
backup_name = f"{safe_name}.{timestamp}.bak"
|
|
243
|
+
backup_path = self.backup_dir / backup_name
|
|
244
|
+
|
|
245
|
+
with open(backup_path, "w", encoding="utf-8") as f:
|
|
246
|
+
f.write(content)
|
|
247
|
+
|
|
248
|
+
return str(backup_path)
|
|
249
|
+
|
|
250
|
+
def _add_to_history(self, version: FileVersion) -> None:
|
|
251
|
+
"""Add a version to history, maintaining max size."""
|
|
252
|
+
self.history.append(version)
|
|
253
|
+
if len(self.history) > self.max_history:
|
|
254
|
+
# Remove oldest entries
|
|
255
|
+
self.history = self.history[-self.max_history :]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# Convenience functions
|
|
259
|
+
def atomic_write(path: str, content: str) -> None:
|
|
260
|
+
"""Write a file atomically (simple interface)."""
|
|
261
|
+
file_path = Path(path).resolve()
|
|
262
|
+
dir_path = file_path.parent
|
|
263
|
+
|
|
264
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
with tempfile.NamedTemporaryFile(
|
|
268
|
+
mode="w",
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
delete=False,
|
|
271
|
+
dir=str(dir_path),
|
|
272
|
+
prefix=f".{file_path.name}_tmp_",
|
|
273
|
+
suffix=".tmp",
|
|
274
|
+
) as tmp_file:
|
|
275
|
+
tmp_file.write(content)
|
|
276
|
+
temp_path = tmp_file.name
|
|
277
|
+
except Exception as e:
|
|
278
|
+
raise AtomicWriteError(f"Failed to create temp file: {e}")
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
os.replace(temp_path, str(file_path))
|
|
282
|
+
except OSError as e:
|
|
283
|
+
try:
|
|
284
|
+
os.unlink(temp_path)
|
|
285
|
+
except OSError:
|
|
286
|
+
pass # Best effort cleanup
|
|
287
|
+
raise AtomicWriteError(f"Failed to write file: {e}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def atomic_read(path: str) -> str:
|
|
291
|
+
"""Read a file safely."""
|
|
292
|
+
try:
|
|
293
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
294
|
+
return f.read()
|
|
295
|
+
except Exception as e:
|
|
296
|
+
raise AtomicReadError(f"Failed to read file: {e}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SuperQode CLI commands package."""
|