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,574 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Undo Manager - Git-Based Undo/Redo System.
|
|
3
|
+
|
|
4
|
+
Provides reliable undo/redo functionality using Git's object database
|
|
5
|
+
for tracking file changes. Each operation creates a checkpoint that
|
|
6
|
+
can be restored.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Automatic checkpoint creation before agent operations
|
|
10
|
+
- Named checkpoints for easier navigation
|
|
11
|
+
- Restore specific files or entire state
|
|
12
|
+
- View diff between checkpoints
|
|
13
|
+
- Stack-based undo/redo
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from superqode.undo_manager import UndoManager
|
|
17
|
+
|
|
18
|
+
undo = UndoManager()
|
|
19
|
+
|
|
20
|
+
# Before agent operation
|
|
21
|
+
checkpoint_id = undo.create_checkpoint("Before edit")
|
|
22
|
+
|
|
23
|
+
# After operation, if user wants to undo
|
|
24
|
+
undo.undo()
|
|
25
|
+
|
|
26
|
+
# Or restore a specific checkpoint
|
|
27
|
+
undo.restore(checkpoint_id)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
import os
|
|
34
|
+
import subprocess
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# DATA CLASSES
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Checkpoint:
|
|
48
|
+
"""A checkpoint representing a point in time."""
|
|
49
|
+
|
|
50
|
+
id: str # Git commit or stash reference
|
|
51
|
+
name: str
|
|
52
|
+
timestamp: datetime
|
|
53
|
+
message: str = ""
|
|
54
|
+
files_changed: List[str] = field(default_factory=list)
|
|
55
|
+
is_stash: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class FileChange:
|
|
60
|
+
"""A file change between checkpoints."""
|
|
61
|
+
|
|
62
|
+
path: str
|
|
63
|
+
change_type: str # "added", "modified", "deleted"
|
|
64
|
+
old_content: str = ""
|
|
65
|
+
new_content: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# UNDO MANAGER
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class UndoManager:
|
|
74
|
+
"""
|
|
75
|
+
Git-based undo/redo manager.
|
|
76
|
+
|
|
77
|
+
Uses Git's stash and commit system to create reliable checkpoints
|
|
78
|
+
that can be restored.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, working_dir: Optional[Path] = None):
|
|
82
|
+
self.working_dir = working_dir or Path.cwd()
|
|
83
|
+
self._checkpoints: List[Checkpoint] = []
|
|
84
|
+
self._redo_stack: List[Checkpoint] = []
|
|
85
|
+
self._current_index: int = -1
|
|
86
|
+
self._initialized = False
|
|
87
|
+
|
|
88
|
+
# ========================================================================
|
|
89
|
+
# INITIALIZATION
|
|
90
|
+
# ========================================================================
|
|
91
|
+
|
|
92
|
+
def initialize(self) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Initialize the undo manager.
|
|
95
|
+
|
|
96
|
+
Checks if we're in a git repo and sets up tracking.
|
|
97
|
+
Returns True if successful.
|
|
98
|
+
"""
|
|
99
|
+
if self._initialized:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
# Check if git is available
|
|
103
|
+
try:
|
|
104
|
+
result = self._run_git(["rev-parse", "--git-dir"])
|
|
105
|
+
if result.returncode != 0:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
self._initialized = True
|
|
109
|
+
return True
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def _run_git(self, args: List[str], capture_output: bool = True) -> subprocess.CompletedProcess:
|
|
114
|
+
"""Run a git command."""
|
|
115
|
+
cmd = ["git"] + args
|
|
116
|
+
return subprocess.run(
|
|
117
|
+
cmd,
|
|
118
|
+
cwd=str(self.working_dir),
|
|
119
|
+
capture_output=capture_output,
|
|
120
|
+
text=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ========================================================================
|
|
124
|
+
# CHECKPOINT CREATION
|
|
125
|
+
# ========================================================================
|
|
126
|
+
|
|
127
|
+
def create_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
|
|
128
|
+
"""
|
|
129
|
+
Create a checkpoint of the current state.
|
|
130
|
+
|
|
131
|
+
Uses git stash to save changes without affecting the working tree.
|
|
132
|
+
Returns the checkpoint ID if successful.
|
|
133
|
+
"""
|
|
134
|
+
if not self.initialize():
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# Get list of changed files
|
|
139
|
+
status = self._run_git(["status", "--porcelain"])
|
|
140
|
+
changed_files = []
|
|
141
|
+
for line in status.stdout.strip().split("\n"):
|
|
142
|
+
if line.strip():
|
|
143
|
+
# Parse status line: "XY filename"
|
|
144
|
+
parts = line.split(maxsplit=1)
|
|
145
|
+
if len(parts) >= 2:
|
|
146
|
+
changed_files.append(parts[1].strip('"'))
|
|
147
|
+
|
|
148
|
+
if not changed_files:
|
|
149
|
+
# No changes to checkpoint
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
# Create a stash with all changes (including untracked)
|
|
153
|
+
stash_msg = f"superqode-checkpoint: {name or 'Checkpoint'}"
|
|
154
|
+
if message:
|
|
155
|
+
stash_msg += f" - {message}"
|
|
156
|
+
|
|
157
|
+
# Stage all changes including untracked
|
|
158
|
+
self._run_git(["add", "-A"])
|
|
159
|
+
|
|
160
|
+
# Create stash
|
|
161
|
+
result = self._run_git(["stash", "push", "-m", stash_msg, "--include-untracked"])
|
|
162
|
+
|
|
163
|
+
if result.returncode != 0:
|
|
164
|
+
# Unstage changes
|
|
165
|
+
self._run_git(["reset"])
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Get the stash reference
|
|
169
|
+
stash_list = self._run_git(["stash", "list", "-1"])
|
|
170
|
+
if not stash_list.stdout.strip():
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# Parse stash reference (e.g., "stash@{0}: ...")
|
|
174
|
+
stash_ref = stash_list.stdout.split(":")[0].strip()
|
|
175
|
+
|
|
176
|
+
# Immediately restore working directory (we just want the checkpoint)
|
|
177
|
+
self._run_git(["stash", "pop", "--quiet"])
|
|
178
|
+
|
|
179
|
+
# Create checkpoint record
|
|
180
|
+
checkpoint = Checkpoint(
|
|
181
|
+
id=stash_ref,
|
|
182
|
+
name=name or f"Checkpoint {len(self._checkpoints) + 1}",
|
|
183
|
+
timestamp=datetime.now(),
|
|
184
|
+
message=message,
|
|
185
|
+
files_changed=changed_files,
|
|
186
|
+
is_stash=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Clear redo stack when creating new checkpoint
|
|
190
|
+
self._redo_stack.clear()
|
|
191
|
+
|
|
192
|
+
self._checkpoints.append(checkpoint)
|
|
193
|
+
self._current_index = len(self._checkpoints) - 1
|
|
194
|
+
|
|
195
|
+
return checkpoint.id
|
|
196
|
+
|
|
197
|
+
except Exception:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def create_commit_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
|
|
201
|
+
"""
|
|
202
|
+
Create a checkpoint using a commit.
|
|
203
|
+
|
|
204
|
+
More permanent than stash-based checkpoints.
|
|
205
|
+
"""
|
|
206
|
+
if not self.initialize():
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Get changed files
|
|
211
|
+
status = self._run_git(["status", "--porcelain"])
|
|
212
|
+
changed_files = []
|
|
213
|
+
for line in status.stdout.strip().split("\n"):
|
|
214
|
+
if line.strip():
|
|
215
|
+
parts = line.split(maxsplit=1)
|
|
216
|
+
if len(parts) >= 2:
|
|
217
|
+
changed_files.append(parts[1].strip('"'))
|
|
218
|
+
|
|
219
|
+
if not changed_files:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# Stage all changes
|
|
223
|
+
self._run_git(["add", "-A"])
|
|
224
|
+
|
|
225
|
+
# Create commit
|
|
226
|
+
commit_msg = f"[superqode] {name or 'Checkpoint'}"
|
|
227
|
+
if message:
|
|
228
|
+
commit_msg += f": {message}"
|
|
229
|
+
|
|
230
|
+
result = self._run_git(["commit", "-m", commit_msg])
|
|
231
|
+
if result.returncode != 0:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# Get commit hash
|
|
235
|
+
hash_result = self._run_git(["rev-parse", "HEAD"])
|
|
236
|
+
commit_hash = hash_result.stdout.strip()[:8]
|
|
237
|
+
|
|
238
|
+
checkpoint = Checkpoint(
|
|
239
|
+
id=commit_hash,
|
|
240
|
+
name=name or f"Checkpoint {len(self._checkpoints) + 1}",
|
|
241
|
+
timestamp=datetime.now(),
|
|
242
|
+
message=message,
|
|
243
|
+
files_changed=changed_files,
|
|
244
|
+
is_stash=False,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self._redo_stack.clear()
|
|
248
|
+
self._checkpoints.append(checkpoint)
|
|
249
|
+
self._current_index = len(self._checkpoints) - 1
|
|
250
|
+
|
|
251
|
+
return commit_hash
|
|
252
|
+
|
|
253
|
+
except Exception:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
# ========================================================================
|
|
257
|
+
# UNDO / REDO
|
|
258
|
+
# ========================================================================
|
|
259
|
+
|
|
260
|
+
def undo(self) -> Optional[Checkpoint]:
|
|
261
|
+
"""
|
|
262
|
+
Undo to the previous checkpoint.
|
|
263
|
+
|
|
264
|
+
Returns the checkpoint that was restored, or None if nothing to undo.
|
|
265
|
+
"""
|
|
266
|
+
if not self._checkpoints or self._current_index < 0:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
# Save current state to redo stack
|
|
271
|
+
current_state = self._capture_current_state()
|
|
272
|
+
if current_state:
|
|
273
|
+
self._redo_stack.append(current_state)
|
|
274
|
+
|
|
275
|
+
# Get checkpoint to restore
|
|
276
|
+
checkpoint = self._checkpoints[self._current_index]
|
|
277
|
+
|
|
278
|
+
# Restore based on type
|
|
279
|
+
if checkpoint.is_stash:
|
|
280
|
+
# For stash-based, we need to reverse the changes
|
|
281
|
+
# This is tricky - we'll use git checkout
|
|
282
|
+
for file_path in checkpoint.files_changed:
|
|
283
|
+
self._run_git(["checkout", "HEAD", "--", file_path])
|
|
284
|
+
else:
|
|
285
|
+
# For commit-based, reset to previous commit
|
|
286
|
+
if self._current_index > 0:
|
|
287
|
+
prev_checkpoint = self._checkpoints[self._current_index - 1]
|
|
288
|
+
self._run_git(["reset", "--hard", prev_checkpoint.id])
|
|
289
|
+
|
|
290
|
+
self._current_index -= 1
|
|
291
|
+
return checkpoint
|
|
292
|
+
|
|
293
|
+
except Exception:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def redo(self) -> Optional[Checkpoint]:
|
|
297
|
+
"""
|
|
298
|
+
Redo the previously undone checkpoint.
|
|
299
|
+
|
|
300
|
+
Returns the checkpoint that was restored, or None if nothing to redo.
|
|
301
|
+
"""
|
|
302
|
+
if not self._redo_stack:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
checkpoint = self._redo_stack.pop()
|
|
307
|
+
|
|
308
|
+
# Apply the changes
|
|
309
|
+
for file_path in checkpoint.files_changed:
|
|
310
|
+
# This is simplified - full implementation would restore content
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
self._current_index += 1
|
|
314
|
+
return checkpoint
|
|
315
|
+
|
|
316
|
+
except Exception:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def _capture_current_state(self) -> Optional[Checkpoint]:
|
|
320
|
+
"""Capture the current state as a checkpoint for redo."""
|
|
321
|
+
try:
|
|
322
|
+
status = self._run_git(["status", "--porcelain"])
|
|
323
|
+
changed_files = []
|
|
324
|
+
for line in status.stdout.strip().split("\n"):
|
|
325
|
+
if line.strip():
|
|
326
|
+
parts = line.split(maxsplit=1)
|
|
327
|
+
if len(parts) >= 2:
|
|
328
|
+
changed_files.append(parts[1].strip('"'))
|
|
329
|
+
|
|
330
|
+
return Checkpoint(
|
|
331
|
+
id="current",
|
|
332
|
+
name="Current state",
|
|
333
|
+
timestamp=datetime.now(),
|
|
334
|
+
files_changed=changed_files,
|
|
335
|
+
)
|
|
336
|
+
except Exception:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
# ========================================================================
|
|
340
|
+
# RESTORE
|
|
341
|
+
# ========================================================================
|
|
342
|
+
|
|
343
|
+
def restore(self, checkpoint_id: str) -> bool:
|
|
344
|
+
"""
|
|
345
|
+
Restore to a specific checkpoint.
|
|
346
|
+
|
|
347
|
+
Returns True if successful.
|
|
348
|
+
"""
|
|
349
|
+
if not self.initialize():
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
# Find the checkpoint
|
|
353
|
+
checkpoint = None
|
|
354
|
+
index = -1
|
|
355
|
+
for i, cp in enumerate(self._checkpoints):
|
|
356
|
+
if cp.id == checkpoint_id:
|
|
357
|
+
checkpoint = cp
|
|
358
|
+
index = i
|
|
359
|
+
break
|
|
360
|
+
|
|
361
|
+
if not checkpoint:
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
if checkpoint.is_stash:
|
|
366
|
+
# Find and apply the stash
|
|
367
|
+
result = self._run_git(["stash", "apply", checkpoint.id])
|
|
368
|
+
return result.returncode == 0
|
|
369
|
+
else:
|
|
370
|
+
# Reset to commit
|
|
371
|
+
result = self._run_git(["reset", "--hard", checkpoint.id])
|
|
372
|
+
if result.returncode == 0:
|
|
373
|
+
self._current_index = index
|
|
374
|
+
return True
|
|
375
|
+
return False
|
|
376
|
+
except Exception:
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
def restore_file(self, checkpoint_id: str, file_path: str) -> bool:
|
|
380
|
+
"""
|
|
381
|
+
Restore a specific file from a checkpoint.
|
|
382
|
+
|
|
383
|
+
Returns True if successful.
|
|
384
|
+
"""
|
|
385
|
+
if not self.initialize():
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
# Find the checkpoint
|
|
389
|
+
checkpoint = None
|
|
390
|
+
for cp in self._checkpoints:
|
|
391
|
+
if cp.id == checkpoint_id:
|
|
392
|
+
checkpoint = cp
|
|
393
|
+
break
|
|
394
|
+
|
|
395
|
+
if not checkpoint:
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
if checkpoint.is_stash:
|
|
400
|
+
# Restore file from stash
|
|
401
|
+
result = self._run_git(["checkout", checkpoint.id, "--", file_path])
|
|
402
|
+
else:
|
|
403
|
+
# Restore file from commit
|
|
404
|
+
result = self._run_git(["checkout", checkpoint.id, "--", file_path])
|
|
405
|
+
|
|
406
|
+
return result.returncode == 0
|
|
407
|
+
except Exception:
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
# ========================================================================
|
|
411
|
+
# QUERY
|
|
412
|
+
# ========================================================================
|
|
413
|
+
|
|
414
|
+
def get_checkpoints(self, limit: int = 20) -> List[Checkpoint]:
|
|
415
|
+
"""Get list of checkpoints."""
|
|
416
|
+
return self._checkpoints[-limit:]
|
|
417
|
+
|
|
418
|
+
def get_current_checkpoint(self) -> Optional[Checkpoint]:
|
|
419
|
+
"""Get the current checkpoint."""
|
|
420
|
+
if 0 <= self._current_index < len(self._checkpoints):
|
|
421
|
+
return self._checkpoints[self._current_index]
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
def get_changes_since(self, checkpoint_id: str) -> List[FileChange]:
|
|
425
|
+
"""Get list of changes since a checkpoint."""
|
|
426
|
+
if not self.initialize():
|
|
427
|
+
return []
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
# Get diff
|
|
431
|
+
result = self._run_git(["diff", checkpoint_id, "--name-status"])
|
|
432
|
+
|
|
433
|
+
changes = []
|
|
434
|
+
for line in result.stdout.strip().split("\n"):
|
|
435
|
+
if not line.strip():
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
parts = line.split("\t")
|
|
439
|
+
if len(parts) >= 2:
|
|
440
|
+
status = parts[0]
|
|
441
|
+
path = parts[1]
|
|
442
|
+
|
|
443
|
+
change_type = {
|
|
444
|
+
"A": "added",
|
|
445
|
+
"M": "modified",
|
|
446
|
+
"D": "deleted",
|
|
447
|
+
}.get(status[0], "modified")
|
|
448
|
+
|
|
449
|
+
changes.append(
|
|
450
|
+
FileChange(
|
|
451
|
+
path=path,
|
|
452
|
+
change_type=change_type,
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return changes
|
|
457
|
+
|
|
458
|
+
except Exception:
|
|
459
|
+
return []
|
|
460
|
+
|
|
461
|
+
def get_file_diff(self, checkpoint_id: str, file_path: str) -> Tuple[str, str]:
|
|
462
|
+
"""
|
|
463
|
+
Get the old and new content of a file relative to checkpoint.
|
|
464
|
+
|
|
465
|
+
Returns (old_content, new_content).
|
|
466
|
+
"""
|
|
467
|
+
if not self.initialize():
|
|
468
|
+
return ("", "")
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
# Get old content
|
|
472
|
+
old_result = self._run_git(["show", f"{checkpoint_id}:{file_path}"])
|
|
473
|
+
old_content = old_result.stdout if old_result.returncode == 0 else ""
|
|
474
|
+
|
|
475
|
+
# Get current content
|
|
476
|
+
file_path_obj = self.working_dir / file_path
|
|
477
|
+
if file_path_obj.exists():
|
|
478
|
+
new_content = file_path_obj.read_text(encoding="utf-8", errors="ignore")
|
|
479
|
+
else:
|
|
480
|
+
new_content = ""
|
|
481
|
+
|
|
482
|
+
return (old_content, new_content)
|
|
483
|
+
|
|
484
|
+
except Exception:
|
|
485
|
+
return ("", "")
|
|
486
|
+
|
|
487
|
+
def can_undo(self) -> bool:
|
|
488
|
+
"""Check if undo is possible."""
|
|
489
|
+
return len(self._checkpoints) > 0 and self._current_index >= 0
|
|
490
|
+
|
|
491
|
+
def can_redo(self) -> bool:
|
|
492
|
+
"""Check if redo is possible."""
|
|
493
|
+
return len(self._redo_stack) > 0
|
|
494
|
+
|
|
495
|
+
# ========================================================================
|
|
496
|
+
# CLEANUP
|
|
497
|
+
# ========================================================================
|
|
498
|
+
|
|
499
|
+
def clear_old_checkpoints(self, keep_count: int = 50) -> int:
|
|
500
|
+
"""
|
|
501
|
+
Clear old checkpoints to save space.
|
|
502
|
+
|
|
503
|
+
Returns number of checkpoints cleared.
|
|
504
|
+
"""
|
|
505
|
+
if len(self._checkpoints) <= keep_count:
|
|
506
|
+
return 0
|
|
507
|
+
|
|
508
|
+
to_remove = self._checkpoints[:-keep_count]
|
|
509
|
+
removed = 0
|
|
510
|
+
|
|
511
|
+
for checkpoint in to_remove:
|
|
512
|
+
if checkpoint.is_stash:
|
|
513
|
+
# Drop the stash
|
|
514
|
+
try:
|
|
515
|
+
self._run_git(["stash", "drop", checkpoint.id])
|
|
516
|
+
removed += 1
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
self._checkpoints = self._checkpoints[-keep_count:]
|
|
521
|
+
self._current_index = min(self._current_index, len(self._checkpoints) - 1)
|
|
522
|
+
|
|
523
|
+
return removed
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ============================================================================
|
|
527
|
+
# ASYNC VERSION
|
|
528
|
+
# ============================================================================
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class AsyncUndoManager:
|
|
532
|
+
"""Async wrapper for UndoManager."""
|
|
533
|
+
|
|
534
|
+
def __init__(self, working_dir: Optional[Path] = None):
|
|
535
|
+
self._sync_manager = UndoManager(working_dir)
|
|
536
|
+
|
|
537
|
+
async def initialize(self) -> bool:
|
|
538
|
+
"""Initialize the undo manager."""
|
|
539
|
+
loop = asyncio.get_event_loop()
|
|
540
|
+
return await loop.run_in_executor(None, self._sync_manager.initialize)
|
|
541
|
+
|
|
542
|
+
async def create_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
|
|
543
|
+
"""Create a checkpoint."""
|
|
544
|
+
loop = asyncio.get_event_loop()
|
|
545
|
+
return await loop.run_in_executor(
|
|
546
|
+
None, lambda: self._sync_manager.create_checkpoint(name, message)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def undo(self) -> Optional[Checkpoint]:
|
|
550
|
+
"""Undo to previous checkpoint."""
|
|
551
|
+
loop = asyncio.get_event_loop()
|
|
552
|
+
return await loop.run_in_executor(None, self._sync_manager.undo)
|
|
553
|
+
|
|
554
|
+
async def redo(self) -> Optional[Checkpoint]:
|
|
555
|
+
"""Redo previously undone checkpoint."""
|
|
556
|
+
loop = asyncio.get_event_loop()
|
|
557
|
+
return await loop.run_in_executor(None, self._sync_manager.redo)
|
|
558
|
+
|
|
559
|
+
async def restore(self, checkpoint_id: str) -> bool:
|
|
560
|
+
"""Restore to a specific checkpoint."""
|
|
561
|
+
loop = asyncio.get_event_loop()
|
|
562
|
+
return await loop.run_in_executor(None, lambda: self._sync_manager.restore(checkpoint_id))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# ============================================================================
|
|
566
|
+
# EXPORTS
|
|
567
|
+
# ============================================================================
|
|
568
|
+
|
|
569
|
+
__all__ = [
|
|
570
|
+
"Checkpoint",
|
|
571
|
+
"FileChange",
|
|
572
|
+
"UndoManager",
|
|
573
|
+
"AsyncUndoManager",
|
|
574
|
+
]
|