@myrialabs/clopen 0.0.1
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.
- package/.env.example +6 -0
- package/.github/workflows/release.yml +60 -0
- package/.github/workflows/test.yml +40 -0
- package/CONTRIBUTING.md +499 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend/index.ts +156 -0
- package/backend/lib/chat/helpers.ts +42 -0
- package/backend/lib/chat/index.ts +2 -0
- package/backend/lib/chat/stream-manager.ts +1126 -0
- package/backend/lib/database/README.md +77 -0
- package/backend/lib/database/index.ts +119 -0
- package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
- package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
- package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
- package/backend/lib/database/migrations/index.ts +154 -0
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
- package/backend/lib/database/queries/engine-queries.ts +75 -0
- package/backend/lib/database/queries/index.ts +9 -0
- package/backend/lib/database/queries/message-queries.ts +472 -0
- package/backend/lib/database/queries/project-queries.ts +117 -0
- package/backend/lib/database/queries/session-queries.ts +271 -0
- package/backend/lib/database/queries/settings-queries.ts +34 -0
- package/backend/lib/database/queries/snapshot-queries.ts +326 -0
- package/backend/lib/database/queries/utils-queries.ts +59 -0
- package/backend/lib/database/seeders/index.ts +13 -0
- package/backend/lib/database/seeders/settings_seeder.ts +84 -0
- package/backend/lib/database/utils/connection.ts +174 -0
- package/backend/lib/database/utils/index.ts +4 -0
- package/backend/lib/database/utils/migration-runner.ts +118 -0
- package/backend/lib/database/utils/seeder-runner.ts +121 -0
- package/backend/lib/engine/adapters/claude/environment.ts +164 -0
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
- package/backend/lib/engine/adapters/claude/index.ts +1 -0
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
- package/backend/lib/engine/adapters/claude/stream.ts +177 -0
- package/backend/lib/engine/adapters/opencode/index.ts +2 -0
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
- package/backend/lib/engine/adapters/opencode/server.ts +104 -0
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
- package/backend/lib/engine/index.ts +196 -0
- package/backend/lib/engine/types.ts +58 -0
- package/backend/lib/files/file-operations.ts +478 -0
- package/backend/lib/files/file-reading.ts +308 -0
- package/backend/lib/files/file-watcher.ts +383 -0
- package/backend/lib/files/path-browsing.ts +382 -0
- package/backend/lib/git/git-executor.ts +88 -0
- package/backend/lib/git/git-parser.ts +411 -0
- package/backend/lib/git/git-service.ts +505 -0
- package/backend/lib/mcp/README.md +1144 -0
- package/backend/lib/mcp/config.ts +316 -0
- package/backend/lib/mcp/index.ts +35 -0
- package/backend/lib/mcp/project-context.ts +236 -0
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
- package/backend/lib/mcp/servers/helper.ts +143 -0
- package/backend/lib/mcp/servers/index.ts +45 -0
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
- package/backend/lib/mcp/servers/weather/index.ts +31 -0
- package/backend/lib/mcp/stdio-server.ts +103 -0
- package/backend/lib/mcp/types.ts +65 -0
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
- package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
- package/backend/lib/preview/browser/browser-pool.ts +357 -0
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
- package/backend/lib/preview/browser/types.ts +359 -0
- package/backend/lib/preview/index.ts +24 -0
- package/backend/lib/project/index.ts +2 -0
- package/backend/lib/project/status-manager.ts +182 -0
- package/backend/lib/shared/index.ts +2 -0
- package/backend/lib/shared/port-utils.ts +25 -0
- package/backend/lib/shared/process-manager.ts +281 -0
- package/backend/lib/snapshot/blob-store.ts +227 -0
- package/backend/lib/snapshot/gitignore.ts +307 -0
- package/backend/lib/snapshot/helpers.ts +397 -0
- package/backend/lib/snapshot/snapshot-service.ts +483 -0
- package/backend/lib/terminal/helpers.ts +14 -0
- package/backend/lib/terminal/index.ts +8 -0
- package/backend/lib/terminal/pty-manager.ts +4 -0
- package/backend/lib/terminal/pty-session-manager.ts +387 -0
- package/backend/lib/terminal/shell-utils.ts +313 -0
- package/backend/lib/terminal/stream-manager.ts +293 -0
- package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
- package/backend/lib/user/helpers.ts +87 -0
- package/backend/lib/utils/ws.ts +944 -0
- package/backend/lib/vite-dev.ts +353 -0
- package/backend/middleware/cors.ts +15 -0
- package/backend/middleware/error-handler.ts +49 -0
- package/backend/middleware/logger.ts +9 -0
- package/backend/types/api.ts +24 -0
- package/backend/ws/README.md +1505 -0
- package/backend/ws/chat/background.ts +198 -0
- package/backend/ws/chat/index.ts +21 -0
- package/backend/ws/chat/stream.ts +707 -0
- package/backend/ws/engine/claude/accounts.ts +401 -0
- package/backend/ws/engine/claude/index.ts +13 -0
- package/backend/ws/engine/claude/status.ts +43 -0
- package/backend/ws/engine/index.ts +14 -0
- package/backend/ws/engine/opencode/index.ts +11 -0
- package/backend/ws/engine/opencode/status.ts +30 -0
- package/backend/ws/engine/utils.ts +36 -0
- package/backend/ws/files/index.ts +30 -0
- package/backend/ws/files/read.ts +189 -0
- package/backend/ws/files/search.ts +453 -0
- package/backend/ws/files/watch.ts +124 -0
- package/backend/ws/files/write.ts +143 -0
- package/backend/ws/git/branch.ts +106 -0
- package/backend/ws/git/commit.ts +39 -0
- package/backend/ws/git/conflict.ts +68 -0
- package/backend/ws/git/diff.ts +69 -0
- package/backend/ws/git/index.ts +24 -0
- package/backend/ws/git/log.ts +41 -0
- package/backend/ws/git/remote.ts +214 -0
- package/backend/ws/git/staging.ts +84 -0
- package/backend/ws/git/status.ts +90 -0
- package/backend/ws/index.ts +69 -0
- package/backend/ws/mcp/index.ts +61 -0
- package/backend/ws/messages/crud.ts +74 -0
- package/backend/ws/messages/index.ts +14 -0
- package/backend/ws/preview/browser/cleanup.ts +129 -0
- package/backend/ws/preview/browser/console.ts +114 -0
- package/backend/ws/preview/browser/interact.ts +513 -0
- package/backend/ws/preview/browser/mcp.ts +129 -0
- package/backend/ws/preview/browser/native-ui.ts +235 -0
- package/backend/ws/preview/browser/stats.ts +55 -0
- package/backend/ws/preview/browser/tab-info.ts +126 -0
- package/backend/ws/preview/browser/tab.ts +166 -0
- package/backend/ws/preview/browser/webcodecs.ts +293 -0
- package/backend/ws/preview/index.ts +146 -0
- package/backend/ws/projects/crud.ts +113 -0
- package/backend/ws/projects/index.ts +25 -0
- package/backend/ws/projects/presence.ts +46 -0
- package/backend/ws/projects/status.ts +116 -0
- package/backend/ws/sessions/crud.ts +327 -0
- package/backend/ws/sessions/index.ts +33 -0
- package/backend/ws/settings/crud.ts +112 -0
- package/backend/ws/settings/index.ts +14 -0
- package/backend/ws/snapshot/index.ts +17 -0
- package/backend/ws/snapshot/restore.ts +173 -0
- package/backend/ws/snapshot/timeline.ts +141 -0
- package/backend/ws/system/index.ts +14 -0
- package/backend/ws/system/operations.ts +49 -0
- package/backend/ws/terminal/index.ts +40 -0
- package/backend/ws/terminal/persistence.ts +153 -0
- package/backend/ws/terminal/session.ts +382 -0
- package/backend/ws/terminal/stream.ts +79 -0
- package/backend/ws/tunnel/index.ts +14 -0
- package/backend/ws/tunnel/operations.ts +91 -0
- package/backend/ws/types.ts +20 -0
- package/backend/ws/user/crud.ts +156 -0
- package/backend/ws/user/index.ts +14 -0
- package/bin/clopen.ts +307 -0
- package/bun.lock +1352 -0
- package/frontend/App.svelte +34 -0
- package/frontend/app.css +313 -0
- package/frontend/lib/app-environment.ts +10 -0
- package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
- package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
- package/frontend/lib/components/chat/formatters/index.ts +3 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
- package/frontend/lib/components/chat/shared/index.ts +2 -0
- package/frontend/lib/components/chat/shared/utils.ts +116 -0
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
- package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
- package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
- package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
- package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
- package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
- package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
- package/frontend/lib/components/chat/tools/components/index.ts +7 -0
- package/frontend/lib/components/chat/tools/index.ts +26 -0
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
- package/frontend/lib/components/common/Alert.svelte +139 -0
- package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
- package/frontend/lib/components/common/Button.svelte +71 -0
- package/frontend/lib/components/common/Card.svelte +102 -0
- package/frontend/lib/components/common/Checkbox.svelte +48 -0
- package/frontend/lib/components/common/Dialog.svelte +249 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
- package/frontend/lib/components/common/Icon.svelte +58 -0
- package/frontend/lib/components/common/Input.svelte +72 -0
- package/frontend/lib/components/common/Lightbox.svelte +233 -0
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
- package/frontend/lib/components/common/Modal.svelte +177 -0
- package/frontend/lib/components/common/ModalProvider.svelte +28 -0
- package/frontend/lib/components/common/ModelSelector.svelte +110 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
- package/frontend/lib/components/common/NotificationToast.svelte +113 -0
- package/frontend/lib/components/common/PageTemplate.svelte +76 -0
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
- package/frontend/lib/components/common/Select.svelte +98 -0
- package/frontend/lib/components/common/Textarea.svelte +80 -0
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
- package/frontend/lib/components/common/lucide-icons.ts +1642 -0
- package/frontend/lib/components/common/material-icons.ts +1082 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
- package/frontend/lib/components/common/xterm/index.ts +16 -0
- package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
- package/frontend/lib/components/common/xterm/types.ts +31 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
- package/frontend/lib/components/files/FileNode.svelte +384 -0
- package/frontend/lib/components/files/FileTree.svelte +681 -0
- package/frontend/lib/components/files/FileViewer.svelte +728 -0
- package/frontend/lib/components/files/SearchResults.svelte +303 -0
- package/frontend/lib/components/git/BranchManager.svelte +458 -0
- package/frontend/lib/components/git/ChangesSection.svelte +107 -0
- package/frontend/lib/components/git/CommitForm.svelte +76 -0
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
- package/frontend/lib/components/git/DiffViewer.svelte +364 -0
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
- package/frontend/lib/components/git/GitButton.svelte +33 -0
- package/frontend/lib/components/git/GitLog.svelte +361 -0
- package/frontend/lib/components/git/GitModal.svelte +80 -0
- package/frontend/lib/components/history/HistoryModal.svelte +563 -0
- package/frontend/lib/components/history/HistoryView.svelte +615 -0
- package/frontend/lib/components/index.ts +34 -0
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
- package/frontend/lib/components/preview/index.ts +2 -0
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
- package/frontend/lib/components/settings/SettingsView.svelte +36 -0
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
- package/frontend/lib/components/terminal/Terminal.svelte +368 -0
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
- package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
- package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
- package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
- package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
- package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
- package/frontend/lib/constants/preview.ts +45 -0
- package/frontend/lib/services/chat/chat.service.ts +704 -0
- package/frontend/lib/services/chat/index.ts +7 -0
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
- package/frontend/lib/services/notification/index.ts +8 -0
- package/frontend/lib/services/notification/push.service.ts +144 -0
- package/frontend/lib/services/notification/sound.service.ts +127 -0
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
- package/frontend/lib/services/preview/index.ts +23 -0
- package/frontend/lib/services/project/index.ts +8 -0
- package/frontend/lib/services/project/status.service.ts +159 -0
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
- package/frontend/lib/services/terminal/background/index.ts +130 -0
- package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
- package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
- package/frontend/lib/services/terminal/index.ts +14 -0
- package/frontend/lib/services/terminal/persistence.service.ts +260 -0
- package/frontend/lib/services/terminal/project.service.ts +953 -0
- package/frontend/lib/services/terminal/session.service.ts +364 -0
- package/frontend/lib/services/terminal/terminal.service.ts +369 -0
- package/frontend/lib/stores/core/app.svelte.ts +117 -0
- package/frontend/lib/stores/core/files.svelte.ts +73 -0
- package/frontend/lib/stores/core/presence.svelte.ts +48 -0
- package/frontend/lib/stores/core/projects.svelte.ts +317 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
- package/frontend/lib/stores/features/models.svelte.ts +89 -0
- package/frontend/lib/stores/features/settings.svelte.ts +87 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
- package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
- package/frontend/lib/stores/features/user.svelte.ts +96 -0
- package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
- package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
- package/frontend/lib/types/native-ui.ts +73 -0
- package/frontend/lib/utils/chat/date-separator.ts +39 -0
- package/frontend/lib/utils/chat/message-grouper.ts +219 -0
- package/frontend/lib/utils/chat/message-processor.ts +135 -0
- package/frontend/lib/utils/chat/tool-handler.ts +161 -0
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
- package/frontend/lib/utils/click-outside.ts +20 -0
- package/frontend/lib/utils/context-manager.ts +257 -0
- package/frontend/lib/utils/file-icon-mappings.ts +769 -0
- package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
- package/frontend/lib/utils/git-status.ts +68 -0
- package/frontend/lib/utils/platform.ts +113 -0
- package/frontend/lib/utils/port-check.ts +65 -0
- package/frontend/lib/utils/terminalFormatter.ts +207 -0
- package/frontend/lib/utils/theme.ts +6 -0
- package/frontend/lib/utils/tree-visualizer.ts +320 -0
- package/frontend/lib/utils/ws.ts +44 -0
- package/frontend/main.ts +13 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/scripts/generate-icons.ts +87 -0
- package/scripts/pre-publish-check.sh +142 -0
- package/scripts/setup-hooks.sh +134 -0
- package/scripts/validate-branch-name.sh +47 -0
- package/scripts/validate-commit-msg.sh +42 -0
- package/shared/constants/engines.ts +134 -0
- package/shared/types/database/connection.ts +16 -0
- package/shared/types/database/index.ts +6 -0
- package/shared/types/database/schema.ts +141 -0
- package/shared/types/engine/index.ts +45 -0
- package/shared/types/filesystem/index.ts +22 -0
- package/shared/types/git.ts +171 -0
- package/shared/types/messaging/index.ts +239 -0
- package/shared/types/messaging/tool.ts +526 -0
- package/shared/types/network/api.ts +18 -0
- package/shared/types/network/index.ts +5 -0
- package/shared/types/stores/app.ts +23 -0
- package/shared/types/stores/dialog.ts +21 -0
- package/shared/types/stores/index.ts +3 -0
- package/shared/types/stores/settings.ts +15 -0
- package/shared/types/terminal/index.ts +44 -0
- package/shared/types/ui/components.ts +61 -0
- package/shared/types/ui/icons.ts +23 -0
- package/shared/types/ui/index.ts +22 -0
- package/shared/types/ui/notifications.ts +14 -0
- package/shared/types/ui/theme.ts +12 -0
- package/shared/types/websocket/index.ts +43 -0
- package/shared/types/window.d.ts +13 -0
- package/shared/utils/anonymous-user.ts +168 -0
- package/shared/utils/async.ts +10 -0
- package/shared/utils/diff-calculator.ts +184 -0
- package/shared/utils/file-type-detection.ts +166 -0
- package/shared/utils/logger.ts +158 -0
- package/shared/utils/message-formatter.ts +79 -0
- package/shared/utils/path.ts +47 -0
- package/shared/utils/ws-client.ts +768 -0
- package/shared/utils/ws-server.ts +660 -0
- package/static/audio/notification.ogg +0 -0
- package/static/favicon.svg +8 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
- package/static/fonts/dm-sans.css +96 -0
- package/svelte.config.js +20 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot Service for Time Travel Feature
|
|
3
|
+
*
|
|
4
|
+
* Uses git-like content-addressable blob storage for efficient snapshots:
|
|
5
|
+
* - File contents stored as compressed blobs in ~/.clopen/snapshots/blobs/
|
|
6
|
+
* - Each snapshot has a tree file mapping filepath -> blob hash
|
|
7
|
+
* - DB only stores lightweight metadata and hash references
|
|
8
|
+
* - Deduplication: identical file content across snapshots stored once
|
|
9
|
+
* - mtime cache: skip re-reading files that haven't changed
|
|
10
|
+
* - Respects .gitignore rules (via git ls-files or manual parsing)
|
|
11
|
+
* - All files read/written as Buffer (binary-safe for images, PDFs, etc.)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { snapshotQueries } from '../database/queries';
|
|
17
|
+
import { blobStore, type TreeMap } from './blob-store';
|
|
18
|
+
import { getSnapshotFiles } from './gitignore';
|
|
19
|
+
import type { MessageSnapshot, DeltaChanges } from '$shared/types/database/schema';
|
|
20
|
+
import { calculateFileChangeStats } from '$shared/utils/diff-calculator';
|
|
21
|
+
import { debug } from '$shared/utils/logger';
|
|
22
|
+
|
|
23
|
+
interface FileSnapshot {
|
|
24
|
+
[filepath: string]: Buffer; // filepath -> content (Buffer for binary safety)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SnapshotMetadata {
|
|
28
|
+
totalFiles: number;
|
|
29
|
+
totalSize: number;
|
|
30
|
+
capturedAt: string;
|
|
31
|
+
snapshotType: 'full' | 'delta';
|
|
32
|
+
deltaSize?: number;
|
|
33
|
+
storageFormat?: 'blob-store';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Maximum file size to include (5MB)
|
|
37
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
export class SnapshotService {
|
|
40
|
+
private static instance: SnapshotService;
|
|
41
|
+
|
|
42
|
+
private constructor() {}
|
|
43
|
+
|
|
44
|
+
static getInstance(): SnapshotService {
|
|
45
|
+
if (!SnapshotService.instance) {
|
|
46
|
+
SnapshotService.instance = new SnapshotService();
|
|
47
|
+
}
|
|
48
|
+
return SnapshotService.instance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture snapshot of current project state using blob store.
|
|
53
|
+
* Only changed files are read and stored (mtime cache + hash dedup).
|
|
54
|
+
* Respects .gitignore rules for file exclusion.
|
|
55
|
+
*/
|
|
56
|
+
async captureSnapshot(
|
|
57
|
+
projectPath: string,
|
|
58
|
+
projectId: string,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
messageId: string
|
|
61
|
+
): Promise<MessageSnapshot> {
|
|
62
|
+
try {
|
|
63
|
+
// Scan files respecting .gitignore (git ls-files or manual parsing)
|
|
64
|
+
const files = await getSnapshotFiles(projectPath);
|
|
65
|
+
|
|
66
|
+
// Build current tree: hash each file using blob store
|
|
67
|
+
const currentTree: TreeMap = {};
|
|
68
|
+
const readContents = new Map<string, Buffer>();
|
|
69
|
+
let totalSize = 0;
|
|
70
|
+
|
|
71
|
+
for (const filepath of files) {
|
|
72
|
+
try {
|
|
73
|
+
const stat = await fs.stat(filepath);
|
|
74
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
75
|
+
debug.warn('snapshot', `Skipping large file: ${filepath} (${stat.size} bytes)`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const relativePath = path.relative(projectPath, filepath);
|
|
80
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
81
|
+
|
|
82
|
+
const result = await blobStore.hashFile(normalizedPath, filepath);
|
|
83
|
+
currentTree[normalizedPath] = result.hash;
|
|
84
|
+
totalSize += stat.size;
|
|
85
|
+
|
|
86
|
+
if (result.content !== null) {
|
|
87
|
+
readContents.set(normalizedPath, result.content);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
debug.warn('snapshot', `Could not process file ${filepath}:`, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get previous snapshot's tree for delta computation
|
|
95
|
+
const previousSnapshots = snapshotQueries.getBySessionId(sessionId);
|
|
96
|
+
const previousSnapshot = previousSnapshots.length > 0
|
|
97
|
+
? previousSnapshots[previousSnapshots.length - 1]
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
let previousTree: TreeMap = {};
|
|
101
|
+
if (previousSnapshot) {
|
|
102
|
+
previousTree = await this.getSnapshotTree(previousSnapshot);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Compute delta by comparing tree hashes (fast!)
|
|
106
|
+
const delta = this.calculateTreeDelta(previousTree, currentTree);
|
|
107
|
+
const deltaSize =
|
|
108
|
+
Object.keys(delta.added).length +
|
|
109
|
+
Object.keys(delta.modified).length +
|
|
110
|
+
delta.deleted.length;
|
|
111
|
+
|
|
112
|
+
// Calculate line-level file change stats for changed files only
|
|
113
|
+
const fileStats = await this.calculateChangeStats(
|
|
114
|
+
previousTree, currentTree, delta, readContents
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const metadata: SnapshotMetadata = {
|
|
118
|
+
totalFiles: Object.keys(currentTree).length,
|
|
119
|
+
totalSize,
|
|
120
|
+
capturedAt: new Date().toISOString(),
|
|
121
|
+
snapshotType: 'delta',
|
|
122
|
+
deltaSize,
|
|
123
|
+
storageFormat: 'blob-store'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const snapshotId = `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
127
|
+
|
|
128
|
+
// Store tree file to disk
|
|
129
|
+
const treeHash = await blobStore.storeTree(snapshotId, currentTree);
|
|
130
|
+
|
|
131
|
+
// Store lightweight record in DB (no file content!)
|
|
132
|
+
const dbSnapshot = snapshotQueries.createSnapshot({
|
|
133
|
+
id: snapshotId,
|
|
134
|
+
message_id: messageId,
|
|
135
|
+
session_id: sessionId,
|
|
136
|
+
project_id: projectId,
|
|
137
|
+
files_snapshot: {},
|
|
138
|
+
project_metadata: metadata,
|
|
139
|
+
snapshot_type: 'delta',
|
|
140
|
+
parent_snapshot_id: previousSnapshot?.id,
|
|
141
|
+
delta_changes: delta,
|
|
142
|
+
files_changed: fileStats.filesChanged,
|
|
143
|
+
insertions: fileStats.insertions,
|
|
144
|
+
deletions: fileStats.deletions,
|
|
145
|
+
tree_hash: treeHash
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const typeLabel = previousSnapshot ? 'delta' : 'initial delta';
|
|
149
|
+
debug.log('snapshot', `Created ${typeLabel} snapshot [blob-store]: ${deltaSize} changes (${Object.keys(delta.added).length} added, ${Object.keys(delta.modified).length} modified, ${delta.deleted.length} deleted) - ${fileStats.filesChanged} files, +${fileStats.insertions}/-${fileStats.deletions} lines`);
|
|
150
|
+
return dbSnapshot;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
debug.error('snapshot', 'Error capturing snapshot:', error);
|
|
153
|
+
throw new Error(`Failed to capture snapshot: ${error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Calculate delta between two trees by comparing hashes.
|
|
159
|
+
*/
|
|
160
|
+
private calculateTreeDelta(
|
|
161
|
+
previousTree: TreeMap,
|
|
162
|
+
currentTree: TreeMap
|
|
163
|
+
): DeltaChanges {
|
|
164
|
+
const delta: DeltaChanges = {
|
|
165
|
+
added: {},
|
|
166
|
+
modified: {},
|
|
167
|
+
deleted: []
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const [filepath, hash] of Object.entries(currentTree)) {
|
|
171
|
+
if (!previousTree[filepath]) {
|
|
172
|
+
delta.added[filepath] = hash;
|
|
173
|
+
} else if (previousTree[filepath] !== hash) {
|
|
174
|
+
delta.modified[filepath] = hash;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const filepath of Object.keys(previousTree)) {
|
|
179
|
+
if (!currentTree[filepath]) {
|
|
180
|
+
delta.deleted.push(filepath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return delta;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Calculate line-level change stats for changed files.
|
|
189
|
+
* Only reads blob content for files that actually changed.
|
|
190
|
+
*/
|
|
191
|
+
private async calculateChangeStats(
|
|
192
|
+
previousTree: TreeMap,
|
|
193
|
+
currentTree: TreeMap,
|
|
194
|
+
delta: DeltaChanges,
|
|
195
|
+
readContents: Map<string, Buffer>
|
|
196
|
+
): Promise<{ filesChanged: number; insertions: number; deletions: number }> {
|
|
197
|
+
const previousSnapshot: Record<string, Buffer> = {};
|
|
198
|
+
const currentSnapshot: Record<string, Buffer> = {};
|
|
199
|
+
|
|
200
|
+
for (const filepath of Object.keys(delta.added)) {
|
|
201
|
+
const hash = currentTree[filepath];
|
|
202
|
+
currentSnapshot[filepath] = readContents.get(filepath) ?? await blobStore.readBlob(hash);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const filepath of Object.keys(delta.modified)) {
|
|
206
|
+
const oldHash = previousTree[filepath];
|
|
207
|
+
const newHash = currentTree[filepath];
|
|
208
|
+
previousSnapshot[filepath] = await blobStore.readBlob(oldHash);
|
|
209
|
+
currentSnapshot[filepath] = readContents.get(filepath) ?? await blobStore.readBlob(newHash);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const filepath of delta.deleted) {
|
|
213
|
+
const oldHash = previousTree[filepath];
|
|
214
|
+
if (oldHash) {
|
|
215
|
+
previousSnapshot[filepath] = await blobStore.readBlob(oldHash);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return calculateFileChangeStats(previousSnapshot, currentSnapshot);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the tree map for a snapshot.
|
|
224
|
+
* New format: read from tree file on disk.
|
|
225
|
+
* Old format: reconstruct from delta chain in DB.
|
|
226
|
+
*/
|
|
227
|
+
private async getSnapshotTree(snapshot: MessageSnapshot): Promise<TreeMap> {
|
|
228
|
+
if (snapshot.tree_hash) {
|
|
229
|
+
try {
|
|
230
|
+
return await blobStore.readTree(snapshot.id);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
debug.warn('snapshot', `Could not read tree file for ${snapshot.id}, falling back to chain replay:`, err);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Old format: reconstruct complete state from delta chain (returns string content)
|
|
237
|
+
const fileSnapshot = await this.reconstructSnapshotLegacy(snapshot);
|
|
238
|
+
|
|
239
|
+
// Convert legacy FileSnapshot to TreeMap by hashing and storing each file as blob
|
|
240
|
+
const tree: TreeMap = {};
|
|
241
|
+
for (const [filepath, content] of Object.entries(fileSnapshot)) {
|
|
242
|
+
const hash = await blobStore.storeBlob(content);
|
|
243
|
+
tree[filepath] = hash;
|
|
244
|
+
}
|
|
245
|
+
return tree;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Restore project to a previous snapshot.
|
|
250
|
+
* Only modifies files that are different from current state.
|
|
251
|
+
* Uses .gitignore-aware scanning for current state comparison.
|
|
252
|
+
*/
|
|
253
|
+
async restoreSnapshot(
|
|
254
|
+
projectPath: string,
|
|
255
|
+
snapshot: MessageSnapshot
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
try {
|
|
258
|
+
const targetState = await this.reconstructSnapshot(snapshot);
|
|
259
|
+
|
|
260
|
+
// Scan current files respecting .gitignore
|
|
261
|
+
const currentFiles = await getSnapshotFiles(projectPath);
|
|
262
|
+
const currentState = await this.createFileSnapshot(projectPath, currentFiles);
|
|
263
|
+
|
|
264
|
+
let restoredCount = 0;
|
|
265
|
+
let deletedCount = 0;
|
|
266
|
+
|
|
267
|
+
debug.log('snapshot', 'SNAPSHOT RESTORE START');
|
|
268
|
+
debug.log('snapshot', `Snapshot ID: ${snapshot.id}`);
|
|
269
|
+
debug.log('snapshot', `Message ID: ${snapshot.message_id}`);
|
|
270
|
+
debug.log('snapshot', `Project path: ${projectPath}`);
|
|
271
|
+
debug.log('snapshot', `Target state files: ${Object.keys(targetState).length}`);
|
|
272
|
+
debug.log('snapshot', `Current state files: ${currentFiles.length}`);
|
|
273
|
+
|
|
274
|
+
// Delete files that exist now but not in target state
|
|
275
|
+
for (const currentFile of currentFiles) {
|
|
276
|
+
const relativePath = path.relative(projectPath, currentFile);
|
|
277
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
278
|
+
|
|
279
|
+
if (!targetState[normalizedPath]) {
|
|
280
|
+
try {
|
|
281
|
+
await fs.unlink(currentFile);
|
|
282
|
+
debug.log('snapshot', `Deleted: ${currentFile}`);
|
|
283
|
+
deletedCount++;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
debug.warn('snapshot', `Could not delete ${currentFile}:`, err);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Write only files that are different or don't exist
|
|
291
|
+
for (const [relativePath, targetContent] of Object.entries(targetState)) {
|
|
292
|
+
const fullPath = path.join(projectPath, relativePath);
|
|
293
|
+
const currentContent = currentState[relativePath];
|
|
294
|
+
|
|
295
|
+
// Compare as Buffer (binary-safe comparison)
|
|
296
|
+
const isDifferent = !currentContent || !currentContent.equals(targetContent);
|
|
297
|
+
|
|
298
|
+
if (isDifferent) {
|
|
299
|
+
const dir = path.dirname(fullPath);
|
|
300
|
+
await fs.mkdir(dir, { recursive: true });
|
|
301
|
+
// Write as Buffer directly — no encoding, preserves binary files
|
|
302
|
+
await fs.writeFile(fullPath, targetContent);
|
|
303
|
+
|
|
304
|
+
const action = currentContent === undefined ? 'Created' : 'Modified';
|
|
305
|
+
debug.log('snapshot', `${action}: ${fullPath}`);
|
|
306
|
+
restoredCount++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
debug.log('snapshot', `Project restored successfully: ${restoredCount} files restored, ${deletedCount} files deleted`);
|
|
311
|
+
debug.log('snapshot', 'SNAPSHOT RESTORE COMPLETE');
|
|
312
|
+
} catch (error) {
|
|
313
|
+
debug.error('snapshot', 'Error restoring snapshot:', error);
|
|
314
|
+
throw new Error(`Failed to restore snapshot: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Reconstruct the complete file state from a snapshot.
|
|
320
|
+
* New format (tree_hash): Read tree -> resolve blobs (O(1), no chain replay).
|
|
321
|
+
* Old format: Replay delta chain from root (legacy).
|
|
322
|
+
*/
|
|
323
|
+
private async reconstructSnapshot(snapshot: MessageSnapshot): Promise<FileSnapshot> {
|
|
324
|
+
if (snapshot.tree_hash) {
|
|
325
|
+
try {
|
|
326
|
+
const tree = await blobStore.readTree(snapshot.id);
|
|
327
|
+
return await blobStore.resolveTree(tree);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
debug.warn('snapshot', `Could not resolve tree for ${snapshot.id}, falling back to legacy:`, err);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return this.reconstructSnapshotLegacy(snapshot);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Legacy reconstruction: replay all deltas from root to target snapshot.
|
|
338
|
+
*/
|
|
339
|
+
private async reconstructSnapshotLegacy(snapshot: MessageSnapshot): Promise<FileSnapshot> {
|
|
340
|
+
const chain = await this.getSnapshotChain(snapshot);
|
|
341
|
+
let state: FileSnapshot = {};
|
|
342
|
+
|
|
343
|
+
for (const deltaSnapshot of chain) {
|
|
344
|
+
if (!deltaSnapshot.delta_changes) {
|
|
345
|
+
debug.warn('snapshot', `Delta snapshot ${deltaSnapshot.id} missing delta_changes`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const delta = JSON.parse(deltaSnapshot.delta_changes) as DeltaChanges;
|
|
350
|
+
state = this.applyDelta(state, delta);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return state;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the chain of snapshots from the first snapshot to the target.
|
|
358
|
+
*/
|
|
359
|
+
private async getSnapshotChain(targetSnapshot: MessageSnapshot): Promise<MessageSnapshot[]> {
|
|
360
|
+
const chain: MessageSnapshot[] = [];
|
|
361
|
+
let current: MessageSnapshot | null = targetSnapshot;
|
|
362
|
+
|
|
363
|
+
while (current) {
|
|
364
|
+
chain.unshift(current);
|
|
365
|
+
|
|
366
|
+
if (!current.parent_snapshot_id) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const parent = snapshotQueries.getById(current.parent_snapshot_id);
|
|
371
|
+
if (!parent) {
|
|
372
|
+
throw new Error(`Parent snapshot ${current.parent_snapshot_id} not found`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
current = parent;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return chain;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Apply a delta to a file state (legacy format - full content in delta as strings).
|
|
383
|
+
* Converts string content to Buffer for the new binary-safe interface.
|
|
384
|
+
*/
|
|
385
|
+
private applyDelta(state: FileSnapshot, delta: DeltaChanges): FileSnapshot {
|
|
386
|
+
const newState = { ...state };
|
|
387
|
+
|
|
388
|
+
for (const [filepath, content] of Object.entries(delta.added)) {
|
|
389
|
+
newState[filepath] = Buffer.from(content, 'utf-8');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const [filepath, content] of Object.entries(delta.modified)) {
|
|
393
|
+
newState[filepath] = Buffer.from(content, 'utf-8');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const filepath of delta.deleted) {
|
|
397
|
+
delete newState[filepath];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return newState;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get diff between current state and a snapshot
|
|
405
|
+
*/
|
|
406
|
+
async getDiff(
|
|
407
|
+
projectPath: string,
|
|
408
|
+
snapshot: MessageSnapshot
|
|
409
|
+
): Promise<{
|
|
410
|
+
added: string[];
|
|
411
|
+
modified: string[];
|
|
412
|
+
deleted: string[];
|
|
413
|
+
}> {
|
|
414
|
+
try {
|
|
415
|
+
const snapshotFiles = await this.reconstructSnapshot(snapshot);
|
|
416
|
+
const currentSnapshot = await this.createFileSnapshot(
|
|
417
|
+
projectPath,
|
|
418
|
+
await getSnapshotFiles(projectPath)
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const added: string[] = [];
|
|
422
|
+
const modified: string[] = [];
|
|
423
|
+
const deleted: string[] = [];
|
|
424
|
+
|
|
425
|
+
for (const [filepath, content] of Object.entries(currentSnapshot)) {
|
|
426
|
+
if (!snapshotFiles[filepath]) {
|
|
427
|
+
added.push(filepath);
|
|
428
|
+
} else if (!snapshotFiles[filepath].equals(content)) {
|
|
429
|
+
modified.push(filepath);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const filepath of Object.keys(snapshotFiles)) {
|
|
434
|
+
if (!currentSnapshot[filepath]) {
|
|
435
|
+
deleted.push(filepath);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { added, modified, deleted };
|
|
440
|
+
} catch (error) {
|
|
441
|
+
debug.error('snapshot', 'Error getting diff:', error);
|
|
442
|
+
throw new Error(`Failed to get diff: ${error}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create snapshot of file contents (used for restore comparison and getDiff).
|
|
448
|
+
* Reads as Buffer for binary-safe handling.
|
|
449
|
+
*/
|
|
450
|
+
private async createFileSnapshot(
|
|
451
|
+
projectPath: string,
|
|
452
|
+
files: string[]
|
|
453
|
+
): Promise<FileSnapshot> {
|
|
454
|
+
const snapshot: FileSnapshot = {};
|
|
455
|
+
|
|
456
|
+
for (const filepath of files) {
|
|
457
|
+
try {
|
|
458
|
+
const stats = await fs.stat(filepath);
|
|
459
|
+
if (stats.size > MAX_FILE_SIZE) continue;
|
|
460
|
+
|
|
461
|
+
// Read as Buffer — no encoding, preserves binary files
|
|
462
|
+
const content = await fs.readFile(filepath);
|
|
463
|
+
const relativePath = path.relative(projectPath, filepath);
|
|
464
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
465
|
+
snapshot[normalizedPath] = content;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
debug.warn('snapshot', `Could not read file ${filepath}:`, error);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return snapshot;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Clean up old snapshots (older than 30 days)
|
|
476
|
+
*/
|
|
477
|
+
async cleanupOldSnapshots(): Promise<void> {
|
|
478
|
+
// This could be implemented later if needed
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Export singleton instance
|
|
483
|
+
export const snapshotService = SnapshotService.getInstance();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal domain helper functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Bun-compatible existsSync implementation
|
|
6
|
+
export async function existsSync(filePath: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
const file = Bun.file(filePath);
|
|
9
|
+
await file.stat();
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|