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,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snapshot Manager for Ephemeral Workspace.
|
|
3
|
+
|
|
4
|
+
Captures the state of modified files and enables full reversion
|
|
5
|
+
after QE session completes. Uses efficient in-memory tracking
|
|
6
|
+
with disk backup for large files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Optional, Set
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class FileSnapshot:
|
|
23
|
+
"""Snapshot of a single file's state."""
|
|
24
|
+
|
|
25
|
+
path: Path
|
|
26
|
+
original_content: Optional[bytes] # None if file didn't exist
|
|
27
|
+
original_hash: Optional[str]
|
|
28
|
+
existed: bool
|
|
29
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def was_created(self) -> bool:
|
|
33
|
+
"""True if this file was created (didn't exist before)."""
|
|
34
|
+
return not self.existed
|
|
35
|
+
|
|
36
|
+
def content_changed(self, current_content: bytes) -> bool:
|
|
37
|
+
"""Check if content has changed from original."""
|
|
38
|
+
if self.original_content is None:
|
|
39
|
+
return True
|
|
40
|
+
current_hash = hashlib.sha256(current_content).hexdigest()
|
|
41
|
+
return current_hash != self.original_hash
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DirectorySnapshot:
|
|
46
|
+
"""Snapshot of a directory that was created."""
|
|
47
|
+
|
|
48
|
+
path: Path
|
|
49
|
+
existed: bool
|
|
50
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SnapshotManager:
|
|
54
|
+
"""
|
|
55
|
+
Manages file snapshots for ephemeral workspace.
|
|
56
|
+
|
|
57
|
+
Tracks all file modifications during a QE session and enables
|
|
58
|
+
complete reversion to original state.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
snapshot = SnapshotManager(project_root)
|
|
62
|
+
snapshot.start_session()
|
|
63
|
+
|
|
64
|
+
# Track file before modification
|
|
65
|
+
snapshot.capture_file(Path("src/main.py"))
|
|
66
|
+
|
|
67
|
+
# ... agent modifies files ...
|
|
68
|
+
|
|
69
|
+
# Revert everything
|
|
70
|
+
snapshot.revert_all()
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Files larger than this are backed up to disk
|
|
74
|
+
LARGE_FILE_THRESHOLD = 10 * 1024 * 1024 # 10MB
|
|
75
|
+
|
|
76
|
+
def __init__(self, project_root: Path):
|
|
77
|
+
self.project_root = project_root.resolve()
|
|
78
|
+
self.session_id: Optional[str] = None
|
|
79
|
+
self.session_start: Optional[datetime] = None
|
|
80
|
+
|
|
81
|
+
# Tracking state
|
|
82
|
+
self._file_snapshots: Dict[Path, FileSnapshot] = {}
|
|
83
|
+
self._dir_snapshots: Dict[Path, DirectorySnapshot] = {}
|
|
84
|
+
self._large_file_backup_dir: Optional[Path] = None
|
|
85
|
+
|
|
86
|
+
# Statistics
|
|
87
|
+
self._files_modified: Set[Path] = set()
|
|
88
|
+
self._files_created: Set[Path] = set()
|
|
89
|
+
self._files_deleted: Set[Path] = set()
|
|
90
|
+
self._dirs_created: Set[Path] = set()
|
|
91
|
+
|
|
92
|
+
def start_session(self, session_id: Optional[str] = None) -> str:
|
|
93
|
+
"""Start a new snapshot session."""
|
|
94
|
+
if self.session_id:
|
|
95
|
+
raise RuntimeError("Session already active. Call revert_all() or end_session() first.")
|
|
96
|
+
|
|
97
|
+
self.session_id = session_id or f"qe-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
98
|
+
self.session_start = datetime.now()
|
|
99
|
+
|
|
100
|
+
# Create temp dir for large file backups
|
|
101
|
+
self._large_file_backup_dir = Path(tempfile.mkdtemp(prefix=f"superqode-{self.session_id}-"))
|
|
102
|
+
|
|
103
|
+
# Reset tracking
|
|
104
|
+
self._file_snapshots.clear()
|
|
105
|
+
self._dir_snapshots.clear()
|
|
106
|
+
self._files_modified.clear()
|
|
107
|
+
self._files_created.clear()
|
|
108
|
+
self._files_deleted.clear()
|
|
109
|
+
self._dirs_created.clear()
|
|
110
|
+
|
|
111
|
+
return self.session_id
|
|
112
|
+
|
|
113
|
+
def capture_file(self, file_path: Path) -> FileSnapshot:
|
|
114
|
+
"""
|
|
115
|
+
Capture a file's state before modification.
|
|
116
|
+
|
|
117
|
+
Call this BEFORE any modification to the file.
|
|
118
|
+
"""
|
|
119
|
+
if not self.session_id:
|
|
120
|
+
raise RuntimeError("No active session. Call start_session() first.")
|
|
121
|
+
|
|
122
|
+
abs_path = (self.project_root / file_path).resolve()
|
|
123
|
+
rel_path = abs_path.relative_to(self.project_root)
|
|
124
|
+
|
|
125
|
+
# Already captured
|
|
126
|
+
if rel_path in self._file_snapshots:
|
|
127
|
+
return self._file_snapshots[rel_path]
|
|
128
|
+
|
|
129
|
+
# Capture current state
|
|
130
|
+
if abs_path.exists() and abs_path.is_file():
|
|
131
|
+
content = abs_path.read_bytes()
|
|
132
|
+
content_hash = hashlib.sha256(content).hexdigest()
|
|
133
|
+
|
|
134
|
+
# Large files go to disk backup
|
|
135
|
+
if len(content) > self.LARGE_FILE_THRESHOLD:
|
|
136
|
+
backup_path = self._large_file_backup_dir / rel_path
|
|
137
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
shutil.copy2(abs_path, backup_path)
|
|
139
|
+
original_content = None # Don't keep in memory
|
|
140
|
+
else:
|
|
141
|
+
original_content = content
|
|
142
|
+
|
|
143
|
+
snapshot = FileSnapshot(
|
|
144
|
+
path=rel_path,
|
|
145
|
+
original_content=original_content,
|
|
146
|
+
original_hash=content_hash,
|
|
147
|
+
existed=True,
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
# File doesn't exist yet
|
|
151
|
+
snapshot = FileSnapshot(
|
|
152
|
+
path=rel_path,
|
|
153
|
+
original_content=None,
|
|
154
|
+
original_hash=None,
|
|
155
|
+
existed=False,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self._file_snapshots[rel_path] = snapshot
|
|
159
|
+
return snapshot
|
|
160
|
+
|
|
161
|
+
def capture_directory(self, dir_path: Path) -> DirectorySnapshot:
|
|
162
|
+
"""Capture a directory's existence state before creation."""
|
|
163
|
+
if not self.session_id:
|
|
164
|
+
raise RuntimeError("No active session. Call start_session() first.")
|
|
165
|
+
|
|
166
|
+
abs_path = (self.project_root / dir_path).resolve()
|
|
167
|
+
rel_path = abs_path.relative_to(self.project_root)
|
|
168
|
+
|
|
169
|
+
if rel_path in self._dir_snapshots:
|
|
170
|
+
return self._dir_snapshots[rel_path]
|
|
171
|
+
|
|
172
|
+
snapshot = DirectorySnapshot(
|
|
173
|
+
path=rel_path,
|
|
174
|
+
existed=abs_path.exists(),
|
|
175
|
+
)
|
|
176
|
+
self._dir_snapshots[rel_path] = snapshot
|
|
177
|
+
|
|
178
|
+
if not snapshot.existed:
|
|
179
|
+
self._dirs_created.add(rel_path)
|
|
180
|
+
|
|
181
|
+
return snapshot
|
|
182
|
+
|
|
183
|
+
def record_modification(self, file_path: Path) -> None:
|
|
184
|
+
"""Record that a file was modified (after capturing)."""
|
|
185
|
+
rel_path = Path(file_path)
|
|
186
|
+
if rel_path in self._file_snapshots:
|
|
187
|
+
snapshot = self._file_snapshots[rel_path]
|
|
188
|
+
if snapshot.existed:
|
|
189
|
+
self._files_modified.add(rel_path)
|
|
190
|
+
else:
|
|
191
|
+
self._files_created.add(rel_path)
|
|
192
|
+
|
|
193
|
+
def record_deletion(self, file_path: Path) -> None:
|
|
194
|
+
"""Record that a file was deleted."""
|
|
195
|
+
rel_path = Path(file_path)
|
|
196
|
+
if rel_path in self._file_snapshots and self._file_snapshots[rel_path].existed:
|
|
197
|
+
self._files_deleted.add(rel_path)
|
|
198
|
+
|
|
199
|
+
def revert_file(self, file_path: Path) -> bool:
|
|
200
|
+
"""Revert a single file to its original state."""
|
|
201
|
+
rel_path = Path(file_path)
|
|
202
|
+
if rel_path not in self._file_snapshots:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
snapshot = self._file_snapshots[rel_path]
|
|
206
|
+
abs_path = self.project_root / rel_path
|
|
207
|
+
|
|
208
|
+
if not snapshot.existed:
|
|
209
|
+
# File was created during session - delete it
|
|
210
|
+
if abs_path.exists():
|
|
211
|
+
abs_path.unlink()
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
# Restore original content
|
|
215
|
+
if snapshot.original_content is not None:
|
|
216
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
abs_path.write_bytes(snapshot.original_content)
|
|
218
|
+
else:
|
|
219
|
+
# Large file - restore from backup
|
|
220
|
+
backup_path = self._large_file_backup_dir / rel_path
|
|
221
|
+
if backup_path.exists():
|
|
222
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
shutil.copy2(backup_path, abs_path)
|
|
224
|
+
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
def revert_all(self) -> Dict[str, List[str]]:
|
|
228
|
+
"""
|
|
229
|
+
Revert ALL changes made during the session.
|
|
230
|
+
|
|
231
|
+
Returns a summary of what was reverted.
|
|
232
|
+
"""
|
|
233
|
+
if not self.session_id:
|
|
234
|
+
return {"error": "No active session"}
|
|
235
|
+
|
|
236
|
+
reverted = {
|
|
237
|
+
"files_restored": [],
|
|
238
|
+
"files_deleted": [],
|
|
239
|
+
"dirs_deleted": [],
|
|
240
|
+
"errors": [],
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Revert files
|
|
244
|
+
for rel_path, snapshot in self._file_snapshots.items():
|
|
245
|
+
abs_path = self.project_root / rel_path
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if not snapshot.existed:
|
|
249
|
+
# Delete files that were created
|
|
250
|
+
if abs_path.exists():
|
|
251
|
+
abs_path.unlink()
|
|
252
|
+
reverted["files_deleted"].append(str(rel_path))
|
|
253
|
+
else:
|
|
254
|
+
# Restore original content
|
|
255
|
+
if snapshot.original_content is not None:
|
|
256
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
abs_path.write_bytes(snapshot.original_content)
|
|
258
|
+
else:
|
|
259
|
+
# Large file from backup
|
|
260
|
+
backup_path = self._large_file_backup_dir / rel_path
|
|
261
|
+
if backup_path.exists():
|
|
262
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
shutil.copy2(backup_path, abs_path)
|
|
264
|
+
reverted["files_restored"].append(str(rel_path))
|
|
265
|
+
except Exception as e:
|
|
266
|
+
reverted["errors"].append(f"{rel_path}: {e}")
|
|
267
|
+
|
|
268
|
+
# Remove created directories (in reverse order - deepest first)
|
|
269
|
+
created_dirs = sorted(self._dirs_created, key=lambda p: len(p.parts), reverse=True)
|
|
270
|
+
for rel_path in created_dirs:
|
|
271
|
+
abs_path = self.project_root / rel_path
|
|
272
|
+
try:
|
|
273
|
+
if abs_path.exists() and abs_path.is_dir():
|
|
274
|
+
# Only remove if empty
|
|
275
|
+
if not any(abs_path.iterdir()):
|
|
276
|
+
abs_path.rmdir()
|
|
277
|
+
reverted["dirs_deleted"].append(str(rel_path))
|
|
278
|
+
except Exception as e:
|
|
279
|
+
reverted["errors"].append(f"dir {rel_path}: {e}")
|
|
280
|
+
|
|
281
|
+
return reverted
|
|
282
|
+
|
|
283
|
+
def end_session(self, revert: bool = True) -> Dict[str, any]:
|
|
284
|
+
"""
|
|
285
|
+
End the current session.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
revert: If True, revert all changes. If False, keep changes.
|
|
289
|
+
"""
|
|
290
|
+
if not self.session_id:
|
|
291
|
+
return {"error": "No active session"}
|
|
292
|
+
|
|
293
|
+
result = {
|
|
294
|
+
"session_id": self.session_id,
|
|
295
|
+
"duration_seconds": (datetime.now() - self.session_start).total_seconds(),
|
|
296
|
+
"files_tracked": len(self._file_snapshots),
|
|
297
|
+
"files_modified": list(str(p) for p in self._files_modified),
|
|
298
|
+
"files_created": list(str(p) for p in self._files_created),
|
|
299
|
+
"files_deleted": list(str(p) for p in self._files_deleted),
|
|
300
|
+
"dirs_created": list(str(p) for p in self._dirs_created),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if revert:
|
|
304
|
+
revert_result = self.revert_all()
|
|
305
|
+
result["revert_result"] = revert_result
|
|
306
|
+
|
|
307
|
+
# Cleanup temp backup dir
|
|
308
|
+
if self._large_file_backup_dir and self._large_file_backup_dir.exists():
|
|
309
|
+
shutil.rmtree(self._large_file_backup_dir, ignore_errors=True)
|
|
310
|
+
|
|
311
|
+
# Reset state
|
|
312
|
+
self.session_id = None
|
|
313
|
+
self.session_start = None
|
|
314
|
+
self._file_snapshots.clear()
|
|
315
|
+
self._dir_snapshots.clear()
|
|
316
|
+
self._large_file_backup_dir = None
|
|
317
|
+
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
def get_changes_summary(self) -> Dict[str, any]:
|
|
321
|
+
"""Get a summary of all changes in the current session."""
|
|
322
|
+
return {
|
|
323
|
+
"session_id": self.session_id,
|
|
324
|
+
"files_tracked": len(self._file_snapshots),
|
|
325
|
+
"files_modified": [str(p) for p in self._files_modified],
|
|
326
|
+
"files_created": [str(p) for p in self._files_created],
|
|
327
|
+
"files_deleted": [str(p) for p in self._files_deleted],
|
|
328
|
+
"dirs_created": [str(p) for p in self._dirs_created],
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
def get_modified_content(self, file_path: Path) -> Optional[bytes]:
|
|
332
|
+
"""Get the current (modified) content of a tracked file."""
|
|
333
|
+
abs_path = self.project_root / file_path
|
|
334
|
+
if abs_path.exists():
|
|
335
|
+
return abs_path.read_bytes()
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def get_original_content(self, file_path: Path) -> Optional[bytes]:
|
|
339
|
+
"""Get the original content of a tracked file."""
|
|
340
|
+
rel_path = Path(file_path)
|
|
341
|
+
if rel_path not in self._file_snapshots:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
snapshot = self._file_snapshots[rel_path]
|
|
345
|
+
|
|
346
|
+
if not snapshot.existed:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
if snapshot.original_content is not None:
|
|
350
|
+
return snapshot.original_content
|
|
351
|
+
|
|
352
|
+
# Large file from backup
|
|
353
|
+
backup_path = self._large_file_backup_dir / rel_path
|
|
354
|
+
if backup_path.exists():
|
|
355
|
+
return backup_path.read_bytes()
|
|
356
|
+
|
|
357
|
+
return None
|