@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,308 @@
|
|
|
1
|
+
import { join, extname } from 'path';
|
|
2
|
+
import { readFileWithEncoding, isTextFile } from '$shared/utils/file-type-detection';
|
|
3
|
+
|
|
4
|
+
import { debug } from '$shared/utils/logger';
|
|
5
|
+
|
|
6
|
+
// Bun-compatible readdir implementation (cross-platform)
|
|
7
|
+
async function readdir(path: string): Promise<string[]> {
|
|
8
|
+
let proc;
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
proc = Bun.spawn(['cmd', '/c', 'dir', '/b', path], { stdout: 'pipe', stderr: 'ignore' });
|
|
11
|
+
} else {
|
|
12
|
+
proc = Bun.spawn(['ls', '-1', path], { stdout: 'pipe', stderr: 'ignore' });
|
|
13
|
+
}
|
|
14
|
+
const result = await new Response(proc.stdout).text();
|
|
15
|
+
// Split and clean up, removing \r characters for Windows compatibility
|
|
16
|
+
return result.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Return types
|
|
20
|
+
export interface FileTreeNode {
|
|
21
|
+
name: string;
|
|
22
|
+
type: 'file' | 'directory';
|
|
23
|
+
path: string;
|
|
24
|
+
size?: number;
|
|
25
|
+
modified: string;
|
|
26
|
+
extension?: string;
|
|
27
|
+
children?: FileTreeNode[];
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helper function to build file tree
|
|
32
|
+
export async function buildFileTree(
|
|
33
|
+
rootPath: string,
|
|
34
|
+
maxDepth: number = 3,
|
|
35
|
+
currentDepth: number = 0,
|
|
36
|
+
expandedPaths?: Set<string>
|
|
37
|
+
): Promise<FileTreeNode | null> {
|
|
38
|
+
try {
|
|
39
|
+
const file = Bun.file(rootPath);
|
|
40
|
+
const stats = await file.stat();
|
|
41
|
+
const name = rootPath.split(/[/\\]/).pop() || 'root';
|
|
42
|
+
|
|
43
|
+
if (stats.isFile()) {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
type: 'file',
|
|
47
|
+
path: rootPath,
|
|
48
|
+
size: stats.size,
|
|
49
|
+
modified: stats.mtime.toISOString(),
|
|
50
|
+
extension: extname(rootPath)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (stats.isDirectory()) {
|
|
55
|
+
try {
|
|
56
|
+
const items = await readdir(rootPath);
|
|
57
|
+
const children: FileTreeNode[] = [];
|
|
58
|
+
|
|
59
|
+
// Check if this folder should load children:
|
|
60
|
+
// 1. Initial load (depth 0) - always load
|
|
61
|
+
// 2. Folder is in expandedPaths - load recursively
|
|
62
|
+
const shouldLoadChildren = currentDepth < 1 || (expandedPaths && expandedPaths.has(rootPath));
|
|
63
|
+
|
|
64
|
+
if (shouldLoadChildren) {
|
|
65
|
+
for (const item of items.slice(0, 100)) { // Limit to 100 items per directory
|
|
66
|
+
const itemPath = join(rootPath, item);
|
|
67
|
+
|
|
68
|
+
// Show all files and directories
|
|
69
|
+
try {
|
|
70
|
+
const itemFile = Bun.file(itemPath);
|
|
71
|
+
const itemStats = await itemFile.stat();
|
|
72
|
+
|
|
73
|
+
if (itemStats.isFile()) {
|
|
74
|
+
children.push({
|
|
75
|
+
name: item,
|
|
76
|
+
type: 'file',
|
|
77
|
+
path: itemPath,
|
|
78
|
+
size: itemStats.size,
|
|
79
|
+
modified: itemStats.mtime.toISOString(),
|
|
80
|
+
extension: extname(item)
|
|
81
|
+
});
|
|
82
|
+
} else if (itemStats.isDirectory()) {
|
|
83
|
+
// Check if this directory should be expanded
|
|
84
|
+
const isExpanded = expandedPaths && expandedPaths.has(itemPath);
|
|
85
|
+
|
|
86
|
+
if (isExpanded) {
|
|
87
|
+
// Recursively load children for expanded folders
|
|
88
|
+
const subTree = await buildFileTree(itemPath, maxDepth, currentDepth + 1, expandedPaths);
|
|
89
|
+
if (subTree) {
|
|
90
|
+
children.push(subTree);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// For collapsed directories, just add basic info without loading children
|
|
94
|
+
children.push({
|
|
95
|
+
name: item,
|
|
96
|
+
type: 'directory',
|
|
97
|
+
path: itemPath,
|
|
98
|
+
modified: itemStats.mtime.toISOString(),
|
|
99
|
+
// Add empty children array to indicate it can be expanded
|
|
100
|
+
children: []
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
debug.debug('file', `Cannot access ${itemPath}:`, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Sort children: directories first, then files
|
|
111
|
+
children.sort((a, b) => {
|
|
112
|
+
if (a.type !== b.type) {
|
|
113
|
+
return a.type === 'directory' ? -1 : 1;
|
|
114
|
+
}
|
|
115
|
+
return a.name.localeCompare(b.name);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
type: 'directory',
|
|
121
|
+
path: rootPath,
|
|
122
|
+
children,
|
|
123
|
+
modified: stats.mtime.toISOString()
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
// Permission denied or other error reading directory
|
|
127
|
+
return {
|
|
128
|
+
name,
|
|
129
|
+
type: 'directory',
|
|
130
|
+
path: rootPath,
|
|
131
|
+
error: 'Permission denied',
|
|
132
|
+
modified: stats.mtime.toISOString()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
debug.error('file', `Error building file tree for ${rootPath}:`, error);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Helper function to list directory contents
|
|
145
|
+
export async function listDirectoryContents(dirPath: string): Promise<FileTreeNode[]> {
|
|
146
|
+
// Use stat directly instead of Bun.file for directory checking
|
|
147
|
+
const dirFile = Bun.file(dirPath);
|
|
148
|
+
const stats = await dirFile.stat();
|
|
149
|
+
|
|
150
|
+
if (!stats.isDirectory()) {
|
|
151
|
+
throw new Error('Path is not a directory');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const items = await readdir(dirPath);
|
|
155
|
+
const children: FileTreeNode[] = [];
|
|
156
|
+
|
|
157
|
+
// Show all files and directories
|
|
158
|
+
for (const item of items.slice(0, 100)) { // Limit to 100 items per directory
|
|
159
|
+
|
|
160
|
+
const itemPath = join(dirPath, item);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const itemFile = Bun.file(itemPath);
|
|
164
|
+
const itemStats = await itemFile.stat();
|
|
165
|
+
|
|
166
|
+
if (itemStats.isFile()) {
|
|
167
|
+
children.push({
|
|
168
|
+
name: item,
|
|
169
|
+
type: 'file',
|
|
170
|
+
path: itemPath,
|
|
171
|
+
size: itemStats.size,
|
|
172
|
+
modified: itemStats.mtime.toISOString(),
|
|
173
|
+
extension: extname(item)
|
|
174
|
+
});
|
|
175
|
+
} else if (itemStats.isDirectory()) {
|
|
176
|
+
children.push({
|
|
177
|
+
name: item,
|
|
178
|
+
type: 'directory',
|
|
179
|
+
path: itemPath,
|
|
180
|
+
modified: itemStats.mtime.toISOString(),
|
|
181
|
+
// Add empty children array to indicate it can be expanded
|
|
182
|
+
children: []
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
debug.debug('file', `Cannot access ${itemPath}:`, error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Sort children: directories first, then files
|
|
191
|
+
children.sort((a, b) => {
|
|
192
|
+
if (a.type !== b.type) {
|
|
193
|
+
return a.type === 'directory' ? -1 : 1;
|
|
194
|
+
}
|
|
195
|
+
return a.name.localeCompare(b.name);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return children;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// File content data
|
|
202
|
+
export interface FileContentData {
|
|
203
|
+
content: string;
|
|
204
|
+
size: number;
|
|
205
|
+
modified: string;
|
|
206
|
+
extension: string;
|
|
207
|
+
encoding?: string;
|
|
208
|
+
isBinary?: boolean;
|
|
209
|
+
error?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Helper function to read file contents
|
|
213
|
+
export async function readFileContents(filePath: string): Promise<FileContentData> {
|
|
214
|
+
const file = Bun.file(filePath);
|
|
215
|
+
if (!(await file.exists())) {
|
|
216
|
+
throw new Error('File does not exist');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const stats = await file.stat();
|
|
220
|
+
|
|
221
|
+
// Check if it's a text file
|
|
222
|
+
const isText = await isTextFile(filePath);
|
|
223
|
+
|
|
224
|
+
if (!isText) {
|
|
225
|
+
// Binary file
|
|
226
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
227
|
+
return {
|
|
228
|
+
content: `[Binary file - ${buffer.length} bytes]`,
|
|
229
|
+
size: stats.size,
|
|
230
|
+
modified: stats.mtime.toISOString(),
|
|
231
|
+
extension: extname(filePath),
|
|
232
|
+
isBinary: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Text file - detect encoding and read
|
|
237
|
+
const { content, detectedEncoding } = await readFileWithEncoding(filePath);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
content,
|
|
241
|
+
size: stats.size,
|
|
242
|
+
modified: stats.mtime.toISOString(),
|
|
243
|
+
extension: extname(filePath),
|
|
244
|
+
encoding: detectedEncoding
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Helper function to search files
|
|
249
|
+
export async function searchFiles(rootPath: string, query: string): Promise<FileTreeNode[]> {
|
|
250
|
+
const results: FileTreeNode[] = [];
|
|
251
|
+
|
|
252
|
+
async function searchRecursive(dirPath: string, depth: number = 0) {
|
|
253
|
+
// Limit search depth to prevent infinite recursion
|
|
254
|
+
if (depth > 5) return;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const items = await readdir(dirPath);
|
|
258
|
+
|
|
259
|
+
for (const item of items) {
|
|
260
|
+
// Skip common directories that shouldn't be searched
|
|
261
|
+
const skipDirs = ['node_modules', '.git', '.svelte-kit', 'build', 'dist', 'coverage', '.next', '.nuxt', 'target'];
|
|
262
|
+
if (skipDirs.includes(item)) continue;
|
|
263
|
+
|
|
264
|
+
const itemPath = join(dirPath, item);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const itemFile = Bun.file(itemPath);
|
|
268
|
+
const stats = await itemFile.stat();
|
|
269
|
+
|
|
270
|
+
// Check if item name matches query (case insensitive)
|
|
271
|
+
if (item.toLowerCase().includes(query.toLowerCase())) {
|
|
272
|
+
results.push({
|
|
273
|
+
name: item,
|
|
274
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
275
|
+
path: itemPath,
|
|
276
|
+
size: stats.isFile() ? stats.size : undefined,
|
|
277
|
+
modified: stats.mtime.toISOString(),
|
|
278
|
+
extension: stats.isFile() ? extname(item) : undefined
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Recursively search in directories
|
|
283
|
+
if (stats.isDirectory()) {
|
|
284
|
+
await searchRecursive(itemPath, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// Skip items we can't access
|
|
288
|
+
debug.debug('file', `Cannot access ${itemPath}:`, error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
debug.debug('file', `Cannot read directory ${dirPath}:`, error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await searchRecursive(rootPath);
|
|
297
|
+
|
|
298
|
+
// Sort results: directories first, then files, both alphabetically
|
|
299
|
+
results.sort((a, b) => {
|
|
300
|
+
if (a.type !== b.type) {
|
|
301
|
+
return a.type === 'directory' ? -1 : 1;
|
|
302
|
+
}
|
|
303
|
+
return a.name.localeCompare(b.name);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Limit results to prevent overwhelming the UI
|
|
307
|
+
return results.slice(0, 100);
|
|
308
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Real-time file system watcher using Node's fs.watch (compatible with Bun)
|
|
5
|
+
* Features:
|
|
6
|
+
* - Per-project watcher management
|
|
7
|
+
* - Debounced events to prevent spam
|
|
8
|
+
* - Automatic cleanup on unwatch
|
|
9
|
+
* - Cross-platform support (Windows/Unix)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { watch, type FSWatcher, existsSync } from 'node:fs';
|
|
13
|
+
import { stat } from 'node:fs/promises';
|
|
14
|
+
import { join, relative, normalize, sep } from 'node:path';
|
|
15
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
16
|
+
import { debug } from '$shared/utils/logger';
|
|
17
|
+
import type { FileChange } from '$shared/types/filesystem';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Debounce configuration
|
|
21
|
+
*/
|
|
22
|
+
const DEBOUNCE_MS = 300; // Debounce file change events
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Directories to ignore when watching
|
|
26
|
+
*/
|
|
27
|
+
const IGNORED_DIRS = new Set([
|
|
28
|
+
'node_modules',
|
|
29
|
+
'.git',
|
|
30
|
+
'.svelte-kit',
|
|
31
|
+
'dist',
|
|
32
|
+
'build',
|
|
33
|
+
'.next',
|
|
34
|
+
'.nuxt',
|
|
35
|
+
'.output',
|
|
36
|
+
'__pycache__',
|
|
37
|
+
'.pytest_cache',
|
|
38
|
+
'coverage',
|
|
39
|
+
'.nyc_output',
|
|
40
|
+
'.turbo',
|
|
41
|
+
'.cache',
|
|
42
|
+
'.temp',
|
|
43
|
+
'.tmp',
|
|
44
|
+
'vendor'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Files to ignore
|
|
49
|
+
*/
|
|
50
|
+
const IGNORED_FILES = new Set([
|
|
51
|
+
'.DS_Store',
|
|
52
|
+
'Thumbs.db',
|
|
53
|
+
'.gitkeep',
|
|
54
|
+
'.gitignore~'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Git state files to watch for external git operations
|
|
59
|
+
*/
|
|
60
|
+
const GIT_STATE_FILES = ['index', 'HEAD', 'MERGE_HEAD', 'REBASE_HEAD'];
|
|
61
|
+
const GIT_DEBOUNCE_MS = 500;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Watcher instance for a project
|
|
65
|
+
*/
|
|
66
|
+
interface ProjectWatcher {
|
|
67
|
+
watcher: FSWatcher;
|
|
68
|
+
projectPath: string;
|
|
69
|
+
projectId: string;
|
|
70
|
+
debounceTimer: ReturnType<typeof setTimeout> | null;
|
|
71
|
+
pendingChanges: Map<string, FileChange>;
|
|
72
|
+
subWatchers: Map<string, FSWatcher>;
|
|
73
|
+
gitWatcher: FSWatcher | null;
|
|
74
|
+
gitRefsWatcher: FSWatcher | null;
|
|
75
|
+
gitDebounceTimer: ReturnType<typeof setTimeout> | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* File Watcher Manager
|
|
80
|
+
* Manages file watchers per project
|
|
81
|
+
*/
|
|
82
|
+
class FileWatcherManager {
|
|
83
|
+
private watchers = new Map<string, ProjectWatcher>();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Start watching a project directory
|
|
87
|
+
*/
|
|
88
|
+
async startWatching(projectId: string, projectPath: string): Promise<boolean> {
|
|
89
|
+
// Already watching this project
|
|
90
|
+
if (this.watchers.has(projectId)) {
|
|
91
|
+
debug.log('file', `Already watching project: ${projectId}`);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Normalize path
|
|
97
|
+
const normalizedPath = normalize(projectPath);
|
|
98
|
+
|
|
99
|
+
// Verify path exists and is a directory
|
|
100
|
+
const pathStat = await stat(normalizedPath);
|
|
101
|
+
if (!pathStat.isDirectory()) {
|
|
102
|
+
debug.error('file', `Path is not a directory: ${normalizedPath}`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create main watcher for project root
|
|
107
|
+
const watcher = watch(normalizedPath, { recursive: true }, (eventType, filename) => {
|
|
108
|
+
if (filename) {
|
|
109
|
+
this.handleFileChange(projectId, normalizedPath, filename, eventType);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Handle watcher errors
|
|
114
|
+
watcher.on('error', (error) => {
|
|
115
|
+
debug.error('file', `Watcher error for project ${projectId}:`, error);
|
|
116
|
+
// Try to emit error to clients
|
|
117
|
+
ws.emit.project(projectId, 'files:watch-error', {
|
|
118
|
+
projectId,
|
|
119
|
+
error: error.message || 'File watcher error'
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Store watcher instance
|
|
124
|
+
const projectWatcher: ProjectWatcher = {
|
|
125
|
+
watcher,
|
|
126
|
+
projectPath: normalizedPath,
|
|
127
|
+
projectId,
|
|
128
|
+
debounceTimer: null,
|
|
129
|
+
pendingChanges: new Map(),
|
|
130
|
+
subWatchers: new Map(),
|
|
131
|
+
gitWatcher: null,
|
|
132
|
+
gitRefsWatcher: null,
|
|
133
|
+
gitDebounceTimer: null
|
|
134
|
+
};
|
|
135
|
+
this.watchers.set(projectId, projectWatcher);
|
|
136
|
+
|
|
137
|
+
// Start git state watcher (for external git operations)
|
|
138
|
+
this.startGitWatcher(projectId, normalizedPath);
|
|
139
|
+
|
|
140
|
+
debug.log('file', `Started watching project: ${projectId} at ${normalizedPath}`);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
debug.error('file', `Failed to start watching project ${projectId}:`, error);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stop watching a project directory
|
|
150
|
+
*/
|
|
151
|
+
stopWatching(projectId: string): boolean {
|
|
152
|
+
const projectWatcher = this.watchers.get(projectId);
|
|
153
|
+
if (!projectWatcher) {
|
|
154
|
+
debug.log('file', `Not watching project: ${projectId}`);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Close main watcher
|
|
160
|
+
projectWatcher.watcher.close();
|
|
161
|
+
|
|
162
|
+
// Close all sub-watchers
|
|
163
|
+
for (const subWatcher of projectWatcher.subWatchers.values()) {
|
|
164
|
+
subWatcher.close();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Close git watchers
|
|
168
|
+
projectWatcher.gitWatcher?.close();
|
|
169
|
+
projectWatcher.gitRefsWatcher?.close();
|
|
170
|
+
|
|
171
|
+
// Clear debounce timers
|
|
172
|
+
if (projectWatcher.debounceTimer) {
|
|
173
|
+
clearTimeout(projectWatcher.debounceTimer);
|
|
174
|
+
}
|
|
175
|
+
if (projectWatcher.gitDebounceTimer) {
|
|
176
|
+
clearTimeout(projectWatcher.gitDebounceTimer);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Remove from map
|
|
180
|
+
this.watchers.delete(projectId);
|
|
181
|
+
|
|
182
|
+
debug.log('file', `Stopped watching project: ${projectId}`);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
debug.error('file', `Error stopping watcher for project ${projectId}:`, error);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a project is being watched
|
|
192
|
+
*/
|
|
193
|
+
isWatching(projectId: string): boolean {
|
|
194
|
+
return this.watchers.has(projectId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get all watched project IDs
|
|
199
|
+
*/
|
|
200
|
+
getWatchedProjects(): string[] {
|
|
201
|
+
return Array.from(this.watchers.keys());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handle file change event
|
|
206
|
+
*/
|
|
207
|
+
private async handleFileChange(
|
|
208
|
+
projectId: string,
|
|
209
|
+
projectPath: string,
|
|
210
|
+
filename: string,
|
|
211
|
+
eventType: string
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const projectWatcher = this.watchers.get(projectId);
|
|
214
|
+
if (!projectWatcher) return;
|
|
215
|
+
|
|
216
|
+
// Normalize filename to use forward slashes for consistency
|
|
217
|
+
const normalizedFilename = filename.replace(/\\/g, '/');
|
|
218
|
+
|
|
219
|
+
// Check if should be ignored
|
|
220
|
+
if (this.shouldIgnore(normalizedFilename)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build full path
|
|
225
|
+
const fullPath = join(projectPath, filename);
|
|
226
|
+
|
|
227
|
+
// Determine change type
|
|
228
|
+
let changeType: 'created' | 'modified' | 'deleted';
|
|
229
|
+
if (eventType === 'change') {
|
|
230
|
+
changeType = 'modified';
|
|
231
|
+
} else {
|
|
232
|
+
// 'rename' event — check if file exists to distinguish create from delete
|
|
233
|
+
try {
|
|
234
|
+
await stat(fullPath);
|
|
235
|
+
changeType = 'created';
|
|
236
|
+
} catch {
|
|
237
|
+
changeType = 'deleted';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Create file change object
|
|
242
|
+
const fileChange: FileChange = {
|
|
243
|
+
path: fullPath,
|
|
244
|
+
type: changeType,
|
|
245
|
+
timestamp: new Date().toISOString()
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Add to pending changes (using path as key to dedupe)
|
|
249
|
+
projectWatcher.pendingChanges.set(fullPath, fileChange);
|
|
250
|
+
|
|
251
|
+
// Debounce: clear existing timer and set new one
|
|
252
|
+
if (projectWatcher.debounceTimer) {
|
|
253
|
+
clearTimeout(projectWatcher.debounceTimer);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
projectWatcher.debounceTimer = setTimeout(() => {
|
|
257
|
+
this.flushPendingChanges(projectId);
|
|
258
|
+
}, DEBOUNCE_MS);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if a file/directory should be ignored
|
|
263
|
+
*/
|
|
264
|
+
private shouldIgnore(filename: string): boolean {
|
|
265
|
+
const parts = filename.split('/');
|
|
266
|
+
|
|
267
|
+
// Check each path segment
|
|
268
|
+
for (const part of parts) {
|
|
269
|
+
if (IGNORED_DIRS.has(part) || IGNORED_FILES.has(part)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
// Ignore hidden files and directories (except .env files)
|
|
273
|
+
if (part.startsWith('.') && !part.startsWith('.env')) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Flush pending changes to clients
|
|
284
|
+
*/
|
|
285
|
+
private flushPendingChanges(projectId: string): void {
|
|
286
|
+
const projectWatcher = this.watchers.get(projectId);
|
|
287
|
+
if (!projectWatcher || projectWatcher.pendingChanges.size === 0) return;
|
|
288
|
+
|
|
289
|
+
// Convert pending changes to array
|
|
290
|
+
const changes = Array.from(projectWatcher.pendingChanges.values());
|
|
291
|
+
|
|
292
|
+
// Clear pending changes
|
|
293
|
+
projectWatcher.pendingChanges.clear();
|
|
294
|
+
projectWatcher.debounceTimer = null;
|
|
295
|
+
|
|
296
|
+
// Emit changes to users currently viewing the project
|
|
297
|
+
ws.emit.project(projectId, 'files:changed', {
|
|
298
|
+
projectId,
|
|
299
|
+
changes,
|
|
300
|
+
timestamp: Date.now()
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
debug.log(
|
|
304
|
+
'file',
|
|
305
|
+
`Emitted ${changes.length} file changes for project ${projectId}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Start watching .git directory for external git operations
|
|
311
|
+
* Watches: .git/index (staging), .git/HEAD (branch switch), .git/refs/ (branches/tags)
|
|
312
|
+
*/
|
|
313
|
+
private startGitWatcher(projectId: string, projectPath: string): void {
|
|
314
|
+
const projectWatcher = this.watchers.get(projectId);
|
|
315
|
+
if (!projectWatcher) return;
|
|
316
|
+
|
|
317
|
+
const gitDir = join(projectPath, '.git');
|
|
318
|
+
if (!existsSync(gitDir)) return;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
// Watch .git/ root for index, HEAD, MERGE_HEAD changes
|
|
322
|
+
projectWatcher.gitWatcher = watch(gitDir, (eventType, filename) => {
|
|
323
|
+
if (!filename) return;
|
|
324
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
325
|
+
if (GIT_STATE_FILES.includes(normalized)) {
|
|
326
|
+
this.emitGitChanged(projectId);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
projectWatcher.gitWatcher.on('error', () => {
|
|
330
|
+
// Silently ignore git watcher errors
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Watch .git/refs/ for branch/tag create/delete
|
|
334
|
+
const refsDir = join(gitDir, 'refs');
|
|
335
|
+
if (existsSync(refsDir)) {
|
|
336
|
+
projectWatcher.gitRefsWatcher = watch(refsDir, { recursive: true }, (_eventType, _filename) => {
|
|
337
|
+
this.emitGitChanged(projectId);
|
|
338
|
+
});
|
|
339
|
+
projectWatcher.gitRefsWatcher.on('error', () => {
|
|
340
|
+
// Silently ignore refs watcher errors
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
debug.log('file', `Started git watcher for project: ${projectId}`);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
debug.warn('file', `Failed to start git watcher for project ${projectId}:`, err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Debounced emit of git:changed event
|
|
352
|
+
*/
|
|
353
|
+
private emitGitChanged(projectId: string): void {
|
|
354
|
+
const projectWatcher = this.watchers.get(projectId);
|
|
355
|
+
if (!projectWatcher) return;
|
|
356
|
+
|
|
357
|
+
if (projectWatcher.gitDebounceTimer) {
|
|
358
|
+
clearTimeout(projectWatcher.gitDebounceTimer);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
projectWatcher.gitDebounceTimer = setTimeout(() => {
|
|
362
|
+
projectWatcher.gitDebounceTimer = null;
|
|
363
|
+
ws.emit.project(projectId, 'git:changed', {
|
|
364
|
+
projectId,
|
|
365
|
+
timestamp: Date.now()
|
|
366
|
+
});
|
|
367
|
+
debug.log('file', `Emitted git:changed for project ${projectId}`);
|
|
368
|
+
}, GIT_DEBOUNCE_MS);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Stop all watchers (cleanup)
|
|
373
|
+
*/
|
|
374
|
+
stopAll(): void {
|
|
375
|
+
for (const projectId of this.watchers.keys()) {
|
|
376
|
+
this.stopWatching(projectId);
|
|
377
|
+
}
|
|
378
|
+
debug.log('file', 'Stopped all file watchers');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Export singleton instance
|
|
383
|
+
export const fileWatcher = new FileWatcherManager();
|