@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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TARGETED Process Management for Terminal Command Cancellation
|
|
3
|
+
* Tracks specific session processes without affecting external terminals
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Subprocess } from 'bun';
|
|
7
|
+
|
|
8
|
+
import { debug } from '$shared/utils/logger';
|
|
9
|
+
// Store active child processes globally
|
|
10
|
+
const activeProcesses = new Map<string, Subprocess>();
|
|
11
|
+
// Store child process trees for comprehensive cleanup
|
|
12
|
+
const processTreeMap = new Map<string, number[]>();
|
|
13
|
+
// Track manually terminated sessions
|
|
14
|
+
const manuallyTerminatedSessions = new Set<string>();
|
|
15
|
+
// Platform detection
|
|
16
|
+
const isWindows = process.platform === 'win32';
|
|
17
|
+
|
|
18
|
+
export function setActiveProcess(sessionId: string, process: Subprocess) {
|
|
19
|
+
debug.log('server', '🔧 Setting active process for session:', sessionId, 'PID:', process.pid);
|
|
20
|
+
|
|
21
|
+
// Clean up any existing process for this session first
|
|
22
|
+
const existingProcess = activeProcesses.get(sessionId);
|
|
23
|
+
if (existingProcess && existingProcess.pid) {
|
|
24
|
+
debug.log('server', '🧹 Cleaning up existing process for session:', sessionId);
|
|
25
|
+
try {
|
|
26
|
+
// Use nuclear cleanup for existing process
|
|
27
|
+
nuclearProcessCleanup(sessionId);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
debug.log('server', '⚠️ Failed to clean existing process:', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
activeProcesses.set(sessionId, process);
|
|
34
|
+
processTreeMap.set(sessionId, []);
|
|
35
|
+
|
|
36
|
+
// Add cleanup handlers - ONLY cleanup tracking, not nuclear
|
|
37
|
+
process.exited.then((code) => {
|
|
38
|
+
debug.log('server', `🏁 Process for session ${sessionId} exited with code ${code}`);
|
|
39
|
+
// Just cleanup tracking - don't force kill other processes
|
|
40
|
+
activeProcesses.delete(sessionId);
|
|
41
|
+
processTreeMap.delete(sessionId);
|
|
42
|
+
// Clean up manual termination tracking after some time
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
manuallyTerminatedSessions.delete(sessionId);
|
|
45
|
+
}, 5000);
|
|
46
|
+
}).catch((error) => {
|
|
47
|
+
debug.log('server', `❌ Process error for session ${sessionId}:`, error);
|
|
48
|
+
// Just cleanup tracking - don't force kill other processes
|
|
49
|
+
activeProcesses.delete(sessionId);
|
|
50
|
+
processTreeMap.delete(sessionId);
|
|
51
|
+
// Clean up manual termination tracking
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
manuallyTerminatedSessions.delete(sessionId);
|
|
54
|
+
}, 5000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Start monitoring child processes for this session
|
|
58
|
+
if (process.pid) {
|
|
59
|
+
startChildProcessMonitoring(sessionId, process.pid);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Track child processes without aggressive monitoring
|
|
64
|
+
function startChildProcessMonitoring(sessionId: string, parentPid: number) {
|
|
65
|
+
const monitorInterval = setInterval(async () => {
|
|
66
|
+
try {
|
|
67
|
+
// Only monitor if session still exists and process is alive
|
|
68
|
+
if (!activeProcesses.has(sessionId)) {
|
|
69
|
+
clearInterval(monitorInterval);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const childPids = await discoverChildProcesses(parentPid);
|
|
74
|
+
const currentChildren = processTreeMap.get(sessionId) || [];
|
|
75
|
+
|
|
76
|
+
// Add new children to tracking (passive monitoring only)
|
|
77
|
+
for (const childPid of childPids) {
|
|
78
|
+
if (!currentChildren.includes(childPid)) {
|
|
79
|
+
debug.log('server', `🔍 Discovered new child process: ${childPid} for session: ${sessionId}`);
|
|
80
|
+
currentChildren.push(childPid);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
processTreeMap.set(sessionId, currentChildren);
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
debug.log('server', '⚠️ Child process monitoring error:', error);
|
|
88
|
+
// Don't kill anything on monitoring errors
|
|
89
|
+
}
|
|
90
|
+
}, 5000); // Check every 5 seconds (less aggressive)
|
|
91
|
+
|
|
92
|
+
// Clean up monitoring after reasonable time
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
clearInterval(monitorInterval);
|
|
95
|
+
}, 300000); // Stop after 5 minutes max
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// NUCLEAR: Discover all child processes of a parent
|
|
99
|
+
async function discoverChildProcesses(parentPid: number): Promise<number[]> {
|
|
100
|
+
const childPids: number[] = [];
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
if (isWindows) {
|
|
104
|
+
// Windows: Use WMIC to find all children recursively
|
|
105
|
+
const proc = Bun.spawn(['wmic', 'process', 'where', `ParentProcessId=${parentPid}`, 'get', 'ProcessId', '/format:csv'], {
|
|
106
|
+
stdout: 'pipe',
|
|
107
|
+
stderr: 'ignore'
|
|
108
|
+
});
|
|
109
|
+
const output = await new Response(proc.stdout).text();
|
|
110
|
+
|
|
111
|
+
const lines = output.split('\n');
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const match = line.match(/,(\d+)$/);
|
|
114
|
+
if (match) {
|
|
115
|
+
const childPid = parseInt(match[1]);
|
|
116
|
+
childPids.push(childPid);
|
|
117
|
+
|
|
118
|
+
// Recursively find grandchildren
|
|
119
|
+
const grandChildren = await discoverChildProcesses(childPid);
|
|
120
|
+
childPids.push(...grandChildren);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Unix: Use pgrep to find children
|
|
125
|
+
try {
|
|
126
|
+
const proc = Bun.spawn(['pgrep', '-P', parentPid.toString()], {
|
|
127
|
+
stdout: 'pipe',
|
|
128
|
+
stderr: 'ignore'
|
|
129
|
+
});
|
|
130
|
+
const output = await new Response(proc.stdout).text();
|
|
131
|
+
|
|
132
|
+
const lines = output.trim().split('\n');
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (line.trim()) {
|
|
135
|
+
const childPid = parseInt(line.trim());
|
|
136
|
+
if (!isNaN(childPid)) {
|
|
137
|
+
childPids.push(childPid);
|
|
138
|
+
|
|
139
|
+
// Recursively find grandchildren
|
|
140
|
+
const grandChildren = await discoverChildProcesses(childPid);
|
|
141
|
+
childPids.push(...grandChildren);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// No children found, which is fine
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
debug.log('server', '⚠️ Error discovering child processes:', error);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...new Set(childPids)]; // Remove duplicates
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// TARGETED: Complete process tree cleanup for specific session only
|
|
157
|
+
export async function nuclearProcessCleanup(sessionId: string) {
|
|
158
|
+
debug.log('server', '🎯 TARGETED CLEANUP for session:', sessionId);
|
|
159
|
+
|
|
160
|
+
// Mark this session as manually terminated
|
|
161
|
+
manuallyTerminatedSessions.add(sessionId);
|
|
162
|
+
|
|
163
|
+
const process = activeProcesses.get(sessionId);
|
|
164
|
+
const childPids = processTreeMap.get(sessionId) || [];
|
|
165
|
+
|
|
166
|
+
// Kill all child processes first (bottom-up)
|
|
167
|
+
for (const childPid of childPids) {
|
|
168
|
+
try {
|
|
169
|
+
debug.log('server', `💥 Killing child process: ${childPid}`);
|
|
170
|
+
if (isWindows) {
|
|
171
|
+
const proc = Bun.spawn(['taskkill', '/F', '/PID', childPid.toString()], {
|
|
172
|
+
stdout: 'ignore',
|
|
173
|
+
stderr: 'ignore'
|
|
174
|
+
});
|
|
175
|
+
await proc.exited;
|
|
176
|
+
} else {
|
|
177
|
+
const proc = Bun.spawn(['kill', '-9', childPid.toString()], {
|
|
178
|
+
stdout: 'ignore',
|
|
179
|
+
stderr: 'ignore'
|
|
180
|
+
});
|
|
181
|
+
await proc.exited;
|
|
182
|
+
}
|
|
183
|
+
debug.log('server', `✅ Killed child process: ${childPid}`);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
debug.log('server', `⚠️ Failed to kill child ${childPid}:`, error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Kill parent process
|
|
190
|
+
if (process && process.pid) {
|
|
191
|
+
try {
|
|
192
|
+
debug.log('server', `💥 Killing parent process: ${process.pid}`);
|
|
193
|
+
process.kill();
|
|
194
|
+
debug.log('server', `✅ Killed parent process: ${process.pid}`);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
debug.log('server', '⚠️ Failed to kill parent process:', error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// TARGETED: Only kill processes that belong to this specific session
|
|
201
|
+
// No more nuclear killing of all npm/node processes
|
|
202
|
+
debug.log('server', '🎯 Targeted cleanup completed - only session-specific processes terminated')
|
|
203
|
+
|
|
204
|
+
// Clean up tracking
|
|
205
|
+
activeProcesses.delete(sessionId);
|
|
206
|
+
processTreeMap.delete(sessionId);
|
|
207
|
+
|
|
208
|
+
debug.log('server', '🎯 TARGETED CLEANUP completed for session:', sessionId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function removeActiveProcess(sessionId: string) {
|
|
212
|
+
debug.log('server', '🗑️ Removing active process for session:', sessionId);
|
|
213
|
+
|
|
214
|
+
// Simple cleanup - just remove from tracking
|
|
215
|
+
activeProcesses.delete(sessionId);
|
|
216
|
+
processTreeMap.delete(sessionId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getActiveProcess(sessionId: string): Subprocess | undefined {
|
|
220
|
+
const process = activeProcesses.get(sessionId);
|
|
221
|
+
if (process && !process.pid) {
|
|
222
|
+
// Process is dead, remove it from the map
|
|
223
|
+
debug.log('server', '🪦 Process for session', sessionId, 'is dead, removing from active list');
|
|
224
|
+
activeProcesses.delete(sessionId);
|
|
225
|
+
processTreeMap.delete(sessionId);
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
return process;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getAllActiveProcesses(): Map<string, Subprocess> {
|
|
232
|
+
return activeProcesses;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function getActiveProcessCount(): number {
|
|
236
|
+
return activeProcesses.size;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Enhanced cleanup function with targeted options
|
|
240
|
+
export async function cleanupAllProcesses() {
|
|
241
|
+
debug.log('server', '🎯 TARGETED CLEANUP of all tracked processes...');
|
|
242
|
+
|
|
243
|
+
// Targeted cleanup for each session
|
|
244
|
+
for (const sessionId of activeProcesses.keys()) {
|
|
245
|
+
await nuclearProcessCleanup(sessionId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// REMOVED: Global process killing - only clean up tracked processes
|
|
249
|
+
debug.log('server', '🎯 All tracked processes cleaned up - no global process termination')
|
|
250
|
+
|
|
251
|
+
activeProcesses.clear();
|
|
252
|
+
processTreeMap.clear();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Get comprehensive process status for debugging
|
|
256
|
+
export function getProcessStatus(sessionId: string) {
|
|
257
|
+
const process = activeProcesses.get(sessionId);
|
|
258
|
+
const childPids = processTreeMap.get(sessionId) || [];
|
|
259
|
+
|
|
260
|
+
if (!process) {
|
|
261
|
+
return { exists: false, childProcesses: [] };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
exists: true,
|
|
266
|
+
pid: process.pid,
|
|
267
|
+
killed: process.killed,
|
|
268
|
+
exitCode: process.exitCode,
|
|
269
|
+
signalCode: process.signalCode,
|
|
270
|
+
childProcesses: childPids
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Get all tracked child processes for a session
|
|
275
|
+
export function getChildProcesses(sessionId: string): number[] {
|
|
276
|
+
return processTreeMap.get(sessionId) || [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function wasManuallyTerminated(sessionId: string): boolean {
|
|
280
|
+
return manuallyTerminatedSessions.has(sessionId);
|
|
281
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Addressable Blob Store
|
|
3
|
+
* Git-like storage for snapshot file contents
|
|
4
|
+
*
|
|
5
|
+
* Structure:
|
|
6
|
+
* ~/.clopen/snapshots/blobs/{hash[0:2]}/{hash}.gz - compressed file blobs
|
|
7
|
+
* ~/.clopen/snapshots/trees/{snapshotId}.json - tree maps (filepath -> hash)
|
|
8
|
+
*
|
|
9
|
+
* Deduplication: Same file content across any snapshot is stored only once.
|
|
10
|
+
* Compression: All blobs are gzip compressed to minimize disk usage.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import { gzipSync, gunzipSync } from 'zlib';
|
|
17
|
+
import { debug } from '$shared/utils/logger';
|
|
18
|
+
|
|
19
|
+
const SNAPSHOTS_DIR = join(homedir(), '.clopen', 'snapshots');
|
|
20
|
+
const BLOBS_DIR = join(SNAPSHOTS_DIR, 'blobs');
|
|
21
|
+
const TREES_DIR = join(SNAPSHOTS_DIR, 'trees');
|
|
22
|
+
|
|
23
|
+
export interface TreeMap {
|
|
24
|
+
[filepath: string]: string; // filepath -> blob hash
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FileHashCacheEntry {
|
|
28
|
+
mtimeMs: number;
|
|
29
|
+
size: number;
|
|
30
|
+
hash: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class BlobStore {
|
|
34
|
+
private initialized = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cache: filepath -> { mtimeMs, size, hash }
|
|
38
|
+
* Avoids re-reading files that haven't changed (based on mtime + size).
|
|
39
|
+
*/
|
|
40
|
+
private fileHashCache = new Map<string, FileHashCacheEntry>();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure storage directories exist
|
|
44
|
+
*/
|
|
45
|
+
async init(): Promise<void> {
|
|
46
|
+
if (this.initialized) return;
|
|
47
|
+
await fs.mkdir(BLOBS_DIR, { recursive: true });
|
|
48
|
+
await fs.mkdir(TREES_DIR, { recursive: true });
|
|
49
|
+
this.initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute SHA-256 hash of content (Buffer for binary safety)
|
|
54
|
+
*/
|
|
55
|
+
hashContent(content: Buffer): string {
|
|
56
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
57
|
+
hasher.update(content);
|
|
58
|
+
return hasher.digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get blob file path from hash (using 2-char prefix subdirectory like git)
|
|
63
|
+
*/
|
|
64
|
+
private getBlobPath(hash: string): string {
|
|
65
|
+
const prefix = hash.substring(0, 2);
|
|
66
|
+
return join(BLOBS_DIR, prefix, hash + '.gz');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a blob exists
|
|
71
|
+
*/
|
|
72
|
+
async hasBlob(hash: string): Promise<boolean> {
|
|
73
|
+
try {
|
|
74
|
+
await fs.access(this.getBlobPath(hash));
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Store content as a blob. Returns the hash.
|
|
83
|
+
* If blob already exists (same hash), it's a no-op (deduplication).
|
|
84
|
+
* Accepts Buffer to safely handle both text and binary files.
|
|
85
|
+
*/
|
|
86
|
+
async storeBlob(content: Buffer): Promise<string> {
|
|
87
|
+
await this.init();
|
|
88
|
+
const hash = this.hashContent(content);
|
|
89
|
+
|
|
90
|
+
// Check if already exists (deduplication)
|
|
91
|
+
if (await this.hasBlob(hash)) {
|
|
92
|
+
return hash;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create prefix directory
|
|
96
|
+
const prefixDir = join(BLOBS_DIR, hash.substring(0, 2));
|
|
97
|
+
await fs.mkdir(prefixDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
// Compress and write (Buffer directly, no encoding conversion)
|
|
100
|
+
const compressed = gzipSync(content);
|
|
101
|
+
await fs.writeFile(this.getBlobPath(hash), compressed);
|
|
102
|
+
|
|
103
|
+
return hash;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read blob content by hash. Returns Buffer to safely handle binary files.
|
|
108
|
+
*/
|
|
109
|
+
async readBlob(hash: string): Promise<Buffer> {
|
|
110
|
+
const blobPath = this.getBlobPath(hash);
|
|
111
|
+
const compressed = await fs.readFile(blobPath);
|
|
112
|
+
return gunzipSync(compressed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Store a tree (snapshot state) as a JSON file.
|
|
117
|
+
* Returns the tree hash for reference.
|
|
118
|
+
*/
|
|
119
|
+
async storeTree(snapshotId: string, tree: TreeMap): Promise<string> {
|
|
120
|
+
await this.init();
|
|
121
|
+
const treePath = join(TREES_DIR, `${snapshotId}.json`);
|
|
122
|
+
const content = JSON.stringify(tree);
|
|
123
|
+
const treeHash = this.hashContent(Buffer.from(content, 'utf-8'));
|
|
124
|
+
await fs.writeFile(treePath, content, 'utf-8');
|
|
125
|
+
return treeHash;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read a tree by snapshot ID
|
|
130
|
+
*/
|
|
131
|
+
async readTree(snapshotId: string): Promise<TreeMap> {
|
|
132
|
+
const treePath = join(TREES_DIR, `${snapshotId}.json`);
|
|
133
|
+
const content = await fs.readFile(treePath, 'utf-8');
|
|
134
|
+
return JSON.parse(content) as TreeMap;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a tree exists
|
|
139
|
+
*/
|
|
140
|
+
async hasTree(snapshotId: string): Promise<boolean> {
|
|
141
|
+
try {
|
|
142
|
+
await fs.access(join(TREES_DIR, `${snapshotId}.json`));
|
|
143
|
+
return true;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve a tree to full file contents (as Buffers).
|
|
151
|
+
* Reads all blobs in parallel for performance.
|
|
152
|
+
* Returns { filepath: Buffer } map for binary-safe handling.
|
|
153
|
+
*/
|
|
154
|
+
async resolveTree(tree: TreeMap): Promise<Record<string, Buffer>> {
|
|
155
|
+
const result: Record<string, Buffer> = {};
|
|
156
|
+
|
|
157
|
+
const entries = Object.entries(tree);
|
|
158
|
+
const blobPromises = entries.map(async ([filepath, hash]) => {
|
|
159
|
+
try {
|
|
160
|
+
const content = await this.readBlob(hash);
|
|
161
|
+
return { filepath, content };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
debug.warn('snapshot', `Could not read blob ${hash} for ${filepath}:`, err);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const results = await Promise.all(blobPromises);
|
|
169
|
+
for (const r of results) {
|
|
170
|
+
if (r) {
|
|
171
|
+
result[r.filepath] = r.content;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Hash a file using mtime cache. Returns { hash, content? }.
|
|
180
|
+
* If the file hasn't changed (same mtime+size), returns cached hash without reading content.
|
|
181
|
+
* If the file has changed, reads content, hashes it, stores blob, and caches.
|
|
182
|
+
* Reads as Buffer to safely handle binary files (images, PDFs, etc.).
|
|
183
|
+
*
|
|
184
|
+
* @returns hash and content Buffer (content is null if cache hit and blob already exists)
|
|
185
|
+
*/
|
|
186
|
+
async hashFile(filepath: string, fullPath: string): Promise<{ hash: string; content: Buffer | null; cached: boolean }> {
|
|
187
|
+
await this.init();
|
|
188
|
+
|
|
189
|
+
const stat = await fs.stat(fullPath);
|
|
190
|
+
|
|
191
|
+
// Check mtime cache
|
|
192
|
+
const cached = this.fileHashCache.get(filepath);
|
|
193
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
194
|
+
return { hash: cached.hash, content: null, cached: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// File changed - read as Buffer (binary-safe, no encoding conversion)
|
|
198
|
+
const content = await fs.readFile(fullPath);
|
|
199
|
+
const hash = this.hashContent(content);
|
|
200
|
+
|
|
201
|
+
// Store blob (deduplication handled internally)
|
|
202
|
+
await this.storeBlob(content);
|
|
203
|
+
|
|
204
|
+
// Update cache
|
|
205
|
+
this.fileHashCache.set(filepath, {
|
|
206
|
+
mtimeMs: stat.mtimeMs,
|
|
207
|
+
size: stat.size,
|
|
208
|
+
hash
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return { hash, content, cached: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Delete a tree file (cleanup)
|
|
216
|
+
*/
|
|
217
|
+
async deleteTree(snapshotId: string): Promise<void> {
|
|
218
|
+
try {
|
|
219
|
+
await fs.unlink(join(TREES_DIR, `${snapshotId}.json`));
|
|
220
|
+
} catch {
|
|
221
|
+
// Ignore - might not exist
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Export singleton
|
|
227
|
+
export const blobStore = new BlobStore();
|