@myrialabs/clopen 0.0.5 → 0.0.7
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 +12 -6
- package/.github/workflows/ci.yml +86 -86
- package/CONTRIBUTING.md +499 -499
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/backend/index.ts +144 -165
- package/backend/lib/chat/helpers.ts +42 -42
- package/backend/lib/chat/index.ts +1 -1
- package/backend/lib/chat/stream-manager.ts +1126 -1126
- package/backend/lib/database/README.md +76 -76
- package/backend/lib/database/index.ts +118 -118
- package/backend/lib/database/migrations/001_create_projects_table.ts +30 -30
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +32 -32
- package/backend/lib/database/migrations/003_create_messages_table.ts +31 -31
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -34
- package/backend/lib/database/migrations/005_create_settings_table.ts +23 -23
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +57 -57
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +40 -40
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +61 -61
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -41
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -70
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -156
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -41
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -118
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -18
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -18
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -34
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -32
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -24
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -18
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -32
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -33
- package/backend/lib/database/migrations/index.ts +153 -153
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -87
- package/backend/lib/database/queries/engine-queries.ts +75 -75
- package/backend/lib/database/queries/index.ts +8 -8
- package/backend/lib/database/queries/message-queries.ts +471 -471
- package/backend/lib/database/queries/project-queries.ts +117 -117
- package/backend/lib/database/queries/session-queries.ts +270 -270
- package/backend/lib/database/queries/settings-queries.ts +33 -33
- package/backend/lib/database/queries/snapshot-queries.ts +325 -325
- package/backend/lib/database/queries/utils-queries.ts +58 -58
- package/backend/lib/database/seeders/index.ts +12 -12
- package/backend/lib/database/seeders/settings_seeder.ts +83 -83
- package/backend/lib/database/utils/connection.ts +173 -173
- package/backend/lib/database/utils/index.ts +3 -3
- package/backend/lib/database/utils/migration-runner.ts +117 -117
- package/backend/lib/database/utils/seeder-runner.ts +120 -120
- package/backend/lib/engine/adapters/claude/environment.ts +160 -164
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -60
- package/backend/lib/engine/adapters/claude/index.ts +1 -1
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -38
- package/backend/lib/engine/adapters/claude/stream.ts +177 -177
- package/backend/lib/engine/adapters/opencode/index.ts +2 -2
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -862
- package/backend/lib/engine/adapters/opencode/server.ts +104 -104
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -755
- package/backend/lib/engine/index.ts +196 -196
- package/backend/lib/engine/types.ts +58 -58
- package/backend/lib/files/file-operations.ts +478 -478
- package/backend/lib/files/file-reading.ts +308 -308
- package/backend/lib/files/file-watcher.ts +383 -383
- package/backend/lib/files/path-browsing.ts +382 -382
- package/backend/lib/git/git-executor.ts +89 -88
- package/backend/lib/git/git-parser.ts +411 -411
- package/backend/lib/git/git-service.ts +505 -505
- package/backend/lib/mcp/README.md +1144 -1144
- package/backend/lib/mcp/config.ts +317 -316
- package/backend/lib/mcp/index.ts +35 -35
- package/backend/lib/mcp/project-context.ts +236 -236
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -156
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -419
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -791
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -501
- package/backend/lib/mcp/servers/helper.ts +143 -143
- package/backend/lib/mcp/servers/index.ts +44 -44
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -56
- package/backend/lib/mcp/servers/weather/index.ts +31 -31
- package/backend/lib/mcp/stdio-server.ts +103 -103
- package/backend/lib/mcp/types.ts +65 -65
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -86
- package/backend/lib/preview/browser/browser-console-manager.ts +262 -262
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -222
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -421
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -415
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -512
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +103 -103
- package/backend/lib/preview/browser/browser-pool.ts +357 -357
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -882
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -935
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -695
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -292
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -85
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -438
- package/backend/lib/preview/browser/types.ts +359 -359
- package/backend/lib/preview/index.ts +23 -23
- package/backend/lib/project/index.ts +1 -1
- package/backend/lib/project/status-manager.ts +181 -181
- package/backend/lib/shared/env.ts +124 -0
- package/backend/lib/shared/index.ts +5 -2
- package/backend/lib/shared/port-utils.ts +35 -25
- package/backend/lib/shared/process-manager.ts +280 -280
- package/backend/lib/snapshot/blob-store.ts +227 -227
- package/backend/lib/snapshot/gitignore.ts +307 -307
- package/backend/lib/snapshot/helpers.ts +397 -397
- package/backend/lib/snapshot/snapshot-service.ts +483 -483
- package/backend/lib/terminal/helpers.ts +14 -14
- package/backend/lib/terminal/index.ts +7 -7
- package/backend/lib/terminal/pty-manager.ts +3 -3
- package/backend/lib/terminal/pty-session-manager.ts +370 -387
- package/backend/lib/terminal/shell-utils.ts +315 -312
- package/backend/lib/terminal/stream-manager.ts +292 -292
- package/backend/lib/tunnel/global-tunnel-manager.ts +266 -243
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -311
- package/backend/lib/user/helpers.ts +87 -87
- package/backend/lib/utils/ws.ts +944 -944
- package/backend/middleware/cors.ts +16 -15
- package/backend/middleware/error-handler.ts +50 -49
- package/backend/middleware/logger.ts +9 -9
- package/backend/types/api.ts +24 -24
- package/backend/ws/README.md +1505 -1505
- package/backend/ws/chat/background.ts +198 -198
- package/backend/ws/chat/index.ts +21 -21
- package/backend/ws/chat/stream.ts +707 -707
- package/backend/ws/engine/claude/accounts.ts +399 -401
- package/backend/ws/engine/claude/index.ts +13 -13
- package/backend/ws/engine/claude/status.ts +43 -43
- package/backend/ws/engine/index.ts +14 -14
- package/backend/ws/engine/opencode/index.ts +11 -11
- package/backend/ws/engine/opencode/status.ts +30 -30
- package/backend/ws/engine/utils.ts +36 -36
- package/backend/ws/files/index.ts +30 -30
- package/backend/ws/files/read.ts +189 -189
- package/backend/ws/files/search.ts +453 -453
- package/backend/ws/files/watch.ts +124 -124
- package/backend/ws/files/write.ts +143 -143
- package/backend/ws/git/branch.ts +106 -106
- package/backend/ws/git/commit.ts +39 -39
- package/backend/ws/git/conflict.ts +68 -68
- package/backend/ws/git/diff.ts +69 -69
- package/backend/ws/git/index.ts +24 -24
- package/backend/ws/git/log.ts +41 -41
- package/backend/ws/git/remote.ts +214 -214
- package/backend/ws/git/staging.ts +84 -84
- package/backend/ws/git/status.ts +90 -90
- package/backend/ws/index.ts +69 -69
- package/backend/ws/mcp/index.ts +61 -61
- package/backend/ws/messages/crud.ts +74 -74
- package/backend/ws/messages/index.ts +14 -14
- package/backend/ws/preview/browser/cleanup.ts +129 -129
- package/backend/ws/preview/browser/console.ts +114 -114
- package/backend/ws/preview/browser/interact.ts +513 -513
- package/backend/ws/preview/browser/mcp.ts +129 -129
- package/backend/ws/preview/browser/native-ui.ts +235 -235
- package/backend/ws/preview/browser/stats.ts +55 -55
- package/backend/ws/preview/browser/tab-info.ts +126 -126
- package/backend/ws/preview/browser/tab.ts +166 -166
- package/backend/ws/preview/browser/webcodecs.ts +293 -293
- package/backend/ws/preview/index.ts +146 -146
- package/backend/ws/projects/crud.ts +113 -113
- package/backend/ws/projects/index.ts +25 -25
- package/backend/ws/projects/presence.ts +46 -46
- package/backend/ws/projects/status.ts +116 -116
- package/backend/ws/sessions/crud.ts +327 -327
- package/backend/ws/sessions/index.ts +33 -33
- package/backend/ws/settings/crud.ts +112 -112
- package/backend/ws/settings/index.ts +14 -14
- package/backend/ws/snapshot/index.ts +17 -17
- package/backend/ws/snapshot/restore.ts +173 -173
- package/backend/ws/snapshot/timeline.ts +141 -141
- package/backend/ws/system/index.ts +14 -14
- package/backend/ws/system/operations.ts +49 -49
- package/backend/ws/terminal/index.ts +40 -40
- package/backend/ws/terminal/persistence.ts +153 -153
- package/backend/ws/terminal/session.ts +382 -382
- package/backend/ws/terminal/stream.ts +79 -79
- package/backend/ws/tunnel/index.ts +14 -14
- package/backend/ws/tunnel/operations.ts +91 -91
- package/backend/ws/types.ts +20 -20
- package/backend/ws/user/crud.ts +156 -156
- package/backend/ws/user/index.ts +14 -14
- package/bin/clopen.ts +307 -307
- package/bun.lock +1364 -1352
- package/frontend/App.svelte +38 -34
- package/frontend/app.css +313 -313
- package/frontend/lib/app-environment.ts +10 -10
- package/frontend/lib/components/chat/ChatInterface.svelte +406 -406
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +56 -56
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +223 -223
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +394 -394
- package/frontend/lib/components/chat/formatters/Tools.svelte +69 -69
- package/frontend/lib/components/chat/formatters/index.ts +2 -2
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -421
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -78
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -30
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -33
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -619
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -48
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -31
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -201
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -148
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -216
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -357
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -57
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -478
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -541
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -86
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -86
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -157
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -59
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -124
- package/frontend/lib/components/chat/shared/index.ts +1 -1
- package/frontend/lib/components/chat/shared/utils.ts +115 -115
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -35
- package/frontend/lib/components/chat/tools/BashTool.svelte +45 -45
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -139
- package/frontend/lib/components/chat/tools/EditTool.svelte +47 -47
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +31 -31
- package/frontend/lib/components/chat/tools/GlobTool.svelte +50 -50
- package/frontend/lib/components/chat/tools/GrepTool.svelte +89 -89
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +25 -25
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +30 -30
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +37 -37
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +33 -33
- package/frontend/lib/components/chat/tools/ReadTool.svelte +40 -40
- package/frontend/lib/components/chat/tools/TaskTool.svelte +63 -63
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +74 -74
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +34 -34
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +83 -83
- package/frontend/lib/components/chat/tools/WriteTool.svelte +32 -32
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +78 -78
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +407 -407
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -45
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +18 -18
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +26 -26
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +53 -53
- package/frontend/lib/components/chat/tools/components/index.ts +7 -7
- package/frontend/lib/components/chat/tools/index.ts +25 -25
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +248 -248
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -78
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -391
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -26
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +86 -86
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -108
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -59
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -168
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -44
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -304
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -65
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -53
- package/frontend/lib/components/common/Alert.svelte +138 -138
- package/frontend/lib/components/common/AvatarBubble.svelte +55 -55
- package/frontend/lib/components/common/Button.svelte +71 -71
- package/frontend/lib/components/common/Card.svelte +102 -102
- package/frontend/lib/components/common/Checkbox.svelte +48 -48
- package/frontend/lib/components/common/Dialog.svelte +248 -248
- package/frontend/lib/components/common/FolderBrowser.svelte +842 -842
- package/frontend/lib/components/common/Icon.svelte +57 -57
- package/frontend/lib/components/common/Input.svelte +72 -72
- package/frontend/lib/components/common/Lightbox.svelte +232 -232
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -52
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -48
- package/frontend/lib/components/common/Modal.svelte +177 -177
- package/frontend/lib/components/common/ModalProvider.svelte +27 -27
- package/frontend/lib/components/common/ModelSelector.svelte +110 -110
- package/frontend/lib/components/common/MonacoEditor.svelte +568 -568
- package/frontend/lib/components/common/NotificationToast.svelte +113 -113
- package/frontend/lib/components/common/PageTemplate.svelte +75 -75
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -79
- package/frontend/lib/components/common/Select.svelte +97 -97
- package/frontend/lib/components/common/Textarea.svelte +79 -79
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -44
- package/frontend/lib/components/common/lucide-icons.ts +1642 -1642
- package/frontend/lib/components/common/material-icons.ts +1082 -1082
- package/frontend/lib/components/common/xterm/XTerm.svelte +809 -795
- package/frontend/lib/components/common/xterm/index.ts +15 -15
- package/frontend/lib/components/common/xterm/terminal-config.ts +67 -67
- package/frontend/lib/components/common/xterm/types.ts +30 -30
- package/frontend/lib/components/common/xterm/xterm-service.ts +379 -353
- package/frontend/lib/components/files/FileNode.svelte +383 -383
- package/frontend/lib/components/files/FileTree.svelte +681 -681
- package/frontend/lib/components/files/FileViewer.svelte +728 -728
- package/frontend/lib/components/files/SearchResults.svelte +303 -303
- package/frontend/lib/components/git/BranchManager.svelte +458 -458
- package/frontend/lib/components/git/ChangesSection.svelte +107 -107
- package/frontend/lib/components/git/CommitForm.svelte +76 -76
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -158
- package/frontend/lib/components/git/DiffViewer.svelte +364 -364
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -97
- package/frontend/lib/components/git/GitButton.svelte +33 -33
- package/frontend/lib/components/git/GitLog.svelte +361 -361
- package/frontend/lib/components/git/GitModal.svelte +80 -80
- package/frontend/lib/components/history/HistoryModal.svelte +563 -563
- package/frontend/lib/components/history/HistoryView.svelte +614 -614
- package/frontend/lib/components/index.ts +34 -34
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -549
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -1058
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +756 -756
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -450
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -236
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -224
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +338 -338
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +35 -35
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -155
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -837
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -113
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -296
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -391
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -231
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -210
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -239
- package/frontend/lib/components/preview/index.ts +1 -1
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -235
- package/frontend/lib/components/settings/SettingsView.svelte +36 -36
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -51
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -160
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -76
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -917
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -187
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -203
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -10
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -357
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -205
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -197
- package/frontend/lib/components/terminal/Terminal.svelte +367 -367
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -87
- package/frontend/lib/components/terminal/TerminalView.svelte +54 -54
- package/frontend/lib/components/tunnel/TunnelActive.svelte +157 -142
- package/frontend/lib/components/tunnel/TunnelButton.svelte +60 -54
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +285 -284
- package/frontend/lib/components/tunnel/TunnelModal.svelte +48 -47
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -49
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -382
- package/frontend/lib/components/workspace/MobileNavigator.svelte +394 -403
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -100
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -505
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -162
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -169
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -15
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -17
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -42
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +84 -84
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -37
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -274
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -1261
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -1560
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -150
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -73
- package/frontend/lib/constants/preview.ts +44 -44
- package/frontend/lib/services/chat/chat.service.ts +704 -704
- package/frontend/lib/services/chat/index.ts +6 -6
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -86
- package/frontend/lib/services/notification/index.ts +7 -7
- package/frontend/lib/services/notification/push.service.ts +143 -143
- package/frontend/lib/services/notification/sound.service.ts +126 -126
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -61
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -1499
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -67
- package/frontend/lib/services/preview/index.ts +22 -22
- package/frontend/lib/services/project/index.ts +7 -7
- package/frontend/lib/services/project/status.service.ts +159 -159
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -47
- package/frontend/lib/services/terminal/background/index.ts +129 -129
- package/frontend/lib/services/terminal/background/session-restore.ts +273 -273
- package/frontend/lib/services/terminal/background/stream-manager.ts +285 -285
- package/frontend/lib/services/terminal/index.ts +13 -13
- package/frontend/lib/services/terminal/persistence.service.ts +260 -260
- package/frontend/lib/services/terminal/project.service.ts +952 -952
- package/frontend/lib/services/terminal/session.service.ts +363 -363
- package/frontend/lib/services/terminal/terminal.service.ts +369 -369
- package/frontend/lib/stores/core/app.svelte.ts +117 -117
- package/frontend/lib/stores/core/files.svelte.ts +72 -72
- package/frontend/lib/stores/core/presence.svelte.ts +48 -48
- package/frontend/lib/stores/core/projects.svelte.ts +317 -317
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -383
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -58
- package/frontend/lib/stores/features/models.svelte.ts +89 -89
- package/frontend/lib/stores/features/settings.svelte.ts +87 -87
- package/frontend/lib/stores/features/terminal.svelte.ts +700 -700
- package/frontend/lib/stores/features/tunnel.svelte.ts +163 -161
- package/frontend/lib/stores/features/user.svelte.ts +95 -95
- package/frontend/lib/stores/ui/chat-input.svelte.ts +56 -56
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -61
- package/frontend/lib/stores/ui/dialog.svelte.ts +58 -58
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -214
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -166
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -88
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -179
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -754
- package/frontend/lib/types/native-ui.ts +73 -73
- package/frontend/lib/utils/chat/date-separator.ts +38 -38
- package/frontend/lib/utils/chat/message-grouper.ts +218 -218
- package/frontend/lib/utils/chat/message-processor.ts +134 -134
- package/frontend/lib/utils/chat/tool-handler.ts +160 -160
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -142
- package/frontend/lib/utils/click-outside.ts +20 -20
- package/frontend/lib/utils/context-manager.ts +256 -256
- package/frontend/lib/utils/file-icon-mappings.ts +768 -768
- package/frontend/lib/utils/folder-icon-mappings.ts +1029 -1029
- package/frontend/lib/utils/git-status.ts +68 -68
- package/frontend/lib/utils/platform.ts +112 -112
- package/frontend/lib/utils/port-check.ts +64 -64
- package/frontend/lib/utils/terminalFormatter.ts +206 -206
- package/frontend/lib/utils/theme.ts +6 -6
- package/frontend/lib/utils/tree-visualizer.ts +320 -320
- package/frontend/lib/utils/ws.ts +44 -44
- package/frontend/main.ts +13 -13
- package/index.html +70 -70
- package/package.json +114 -111
- package/scripts/dev.ts +45 -0
- package/scripts/generate-icons.ts +86 -86
- package/scripts/pre-publish-check.sh +142 -142
- package/scripts/setup-hooks.sh +134 -134
- package/scripts/validate-branch-name.sh +47 -47
- package/scripts/validate-commit-msg.sh +42 -42
- package/shared/constants/engines.ts +134 -134
- package/shared/types/database/connection.ts +15 -15
- package/shared/types/database/index.ts +5 -5
- package/shared/types/database/schema.ts +140 -140
- package/shared/types/engine/index.ts +45 -45
- package/shared/types/filesystem/index.ts +21 -21
- package/shared/types/git.ts +171 -171
- package/shared/types/messaging/index.ts +238 -238
- package/shared/types/messaging/tool.ts +525 -525
- package/shared/types/network/api.ts +17 -17
- package/shared/types/network/index.ts +4 -4
- package/shared/types/stores/app.ts +22 -22
- package/shared/types/stores/dialog.ts +20 -20
- package/shared/types/stores/index.ts +2 -2
- package/shared/types/stores/settings.ts +15 -15
- package/shared/types/terminal/index.ts +43 -43
- package/shared/types/ui/components.ts +60 -60
- package/shared/types/ui/icons.ts +22 -22
- package/shared/types/ui/index.ts +21 -21
- package/shared/types/ui/notifications.ts +13 -13
- package/shared/types/ui/theme.ts +11 -11
- package/shared/types/websocket/index.ts +43 -43
- package/shared/types/window.d.ts +12 -12
- package/shared/utils/anonymous-user.ts +167 -167
- package/shared/utils/async.ts +10 -10
- package/shared/utils/diff-calculator.ts +184 -184
- package/shared/utils/file-type-detection.ts +165 -165
- package/shared/utils/logger.ts +144 -144
- package/shared/utils/message-formatter.ts +79 -79
- package/shared/utils/path.ts +47 -47
- package/shared/utils/ws-client.ts +768 -768
- package/shared/utils/ws-server.ts +660 -660
- package/static/favicon.svg +7 -7
- package/static/fonts/dm-sans.css +96 -96
- package/svelte.config.js +20 -20
- package/tsconfig.json +41 -41
- package/vite.config.ts +50 -33
- package/backend/lib/vite-dev.ts +0 -295
|
@@ -1,505 +1,505 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git Service
|
|
3
|
-
* High-level git operations built on top of executor and parser
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { execGit, isGitRepo, getGitRoot } from './git-executor';
|
|
7
|
-
import {
|
|
8
|
-
parseStatus,
|
|
9
|
-
parseBranches,
|
|
10
|
-
parseAheadBehind,
|
|
11
|
-
parseDiff,
|
|
12
|
-
parseLog,
|
|
13
|
-
parseRemotes,
|
|
14
|
-
parseStashList,
|
|
15
|
-
parseConflictMarkers
|
|
16
|
-
} from './git-parser';
|
|
17
|
-
import type {
|
|
18
|
-
GitStatus,
|
|
19
|
-
GitBranchInfo,
|
|
20
|
-
GitFileDiff,
|
|
21
|
-
GitLogResult,
|
|
22
|
-
GitRemote,
|
|
23
|
-
GitStashEntry,
|
|
24
|
-
GitConflictFile
|
|
25
|
-
} from '$shared/types/git';
|
|
26
|
-
import { debug } from '$shared/utils/logger';
|
|
27
|
-
|
|
28
|
-
export class GitService {
|
|
29
|
-
/**
|
|
30
|
-
* Check if path is a git repo
|
|
31
|
-
*/
|
|
32
|
-
async isRepo(cwd: string): Promise<boolean> {
|
|
33
|
-
return isGitRepo(cwd);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get repository root
|
|
38
|
-
*/
|
|
39
|
-
async getRoot(cwd: string): Promise<string | null> {
|
|
40
|
-
return getGitRoot(cwd);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Initialize a new git repository
|
|
45
|
-
*/
|
|
46
|
-
async init(cwd: string, defaultBranch?: string): Promise<void> {
|
|
47
|
-
const args = ['init'];
|
|
48
|
-
if (defaultBranch) args.push('-b', defaultBranch);
|
|
49
|
-
const result = await execGit(args, cwd);
|
|
50
|
-
if (result.exitCode !== 0) {
|
|
51
|
-
throw new Error(`git init failed: ${result.stderr}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ============================================
|
|
56
|
-
// Status
|
|
57
|
-
// ============================================
|
|
58
|
-
|
|
59
|
-
async getStatus(cwd: string): Promise<GitStatus> {
|
|
60
|
-
const result = await execGit(['status', '--porcelain=v1', '-u'], cwd);
|
|
61
|
-
if (result.exitCode !== 0) {
|
|
62
|
-
throw new Error(`git status failed: ${result.stderr}`);
|
|
63
|
-
}
|
|
64
|
-
return parseStatus(result.stdout);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============================================
|
|
68
|
-
// Staging
|
|
69
|
-
// ============================================
|
|
70
|
-
|
|
71
|
-
async stageFile(cwd: string, filePath: string): Promise<void> {
|
|
72
|
-
const result = await execGit(['add', '--', filePath], cwd);
|
|
73
|
-
if (result.exitCode !== 0) {
|
|
74
|
-
throw new Error(`git add failed: ${result.stderr}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async stageAll(cwd: string): Promise<void> {
|
|
79
|
-
const result = await execGit(['add', '-A'], cwd);
|
|
80
|
-
if (result.exitCode !== 0) {
|
|
81
|
-
throw new Error(`git add -A failed: ${result.stderr}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async unstageFile(cwd: string, filePath: string): Promise<void> {
|
|
86
|
-
// Try normal reset first, fall back to rm --cached for initial commit
|
|
87
|
-
const result = await execGit(['reset', 'HEAD', '--', filePath], cwd);
|
|
88
|
-
if (result.exitCode !== 0) {
|
|
89
|
-
const fallback = await execGit(['rm', '--cached', '--', filePath], cwd);
|
|
90
|
-
if (fallback.exitCode !== 0) {
|
|
91
|
-
throw new Error(`git unstage failed: ${result.stderr}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async unstageAll(cwd: string): Promise<void> {
|
|
97
|
-
// Try normal reset first, fall back to rm --cached for initial commit
|
|
98
|
-
const result = await execGit(['reset', 'HEAD'], cwd);
|
|
99
|
-
if (result.exitCode !== 0) {
|
|
100
|
-
const fallback = await execGit(['rm', '-r', '--cached', '.'], cwd);
|
|
101
|
-
if (fallback.exitCode !== 0) {
|
|
102
|
-
throw new Error(`git unstage all failed: ${result.stderr}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async discardFile(cwd: string, filePath: string): Promise<void> {
|
|
108
|
-
// Check if file is untracked
|
|
109
|
-
const statusResult = await execGit(['status', '--porcelain=v1', '--', filePath], cwd);
|
|
110
|
-
const statusLine = statusResult.stdout.trim();
|
|
111
|
-
|
|
112
|
-
if (statusLine.startsWith('??')) {
|
|
113
|
-
// Untracked file - delete it
|
|
114
|
-
const { unlink } = await import('node:fs/promises');
|
|
115
|
-
const { join } = await import('node:path');
|
|
116
|
-
await unlink(join(cwd, filePath));
|
|
117
|
-
} else {
|
|
118
|
-
// Tracked file - restore it
|
|
119
|
-
const result = await execGit(['checkout', '--', filePath], cwd);
|
|
120
|
-
if (result.exitCode !== 0) {
|
|
121
|
-
throw new Error(`git checkout failed: ${result.stderr}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async discardAll(cwd: string): Promise<void> {
|
|
127
|
-
// Restore tracked files
|
|
128
|
-
await execGit(['checkout', '--', '.'], cwd);
|
|
129
|
-
// Remove untracked files
|
|
130
|
-
await execGit(['clean', '-fd'], cwd);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ============================================
|
|
134
|
-
// Commit
|
|
135
|
-
// ============================================
|
|
136
|
-
|
|
137
|
-
async commit(cwd: string, message: string): Promise<string> {
|
|
138
|
-
const result = await execGit(['commit', '-m', message], cwd);
|
|
139
|
-
if (result.exitCode !== 0) {
|
|
140
|
-
throw new Error(`git commit failed: ${result.stderr}`);
|
|
141
|
-
}
|
|
142
|
-
// Return the commit hash
|
|
143
|
-
const hashResult = await execGit(['rev-parse', 'HEAD'], cwd);
|
|
144
|
-
return hashResult.stdout.trim();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async amendCommit(cwd: string, message?: string): Promise<string> {
|
|
148
|
-
const args = ['commit', '--amend'];
|
|
149
|
-
if (message) {
|
|
150
|
-
args.push('-m', message);
|
|
151
|
-
} else {
|
|
152
|
-
args.push('--no-edit');
|
|
153
|
-
}
|
|
154
|
-
const result = await execGit(args, cwd);
|
|
155
|
-
if (result.exitCode !== 0) {
|
|
156
|
-
throw new Error(`git commit --amend failed: ${result.stderr}`);
|
|
157
|
-
}
|
|
158
|
-
const hashResult = await execGit(['rev-parse', 'HEAD'], cwd);
|
|
159
|
-
return hashResult.stdout.trim();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ============================================
|
|
163
|
-
// Diff
|
|
164
|
-
// ============================================
|
|
165
|
-
|
|
166
|
-
async getDiffUnstaged(cwd: string, filePath?: string): Promise<GitFileDiff[]> {
|
|
167
|
-
const args = ['diff'];
|
|
168
|
-
if (filePath) args.push('--', filePath);
|
|
169
|
-
const result = await execGit(args, cwd);
|
|
170
|
-
return parseDiff(result.stdout);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async getDiffStaged(cwd: string, filePath?: string): Promise<GitFileDiff[]> {
|
|
174
|
-
const args = ['diff', '--cached'];
|
|
175
|
-
if (filePath) args.push('--', filePath);
|
|
176
|
-
const result = await execGit(args, cwd);
|
|
177
|
-
return parseDiff(result.stdout);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async getDiffCommit(cwd: string, commitHash: string): Promise<GitFileDiff[]> {
|
|
181
|
-
const result = await execGit(['diff', `${commitHash}^`, commitHash], cwd);
|
|
182
|
-
return parseDiff(result.stdout);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async getDiffBetween(cwd: string, from: string, to: string): Promise<GitFileDiff[]> {
|
|
186
|
-
const result = await execGit(['diff', from, to], cwd);
|
|
187
|
-
return parseDiff(result.stdout);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ============================================
|
|
191
|
-
// Branches
|
|
192
|
-
// ============================================
|
|
193
|
-
|
|
194
|
-
async getBranches(cwd: string): Promise<GitBranchInfo> {
|
|
195
|
-
const [localResult, remoteResult] = await Promise.all([
|
|
196
|
-
execGit(['branch', '-v', '--no-color'], cwd),
|
|
197
|
-
execGit(['branch', '-r', '-v', '--no-color'], cwd)
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
// Handle empty repo (no commits yet) — branch command returns empty
|
|
201
|
-
if (!localResult.stdout.trim()) {
|
|
202
|
-
// Try to get the initial branch name from HEAD
|
|
203
|
-
const headResult = await execGit(['symbolic-ref', '--short', 'HEAD'], cwd);
|
|
204
|
-
const initialBranch = headResult.exitCode === 0 ? headResult.stdout.trim() : 'main';
|
|
205
|
-
return { current: initialBranch, local: [], remote: [], ahead: 0, behind: 0 };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const branchInfo = parseBranches(localResult.stdout, remoteResult.stdout);
|
|
209
|
-
|
|
210
|
-
// Get ahead/behind for current branch
|
|
211
|
-
if (branchInfo.current) {
|
|
212
|
-
try {
|
|
213
|
-
const abResult = await execGit(
|
|
214
|
-
['rev-list', '--left-right', '--count', `${branchInfo.current}...@{upstream}`],
|
|
215
|
-
cwd
|
|
216
|
-
);
|
|
217
|
-
if (abResult.exitCode === 0) {
|
|
218
|
-
const { ahead, behind } = parseAheadBehind(abResult.stdout);
|
|
219
|
-
branchInfo.ahead = ahead;
|
|
220
|
-
branchInfo.behind = behind;
|
|
221
|
-
|
|
222
|
-
// Update the current branch entry too
|
|
223
|
-
const currentBranch = branchInfo.local.find(b => b.isCurrent);
|
|
224
|
-
if (currentBranch) {
|
|
225
|
-
currentBranch.ahead = ahead;
|
|
226
|
-
currentBranch.behind = behind;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
// No upstream configured
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return branchInfo;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async createBranch(cwd: string, name: string, startPoint?: string): Promise<void> {
|
|
238
|
-
const args = ['checkout', '-b', name];
|
|
239
|
-
if (startPoint) args.push(startPoint);
|
|
240
|
-
const result = await execGit(args, cwd);
|
|
241
|
-
if (result.exitCode !== 0) {
|
|
242
|
-
throw new Error(`git checkout -b failed: ${result.stderr}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async switchBranch(cwd: string, name: string): Promise<void> {
|
|
247
|
-
const result = await execGit(['checkout', name], cwd);
|
|
248
|
-
if (result.exitCode !== 0) {
|
|
249
|
-
throw new Error(`git checkout failed: ${result.stderr}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async deleteBranch(cwd: string, name: string, force = false): Promise<void> {
|
|
254
|
-
const flag = force ? '-D' : '-d';
|
|
255
|
-
const result = await execGit(['branch', flag, name], cwd);
|
|
256
|
-
if (result.exitCode !== 0) {
|
|
257
|
-
throw new Error(`git branch ${flag} failed: ${result.stderr}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async renameBranch(cwd: string, oldName: string, newName: string): Promise<void> {
|
|
262
|
-
const result = await execGit(['branch', '-m', oldName, newName], cwd);
|
|
263
|
-
if (result.exitCode !== 0) {
|
|
264
|
-
throw new Error(`git branch -m failed: ${result.stderr}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async mergeBranch(cwd: string, branchName: string): Promise<{ success: boolean; message: string }> {
|
|
269
|
-
const result = await execGit(['merge', branchName], cwd);
|
|
270
|
-
return {
|
|
271
|
-
success: result.exitCode === 0,
|
|
272
|
-
message: result.exitCode === 0 ? result.stdout : result.stderr
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ============================================
|
|
277
|
-
// Log
|
|
278
|
-
// ============================================
|
|
279
|
-
|
|
280
|
-
async getLog(cwd: string, limit = 50, skip = 0, branch?: string): Promise<GitLogResult> {
|
|
281
|
-
const SEPARATOR = '|||';
|
|
282
|
-
const format = `%H${SEPARATOR}%h${SEPARATOR}%an${SEPARATOR}%ae${SEPARATOR}%aI${SEPARATOR}%P${SEPARATOR}%D%n%s%x00`;
|
|
283
|
-
|
|
284
|
-
const args = [
|
|
285
|
-
'log',
|
|
286
|
-
`--format=${format}`,
|
|
287
|
-
`--max-count=${limit + 1}`, // +1 to check if there are more
|
|
288
|
-
`--skip=${skip}`
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
if (branch) args.push(branch);
|
|
292
|
-
|
|
293
|
-
const result = await execGit(args, cwd);
|
|
294
|
-
if (result.exitCode !== 0) {
|
|
295
|
-
throw new Error(`git log failed: ${result.stderr}`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const commits = parseLog(result.stdout);
|
|
299
|
-
const hasMore = commits.length > limit;
|
|
300
|
-
if (hasMore) commits.pop(); // Remove the extra one
|
|
301
|
-
|
|
302
|
-
// Get total count
|
|
303
|
-
const countResult = await execGit(['rev-list', '--count', branch || 'HEAD'], cwd);
|
|
304
|
-
const total = parseInt(countResult.stdout.trim()) || commits.length;
|
|
305
|
-
|
|
306
|
-
return { commits, total, hasMore };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ============================================
|
|
310
|
-
// Remote Operations
|
|
311
|
-
// ============================================
|
|
312
|
-
|
|
313
|
-
async getRemotes(cwd: string): Promise<GitRemote[]> {
|
|
314
|
-
const result = await execGit(['remote', '-v'], cwd);
|
|
315
|
-
return parseRemotes(result.stdout);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async fetch(cwd: string, remote = 'origin'): Promise<string> {
|
|
319
|
-
const result = await execGit(['fetch', remote, '--prune'], cwd, 60000);
|
|
320
|
-
if (result.exitCode !== 0) {
|
|
321
|
-
throw new Error(`git fetch failed: ${result.stderr}`);
|
|
322
|
-
}
|
|
323
|
-
return result.stderr || result.stdout; // git fetch outputs to stderr
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async pull(cwd: string, remote = 'origin', branch?: string): Promise<{ success: boolean; message: string }> {
|
|
327
|
-
const args = ['pull', remote];
|
|
328
|
-
if (branch) args.push(branch);
|
|
329
|
-
const result = await execGit(args, cwd, 60000);
|
|
330
|
-
return {
|
|
331
|
-
success: result.exitCode === 0,
|
|
332
|
-
message: result.exitCode === 0 ? result.stdout : result.stderr
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async push(cwd: string, remote = 'origin', branch?: string, force = false): Promise<{ success: boolean; message: string }> {
|
|
337
|
-
const args = ['push', remote];
|
|
338
|
-
if (branch) args.push(branch);
|
|
339
|
-
if (force) args.push('--force-with-lease');
|
|
340
|
-
// Set upstream if needed
|
|
341
|
-
args.push('-u');
|
|
342
|
-
const result = await execGit(args, cwd, 60000);
|
|
343
|
-
return {
|
|
344
|
-
success: result.exitCode === 0,
|
|
345
|
-
message: result.exitCode === 0 ? (result.stderr || result.stdout) : result.stderr
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async addRemote(cwd: string, name: string, url: string): Promise<void> {
|
|
350
|
-
const result = await execGit(['remote', 'add', name, url], cwd);
|
|
351
|
-
if (result.exitCode !== 0) {
|
|
352
|
-
throw new Error(`git remote add failed: ${result.stderr}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
async removeRemote(cwd: string, name: string): Promise<void> {
|
|
357
|
-
const result = await execGit(['remote', 'remove', name], cwd);
|
|
358
|
-
if (result.exitCode !== 0) {
|
|
359
|
-
throw new Error(`git remote remove failed: ${result.stderr}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// ============================================
|
|
364
|
-
// Stash
|
|
365
|
-
// ============================================
|
|
366
|
-
|
|
367
|
-
async stashList(cwd: string): Promise<GitStashEntry[]> {
|
|
368
|
-
const result = await execGit(['stash', 'list'], cwd);
|
|
369
|
-
return parseStashList(result.stdout);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async stashSave(cwd: string, message?: string): Promise<void> {
|
|
373
|
-
const args = ['stash', 'push'];
|
|
374
|
-
if (message) args.push('-m', message);
|
|
375
|
-
const result = await execGit(args, cwd);
|
|
376
|
-
if (result.exitCode !== 0) {
|
|
377
|
-
throw new Error(`git stash failed: ${result.stderr}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async stashPop(cwd: string, index = 0): Promise<void> {
|
|
382
|
-
const result = await execGit(['stash', 'pop', `stash@{${index}}`], cwd);
|
|
383
|
-
if (result.exitCode !== 0) {
|
|
384
|
-
throw new Error(`git stash pop failed: ${result.stderr}`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async stashDrop(cwd: string, index = 0): Promise<void> {
|
|
389
|
-
const result = await execGit(['stash', 'drop', `stash@{${index}}`], cwd);
|
|
390
|
-
if (result.exitCode !== 0) {
|
|
391
|
-
throw new Error(`git stash drop failed: ${result.stderr}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// ============================================
|
|
396
|
-
// Tags
|
|
397
|
-
// ============================================
|
|
398
|
-
|
|
399
|
-
async getTags(cwd: string): Promise<{ name: string; hash: string; message: string; date: string; isAnnotated: boolean }[]> {
|
|
400
|
-
const result = await execGit(
|
|
401
|
-
['tag', '-l', '--sort=-creatordate', '--format=%(refname:short)|||%(objectname:short)|||%(contents:subject)|||%(creatordate:iso-strict)|||%(objecttype)'],
|
|
402
|
-
cwd
|
|
403
|
-
);
|
|
404
|
-
if (result.exitCode !== 0) return [];
|
|
405
|
-
|
|
406
|
-
const tags: { name: string; hash: string; message: string; date: string; isAnnotated: boolean }[] = [];
|
|
407
|
-
const lines = result.stdout.split('\n').filter(Boolean);
|
|
408
|
-
for (const line of lines) {
|
|
409
|
-
const parts = line.split('|||');
|
|
410
|
-
if (parts.length >= 2) {
|
|
411
|
-
tags.push({
|
|
412
|
-
name: parts[0],
|
|
413
|
-
hash: parts[1],
|
|
414
|
-
message: parts[2] || '',
|
|
415
|
-
date: parts[3] || '',
|
|
416
|
-
isAnnotated: parts[4] === 'tag'
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return tags;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async createTag(cwd: string, name: string, message?: string, commitHash?: string): Promise<void> {
|
|
424
|
-
const args = ['tag'];
|
|
425
|
-
if (message) {
|
|
426
|
-
args.push('-a', name, '-m', message);
|
|
427
|
-
} else {
|
|
428
|
-
args.push(name);
|
|
429
|
-
}
|
|
430
|
-
if (commitHash) args.push(commitHash);
|
|
431
|
-
const result = await execGit(args, cwd);
|
|
432
|
-
if (result.exitCode !== 0) {
|
|
433
|
-
throw new Error(`git tag failed: ${result.stderr}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async deleteTag(cwd: string, name: string): Promise<void> {
|
|
438
|
-
const result = await execGit(['tag', '-d', name], cwd);
|
|
439
|
-
if (result.exitCode !== 0) {
|
|
440
|
-
throw new Error(`git tag -d failed: ${result.stderr}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async pushTag(cwd: string, name: string, remote = 'origin'): Promise<{ success: boolean; message: string }> {
|
|
445
|
-
const result = await execGit(['push', remote, name], cwd, 60000);
|
|
446
|
-
return {
|
|
447
|
-
success: result.exitCode === 0,
|
|
448
|
-
message: result.exitCode === 0 ? (result.stderr || result.stdout) : result.stderr
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// ============================================
|
|
453
|
-
// Conflict Resolution
|
|
454
|
-
// ============================================
|
|
455
|
-
|
|
456
|
-
async getConflictFiles(cwd: string): Promise<GitConflictFile[]> {
|
|
457
|
-
const status = await this.getStatus(cwd);
|
|
458
|
-
const conflicts: GitConflictFile[] = [];
|
|
459
|
-
|
|
460
|
-
for (const file of status.conflicted) {
|
|
461
|
-
try {
|
|
462
|
-
const { readFile } = await import('node:fs/promises');
|
|
463
|
-
const { join } = await import('node:path');
|
|
464
|
-
const content = await readFile(join(cwd, file.path), 'utf-8');
|
|
465
|
-
const markers = parseConflictMarkers(content);
|
|
466
|
-
conflicts.push({ path: file.path, content, markers });
|
|
467
|
-
} catch (err) {
|
|
468
|
-
debug.error('git', `Failed to read conflict file: ${file.path}`, err);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return conflicts;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async resolveConflict(
|
|
476
|
-
cwd: string,
|
|
477
|
-
filePath: string,
|
|
478
|
-
resolution: 'ours' | 'theirs' | 'custom',
|
|
479
|
-
customContent?: string
|
|
480
|
-
): Promise<void> {
|
|
481
|
-
if (resolution === 'custom' && customContent !== undefined) {
|
|
482
|
-
// Write custom content
|
|
483
|
-
const { writeFile } = await import('node:fs/promises');
|
|
484
|
-
const { join } = await import('node:path');
|
|
485
|
-
await writeFile(join(cwd, filePath), customContent, 'utf-8');
|
|
486
|
-
} else if (resolution === 'ours') {
|
|
487
|
-
await execGit(['checkout', '--ours', '--', filePath], cwd);
|
|
488
|
-
} else if (resolution === 'theirs') {
|
|
489
|
-
await execGit(['checkout', '--theirs', '--', filePath], cwd);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Stage the resolved file
|
|
493
|
-
await this.stageFile(cwd, filePath);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async abortMerge(cwd: string): Promise<void> {
|
|
497
|
-
const result = await execGit(['merge', '--abort'], cwd);
|
|
498
|
-
if (result.exitCode !== 0) {
|
|
499
|
-
throw new Error(`git merge --abort failed: ${result.stderr}`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Singleton
|
|
505
|
-
export const gitService = new GitService();
|
|
1
|
+
/**
|
|
2
|
+
* Git Service
|
|
3
|
+
* High-level git operations built on top of executor and parser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execGit, isGitRepo, getGitRoot } from './git-executor';
|
|
7
|
+
import {
|
|
8
|
+
parseStatus,
|
|
9
|
+
parseBranches,
|
|
10
|
+
parseAheadBehind,
|
|
11
|
+
parseDiff,
|
|
12
|
+
parseLog,
|
|
13
|
+
parseRemotes,
|
|
14
|
+
parseStashList,
|
|
15
|
+
parseConflictMarkers
|
|
16
|
+
} from './git-parser';
|
|
17
|
+
import type {
|
|
18
|
+
GitStatus,
|
|
19
|
+
GitBranchInfo,
|
|
20
|
+
GitFileDiff,
|
|
21
|
+
GitLogResult,
|
|
22
|
+
GitRemote,
|
|
23
|
+
GitStashEntry,
|
|
24
|
+
GitConflictFile
|
|
25
|
+
} from '$shared/types/git';
|
|
26
|
+
import { debug } from '$shared/utils/logger';
|
|
27
|
+
|
|
28
|
+
export class GitService {
|
|
29
|
+
/**
|
|
30
|
+
* Check if path is a git repo
|
|
31
|
+
*/
|
|
32
|
+
async isRepo(cwd: string): Promise<boolean> {
|
|
33
|
+
return isGitRepo(cwd);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get repository root
|
|
38
|
+
*/
|
|
39
|
+
async getRoot(cwd: string): Promise<string | null> {
|
|
40
|
+
return getGitRoot(cwd);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize a new git repository
|
|
45
|
+
*/
|
|
46
|
+
async init(cwd: string, defaultBranch?: string): Promise<void> {
|
|
47
|
+
const args = ['init'];
|
|
48
|
+
if (defaultBranch) args.push('-b', defaultBranch);
|
|
49
|
+
const result = await execGit(args, cwd);
|
|
50
|
+
if (result.exitCode !== 0) {
|
|
51
|
+
throw new Error(`git init failed: ${result.stderr}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// Status
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
async getStatus(cwd: string): Promise<GitStatus> {
|
|
60
|
+
const result = await execGit(['status', '--porcelain=v1', '-u'], cwd);
|
|
61
|
+
if (result.exitCode !== 0) {
|
|
62
|
+
throw new Error(`git status failed: ${result.stderr}`);
|
|
63
|
+
}
|
|
64
|
+
return parseStatus(result.stdout);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// Staging
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
async stageFile(cwd: string, filePath: string): Promise<void> {
|
|
72
|
+
const result = await execGit(['add', '--', filePath], cwd);
|
|
73
|
+
if (result.exitCode !== 0) {
|
|
74
|
+
throw new Error(`git add failed: ${result.stderr}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async stageAll(cwd: string): Promise<void> {
|
|
79
|
+
const result = await execGit(['add', '-A'], cwd);
|
|
80
|
+
if (result.exitCode !== 0) {
|
|
81
|
+
throw new Error(`git add -A failed: ${result.stderr}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async unstageFile(cwd: string, filePath: string): Promise<void> {
|
|
86
|
+
// Try normal reset first, fall back to rm --cached for initial commit
|
|
87
|
+
const result = await execGit(['reset', 'HEAD', '--', filePath], cwd);
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
const fallback = await execGit(['rm', '--cached', '--', filePath], cwd);
|
|
90
|
+
if (fallback.exitCode !== 0) {
|
|
91
|
+
throw new Error(`git unstage failed: ${result.stderr}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async unstageAll(cwd: string): Promise<void> {
|
|
97
|
+
// Try normal reset first, fall back to rm --cached for initial commit
|
|
98
|
+
const result = await execGit(['reset', 'HEAD'], cwd);
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
const fallback = await execGit(['rm', '-r', '--cached', '.'], cwd);
|
|
101
|
+
if (fallback.exitCode !== 0) {
|
|
102
|
+
throw new Error(`git unstage all failed: ${result.stderr}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async discardFile(cwd: string, filePath: string): Promise<void> {
|
|
108
|
+
// Check if file is untracked
|
|
109
|
+
const statusResult = await execGit(['status', '--porcelain=v1', '--', filePath], cwd);
|
|
110
|
+
const statusLine = statusResult.stdout.trim();
|
|
111
|
+
|
|
112
|
+
if (statusLine.startsWith('??')) {
|
|
113
|
+
// Untracked file - delete it
|
|
114
|
+
const { unlink } = await import('node:fs/promises');
|
|
115
|
+
const { join } = await import('node:path');
|
|
116
|
+
await unlink(join(cwd, filePath));
|
|
117
|
+
} else {
|
|
118
|
+
// Tracked file - restore it
|
|
119
|
+
const result = await execGit(['checkout', '--', filePath], cwd);
|
|
120
|
+
if (result.exitCode !== 0) {
|
|
121
|
+
throw new Error(`git checkout failed: ${result.stderr}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async discardAll(cwd: string): Promise<void> {
|
|
127
|
+
// Restore tracked files
|
|
128
|
+
await execGit(['checkout', '--', '.'], cwd);
|
|
129
|
+
// Remove untracked files
|
|
130
|
+
await execGit(['clean', '-fd'], cwd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// Commit
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
async commit(cwd: string, message: string): Promise<string> {
|
|
138
|
+
const result = await execGit(['commit', '-m', message], cwd);
|
|
139
|
+
if (result.exitCode !== 0) {
|
|
140
|
+
throw new Error(`git commit failed: ${result.stderr}`);
|
|
141
|
+
}
|
|
142
|
+
// Return the commit hash
|
|
143
|
+
const hashResult = await execGit(['rev-parse', 'HEAD'], cwd);
|
|
144
|
+
return hashResult.stdout.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async amendCommit(cwd: string, message?: string): Promise<string> {
|
|
148
|
+
const args = ['commit', '--amend'];
|
|
149
|
+
if (message) {
|
|
150
|
+
args.push('-m', message);
|
|
151
|
+
} else {
|
|
152
|
+
args.push('--no-edit');
|
|
153
|
+
}
|
|
154
|
+
const result = await execGit(args, cwd);
|
|
155
|
+
if (result.exitCode !== 0) {
|
|
156
|
+
throw new Error(`git commit --amend failed: ${result.stderr}`);
|
|
157
|
+
}
|
|
158
|
+
const hashResult = await execGit(['rev-parse', 'HEAD'], cwd);
|
|
159
|
+
return hashResult.stdout.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================
|
|
163
|
+
// Diff
|
|
164
|
+
// ============================================
|
|
165
|
+
|
|
166
|
+
async getDiffUnstaged(cwd: string, filePath?: string): Promise<GitFileDiff[]> {
|
|
167
|
+
const args = ['diff'];
|
|
168
|
+
if (filePath) args.push('--', filePath);
|
|
169
|
+
const result = await execGit(args, cwd);
|
|
170
|
+
return parseDiff(result.stdout);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getDiffStaged(cwd: string, filePath?: string): Promise<GitFileDiff[]> {
|
|
174
|
+
const args = ['diff', '--cached'];
|
|
175
|
+
if (filePath) args.push('--', filePath);
|
|
176
|
+
const result = await execGit(args, cwd);
|
|
177
|
+
return parseDiff(result.stdout);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getDiffCommit(cwd: string, commitHash: string): Promise<GitFileDiff[]> {
|
|
181
|
+
const result = await execGit(['diff', `${commitHash}^`, commitHash], cwd);
|
|
182
|
+
return parseDiff(result.stdout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getDiffBetween(cwd: string, from: string, to: string): Promise<GitFileDiff[]> {
|
|
186
|
+
const result = await execGit(['diff', from, to], cwd);
|
|
187
|
+
return parseDiff(result.stdout);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================
|
|
191
|
+
// Branches
|
|
192
|
+
// ============================================
|
|
193
|
+
|
|
194
|
+
async getBranches(cwd: string): Promise<GitBranchInfo> {
|
|
195
|
+
const [localResult, remoteResult] = await Promise.all([
|
|
196
|
+
execGit(['branch', '-v', '--no-color'], cwd),
|
|
197
|
+
execGit(['branch', '-r', '-v', '--no-color'], cwd)
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
// Handle empty repo (no commits yet) — branch command returns empty
|
|
201
|
+
if (!localResult.stdout.trim()) {
|
|
202
|
+
// Try to get the initial branch name from HEAD
|
|
203
|
+
const headResult = await execGit(['symbolic-ref', '--short', 'HEAD'], cwd);
|
|
204
|
+
const initialBranch = headResult.exitCode === 0 ? headResult.stdout.trim() : 'main';
|
|
205
|
+
return { current: initialBranch, local: [], remote: [], ahead: 0, behind: 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const branchInfo = parseBranches(localResult.stdout, remoteResult.stdout);
|
|
209
|
+
|
|
210
|
+
// Get ahead/behind for current branch
|
|
211
|
+
if (branchInfo.current) {
|
|
212
|
+
try {
|
|
213
|
+
const abResult = await execGit(
|
|
214
|
+
['rev-list', '--left-right', '--count', `${branchInfo.current}...@{upstream}`],
|
|
215
|
+
cwd
|
|
216
|
+
);
|
|
217
|
+
if (abResult.exitCode === 0) {
|
|
218
|
+
const { ahead, behind } = parseAheadBehind(abResult.stdout);
|
|
219
|
+
branchInfo.ahead = ahead;
|
|
220
|
+
branchInfo.behind = behind;
|
|
221
|
+
|
|
222
|
+
// Update the current branch entry too
|
|
223
|
+
const currentBranch = branchInfo.local.find(b => b.isCurrent);
|
|
224
|
+
if (currentBranch) {
|
|
225
|
+
currentBranch.ahead = ahead;
|
|
226
|
+
currentBranch.behind = behind;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// No upstream configured
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return branchInfo;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async createBranch(cwd: string, name: string, startPoint?: string): Promise<void> {
|
|
238
|
+
const args = ['checkout', '-b', name];
|
|
239
|
+
if (startPoint) args.push(startPoint);
|
|
240
|
+
const result = await execGit(args, cwd);
|
|
241
|
+
if (result.exitCode !== 0) {
|
|
242
|
+
throw new Error(`git checkout -b failed: ${result.stderr}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async switchBranch(cwd: string, name: string): Promise<void> {
|
|
247
|
+
const result = await execGit(['checkout', name], cwd);
|
|
248
|
+
if (result.exitCode !== 0) {
|
|
249
|
+
throw new Error(`git checkout failed: ${result.stderr}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async deleteBranch(cwd: string, name: string, force = false): Promise<void> {
|
|
254
|
+
const flag = force ? '-D' : '-d';
|
|
255
|
+
const result = await execGit(['branch', flag, name], cwd);
|
|
256
|
+
if (result.exitCode !== 0) {
|
|
257
|
+
throw new Error(`git branch ${flag} failed: ${result.stderr}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async renameBranch(cwd: string, oldName: string, newName: string): Promise<void> {
|
|
262
|
+
const result = await execGit(['branch', '-m', oldName, newName], cwd);
|
|
263
|
+
if (result.exitCode !== 0) {
|
|
264
|
+
throw new Error(`git branch -m failed: ${result.stderr}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async mergeBranch(cwd: string, branchName: string): Promise<{ success: boolean; message: string }> {
|
|
269
|
+
const result = await execGit(['merge', branchName], cwd);
|
|
270
|
+
return {
|
|
271
|
+
success: result.exitCode === 0,
|
|
272
|
+
message: result.exitCode === 0 ? result.stdout : result.stderr
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================
|
|
277
|
+
// Log
|
|
278
|
+
// ============================================
|
|
279
|
+
|
|
280
|
+
async getLog(cwd: string, limit = 50, skip = 0, branch?: string): Promise<GitLogResult> {
|
|
281
|
+
const SEPARATOR = '|||';
|
|
282
|
+
const format = `%H${SEPARATOR}%h${SEPARATOR}%an${SEPARATOR}%ae${SEPARATOR}%aI${SEPARATOR}%P${SEPARATOR}%D%n%s%x00`;
|
|
283
|
+
|
|
284
|
+
const args = [
|
|
285
|
+
'log',
|
|
286
|
+
`--format=${format}`,
|
|
287
|
+
`--max-count=${limit + 1}`, // +1 to check if there are more
|
|
288
|
+
`--skip=${skip}`
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
if (branch) args.push(branch);
|
|
292
|
+
|
|
293
|
+
const result = await execGit(args, cwd);
|
|
294
|
+
if (result.exitCode !== 0) {
|
|
295
|
+
throw new Error(`git log failed: ${result.stderr}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const commits = parseLog(result.stdout);
|
|
299
|
+
const hasMore = commits.length > limit;
|
|
300
|
+
if (hasMore) commits.pop(); // Remove the extra one
|
|
301
|
+
|
|
302
|
+
// Get total count
|
|
303
|
+
const countResult = await execGit(['rev-list', '--count', branch || 'HEAD'], cwd);
|
|
304
|
+
const total = parseInt(countResult.stdout.trim()) || commits.length;
|
|
305
|
+
|
|
306
|
+
return { commits, total, hasMore };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================
|
|
310
|
+
// Remote Operations
|
|
311
|
+
// ============================================
|
|
312
|
+
|
|
313
|
+
async getRemotes(cwd: string): Promise<GitRemote[]> {
|
|
314
|
+
const result = await execGit(['remote', '-v'], cwd);
|
|
315
|
+
return parseRemotes(result.stdout);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async fetch(cwd: string, remote = 'origin'): Promise<string> {
|
|
319
|
+
const result = await execGit(['fetch', remote, '--prune'], cwd, 60000);
|
|
320
|
+
if (result.exitCode !== 0) {
|
|
321
|
+
throw new Error(`git fetch failed: ${result.stderr}`);
|
|
322
|
+
}
|
|
323
|
+
return result.stderr || result.stdout; // git fetch outputs to stderr
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async pull(cwd: string, remote = 'origin', branch?: string): Promise<{ success: boolean; message: string }> {
|
|
327
|
+
const args = ['pull', remote];
|
|
328
|
+
if (branch) args.push(branch);
|
|
329
|
+
const result = await execGit(args, cwd, 60000);
|
|
330
|
+
return {
|
|
331
|
+
success: result.exitCode === 0,
|
|
332
|
+
message: result.exitCode === 0 ? result.stdout : result.stderr
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async push(cwd: string, remote = 'origin', branch?: string, force = false): Promise<{ success: boolean; message: string }> {
|
|
337
|
+
const args = ['push', remote];
|
|
338
|
+
if (branch) args.push(branch);
|
|
339
|
+
if (force) args.push('--force-with-lease');
|
|
340
|
+
// Set upstream if needed
|
|
341
|
+
args.push('-u');
|
|
342
|
+
const result = await execGit(args, cwd, 60000);
|
|
343
|
+
return {
|
|
344
|
+
success: result.exitCode === 0,
|
|
345
|
+
message: result.exitCode === 0 ? (result.stderr || result.stdout) : result.stderr
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async addRemote(cwd: string, name: string, url: string): Promise<void> {
|
|
350
|
+
const result = await execGit(['remote', 'add', name, url], cwd);
|
|
351
|
+
if (result.exitCode !== 0) {
|
|
352
|
+
throw new Error(`git remote add failed: ${result.stderr}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async removeRemote(cwd: string, name: string): Promise<void> {
|
|
357
|
+
const result = await execGit(['remote', 'remove', name], cwd);
|
|
358
|
+
if (result.exitCode !== 0) {
|
|
359
|
+
throw new Error(`git remote remove failed: ${result.stderr}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================
|
|
364
|
+
// Stash
|
|
365
|
+
// ============================================
|
|
366
|
+
|
|
367
|
+
async stashList(cwd: string): Promise<GitStashEntry[]> {
|
|
368
|
+
const result = await execGit(['stash', 'list'], cwd);
|
|
369
|
+
return parseStashList(result.stdout);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async stashSave(cwd: string, message?: string): Promise<void> {
|
|
373
|
+
const args = ['stash', 'push'];
|
|
374
|
+
if (message) args.push('-m', message);
|
|
375
|
+
const result = await execGit(args, cwd);
|
|
376
|
+
if (result.exitCode !== 0) {
|
|
377
|
+
throw new Error(`git stash failed: ${result.stderr}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async stashPop(cwd: string, index = 0): Promise<void> {
|
|
382
|
+
const result = await execGit(['stash', 'pop', `stash@{${index}}`], cwd);
|
|
383
|
+
if (result.exitCode !== 0) {
|
|
384
|
+
throw new Error(`git stash pop failed: ${result.stderr}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async stashDrop(cwd: string, index = 0): Promise<void> {
|
|
389
|
+
const result = await execGit(['stash', 'drop', `stash@{${index}}`], cwd);
|
|
390
|
+
if (result.exitCode !== 0) {
|
|
391
|
+
throw new Error(`git stash drop failed: ${result.stderr}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ============================================
|
|
396
|
+
// Tags
|
|
397
|
+
// ============================================
|
|
398
|
+
|
|
399
|
+
async getTags(cwd: string): Promise<{ name: string; hash: string; message: string; date: string; isAnnotated: boolean }[]> {
|
|
400
|
+
const result = await execGit(
|
|
401
|
+
['tag', '-l', '--sort=-creatordate', '--format=%(refname:short)|||%(objectname:short)|||%(contents:subject)|||%(creatordate:iso-strict)|||%(objecttype)'],
|
|
402
|
+
cwd
|
|
403
|
+
);
|
|
404
|
+
if (result.exitCode !== 0) return [];
|
|
405
|
+
|
|
406
|
+
const tags: { name: string; hash: string; message: string; date: string; isAnnotated: boolean }[] = [];
|
|
407
|
+
const lines = result.stdout.split('\n').filter(Boolean);
|
|
408
|
+
for (const line of lines) {
|
|
409
|
+
const parts = line.split('|||');
|
|
410
|
+
if (parts.length >= 2) {
|
|
411
|
+
tags.push({
|
|
412
|
+
name: parts[0],
|
|
413
|
+
hash: parts[1],
|
|
414
|
+
message: parts[2] || '',
|
|
415
|
+
date: parts[3] || '',
|
|
416
|
+
isAnnotated: parts[4] === 'tag'
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return tags;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async createTag(cwd: string, name: string, message?: string, commitHash?: string): Promise<void> {
|
|
424
|
+
const args = ['tag'];
|
|
425
|
+
if (message) {
|
|
426
|
+
args.push('-a', name, '-m', message);
|
|
427
|
+
} else {
|
|
428
|
+
args.push(name);
|
|
429
|
+
}
|
|
430
|
+
if (commitHash) args.push(commitHash);
|
|
431
|
+
const result = await execGit(args, cwd);
|
|
432
|
+
if (result.exitCode !== 0) {
|
|
433
|
+
throw new Error(`git tag failed: ${result.stderr}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async deleteTag(cwd: string, name: string): Promise<void> {
|
|
438
|
+
const result = await execGit(['tag', '-d', name], cwd);
|
|
439
|
+
if (result.exitCode !== 0) {
|
|
440
|
+
throw new Error(`git tag -d failed: ${result.stderr}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async pushTag(cwd: string, name: string, remote = 'origin'): Promise<{ success: boolean; message: string }> {
|
|
445
|
+
const result = await execGit(['push', remote, name], cwd, 60000);
|
|
446
|
+
return {
|
|
447
|
+
success: result.exitCode === 0,
|
|
448
|
+
message: result.exitCode === 0 ? (result.stderr || result.stdout) : result.stderr
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================
|
|
453
|
+
// Conflict Resolution
|
|
454
|
+
// ============================================
|
|
455
|
+
|
|
456
|
+
async getConflictFiles(cwd: string): Promise<GitConflictFile[]> {
|
|
457
|
+
const status = await this.getStatus(cwd);
|
|
458
|
+
const conflicts: GitConflictFile[] = [];
|
|
459
|
+
|
|
460
|
+
for (const file of status.conflicted) {
|
|
461
|
+
try {
|
|
462
|
+
const { readFile } = await import('node:fs/promises');
|
|
463
|
+
const { join } = await import('node:path');
|
|
464
|
+
const content = await readFile(join(cwd, file.path), 'utf-8');
|
|
465
|
+
const markers = parseConflictMarkers(content);
|
|
466
|
+
conflicts.push({ path: file.path, content, markers });
|
|
467
|
+
} catch (err) {
|
|
468
|
+
debug.error('git', `Failed to read conflict file: ${file.path}`, err);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return conflicts;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async resolveConflict(
|
|
476
|
+
cwd: string,
|
|
477
|
+
filePath: string,
|
|
478
|
+
resolution: 'ours' | 'theirs' | 'custom',
|
|
479
|
+
customContent?: string
|
|
480
|
+
): Promise<void> {
|
|
481
|
+
if (resolution === 'custom' && customContent !== undefined) {
|
|
482
|
+
// Write custom content
|
|
483
|
+
const { writeFile } = await import('node:fs/promises');
|
|
484
|
+
const { join } = await import('node:path');
|
|
485
|
+
await writeFile(join(cwd, filePath), customContent, 'utf-8');
|
|
486
|
+
} else if (resolution === 'ours') {
|
|
487
|
+
await execGit(['checkout', '--ours', '--', filePath], cwd);
|
|
488
|
+
} else if (resolution === 'theirs') {
|
|
489
|
+
await execGit(['checkout', '--theirs', '--', filePath], cwd);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Stage the resolved file
|
|
493
|
+
await this.stageFile(cwd, filePath);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async abortMerge(cwd: string): Promise<void> {
|
|
497
|
+
const result = await execGit(['merge', '--abort'], cwd);
|
|
498
|
+
if (result.exitCode !== 0) {
|
|
499
|
+
throw new Error(`git merge --abort failed: ${result.stderr}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Singleton
|
|
505
|
+
export const gitService = new GitService();
|