centaurus-cli 3.0.1 → 3.1.0
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/dist/ai/types.js +0 -1
- package/dist/ai/types.js.map +1 -1
- package/dist/cli-adapter.js +5047 -5158
- package/dist/cli-adapter.js.map +1 -1
- package/dist/commands/CommandParser.js +372 -315
- package/dist/commands/CommandParser.js.map +1 -1
- package/dist/config/build-config.js +11 -42
- package/dist/config/build-config.js.map +1 -1
- package/dist/config/defaultConfig.js +94 -82
- package/dist/config/defaultConfig.js.map +1 -1
- package/dist/config/manager.js +144 -160
- package/dist/config/manager.js.map +1 -1
- package/dist/config/mcp-config-manager.js +411 -364
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/models.js +118 -185
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.js +186 -184
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.js +33 -26
- package/dist/config/types.js.map +1 -1
- package/dist/context/command-detector.js +63 -67
- package/dist/context/command-detector.js.map +1 -1
- package/dist/context/context-manager.js +533 -518
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/docker-handler.js +518 -576
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.js +1050 -1109
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/handlers/wsl-handler.js +558 -630
- package/dist/context/handlers/wsl-handler.js.map +1 -1
- package/dist/context/index.js +42 -6
- package/dist/context/index.js.map +1 -1
- package/dist/context/subshell-handler.js +0 -4
- package/dist/context/subshell-handler.js.map +1 -1
- package/dist/context/types.js +20 -31
- package/dist/context/types.js.map +1 -1
- package/dist/hooks/useConnectivity.js +13 -10
- package/dist/hooks/useConnectivity.js.map +1 -1
- package/dist/hooks/useTerminalDimensions.js +67 -79
- package/dist/hooks/useTerminalDimensions.js.map +1 -1
- package/dist/index.js +228 -251
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.js +297 -260
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server-manager.js +139 -155
- package/dist/mcp/mcp-server-manager.js.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.js +74 -94
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
- package/dist/services/ai-autocomplete-agent.js +169 -181
- package/dist/services/ai-autocomplete-agent.js.map +1 -1
- package/dist/services/ai-context-injector.js +180 -93
- package/dist/services/ai-context-injector.js.map +1 -1
- package/dist/services/ai-service-client.js +513 -456
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js +443 -441
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +162 -198
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/background-task-manager.js +258 -282
- package/dist/services/background-task-manager.js.map +1 -1
- package/dist/services/checkpoint-manager.js +1526 -1512
- package/dist/services/checkpoint-manager.js.map +1 -1
- package/dist/services/clipboard-service.js +151 -200
- package/dist/services/clipboard-service.js.map +1 -1
- package/dist/services/connectivity-manager.js +63 -65
- package/dist/services/connectivity-manager.js.map +1 -1
- package/dist/services/conversation-manager.js +118 -121
- package/dist/services/conversation-manager.js.map +1 -1
- package/dist/services/environment-context-injector.js +160 -187
- package/dist/services/environment-context-injector.js.map +1 -1
- package/dist/services/fast-context-agent.js +203 -243
- package/dist/services/fast-context-agent.js.map +1 -1
- package/dist/services/input-detection-agent.js +190 -202
- package/dist/services/input-detection-agent.js.map +1 -1
- package/dist/services/input-requirement-detector.js +155 -189
- package/dist/services/input-requirement-detector.js.map +1 -1
- package/dist/services/local-chat-storage.js +342 -365
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/monitored-shell-manager.js +225 -233
- package/dist/services/monitored-shell-manager.js.map +1 -1
- package/dist/services/ollama-service.js +293 -310
- package/dist/services/ollama-service.js.map +1 -1
- package/dist/services/rules-storage.js +142 -0
- package/dist/services/rules-storage.js.map +1 -0
- package/dist/services/session-quota-manager.js +219 -235
- package/dist/services/session-quota-manager.js.map +1 -1
- package/dist/services/shell-input-agent.js +299 -334
- package/dist/services/shell-input-agent.js.map +1 -1
- package/dist/services/sub-agent-manager.js +459 -501
- package/dist/services/sub-agent-manager.js.map +1 -1
- package/dist/services/warpify-detector.js +133 -183
- package/dist/services/warpify-detector.js.map +1 -1
- package/dist/services/workflow-storage.js +202 -217
- package/dist/services/workflow-storage.js.map +1 -1
- package/dist/test-ssh-handler.js +148 -193
- package/dist/test-ssh-handler.js.map +1 -1
- package/dist/tools/add-mcp.js +161 -0
- package/dist/tools/add-mcp.js.map +1 -0
- package/dist/tools/background-command.js +240 -273
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.js +447 -440
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/create-image.js +172 -202
- package/dist/tools/create-image.js.map +1 -1
- package/dist/tools/enter-remote-session.js +169 -215
- package/dist/tools/enter-remote-session.js.map +1 -1
- package/dist/tools/fast-context.js +60 -67
- package/dist/tools/fast-context.js.map +1 -1
- package/dist/tools/file-ops.js +601 -572
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/find-files.js +262 -303
- package/dist/tools/find-files.js.map +1 -1
- package/dist/tools/get-diff.js +423 -406
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/grep-search.js +966 -948
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/inspect-symbol.js +308 -323
- package/dist/tools/inspect-symbol.js.map +1 -1
- package/dist/tools/plan-mode.js +459 -503
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/read-binary-file.js +160 -190
- package/dist/tools/read-binary-file.js.map +1 -1
- package/dist/tools/registry.js +100 -84
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/reproduce_issue.js +170 -151
- package/dist/tools/reproduce_issue.js.map +1 -1
- package/dist/tools/sub-agent.js +223 -228
- package/dist/tools/sub-agent.js.map +1 -1
- package/dist/tools/task-complete.js +28 -27
- package/dist/tools/task-complete.js.map +1 -1
- package/dist/tools/types.js +0 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/validation.js +96 -118
- package/dist/tools/validation.js.map +1 -1
- package/dist/tools/web-search.js +194 -194
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/workflow-tool.js +77 -82
- package/dist/tools/workflow-tool.js.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rule.js +1 -0
- package/dist/types/rule.js.map +1 -0
- package/dist/types/workflow.js +0 -7
- package/dist/types/workflow.js.map +1 -1
- package/dist/ui/components/AgentTimer.js +24 -25
- package/dist/ui/components/AgentTimer.js.map +1 -1
- package/dist/ui/components/App.js +3266 -3263
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/AuthScreen.js +22 -34
- package/dist/ui/components/AuthScreen.js.map +1 -1
- package/dist/ui/components/AuthWelcomeScreen.js +30 -24
- package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
- package/dist/ui/components/Breadcrumbs.js +53 -82
- package/dist/ui/components/Breadcrumbs.js.map +1 -1
- package/dist/ui/components/CircularSelectInput.js +59 -67
- package/dist/ui/components/CircularSelectInput.js.map +1 -1
- package/dist/ui/components/ClipboardFileAutocomplete.js +78 -39
- package/dist/ui/components/ClipboardFileAutocomplete.js.map +1 -1
- package/dist/ui/components/CodeBlock.js +24 -42
- package/dist/ui/components/CodeBlock.js.map +1 -1
- package/dist/ui/components/ConfigViewer.js +18 -25
- package/dist/ui/components/ConfigViewer.js.map +1 -1
- package/dist/ui/components/ConfirmPrompt.js +49 -71
- package/dist/ui/components/ConfirmPrompt.js.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +32 -83
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/ContextWindowIndicator.js +34 -49
- package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.js +104 -106
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
- package/dist/ui/components/DiffViewer.js +68 -121
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/ErrorBoundary.js +40 -48
- package/dist/ui/components/ErrorBoundary.js.map +1 -1
- package/dist/ui/components/FileCreationPreview.js +29 -60
- package/dist/ui/components/FileCreationPreview.js.map +1 -1
- package/dist/ui/components/FileOperation.js +34 -29
- package/dist/ui/components/FileOperation.js.map +1 -1
- package/dist/ui/components/FileTagAutocomplete.js +55 -25
- package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
- package/dist/ui/components/FontRecommendation.js.map +1 -1
- package/dist/ui/components/GitDiffBreadcrumb.js +29 -0
- package/dist/ui/components/GitDiffBreadcrumb.js.map +1 -0
- package/dist/ui/components/InputBox.js +1620 -2150
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.js +234 -352
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/KeyboardHelp.js +34 -35
- package/dist/ui/components/KeyboardHelp.js.map +1 -1
- package/dist/ui/components/LoadingIndicator.js +22 -25
- package/dist/ui/components/LoadingIndicator.js.map +1 -1
- package/dist/ui/components/MCPAddScreen.js +40 -51
- package/dist/ui/components/MCPAddScreen.js.map +1 -1
- package/dist/ui/components/MCPListScreen.js +40 -48
- package/dist/ui/components/MCPListScreen.js.map +1 -1
- package/dist/ui/components/MCPServerListScreen.js +49 -56
- package/dist/ui/components/MCPServerListScreen.js.map +1 -1
- package/dist/ui/components/MarkdownRenderer.js +69 -96
- package/dist/ui/components/MarkdownRenderer.js.map +1 -1
- package/dist/ui/components/MessageBox.js +66 -48
- package/dist/ui/components/MessageBox.js.map +1 -1
- package/dist/ui/components/MessageDisplay.js +150 -142
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/MonitorModeAIPanel.js +46 -65
- package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
- package/dist/ui/components/MultiLineInput.js +243 -277
- package/dist/ui/components/MultiLineInput.js.map +1 -1
- package/dist/ui/components/PasswordPrompt.js +37 -18
- package/dist/ui/components/PasswordPrompt.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +27 -38
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/PlanReviewScreen.js +46 -50
- package/dist/ui/components/PlanReviewScreen.js.map +1 -1
- package/dist/ui/components/RulesEditorScreen.js +81 -0
- package/dist/ui/components/RulesEditorScreen.js.map +1 -0
- package/dist/ui/components/SelectPrompt.js +19 -8
- package/dist/ui/components/SelectPrompt.js.map +1 -1
- package/dist/ui/components/ShimmerText.js +44 -0
- package/dist/ui/components/ShimmerText.js.map +1 -0
- package/dist/ui/components/SlashCommandAutocomplete.js +49 -22
- package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
- package/dist/ui/components/StatusBar.js +56 -87
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +116 -99
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.js +28 -23
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.js +44 -70
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
- package/dist/ui/components/ThinkingDisplay.js +44 -41
- package/dist/ui/components/ThinkingDisplay.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +772 -1326
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +53 -84
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/ui/components/ToolResult.js +22 -15
- package/dist/ui/components/ToolResult.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.js +88 -120
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
- package/dist/ui/components/WelcomeBanner.js +176 -26
- package/dist/ui/components/WelcomeBanner.js.map +1 -1
- package/dist/ui/components/WorkflowCreatorScreen.js +94 -161
- package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
- package/dist/utils/ansi-encoder.js +30 -61
- package/dist/utils/ansi-encoder.js.map +1 -1
- package/dist/utils/chat-formatter.js +327 -305
- package/dist/utils/chat-formatter.js.map +1 -1
- package/dist/utils/command-history.js +152 -174
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/context-sanitizer.js +49 -112
- package/dist/utils/context-sanitizer.js.map +1 -1
- package/dist/utils/conversation-logger.js +292 -324
- package/dist/utils/conversation-logger.js.map +1 -1
- package/dist/utils/custom-commands-manager.js +126 -131
- package/dist/utils/custom-commands-manager.js.map +1 -1
- package/dist/utils/editor-utils.js +732 -837
- package/dist/utils/editor-utils.js.map +1 -1
- package/dist/utils/file.js +174 -213
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/git-stats.js +169 -0
- package/dist/utils/git-stats.js.map +1 -0
- package/dist/utils/input-classifier.js +960 -482
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/logger.js +48 -73
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markdown-parser.js +277 -310
- package/dist/utils/markdown-parser.js.map +1 -1
- package/dist/utils/rule-reference-resolver.js +54 -0
- package/dist/utils/rule-reference-resolver.js.map +1 -0
- package/dist/utils/shell.js +144 -156
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/state.js +23 -22
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/syntax-checker.js +279 -327
- package/dist/utils/syntax-checker.js.map +1 -1
- package/dist/utils/terminal-output.js +199 -302
- package/dist/utils/terminal-output.js.map +1 -1
- package/dist/utils/text-clipboard.js +47 -70
- package/dist/utils/text-clipboard.js.map +1 -1
- package/dist/utils/unicode-sanitizer.js +134 -197
- package/dist/utils/unicode-sanitizer.js.map +1 -1
- package/dist/utils/version-checker.js +46 -56
- package/dist/utils/version-checker.js.map +1 -1
- package/package.json +6 -4
- package/dist/ai/types.d.ts +0 -20
- package/dist/ai/types.d.ts.map +0 -1
- package/dist/cli-adapter.d.ts +0 -514
- package/dist/cli-adapter.d.ts.map +0 -1
- package/dist/commands/CommandParser.d.ts +0 -27
- package/dist/commands/CommandParser.d.ts.map +0 -1
- package/dist/config/build-config.d.ts +0 -42
- package/dist/config/build-config.d.ts.map +0 -1
- package/dist/config/defaultConfig.d.ts +0 -79
- package/dist/config/defaultConfig.d.ts.map +0 -1
- package/dist/config/manager.d.ts +0 -62
- package/dist/config/manager.d.ts.map +0 -1
- package/dist/config/mcp-config-manager.d.ts +0 -79
- package/dist/config/mcp-config-manager.d.ts.map +0 -1
- package/dist/config/models.d.ts +0 -83
- package/dist/config/models.d.ts.map +0 -1
- package/dist/config/slash-commands.d.ts +0 -23
- package/dist/config/slash-commands.d.ts.map +0 -1
- package/dist/config/types.d.ts +0 -35
- package/dist/config/types.d.ts.map +0 -1
- package/dist/context/command-detector.d.ts +0 -50
- package/dist/context/command-detector.d.ts.map +0 -1
- package/dist/context/context-manager.d.ts +0 -157
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/handlers/docker-handler.d.ts +0 -130
- package/dist/context/handlers/docker-handler.d.ts.map +0 -1
- package/dist/context/handlers/ssh-handler.d.ts +0 -201
- package/dist/context/handlers/ssh-handler.d.ts.map +0 -1
- package/dist/context/handlers/wsl-handler.d.ts +0 -146
- package/dist/context/handlers/wsl-handler.d.ts.map +0 -1
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/subshell-handler.d.ts +0 -165
- package/dist/context/subshell-handler.d.ts.map +0 -1
- package/dist/context/types.d.ts +0 -70
- package/dist/context/types.d.ts.map +0 -1
- package/dist/hooks/useConnectivity.d.ts +0 -2
- package/dist/hooks/useConnectivity.d.ts.map +0 -1
- package/dist/hooks/useTerminalDimensions.d.ts +0 -41
- package/dist/hooks/useTerminalDimensions.d.ts.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/mcp/mcp-command-handler.d.ts +0 -47
- package/dist/mcp/mcp-command-handler.d.ts.map +0 -1
- package/dist/mcp/mcp-server-manager.d.ts +0 -30
- package/dist/mcp/mcp-server-manager.d.ts.map +0 -1
- package/dist/mcp/mcp-tool-wrapper.d.ts +0 -12
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +0 -1
- package/dist/services/ai-autocomplete-agent.d.ts +0 -39
- package/dist/services/ai-autocomplete-agent.d.ts.map +0 -1
- package/dist/services/ai-context-injector.d.ts +0 -41
- package/dist/services/ai-context-injector.d.ts.map +0 -1
- package/dist/services/ai-service-client.d.ts +0 -128
- package/dist/services/ai-service-client.d.ts.map +0 -1
- package/dist/services/api-client.d.ts +0 -353
- package/dist/services/api-client.d.ts.map +0 -1
- package/dist/services/auth-handler.d.ts +0 -30
- package/dist/services/auth-handler.d.ts.map +0 -1
- package/dist/services/background-task-manager.d.ts +0 -114
- package/dist/services/background-task-manager.d.ts.map +0 -1
- package/dist/services/checkpoint-manager.d.ts +0 -204
- package/dist/services/checkpoint-manager.d.ts.map +0 -1
- package/dist/services/clipboard-service.d.ts +0 -37
- package/dist/services/clipboard-service.d.ts.map +0 -1
- package/dist/services/connectivity-manager.d.ts +0 -18
- package/dist/services/connectivity-manager.d.ts.map +0 -1
- package/dist/services/conversation-manager.d.ts +0 -73
- package/dist/services/conversation-manager.d.ts.map +0 -1
- package/dist/services/environment-context-injector.d.ts +0 -69
- package/dist/services/environment-context-injector.d.ts.map +0 -1
- package/dist/services/fast-context-agent.d.ts +0 -12
- package/dist/services/fast-context-agent.d.ts.map +0 -1
- package/dist/services/input-detection-agent.d.ts +0 -40
- package/dist/services/input-detection-agent.d.ts.map +0 -1
- package/dist/services/input-requirement-detector.d.ts +0 -28
- package/dist/services/input-requirement-detector.d.ts.map +0 -1
- package/dist/services/local-chat-storage.d.ts +0 -182
- package/dist/services/local-chat-storage.d.ts.map +0 -1
- package/dist/services/monitored-shell-manager.d.ts +0 -120
- package/dist/services/monitored-shell-manager.d.ts.map +0 -1
- package/dist/services/ollama-service.d.ts +0 -197
- package/dist/services/ollama-service.d.ts.map +0 -1
- package/dist/services/session-quota-manager.d.ts +0 -101
- package/dist/services/session-quota-manager.d.ts.map +0 -1
- package/dist/services/shell-input-agent.d.ts +0 -89
- package/dist/services/shell-input-agent.d.ts.map +0 -1
- package/dist/services/sub-agent-manager.d.ts +0 -140
- package/dist/services/sub-agent-manager.d.ts.map +0 -1
- package/dist/services/warpify-detector.d.ts +0 -43
- package/dist/services/warpify-detector.d.ts.map +0 -1
- package/dist/services/workflow-storage.d.ts +0 -72
- package/dist/services/workflow-storage.d.ts.map +0 -1
- package/dist/test-ssh-handler.d.ts +0 -8
- package/dist/test-ssh-handler.d.ts.map +0 -1
- package/dist/tools/background-command.d.ts +0 -11
- package/dist/tools/background-command.d.ts.map +0 -1
- package/dist/tools/command.d.ts +0 -3
- package/dist/tools/command.d.ts.map +0 -1
- package/dist/tools/create-image.d.ts +0 -10
- package/dist/tools/create-image.d.ts.map +0 -1
- package/dist/tools/enter-remote-session.d.ts +0 -48
- package/dist/tools/enter-remote-session.d.ts.map +0 -1
- package/dist/tools/fast-context.d.ts +0 -3
- package/dist/tools/fast-context.d.ts.map +0 -1
- package/dist/tools/file-ops.d.ts +0 -7
- package/dist/tools/file-ops.d.ts.map +0 -1
- package/dist/tools/find-files.d.ts +0 -49
- package/dist/tools/find-files.d.ts.map +0 -1
- package/dist/tools/get-diff.d.ts +0 -14
- package/dist/tools/get-diff.d.ts.map +0 -1
- package/dist/tools/grep-search.d.ts +0 -155
- package/dist/tools/grep-search.d.ts.map +0 -1
- package/dist/tools/inspect-symbol.d.ts +0 -32
- package/dist/tools/inspect-symbol.d.ts.map +0 -1
- package/dist/tools/plan-mode.d.ts +0 -140
- package/dist/tools/plan-mode.d.ts.map +0 -1
- package/dist/tools/read-binary-file.d.ts +0 -10
- package/dist/tools/read-binary-file.d.ts.map +0 -1
- package/dist/tools/registry.d.ts +0 -31
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/reproduce_issue.d.ts +0 -2
- package/dist/tools/reproduce_issue.d.ts.map +0 -1
- package/dist/tools/sub-agent.d.ts +0 -9
- package/dist/tools/sub-agent.d.ts.map +0 -1
- package/dist/tools/task-complete.d.ts +0 -3
- package/dist/tools/task-complete.d.ts.map +0 -1
- package/dist/tools/types.d.ts +0 -40
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/validation.d.ts +0 -47
- package/dist/tools/validation.d.ts.map +0 -1
- package/dist/tools/web-search.d.ts +0 -24
- package/dist/tools/web-search.d.ts.map +0 -1
- package/dist/tools/workflow-tool.d.ts +0 -11
- package/dist/tools/workflow-tool.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -123
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/workflow.d.ts +0 -110
- package/dist/types/workflow.d.ts.map +0 -1
- package/dist/ui/components/AgentTimer.d.ts +0 -7
- package/dist/ui/components/AgentTimer.d.ts.map +0 -1
- package/dist/ui/components/App.d.ts +0 -197
- package/dist/ui/components/App.d.ts.map +0 -1
- package/dist/ui/components/AuthScreen.d.ts +0 -8
- package/dist/ui/components/AuthScreen.d.ts.map +0 -1
- package/dist/ui/components/AuthWelcomeScreen.d.ts +0 -8
- package/dist/ui/components/AuthWelcomeScreen.d.ts.map +0 -1
- package/dist/ui/components/Breadcrumbs.d.ts +0 -13
- package/dist/ui/components/Breadcrumbs.d.ts.map +0 -1
- package/dist/ui/components/CircularSelectInput.d.ts +0 -24
- package/dist/ui/components/CircularSelectInput.d.ts.map +0 -1
- package/dist/ui/components/ClipboardFileAutocomplete.d.ts +0 -10
- package/dist/ui/components/ClipboardFileAutocomplete.d.ts.map +0 -1
- package/dist/ui/components/CodeBlock.d.ts +0 -9
- package/dist/ui/components/CodeBlock.d.ts.map +0 -1
- package/dist/ui/components/ConfigViewer.d.ts +0 -11
- package/dist/ui/components/ConfigViewer.d.ts.map +0 -1
- package/dist/ui/components/ConfirmPrompt.d.ts +0 -13
- package/dist/ui/components/ConfirmPrompt.d.ts.map +0 -1
- package/dist/ui/components/ConnectionStatusMessage.d.ts +0 -17
- package/dist/ui/components/ConnectionStatusMessage.d.ts.map +0 -1
- package/dist/ui/components/ContextWindowIndicator.d.ts +0 -8
- package/dist/ui/components/ContextWindowIndicator.d.ts.map +0 -1
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts +0 -17
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +0 -1
- package/dist/ui/components/DiffViewer.d.ts +0 -9
- package/dist/ui/components/DiffViewer.d.ts.map +0 -1
- package/dist/ui/components/ErrorBoundary.d.ts +0 -17
- package/dist/ui/components/ErrorBoundary.d.ts.map +0 -1
- package/dist/ui/components/FileCreationPreview.d.ts +0 -8
- package/dist/ui/components/FileCreationPreview.d.ts.map +0 -1
- package/dist/ui/components/FileOperation.d.ts +0 -10
- package/dist/ui/components/FileOperation.d.ts.map +0 -1
- package/dist/ui/components/FileTagAutocomplete.d.ts +0 -11
- package/dist/ui/components/FileTagAutocomplete.d.ts.map +0 -1
- package/dist/ui/components/FontRecommendation.d.ts +0 -1
- package/dist/ui/components/FontRecommendation.d.ts.map +0 -1
- package/dist/ui/components/InputBox.d.ts +0 -42
- package/dist/ui/components/InputBox.d.ts.map +0 -1
- package/dist/ui/components/InteractiveShell.d.ts +0 -30
- package/dist/ui/components/InteractiveShell.d.ts.map +0 -1
- package/dist/ui/components/KeyboardHelp.d.ts +0 -7
- package/dist/ui/components/KeyboardHelp.d.ts.map +0 -1
- package/dist/ui/components/LoadingIndicator.d.ts +0 -3
- package/dist/ui/components/LoadingIndicator.d.ts.map +0 -1
- package/dist/ui/components/MCPAddScreen.d.ts +0 -13
- package/dist/ui/components/MCPAddScreen.d.ts.map +0 -1
- package/dist/ui/components/MCPListScreen.d.ts +0 -17
- package/dist/ui/components/MCPListScreen.d.ts.map +0 -1
- package/dist/ui/components/MCPServerListScreen.d.ts +0 -16
- package/dist/ui/components/MCPServerListScreen.d.ts.map +0 -1
- package/dist/ui/components/MarkdownRenderer.d.ts +0 -8
- package/dist/ui/components/MarkdownRenderer.d.ts.map +0 -1
- package/dist/ui/components/MessageBox.d.ts +0 -10
- package/dist/ui/components/MessageBox.d.ts.map +0 -1
- package/dist/ui/components/MessageDisplay.d.ts +0 -14
- package/dist/ui/components/MessageDisplay.d.ts.map +0 -1
- package/dist/ui/components/MonitorModeAIPanel.d.ts +0 -23
- package/dist/ui/components/MonitorModeAIPanel.d.ts.map +0 -1
- package/dist/ui/components/MultiLineInput.d.ts +0 -13
- package/dist/ui/components/MultiLineInput.d.ts.map +0 -1
- package/dist/ui/components/PasswordPrompt.d.ts +0 -9
- package/dist/ui/components/PasswordPrompt.d.ts.map +0 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +0 -20
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +0 -1
- package/dist/ui/components/PlanReviewScreen.d.ts +0 -14
- package/dist/ui/components/PlanReviewScreen.d.ts.map +0 -1
- package/dist/ui/components/SelectPrompt.d.ts +0 -12
- package/dist/ui/components/SelectPrompt.d.ts.map +0 -1
- package/dist/ui/components/SlashCommandAutocomplete.d.ts +0 -13
- package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +0 -1
- package/dist/ui/components/StatusBar.d.ts +0 -14
- package/dist/ui/components/StatusBar.d.ts.map +0 -1
- package/dist/ui/components/StreamingMessageDisplay.d.ts +0 -15
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +0 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts +0 -14
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +0 -1
- package/dist/ui/components/TaskProgressIndicator.d.ts +0 -18
- package/dist/ui/components/TaskProgressIndicator.d.ts.map +0 -1
- package/dist/ui/components/ThinkingDisplay.d.ts +0 -15
- package/dist/ui/components/ThinkingDisplay.d.ts.map +0 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts +0 -8
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +0 -1
- package/dist/ui/components/ToolExecutionStatus.d.ts +0 -10
- package/dist/ui/components/ToolExecutionStatus.d.ts.map +0 -1
- package/dist/ui/components/ToolResult.d.ts +0 -10
- package/dist/ui/components/ToolResult.d.ts.map +0 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts +0 -9
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +0 -1
- package/dist/ui/components/WelcomeBanner.d.ts +0 -3
- package/dist/ui/components/WelcomeBanner.d.ts.map +0 -1
- package/dist/ui/components/WorkflowCreatorScreen.d.ts +0 -25
- package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +0 -1
- package/dist/utils/ansi-encoder.d.ts +0 -7
- package/dist/utils/ansi-encoder.d.ts.map +0 -1
- package/dist/utils/chat-formatter.d.ts +0 -12
- package/dist/utils/chat-formatter.d.ts.map +0 -1
- package/dist/utils/command-history.d.ts +0 -24
- package/dist/utils/command-history.d.ts.map +0 -1
- package/dist/utils/context-sanitizer.d.ts +0 -50
- package/dist/utils/context-sanitizer.d.ts.map +0 -1
- package/dist/utils/conversation-logger.d.ts +0 -142
- package/dist/utils/conversation-logger.d.ts.map +0 -1
- package/dist/utils/custom-commands-manager.d.ts +0 -59
- package/dist/utils/custom-commands-manager.d.ts.map +0 -1
- package/dist/utils/editor-utils.d.ts +0 -101
- package/dist/utils/editor-utils.d.ts.map +0 -1
- package/dist/utils/file.d.ts +0 -61
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/input-classifier.d.ts +0 -25
- package/dist/utils/input-classifier.d.ts.map +0 -1
- package/dist/utils/logger.d.ts +0 -17
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/markdown-parser.d.ts +0 -60
- package/dist/utils/markdown-parser.d.ts.map +0 -1
- package/dist/utils/shell.d.ts +0 -47
- package/dist/utils/shell.d.ts.map +0 -1
- package/dist/utils/state.d.ts +0 -13
- package/dist/utils/state.d.ts.map +0 -1
- package/dist/utils/syntax-checker.d.ts +0 -24
- package/dist/utils/syntax-checker.d.ts.map +0 -1
- package/dist/utils/terminal-output.d.ts +0 -25
- package/dist/utils/terminal-output.d.ts.map +0 -1
- package/dist/utils/text-clipboard.d.ts +0 -12
- package/dist/utils/text-clipboard.d.ts.map +0 -1
- package/dist/utils/unicode-sanitizer.d.ts +0 -44
- package/dist/utils/unicode-sanitizer.d.ts.map +0 -1
- package/dist/utils/version-checker.d.ts +0 -14
- package/dist/utils/version-checker.d.ts.map +0 -1
|
@@ -1,1172 +1,1113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import { Client } from "ssh2";
|
|
2
|
+
import { SubshellConnectionError, SubshellExecutionError } from "../types.js";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { quickLog } from "../../utils/conversation-logger.js";
|
|
8
|
+
class SSHHandler {
|
|
9
|
+
type = "ssh";
|
|
10
|
+
detectionPatterns = [
|
|
11
|
+
/^ssh\s+/,
|
|
12
|
+
/^ssh\s+-[^\s]+\s+/
|
|
13
|
+
];
|
|
14
|
+
_client = null;
|
|
15
|
+
sftpClient = null;
|
|
16
|
+
config = null;
|
|
17
|
+
currentWorkingDirectory = "~";
|
|
18
|
+
shellType = "bash";
|
|
19
|
+
osType = "linux";
|
|
20
|
+
sessionId = "";
|
|
21
|
+
onPasswordRequest;
|
|
22
|
+
_isConnected = false;
|
|
23
|
+
onDisconnectCallback;
|
|
24
|
+
/**
|
|
25
|
+
* Get the underlying SSH client (used for PTY commands)
|
|
26
|
+
*/
|
|
27
|
+
get client() {
|
|
28
|
+
return this._client;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set password request callback
|
|
32
|
+
*/
|
|
33
|
+
setPasswordRequestCallback(callback) {
|
|
34
|
+
this.onPasswordRequest = callback;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Set callback to be called when the connection is lost
|
|
38
|
+
*/
|
|
39
|
+
setDisconnectCallback(callback) {
|
|
40
|
+
this.onDisconnectCallback = callback;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if the handler is currently connected
|
|
44
|
+
*/
|
|
45
|
+
isConnected() {
|
|
46
|
+
return this._isConnected && this._client !== null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Detect if a command should trigger this handler
|
|
50
|
+
*/
|
|
51
|
+
detect(command) {
|
|
52
|
+
return this.detectionPatterns.some((pattern) => pattern.test(command));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Attach to an existing SSH session detected from PTY output.
|
|
56
|
+
* This is used for "Warpify" mode where the user has already established
|
|
57
|
+
* an SSH connection in the terminal and pressed Alt+E to attach.
|
|
58
|
+
*
|
|
59
|
+
* Unlike connect(), this does NOT establish a new SSH connection.
|
|
60
|
+
* It creates a context based on the detected session info for AI awareness.
|
|
61
|
+
* Commands will continue to flow through the PTY, not via ssh2 library.
|
|
62
|
+
*
|
|
63
|
+
* @param connectionString - The detected connection string (e.g., "user@hostname")
|
|
64
|
+
*/
|
|
65
|
+
attachFromPtyOutput(connectionString) {
|
|
66
|
+
this.sessionId = `warpified-${Date.now()}`;
|
|
67
|
+
const parts = connectionString.split("@");
|
|
68
|
+
const username = parts.length > 1 ? parts[0] : "user";
|
|
69
|
+
const hostname = parts.length > 1 ? parts[1] : connectionString;
|
|
70
|
+
this.config = {
|
|
71
|
+
host: hostname,
|
|
72
|
+
port: 22,
|
|
73
|
+
username
|
|
74
|
+
};
|
|
75
|
+
this._isConnected = true;
|
|
76
|
+
return {
|
|
77
|
+
type: "ssh",
|
|
78
|
+
handler: this,
|
|
79
|
+
metadata: {
|
|
80
|
+
hostname,
|
|
81
|
+
username,
|
|
82
|
+
workingDirectory: "~",
|
|
83
|
+
shell: "bash",
|
|
84
|
+
// Assume bash for warpified sessions
|
|
85
|
+
os: "linux",
|
|
86
|
+
port: 22
|
|
87
|
+
},
|
|
88
|
+
connectionState: "connected",
|
|
89
|
+
sessionId: this.sessionId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Connect to the SSH server
|
|
94
|
+
*/
|
|
95
|
+
async connect(command, cwd) {
|
|
96
|
+
this.sessionId = randomBytes(16).toString("hex");
|
|
97
|
+
try {
|
|
98
|
+
this.config = this.parseSSHCommand(command);
|
|
99
|
+
await this.establishConnection(this.config);
|
|
100
|
+
try {
|
|
101
|
+
await this.initializeSFTP();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
this.sftpClient = null;
|
|
104
|
+
}
|
|
105
|
+
this.shellType = await this.detectShellType();
|
|
106
|
+
this.osType = await this.detectOSType();
|
|
107
|
+
this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
|
|
108
|
+
await this.injectShellIntegration();
|
|
109
|
+
return {
|
|
110
|
+
type: "ssh",
|
|
111
|
+
handler: this,
|
|
112
|
+
metadata: {
|
|
113
|
+
hostname: this.config.host,
|
|
114
|
+
username: this.config.username,
|
|
115
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
116
|
+
shell: this.shellType,
|
|
117
|
+
os: this.osType,
|
|
118
|
+
port: this.config.port
|
|
119
|
+
},
|
|
120
|
+
connectionState: "connected",
|
|
121
|
+
sessionId: this.sessionId
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new SubshellConnectionError(
|
|
125
|
+
"ssh",
|
|
126
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
127
|
+
true
|
|
128
|
+
);
|
|
35
129
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Connect from an existing remote context (nested session)
|
|
133
|
+
*/
|
|
134
|
+
async connectFromRemote(command, cwd, parentContext) {
|
|
135
|
+
this.sessionId = randomBytes(16).toString("hex");
|
|
136
|
+
try {
|
|
137
|
+
this.config = this.parseSSHCommand(command);
|
|
138
|
+
this.config.isNested = true;
|
|
139
|
+
let streamFactory;
|
|
140
|
+
if (parentContext.handler && typeof parentContext.handler.createStream === "function") {
|
|
141
|
+
streamFactory = () => parentContext.handler.createStream(this.config.host, this.config.port);
|
|
142
|
+
} else {
|
|
143
|
+
throw new SubshellConnectionError("ssh", `Parent context (${parentContext.type}) does not support nested connections via tunneling.`, false);
|
|
144
|
+
}
|
|
145
|
+
await this.establishConnection(this.config, void 0, streamFactory);
|
|
146
|
+
try {
|
|
147
|
+
await this.initializeSFTP();
|
|
148
|
+
} catch (e) {
|
|
149
|
+
this.sftpClient = null;
|
|
150
|
+
}
|
|
151
|
+
this.shellType = await this.detectShellType();
|
|
152
|
+
this.osType = await this.detectOSType();
|
|
153
|
+
this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
|
|
154
|
+
await this.injectShellIntegration();
|
|
155
|
+
return {
|
|
156
|
+
type: "ssh",
|
|
157
|
+
handler: this,
|
|
158
|
+
metadata: {
|
|
159
|
+
hostname: this.config.host,
|
|
160
|
+
username: this.config.username,
|
|
161
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
162
|
+
shell: this.shellType,
|
|
163
|
+
os: this.osType,
|
|
164
|
+
port: this.config.port
|
|
165
|
+
},
|
|
166
|
+
connectionState: "connected",
|
|
167
|
+
sessionId: this.sessionId
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new SubshellConnectionError(
|
|
171
|
+
"ssh",
|
|
172
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
173
|
+
true
|
|
174
|
+
);
|
|
41
175
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a network stream/tunnel through this SSH connection
|
|
179
|
+
*/
|
|
180
|
+
async createStream(host, port) {
|
|
181
|
+
if (!this._client || !this._isConnected) {
|
|
182
|
+
throw new Error("Not connected to SSH server");
|
|
47
183
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
this._client.forwardOut("127.0.0.1", 0, host, port, (err, stream) => {
|
|
186
|
+
if (err) {
|
|
187
|
+
reject(err);
|
|
188
|
+
} else {
|
|
189
|
+
resolve(stream);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Disconnect from the SSH server
|
|
196
|
+
*/
|
|
197
|
+
async disconnect() {
|
|
198
|
+
this._isConnected = false;
|
|
199
|
+
if (this.sftpClient) {
|
|
200
|
+
this.sftpClient.end();
|
|
201
|
+
this.sftpClient = null;
|
|
53
202
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
detect(command) {
|
|
58
|
-
return this.detectionPatterns.some(pattern => pattern.test(command));
|
|
203
|
+
if (this._client) {
|
|
204
|
+
this._client.end();
|
|
205
|
+
this._client = null;
|
|
59
206
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
* @param connectionString - The detected connection string (e.g., "user@hostname")
|
|
70
|
-
*/
|
|
71
|
-
attachFromPtyOutput(connectionString) {
|
|
72
|
-
this.sessionId = `warpified-${Date.now()}`;
|
|
73
|
-
// Parse connection string
|
|
74
|
-
const parts = connectionString.split('@');
|
|
75
|
-
const username = parts.length > 1 ? parts[0] : 'user';
|
|
76
|
-
const hostname = parts.length > 1 ? parts[1] : connectionString;
|
|
77
|
-
// Create minimal config for context tracking (no actual SSH connection)
|
|
78
|
-
this.config = {
|
|
79
|
-
host: hostname,
|
|
80
|
-
port: 22,
|
|
81
|
-
username: username,
|
|
82
|
-
};
|
|
83
|
-
// Mark as "connected" for context purposes (PTY handles actual connection)
|
|
84
|
-
this._isConnected = true;
|
|
85
|
-
return {
|
|
86
|
-
type: 'ssh',
|
|
87
|
-
handler: this,
|
|
88
|
-
metadata: {
|
|
89
|
-
hostname: hostname,
|
|
90
|
-
username: username,
|
|
91
|
-
workingDirectory: '~',
|
|
92
|
-
shell: 'bash', // Assume bash for warpified sessions
|
|
93
|
-
os: 'linux',
|
|
94
|
-
port: 22,
|
|
95
|
-
},
|
|
96
|
-
connectionState: 'connected',
|
|
97
|
-
sessionId: this.sessionId,
|
|
98
|
-
};
|
|
207
|
+
this.config = null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Execute a command in the SSH session
|
|
211
|
+
*/
|
|
212
|
+
async executeCommand(command) {
|
|
213
|
+
if (!this._client || !this._isConnected) {
|
|
214
|
+
throw new SubshellExecutionError(command, "Not connected to SSH server");
|
|
99
215
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Inject shell integration script
|
|
124
|
-
await this.injectShellIntegration();
|
|
125
|
-
return {
|
|
126
|
-
type: 'ssh',
|
|
127
|
-
handler: this,
|
|
128
|
-
metadata: {
|
|
129
|
-
hostname: this.config.host,
|
|
130
|
-
username: this.config.username,
|
|
131
|
-
workingDirectory: this.currentWorkingDirectory,
|
|
132
|
-
shell: this.shellType,
|
|
133
|
-
os: this.osType,
|
|
134
|
-
port: this.config.port,
|
|
135
|
-
},
|
|
136
|
-
connectionState: 'connected',
|
|
137
|
-
sessionId: this.sessionId,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
throw new SubshellConnectionError('ssh', error instanceof Error ? error.message : 'Unknown error', true);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Connect from an existing remote context (nested session)
|
|
146
|
-
*/
|
|
147
|
-
async connectFromRemote(command, cwd, parentContext) {
|
|
148
|
-
this.sessionId = randomBytes(16).toString('hex');
|
|
149
|
-
try {
|
|
150
|
-
// Parse SSH command
|
|
151
|
-
this.config = this.parseSSHCommand(command);
|
|
152
|
-
// Mark as nested session to disable implicit local authentication
|
|
153
|
-
this.config.isNested = true;
|
|
154
|
-
// Check if parent supports streaming
|
|
155
|
-
// Check if parent supports streaming
|
|
156
|
-
let streamFactory;
|
|
157
|
-
if (parentContext.handler && typeof parentContext.handler.createStream === 'function') {
|
|
158
|
-
streamFactory = () => parentContext.handler.createStream(this.config.host, this.config.port);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
throw new SubshellConnectionError('ssh', `Parent context (${parentContext.type}) does not support nested connections via tunneling.`, false);
|
|
162
|
-
}
|
|
163
|
-
// Establish SSH connection using the tunnel stream factory
|
|
164
|
-
await this.establishConnection(this.config, undefined, streamFactory);
|
|
165
|
-
// Initialize SFTP (non-fatal)
|
|
166
|
-
try {
|
|
167
|
-
await this.initializeSFTP();
|
|
168
|
-
}
|
|
169
|
-
catch (e) {
|
|
170
|
-
this.sftpClient = null;
|
|
216
|
+
const pwdTag = `__CENTAURUS_PWD_${this.sessionId}__`;
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const timeout = setTimeout(() => {
|
|
219
|
+
reject(new SubshellExecutionError(command, "Command timed out after 30 seconds"));
|
|
220
|
+
}, 3e4);
|
|
221
|
+
this._client.exec(
|
|
222
|
+
`cd "${this.currentWorkingDirectory}" && ${command}; echo "${pwdTag}:$(pwd)"`,
|
|
223
|
+
(err, stream) => {
|
|
224
|
+
if (err) {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
reject(new SubshellExecutionError(command, err.message));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let stdout = "";
|
|
230
|
+
let stderr = "";
|
|
231
|
+
stream.on("close", (code) => {
|
|
232
|
+
clearTimeout(timeout);
|
|
233
|
+
const pwdRegex = new RegExp(`${pwdTag}:(.+)$`, "m");
|
|
234
|
+
const pwdMatch = stdout.match(pwdRegex);
|
|
235
|
+
if (pwdMatch) {
|
|
236
|
+
this.currentWorkingDirectory = pwdMatch[1].trim();
|
|
237
|
+
const removeRegex = new RegExp(`${pwdTag}:.+$`, "m");
|
|
238
|
+
stdout = stdout.replace(removeRegex, "").trim();
|
|
171
239
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
|
|
177
|
-
// Inject shell integration script
|
|
178
|
-
await this.injectShellIntegration();
|
|
179
|
-
return {
|
|
180
|
-
type: 'ssh',
|
|
181
|
-
handler: this,
|
|
182
|
-
metadata: {
|
|
183
|
-
hostname: this.config.host,
|
|
184
|
-
username: this.config.username,
|
|
185
|
-
workingDirectory: this.currentWorkingDirectory,
|
|
186
|
-
shell: this.shellType,
|
|
187
|
-
os: this.osType,
|
|
188
|
-
port: this.config.port,
|
|
189
|
-
},
|
|
190
|
-
connectionState: 'connected',
|
|
191
|
-
sessionId: this.sessionId,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
throw new SubshellConnectionError('ssh', error instanceof Error ? error.message : 'Unknown error', true);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Create a network stream/tunnel through this SSH connection
|
|
200
|
-
*/
|
|
201
|
-
async createStream(host, port) {
|
|
202
|
-
if (!this._client || !this._isConnected) {
|
|
203
|
-
throw new Error('Not connected to SSH server');
|
|
204
|
-
}
|
|
205
|
-
return new Promise((resolve, reject) => {
|
|
206
|
-
// Use arbitrary source IP/port (127.0.0.1:0) for the forwardOut call
|
|
207
|
-
this._client.forwardOut('127.0.0.1', 0, host, port, (err, stream) => {
|
|
208
|
-
if (err) {
|
|
209
|
-
reject(err);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
resolve(stream);
|
|
213
|
-
}
|
|
240
|
+
resolve({
|
|
241
|
+
stdout,
|
|
242
|
+
stderr,
|
|
243
|
+
exitCode: code || 0
|
|
214
244
|
});
|
|
245
|
+
});
|
|
246
|
+
stream.on("data", (data) => {
|
|
247
|
+
stdout += data.toString();
|
|
248
|
+
});
|
|
249
|
+
stream.stderr.on("data", (data) => {
|
|
250
|
+
stderr += data.toString();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Read a file from the remote filesystem
|
|
258
|
+
*/
|
|
259
|
+
async readFile(path2) {
|
|
260
|
+
const absolutePath = this.resolveAbsolutePath(path2);
|
|
261
|
+
if (this.sftpClient) {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
this.sftpClient.readFile(absolutePath, "utf8", (err, data) => {
|
|
264
|
+
if (err) {
|
|
265
|
+
reject(new Error(`Failed to read file ${path2}: ${err.message}`));
|
|
266
|
+
} else {
|
|
267
|
+
resolve(data);
|
|
268
|
+
}
|
|
215
269
|
});
|
|
270
|
+
});
|
|
216
271
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (this.sftpClient) {
|
|
224
|
-
this.sftpClient.end();
|
|
225
|
-
this.sftpClient = null;
|
|
226
|
-
}
|
|
227
|
-
if (this._client) {
|
|
228
|
-
this._client.end();
|
|
229
|
-
this._client = null;
|
|
230
|
-
}
|
|
231
|
-
this.config = null;
|
|
272
|
+
const result = await this.executeCommand(`base64 "${absolutePath}" || cat "${absolutePath}" | base64`);
|
|
273
|
+
const base64Out = result.stdout.replace(/\s+/g, "");
|
|
274
|
+
try {
|
|
275
|
+
return Buffer.from(base64Out, "base64").toString("utf8");
|
|
276
|
+
} catch (e) {
|
|
277
|
+
throw new Error(`Failed to decode file ${path2}: ${e.message}`);
|
|
232
278
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
reject(new SubshellExecutionError(command, err.message));
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
let stdout = '';
|
|
252
|
-
let stderr = '';
|
|
253
|
-
stream.on('close', (code) => {
|
|
254
|
-
clearTimeout(timeout);
|
|
255
|
-
// Extract working directory from output using unique tag
|
|
256
|
-
// We use a constructed regex to match the specific tag for this session
|
|
257
|
-
const pwdRegex = new RegExp(`${pwdTag}:(.+)$`, 'm');
|
|
258
|
-
const pwdMatch = stdout.match(pwdRegex);
|
|
259
|
-
if (pwdMatch) {
|
|
260
|
-
this.currentWorkingDirectory = pwdMatch[1].trim();
|
|
261
|
-
// Remove the tag line from stdout
|
|
262
|
-
const removeRegex = new RegExp(`${pwdTag}:.+$`, 'm');
|
|
263
|
-
stdout = stdout.replace(removeRegex, '').trim();
|
|
264
|
-
}
|
|
265
|
-
resolve({
|
|
266
|
-
stdout,
|
|
267
|
-
stderr,
|
|
268
|
-
exitCode: code || 0,
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
stream.on('data', (data) => {
|
|
272
|
-
stdout += data.toString();
|
|
273
|
-
});
|
|
274
|
-
stream.stderr.on('data', (data) => {
|
|
275
|
-
stderr += data.toString();
|
|
276
|
-
});
|
|
277
|
-
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Write a file to the remote filesystem
|
|
282
|
+
*/
|
|
283
|
+
async writeFile(path2, content) {
|
|
284
|
+
const absolutePath = this.resolveAbsolutePath(path2);
|
|
285
|
+
if (this.sftpClient) {
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const options = Buffer.isBuffer(content) ? void 0 : "utf8";
|
|
288
|
+
this.sftpClient.writeFile(absolutePath, content, options, (err) => {
|
|
289
|
+
if (err) {
|
|
290
|
+
reject(new Error(`Failed to write file ${path2}: ${err.message}`));
|
|
291
|
+
} else {
|
|
292
|
+
resolve();
|
|
293
|
+
}
|
|
278
294
|
});
|
|
295
|
+
});
|
|
279
296
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (this.sftpClient) {
|
|
287
|
-
return new Promise((resolve, reject) => {
|
|
288
|
-
this.sftpClient.readFile(absolutePath, 'utf8', (err, data) => {
|
|
289
|
-
if (err) {
|
|
290
|
-
reject(new Error(`Failed to read file ${path}: ${err.message}`));
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
resolve(data);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
// Fallback: use base64 over exec
|
|
299
|
-
const result = await this.executeCommand(`base64 "${absolutePath}" || cat "${absolutePath}" | base64`);
|
|
300
|
-
const base64Out = result.stdout.replace(/\s+/g, '');
|
|
301
|
-
try {
|
|
302
|
-
return Buffer.from(base64Out, 'base64').toString('utf8');
|
|
303
|
-
}
|
|
304
|
-
catch (e) {
|
|
305
|
-
throw new Error(`Failed to decode file ${path}: ${e.message}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Write a file to the remote filesystem
|
|
310
|
-
*/
|
|
311
|
-
async writeFile(path, content) {
|
|
312
|
-
const absolutePath = this.resolveAbsolutePath(path);
|
|
313
|
-
// Prefer SFTP when available
|
|
314
|
-
if (this.sftpClient) {
|
|
315
|
-
return new Promise((resolve, reject) => {
|
|
316
|
-
// ssh2 sftp writeFile supports Buffer or string
|
|
317
|
-
// If buffer, encoding is ignored/not needed
|
|
318
|
-
const options = Buffer.isBuffer(content) ? undefined : 'utf8';
|
|
319
|
-
this.sftpClient.writeFile(absolutePath, content, options, (err) => {
|
|
320
|
-
if (err) {
|
|
321
|
-
reject(new Error(`Failed to write file ${path}: ${err.message}`));
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
resolve();
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
// Fallback: base64 via exec (robust against special chars), chunked
|
|
330
|
-
const inputBuffer = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
|
|
331
|
-
const base64Content = inputBuffer.toString('base64');
|
|
332
|
-
const CHUNK_SIZE = 32000;
|
|
333
|
-
// Truncate file first
|
|
334
|
-
let result = await this.executeCommand(`: > "${absolutePath}"`);
|
|
335
|
-
if (result.exitCode !== 0) {
|
|
336
|
-
throw new Error(`Failed to initialize file ${path}: ${result.stderr || 'unknown error'}`);
|
|
337
|
-
}
|
|
338
|
-
for (let i = 0; i < base64Content.length; i += CHUNK_SIZE) {
|
|
339
|
-
const chunk = base64Content.slice(i, i + CHUNK_SIZE);
|
|
340
|
-
result = await this.executeCommand(`echo "${chunk}" | base64 -d >> "${absolutePath}"`);
|
|
341
|
-
if (result.exitCode !== 0) {
|
|
342
|
-
throw new Error(`Failed to write file chunk for ${path}: ${result.stderr || 'unknown error'}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
297
|
+
const inputBuffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
298
|
+
const base64Content = inputBuffer.toString("base64");
|
|
299
|
+
const CHUNK_SIZE = 32e3;
|
|
300
|
+
let result = await this.executeCommand(`: > "${absolutePath}"`);
|
|
301
|
+
if (result.exitCode !== 0) {
|
|
302
|
+
throw new Error(`Failed to initialize file ${path2}: ${result.stderr || "unknown error"}`);
|
|
345
303
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (this.sftpClient) {
|
|
353
|
-
return new Promise((resolve, reject) => {
|
|
354
|
-
this.sftpClient.readdir(absolutePath, (err, list) => {
|
|
355
|
-
if (err) {
|
|
356
|
-
reject(new Error(`Failed to list directory ${path}: ${err.message}`));
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
const entries = list.map(item => ({
|
|
360
|
-
name: item.filename,
|
|
361
|
-
type: item.attrs.isDirectory() ? 'directory' : 'file',
|
|
362
|
-
size: item.attrs.size,
|
|
363
|
-
permissions: item.attrs.mode?.toString(8),
|
|
364
|
-
}));
|
|
365
|
-
resolve(entries);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
// Fallback: parse ls -la output
|
|
371
|
-
const result = await this.executeCommand(`ls -la "${absolutePath}"`);
|
|
372
|
-
if (result.exitCode !== 0) {
|
|
373
|
-
throw new Error(`Failed to list directory ${path}: ${result.stderr || 'unknown error'}`);
|
|
374
|
-
}
|
|
375
|
-
return this.parseDirectoryListing(result.stdout);
|
|
304
|
+
for (let i = 0; i < base64Content.length; i += CHUNK_SIZE) {
|
|
305
|
+
const chunk = base64Content.slice(i, i + CHUNK_SIZE);
|
|
306
|
+
result = await this.executeCommand(`echo "${chunk}" | base64 -d >> "${absolutePath}"`);
|
|
307
|
+
if (result.exitCode !== 0) {
|
|
308
|
+
throw new Error(`Failed to write file chunk for ${path2}: ${result.stderr || "unknown error"}`);
|
|
309
|
+
}
|
|
376
310
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* List directory contents
|
|
314
|
+
*/
|
|
315
|
+
async listDirectory(path2) {
|
|
316
|
+
const absolutePath = this.resolveAbsolutePath(path2);
|
|
317
|
+
if (this.sftpClient) {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
this.sftpClient.readdir(absolutePath, (err, list) => {
|
|
320
|
+
if (err) {
|
|
321
|
+
reject(new Error(`Failed to list directory ${path2}: ${err.message}`));
|
|
322
|
+
} else {
|
|
323
|
+
const entries = list.map((item) => ({
|
|
324
|
+
name: item.filename,
|
|
325
|
+
type: item.attrs.isDirectory() ? "directory" : "file",
|
|
326
|
+
size: item.attrs.size,
|
|
327
|
+
permissions: item.attrs.mode?.toString(8)
|
|
328
|
+
}));
|
|
329
|
+
resolve(entries);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
398
333
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
async getCurrentWorkingDirectory() {
|
|
403
|
-
const result = await this.executeCommand('pwd');
|
|
404
|
-
return result.stdout.trim();
|
|
334
|
+
const result = await this.executeCommand(`ls -la "${absolutePath}"`);
|
|
335
|
+
if (result.exitCode !== 0) {
|
|
336
|
+
throw new Error(`Failed to list directory ${path2}: ${result.stderr || "unknown error"}`);
|
|
405
337
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
338
|
+
return this.parseDirectoryListing(result.stdout);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Search for files matching a pattern
|
|
342
|
+
*/
|
|
343
|
+
async searchFiles(pattern, directory) {
|
|
344
|
+
const absolutePath = this.resolveAbsolutePath(directory);
|
|
345
|
+
const escapedPattern = pattern.replace(/'/g, "'\\''");
|
|
346
|
+
const command = `grep -rn '${escapedPattern}' "${absolutePath}" 2>/dev/null || true`;
|
|
347
|
+
const result = await this.executeCommand(command);
|
|
348
|
+
const results = [];
|
|
349
|
+
const lines = result.stdout.split("\n").filter((line) => line.trim());
|
|
350
|
+
for (const line of lines) {
|
|
351
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
352
|
+
if (match) {
|
|
353
|
+
results.push({
|
|
354
|
+
file: match[1],
|
|
355
|
+
line: parseInt(match[2], 10),
|
|
356
|
+
content: match[3]
|
|
357
|
+
});
|
|
358
|
+
}
|
|
411
359
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
360
|
+
return results;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get the current working directory
|
|
364
|
+
*/
|
|
365
|
+
async getCurrentWorkingDirectory() {
|
|
366
|
+
const result = await this.executeCommand("pwd");
|
|
367
|
+
return result.stdout.trim();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get the shell type
|
|
371
|
+
*/
|
|
372
|
+
async getShellType() {
|
|
373
|
+
return this.shellType;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get the operating system type
|
|
377
|
+
*/
|
|
378
|
+
async getOSType() {
|
|
379
|
+
return this.osType;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get breadcrumb information for the UI
|
|
383
|
+
*/
|
|
384
|
+
getBreadcrumbs() {
|
|
385
|
+
if (!this.config) {
|
|
386
|
+
return [];
|
|
417
387
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
388
|
+
return [
|
|
389
|
+
{
|
|
390
|
+
label: "ssh",
|
|
391
|
+
color: "cyan"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
label: `${this.config.username}@${this.config.host}`,
|
|
395
|
+
color: "cyan"
|
|
396
|
+
}
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
// Private helper methods
|
|
400
|
+
/**
|
|
401
|
+
* Parse SSH command to extract connection details
|
|
402
|
+
*/
|
|
403
|
+
parseSSHCommand(command) {
|
|
404
|
+
const parts = this.tokenizeSSHCommand(command);
|
|
405
|
+
let host = "";
|
|
406
|
+
let port = 22;
|
|
407
|
+
let username = "";
|
|
408
|
+
let identityFilePath;
|
|
409
|
+
const optionsWithValue = /* @__PURE__ */ new Set([
|
|
410
|
+
"-b",
|
|
411
|
+
"-c",
|
|
412
|
+
"-D",
|
|
413
|
+
"-E",
|
|
414
|
+
"-F",
|
|
415
|
+
"-I",
|
|
416
|
+
"-J",
|
|
417
|
+
"-L",
|
|
418
|
+
"-l",
|
|
419
|
+
"-m",
|
|
420
|
+
"-O",
|
|
421
|
+
"-o",
|
|
422
|
+
"-p",
|
|
423
|
+
"-Q",
|
|
424
|
+
"-R",
|
|
425
|
+
"-S",
|
|
426
|
+
"-W",
|
|
427
|
+
"-w"
|
|
428
|
+
]);
|
|
429
|
+
for (let i = 1; i < parts.length; i++) {
|
|
430
|
+
const part = parts[i];
|
|
431
|
+
if (part === "--") {
|
|
432
|
+
if (!host && i + 1 < parts.length) {
|
|
433
|
+
host = parts[i + 1];
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
if (part === "-p" && i + 1 < parts.length) {
|
|
438
|
+
port = this.parsePort(parts[i + 1], port);
|
|
439
|
+
i++;
|
|
440
|
+
} else if (part.startsWith("-p") && part.length > 2) {
|
|
441
|
+
port = this.parsePort(part.slice(2), port);
|
|
442
|
+
} else if (part === "-l" && i + 1 < parts.length) {
|
|
443
|
+
username = parts[i + 1];
|
|
444
|
+
i++;
|
|
445
|
+
} else if (part.startsWith("-l") && part.length > 2) {
|
|
446
|
+
username = part.slice(2);
|
|
447
|
+
} else if (part === "-i" && i + 1 < parts.length) {
|
|
448
|
+
identityFilePath = parts[i + 1];
|
|
449
|
+
i++;
|
|
450
|
+
} else if (part.startsWith("-i") && part.length > 2) {
|
|
451
|
+
identityFilePath = part.slice(2);
|
|
452
|
+
} else if (part === "-o" && i + 1 < parts.length) {
|
|
453
|
+
const optionToken = parts[i + 1];
|
|
454
|
+
if (optionToken.includes("=")) {
|
|
455
|
+
this.applySSHOption(
|
|
456
|
+
optionToken,
|
|
457
|
+
(value) => {
|
|
458
|
+
port = value;
|
|
459
|
+
},
|
|
460
|
+
(value) => {
|
|
461
|
+
username = value;
|
|
429
462
|
},
|
|
430
|
-
{
|
|
431
|
-
|
|
432
|
-
color: 'cyan',
|
|
463
|
+
(value) => {
|
|
464
|
+
identityFilePath = value;
|
|
433
465
|
},
|
|
434
|
-
|
|
466
|
+
port
|
|
467
|
+
);
|
|
468
|
+
i++;
|
|
469
|
+
} else if (i + 2 < parts.length) {
|
|
470
|
+
this.applySSHOption(
|
|
471
|
+
`${optionToken}=${parts[i + 2]}`,
|
|
472
|
+
(value) => {
|
|
473
|
+
port = value;
|
|
474
|
+
},
|
|
475
|
+
(value) => {
|
|
476
|
+
username = value;
|
|
477
|
+
},
|
|
478
|
+
(value) => {
|
|
479
|
+
identityFilePath = value;
|
|
480
|
+
},
|
|
481
|
+
port
|
|
482
|
+
);
|
|
483
|
+
i += 2;
|
|
484
|
+
} else {
|
|
485
|
+
i++;
|
|
486
|
+
}
|
|
487
|
+
} else if (part.startsWith("-o") && part.length > 2) {
|
|
488
|
+
this.applySSHOption(part.slice(2), (value) => {
|
|
489
|
+
port = value;
|
|
490
|
+
}, (value) => {
|
|
491
|
+
username = value;
|
|
492
|
+
}, (value) => {
|
|
493
|
+
identityFilePath = value;
|
|
494
|
+
}, port);
|
|
495
|
+
} else if (part.startsWith("-") && part !== "-p") {
|
|
496
|
+
if (optionsWithValue.has(part) && i + 1 < parts.length) {
|
|
497
|
+
i++;
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
} else if (!host) {
|
|
501
|
+
if (part.includes("@")) {
|
|
502
|
+
const atIndex = part.indexOf("@");
|
|
503
|
+
const user = part.slice(0, atIndex);
|
|
504
|
+
const hostname = part.slice(atIndex + 1);
|
|
505
|
+
if (user) {
|
|
506
|
+
username = user;
|
|
507
|
+
}
|
|
508
|
+
host = hostname;
|
|
509
|
+
} else {
|
|
510
|
+
host = part;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
435
513
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
'-b', '-c', '-D', '-E', '-F', '-I', '-J', '-L', '-l', '-m', '-O', '-o', '-p', '-Q', '-R', '-S', '-W', '-w'
|
|
450
|
-
]);
|
|
451
|
-
// Parse command line arguments
|
|
452
|
-
for (let i = 1; i < parts.length; i++) {
|
|
453
|
-
const part = parts[i];
|
|
454
|
-
if (part === '--') {
|
|
455
|
-
// End of options; next token is the host
|
|
456
|
-
if (!host && i + 1 < parts.length) {
|
|
457
|
-
host = parts[i + 1];
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
if (part === '-p' && i + 1 < parts.length) {
|
|
462
|
-
port = this.parsePort(parts[i + 1], port);
|
|
463
|
-
i++;
|
|
464
|
-
}
|
|
465
|
-
else if (part.startsWith('-p') && part.length > 2) {
|
|
466
|
-
port = this.parsePort(part.slice(2), port);
|
|
467
|
-
}
|
|
468
|
-
else if (part === '-l' && i + 1 < parts.length) {
|
|
469
|
-
username = parts[i + 1];
|
|
470
|
-
i++;
|
|
471
|
-
}
|
|
472
|
-
else if (part.startsWith('-l') && part.length > 2) {
|
|
473
|
-
username = part.slice(2);
|
|
474
|
-
}
|
|
475
|
-
else if (part === '-i' && i + 1 < parts.length) {
|
|
476
|
-
identityFilePath = parts[i + 1];
|
|
477
|
-
i++;
|
|
478
|
-
}
|
|
479
|
-
else if (part.startsWith('-i') && part.length > 2) {
|
|
480
|
-
identityFilePath = part.slice(2);
|
|
481
|
-
}
|
|
482
|
-
else if (part === '-o' && i + 1 < parts.length) {
|
|
483
|
-
// Support both "-o Key=Value" and "-o Key Value" styles.
|
|
484
|
-
const optionToken = parts[i + 1];
|
|
485
|
-
if (optionToken.includes('=')) {
|
|
486
|
-
this.applySSHOption(optionToken, (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
|
|
487
|
-
i++;
|
|
488
|
-
}
|
|
489
|
-
else if (i + 2 < parts.length) {
|
|
490
|
-
this.applySSHOption(`${optionToken}=${parts[i + 2]}`, (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
|
|
491
|
-
i += 2;
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
i++;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
else if (part.startsWith('-o') && part.length > 2) {
|
|
498
|
-
this.applySSHOption(part.slice(2), (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
|
|
499
|
-
}
|
|
500
|
-
else if (part.startsWith('-') && part !== '-p') {
|
|
501
|
-
// Skip flags and their value, if any
|
|
502
|
-
if (optionsWithValue.has(part) && i + 1 < parts.length) {
|
|
503
|
-
i++;
|
|
504
|
-
}
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
else if (!host) {
|
|
508
|
-
// This should be the host (possibly with username)
|
|
509
|
-
if (part.includes('@')) {
|
|
510
|
-
const atIndex = part.indexOf('@');
|
|
511
|
-
const user = part.slice(0, atIndex);
|
|
512
|
-
const hostname = part.slice(atIndex + 1);
|
|
513
|
-
if (user) {
|
|
514
|
-
username = user;
|
|
515
|
-
}
|
|
516
|
-
host = hostname;
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
host = part;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// Default username to current user if not specified
|
|
524
|
-
if (!username) {
|
|
525
|
-
username = process.env.USER || process.env.USERNAME || 'user';
|
|
526
|
-
}
|
|
527
|
-
if (!host) {
|
|
528
|
-
throw new Error('Could not parse SSH host from command');
|
|
529
|
-
}
|
|
530
|
-
const parsedConfig = {
|
|
531
|
-
host,
|
|
532
|
-
port,
|
|
533
|
-
username,
|
|
534
|
-
};
|
|
535
|
-
if (identityFilePath) {
|
|
536
|
-
parsedConfig.privateKey = this.loadIdentityFile(identityFilePath);
|
|
537
|
-
}
|
|
538
|
-
return parsedConfig;
|
|
514
|
+
if (!username) {
|
|
515
|
+
username = process.env.USER || process.env.USERNAME || "user";
|
|
516
|
+
}
|
|
517
|
+
if (!host) {
|
|
518
|
+
throw new Error("Could not parse SSH host from command");
|
|
519
|
+
}
|
|
520
|
+
const parsedConfig = {
|
|
521
|
+
host,
|
|
522
|
+
port,
|
|
523
|
+
username
|
|
524
|
+
};
|
|
525
|
+
if (identityFilePath) {
|
|
526
|
+
parsedConfig.privateKey = this.loadIdentityFile(identityFilePath);
|
|
539
527
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
528
|
+
return parsedConfig;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Establish SSH connection
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* Establish SSH connection.
|
|
535
|
+
* Handles authentication retries (agent -> keys -> password).
|
|
536
|
+
* For nested connections, uses streamFactory to get a fresh stream for each attempt.
|
|
537
|
+
*/
|
|
538
|
+
async establishConnection(config, stream, streamFactory) {
|
|
539
|
+
let promptedForPassword = false;
|
|
540
|
+
let firstError;
|
|
541
|
+
quickLog(`[SSH Auth] Starting auth for ${config.username}@${config.host}:${config.port}
|
|
542
|
+
`);
|
|
543
|
+
quickLog(`[SSH Auth] Has privateKey: ${!!config.privateKey}, Has password: ${!!config.password}
|
|
544
|
+
`);
|
|
545
|
+
try {
|
|
546
|
+
quickLog(`[SSH Auth] Attempt 1: agent/none (tryKeyboard=false)
|
|
547
|
+
`);
|
|
548
|
+
const currentStream = streamFactory ? await streamFactory() : stream;
|
|
549
|
+
await this.establishConnectionOnce(config, currentStream, false, () => {
|
|
550
|
+
promptedForPassword = true;
|
|
551
|
+
});
|
|
552
|
+
quickLog(`[SSH Auth] Attempt 1 succeeded (agent or none auth)
|
|
553
|
+
`);
|
|
554
|
+
return;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
557
|
+
quickLog(`[SSH Auth] Attempt 1 failed: ${errMsg}, promptedForPassword=${promptedForPassword}
|
|
558
|
+
`);
|
|
559
|
+
firstError = error;
|
|
560
|
+
if (!this.isAuthenticationFailure(error) || promptedForPassword) {
|
|
561
|
+
quickLog(`[SSH Auth] Stopping early: isAuthFailure=${this.isAuthenticationFailure(error)}, prompted=${promptedForPassword}
|
|
562
|
+
`);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!config.privateKey && !config.isNested) {
|
|
567
|
+
const defaultKeys = this.loadDefaultIdentityFiles(config.host);
|
|
568
|
+
quickLog(`[SSH Auth] Found ${defaultKeys.length} default identity file(s)
|
|
569
|
+
`);
|
|
570
|
+
for (let ki = 0; ki < defaultKeys.length; ki++) {
|
|
571
|
+
const key = defaultKeys[ki];
|
|
553
572
|
try {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
catch (error) {
|
|
564
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
565
|
-
quickLog(`[SSH Auth] Attempt 1 failed: ${errMsg}, promptedForPassword=${promptedForPassword}\n`);
|
|
566
|
-
firstError = error;
|
|
567
|
-
// Stop early for non-auth failures or if user was already prompted.
|
|
568
|
-
if (!this.isAuthenticationFailure(error) || promptedForPassword) {
|
|
569
|
-
quickLog(`[SSH Auth] Stopping early: isAuthFailure=${this.isAuthenticationFailure(error)}, prompted=${promptedForPassword}\n`);
|
|
570
|
-
throw error;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// If no explicit key was provided, try default identity files before prompting for password.
|
|
574
|
-
// Skip this for nested sessions to avoid using local keys where remote ones are expected.
|
|
575
|
-
if (!config.privateKey && !config.isNested) {
|
|
576
|
-
const defaultKeys = this.loadDefaultIdentityFiles(config.host);
|
|
577
|
-
quickLog(`[SSH Auth] Found ${defaultKeys.length} default identity file(s)\n`);
|
|
578
|
-
for (let ki = 0; ki < defaultKeys.length; ki++) {
|
|
579
|
-
const key = defaultKeys[ki];
|
|
580
|
-
try {
|
|
581
|
-
quickLog(`[SSH Auth] Trying key file ${ki + 1}/${defaultKeys.length} (${key.length} bytes)\n`);
|
|
582
|
-
// Get fresh stream if factory provided
|
|
583
|
-
const keyStream = streamFactory ? await streamFactory() : stream;
|
|
584
|
-
const keyConfig = {
|
|
585
|
-
...config,
|
|
586
|
-
privateKey: key,
|
|
587
|
-
password: undefined,
|
|
588
|
-
};
|
|
589
|
-
await this.establishConnectionOnce(keyConfig, keyStream, false, () => {
|
|
590
|
-
promptedForPassword = true;
|
|
591
|
-
});
|
|
592
|
-
quickLog(`[SSH Auth] Key file ${ki + 1} succeeded!\n`);
|
|
593
|
-
config.privateKey = key;
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
catch (keyError) {
|
|
597
|
-
const keyErrMsg = keyError instanceof Error ? keyError.message : String(keyError);
|
|
598
|
-
quickLog(`[SSH Auth] Key file ${ki + 1} failed: ${keyErrMsg}\n`);
|
|
599
|
-
if (promptedForPassword) {
|
|
600
|
-
// User already provided a secret once for this connection attempt.
|
|
601
|
-
throw keyError;
|
|
602
|
-
}
|
|
603
|
-
// Continue trying other keys for expected auth/key-format failures.
|
|
604
|
-
if (this.isAuthenticationFailure(keyError) || this.isKeyParseFailure(keyError)) {
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
throw keyError;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
quickLog(`[SSH Auth] Explicit key provided, skipping default key search\n`);
|
|
613
|
-
}
|
|
614
|
-
// Password retry (only if callback exists).
|
|
615
|
-
quickLog(`[SSH Auth] All key-based auth methods exhausted, falling back to password\n`);
|
|
616
|
-
if (!this.onPasswordRequest) {
|
|
617
|
-
throw (firstError instanceof Error ? firstError : new Error('SSH authentication failed'));
|
|
618
|
-
}
|
|
619
|
-
const password = await this.requestPassword(config);
|
|
620
|
-
config.password = password;
|
|
621
|
-
config.privateKey = undefined;
|
|
622
|
-
// Get fresh stream for password attempt
|
|
623
|
-
const passwordStream = streamFactory ? await streamFactory() : stream;
|
|
624
|
-
await this.establishConnectionOnce(config, passwordStream, true, () => {
|
|
573
|
+
quickLog(`[SSH Auth] Trying key file ${ki + 1}/${defaultKeys.length} (${key.length} bytes)
|
|
574
|
+
`);
|
|
575
|
+
const keyStream = streamFactory ? await streamFactory() : stream;
|
|
576
|
+
const keyConfig = {
|
|
577
|
+
...config,
|
|
578
|
+
privateKey: key,
|
|
579
|
+
password: void 0
|
|
580
|
+
};
|
|
581
|
+
await this.establishConnectionOnce(keyConfig, keyStream, false, () => {
|
|
625
582
|
promptedForPassword = true;
|
|
626
|
-
|
|
583
|
+
});
|
|
584
|
+
quickLog(`[SSH Auth] Key file ${ki + 1} succeeded!
|
|
585
|
+
`);
|
|
586
|
+
config.privateKey = key;
|
|
587
|
+
return;
|
|
588
|
+
} catch (keyError) {
|
|
589
|
+
const keyErrMsg = keyError instanceof Error ? keyError.message : String(keyError);
|
|
590
|
+
quickLog(`[SSH Auth] Key file ${ki + 1} failed: ${keyErrMsg}
|
|
591
|
+
`);
|
|
592
|
+
if (promptedForPassword) {
|
|
593
|
+
throw keyError;
|
|
594
|
+
}
|
|
595
|
+
if (this.isAuthenticationFailure(keyError) || this.isKeyParseFailure(keyError)) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
throw keyError;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
quickLog(`[SSH Auth] Explicit key provided, skipping default key search
|
|
603
|
+
`);
|
|
627
604
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
* Set to false for key-based attempts so the server doesn't prompt for password prematurely.
|
|
633
|
-
* Set to true only for the final password-retry attempt.
|
|
634
|
-
*/
|
|
635
|
-
async establishConnectionOnce(config, stream, enableKeyboard, onPasswordPrompt) {
|
|
636
|
-
return new Promise((resolve, reject) => {
|
|
637
|
-
const client = new Client();
|
|
638
|
-
this._client = client;
|
|
639
|
-
let settled = false;
|
|
640
|
-
const settleResolve = () => {
|
|
641
|
-
if (settled) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
settled = true;
|
|
645
|
-
this._isConnected = true;
|
|
646
|
-
resolve();
|
|
647
|
-
};
|
|
648
|
-
const settleReject = (error) => {
|
|
649
|
-
if (settled) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
settled = true;
|
|
653
|
-
this._isConnected = false;
|
|
654
|
-
this._client = null;
|
|
655
|
-
try {
|
|
656
|
-
client.end();
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
// Ignore cleanup errors
|
|
660
|
-
}
|
|
661
|
-
reject(error);
|
|
662
|
-
};
|
|
663
|
-
client.on('ready', () => {
|
|
664
|
-
settleResolve();
|
|
665
|
-
});
|
|
666
|
-
client.on('error', (err) => {
|
|
667
|
-
settleReject(new Error(`ssh connection failed: ${err.message}`));
|
|
668
|
-
});
|
|
669
|
-
client.on('keyboard-interactive', async (_name, _instructions, _lang, prompts, finish) => {
|
|
670
|
-
if (!prompts || prompts.length === 0 || !this.onPasswordRequest) {
|
|
671
|
-
finish([]);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
try {
|
|
675
|
-
const responses = [];
|
|
676
|
-
for (const prompt of prompts) {
|
|
677
|
-
onPasswordPrompt();
|
|
678
|
-
const message = this.buildPasswordPromptMessage(config, prompt.prompt);
|
|
679
|
-
const response = await this.requestPassword(config, message);
|
|
680
|
-
responses.push(response);
|
|
681
|
-
if (!prompt.echo) {
|
|
682
|
-
config.password = response;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
finish(responses);
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
finish([]);
|
|
689
|
-
settleReject(error instanceof Error ? error : new Error('Password input cancelled'));
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
// Listen for unexpected disconnections
|
|
693
|
-
client.on('close', () => {
|
|
694
|
-
if (this._isConnected) {
|
|
695
|
-
// Unexpected disconnect (not initiated by us calling disconnect())
|
|
696
|
-
this._isConnected = false;
|
|
697
|
-
this._client = null;
|
|
698
|
-
if (this.onDisconnectCallback) {
|
|
699
|
-
this.onDisconnectCallback('Connection closed by server');
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
client.on('end', () => {
|
|
704
|
-
if (this._isConnected) {
|
|
705
|
-
// Unexpected disconnect
|
|
706
|
-
this._isConnected = false;
|
|
707
|
-
this._client = null;
|
|
708
|
-
if (this.onDisconnectCallback) {
|
|
709
|
-
this.onDisconnectCallback('Connection ended');
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
client.connect(this.buildConnectConfig(config, stream, enableKeyboard));
|
|
714
|
-
});
|
|
605
|
+
quickLog(`[SSH Auth] All key-based auth methods exhausted, falling back to password
|
|
606
|
+
`);
|
|
607
|
+
if (!this.onPasswordRequest) {
|
|
608
|
+
throw firstError instanceof Error ? firstError : new Error("SSH authentication failed");
|
|
715
609
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// Just try to use it
|
|
748
|
-
connectConfig.agent = winPipe;
|
|
749
|
-
quickLog(`[SSH Auth] Using Windows agent pipe (blind trust)\n`);
|
|
750
|
-
}
|
|
751
|
-
catch (e) {
|
|
752
|
-
// Named pipe not available (agent service not running).
|
|
753
|
-
// Fall through to key file / password auth.
|
|
754
|
-
quickLog(`[SSH Auth] Windows agent pipe error: ${e.message}\n`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
quickLog(`[SSH Auth] No SSH agent available (no SSH_AUTH_SOCK, not Windows)\n`);
|
|
759
|
-
}
|
|
610
|
+
const password = await this.requestPassword(config);
|
|
611
|
+
config.password = password;
|
|
612
|
+
config.privateKey = void 0;
|
|
613
|
+
const passwordStream = streamFactory ? await streamFactory() : stream;
|
|
614
|
+
await this.establishConnectionOnce(config, passwordStream, true, () => {
|
|
615
|
+
promptedForPassword = true;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Single SSH connection attempt.
|
|
620
|
+
* Password retry logic is handled by establishConnection().
|
|
621
|
+
* @param enableKeyboard - Whether to enable keyboard-interactive auth.
|
|
622
|
+
* Set to false for key-based attempts so the server doesn't prompt for password prematurely.
|
|
623
|
+
* Set to true only for the final password-retry attempt.
|
|
624
|
+
*/
|
|
625
|
+
async establishConnectionOnce(config, stream, enableKeyboard, onPasswordPrompt) {
|
|
626
|
+
return new Promise((resolve, reject) => {
|
|
627
|
+
const client = new Client();
|
|
628
|
+
this._client = client;
|
|
629
|
+
let settled = false;
|
|
630
|
+
const settleResolve = () => {
|
|
631
|
+
if (settled) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
settled = true;
|
|
635
|
+
this._isConnected = true;
|
|
636
|
+
resolve();
|
|
637
|
+
};
|
|
638
|
+
const settleReject = (error) => {
|
|
639
|
+
if (settled) {
|
|
640
|
+
return;
|
|
760
641
|
}
|
|
761
|
-
|
|
762
|
-
|
|
642
|
+
settled = true;
|
|
643
|
+
this._isConnected = false;
|
|
644
|
+
this._client = null;
|
|
645
|
+
try {
|
|
646
|
+
client.end();
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
reject(error);
|
|
650
|
+
};
|
|
651
|
+
client.on("ready", () => {
|
|
652
|
+
settleResolve();
|
|
653
|
+
});
|
|
654
|
+
client.on("error", (err) => {
|
|
655
|
+
settleReject(new Error(`ssh connection failed: ${err.message}`));
|
|
656
|
+
});
|
|
657
|
+
client.on("keyboard-interactive", async (_name, _instructions, _lang, prompts, finish) => {
|
|
658
|
+
if (!prompts || prompts.length === 0 || !this.onPasswordRequest) {
|
|
659
|
+
finish([]);
|
|
660
|
+
return;
|
|
763
661
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
662
|
+
try {
|
|
663
|
+
const responses = [];
|
|
664
|
+
for (const prompt of prompts) {
|
|
665
|
+
onPasswordPrompt();
|
|
666
|
+
const message = this.buildPasswordPromptMessage(config, prompt.prompt);
|
|
667
|
+
const response = await this.requestPassword(config, message);
|
|
668
|
+
responses.push(response);
|
|
669
|
+
if (!prompt.echo) {
|
|
670
|
+
config.password = response;
|
|
768
671
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
672
|
+
}
|
|
673
|
+
finish(responses);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
finish([]);
|
|
676
|
+
settleReject(error instanceof Error ? error : new Error("Password input cancelled"));
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
client.on("close", () => {
|
|
680
|
+
if (this._isConnected) {
|
|
681
|
+
this._isConnected = false;
|
|
682
|
+
this._client = null;
|
|
683
|
+
if (this.onDisconnectCallback) {
|
|
684
|
+
this.onDisconnectCallback("Connection closed by server");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
client.on("end", () => {
|
|
689
|
+
if (this._isConnected) {
|
|
690
|
+
this._isConnected = false;
|
|
691
|
+
this._client = null;
|
|
692
|
+
if (this.onDisconnectCallback) {
|
|
693
|
+
this.onDisconnectCallback("Connection ended");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
client.connect(this.buildConnectConfig(config, stream, enableKeyboard));
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Build ssh2 connection config.
|
|
702
|
+
* @param enableKeyboard - When true, enables keyboard-interactive auth.
|
|
703
|
+
* This should only be true for the final password-retry attempt;
|
|
704
|
+
* otherwise the server's keyboard-interactive challenge fires before
|
|
705
|
+
* key-based auth methods (agent, key files) have been tried.
|
|
706
|
+
*/
|
|
707
|
+
buildConnectConfig(config, stream, enableKeyboard = false) {
|
|
708
|
+
const connectConfig = {
|
|
709
|
+
host: config.host,
|
|
710
|
+
port: config.port,
|
|
711
|
+
username: config.username,
|
|
712
|
+
tryKeyboard: enableKeyboard
|
|
713
|
+
};
|
|
714
|
+
if (stream) {
|
|
715
|
+
connectConfig.sock = stream;
|
|
774
716
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
717
|
+
if (!config.isNested) {
|
|
718
|
+
if (process.env.SSH_AUTH_SOCK) {
|
|
719
|
+
connectConfig.agent = process.env.SSH_AUTH_SOCK;
|
|
720
|
+
quickLog(`[SSH Auth] Using SSH_AUTH_SOCK agent: ${process.env.SSH_AUTH_SOCK}
|
|
721
|
+
`);
|
|
722
|
+
} else if (os.platform() === "win32") {
|
|
723
|
+
const winPipe = ["", "", ".", "pipe", "openssh-ssh-agent"].join("\\");
|
|
724
|
+
quickLog(`[SSH Auth] Windows detected, checking agent pipe: ${winPipe}
|
|
725
|
+
`);
|
|
779
726
|
try {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
727
|
+
connectConfig.agent = winPipe;
|
|
728
|
+
quickLog(`[SSH Auth] Using Windows agent pipe (blind trust)
|
|
729
|
+
`);
|
|
730
|
+
} catch (e) {
|
|
731
|
+
quickLog(`[SSH Auth] Windows agent pipe error: ${e.message}
|
|
732
|
+
`);
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
quickLog(`[SSH Auth] No SSH agent available (no SSH_AUTH_SOCK, not Windows)
|
|
736
|
+
`);
|
|
737
|
+
}
|
|
785
738
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (trimmedPrompt) {
|
|
789
|
-
return trimmedPrompt;
|
|
790
|
-
}
|
|
791
|
-
return `Password for ${config.username}@${config.host}:`;
|
|
739
|
+
if (config.password) {
|
|
740
|
+
connectConfig.password = config.password;
|
|
792
741
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
742
|
+
if (config.privateKey) {
|
|
743
|
+
connectConfig.privateKey = config.privateKey;
|
|
744
|
+
if (config.passphrase) {
|
|
745
|
+
connectConfig.passphrase = config.passphrase;
|
|
746
|
+
}
|
|
798
747
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
748
|
+
connectConfig.keepaliveInterval = 1e4;
|
|
749
|
+
connectConfig.keepaliveCountMax = 3;
|
|
750
|
+
return connectConfig;
|
|
751
|
+
}
|
|
752
|
+
async requestPassword(config, promptMessage) {
|
|
753
|
+
if (!this.onPasswordRequest) {
|
|
754
|
+
throw new Error("Password authentication required but no password prompt handler is configured");
|
|
805
755
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
756
|
+
try {
|
|
757
|
+
return await this.onPasswordRequest(
|
|
758
|
+
promptMessage || `Password for ${config.username}@${config.host}:`
|
|
759
|
+
);
|
|
760
|
+
} catch {
|
|
761
|
+
throw new Error("Password input cancelled");
|
|
809
762
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const equalsIndex = trimmed.indexOf('=');
|
|
816
|
-
const key = (equalsIndex >= 0 ? trimmed.slice(0, equalsIndex) : trimmed).trim().toLowerCase();
|
|
817
|
-
const value = (equalsIndex >= 0 ? trimmed.slice(equalsIndex + 1) : '').trim();
|
|
818
|
-
if (!value) {
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
if (key === 'identityfile') {
|
|
822
|
-
setIdentityFile(value);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
if (key === 'user') {
|
|
826
|
-
setUsername(value);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
if (key === 'port') {
|
|
830
|
-
setPort(this.parsePort(value, fallbackPort));
|
|
831
|
-
}
|
|
763
|
+
}
|
|
764
|
+
buildPasswordPromptMessage(config, rawPrompt) {
|
|
765
|
+
const trimmedPrompt = rawPrompt?.trim();
|
|
766
|
+
if (trimmedPrompt) {
|
|
767
|
+
return trimmedPrompt;
|
|
832
768
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
769
|
+
return `Password for ${config.username}@${config.host}:`;
|
|
770
|
+
}
|
|
771
|
+
isAuthenticationFailure(error) {
|
|
772
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
773
|
+
return message.includes("all configured authentication methods failed") || message.includes("authentication failed") || message.includes("permission denied");
|
|
774
|
+
}
|
|
775
|
+
isKeyParseFailure(error) {
|
|
776
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
777
|
+
return message.includes("cannot parse privatekey") || message.includes("bad passphrase") || message.includes("no passphrase given") || message.includes("invalid private key");
|
|
778
|
+
}
|
|
779
|
+
parsePort(value, fallbackPort) {
|
|
780
|
+
const parsed = Number.parseInt(value, 10);
|
|
781
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackPort;
|
|
782
|
+
}
|
|
783
|
+
applySSHOption(option, setPort, setUsername, setIdentityFile, fallbackPort) {
|
|
784
|
+
const trimmed = option.trim();
|
|
785
|
+
if (!trimmed) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const equalsIndex = trimmed.indexOf("=");
|
|
789
|
+
const key = (equalsIndex >= 0 ? trimmed.slice(0, equalsIndex) : trimmed).trim().toLowerCase();
|
|
790
|
+
const value = (equalsIndex >= 0 ? trimmed.slice(equalsIndex + 1) : "").trim();
|
|
791
|
+
if (!value) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (key === "identityfile") {
|
|
795
|
+
setIdentityFile(value);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (key === "user") {
|
|
799
|
+
setUsername(value);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (key === "port") {
|
|
803
|
+
setPort(this.parsePort(value, fallbackPort));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
tokenizeSSHCommand(command) {
|
|
807
|
+
const tokens = [];
|
|
808
|
+
let current = "";
|
|
809
|
+
let quote = null;
|
|
810
|
+
let escaped = false;
|
|
811
|
+
for (const char of command.trim()) {
|
|
812
|
+
if (escaped) {
|
|
813
|
+
current += char;
|
|
814
|
+
escaped = false;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (char === "\\" && quote !== "'") {
|
|
818
|
+
escaped = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (quote) {
|
|
822
|
+
if (char === quote) {
|
|
823
|
+
quote = null;
|
|
824
|
+
} else {
|
|
825
|
+
current += char;
|
|
826
|
+
}
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (char === '"' || char === "'") {
|
|
830
|
+
quote = char;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (/\s/.test(char)) {
|
|
870
834
|
if (current) {
|
|
871
|
-
|
|
835
|
+
tokens.push(current);
|
|
836
|
+
current = "";
|
|
872
837
|
}
|
|
873
|
-
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
current += char;
|
|
874
841
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
try {
|
|
878
|
-
return fs.readFileSync(resolvedPath);
|
|
879
|
-
}
|
|
880
|
-
catch (error) {
|
|
881
|
-
if (error?.code === 'ENOENT') {
|
|
882
|
-
throw new Error(`SSH identity file not found: ${resolvedPath}`);
|
|
883
|
-
}
|
|
884
|
-
throw new Error(`Failed to read SSH identity file "${resolvedPath}": ${error?.message || 'Unknown error'}`);
|
|
885
|
-
}
|
|
842
|
+
if (current) {
|
|
843
|
+
tokens.push(current);
|
|
886
844
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
|
|
845
|
+
return tokens;
|
|
846
|
+
}
|
|
847
|
+
loadIdentityFile(identityPath) {
|
|
848
|
+
const resolvedPath = this.resolveIdentityPath(identityPath);
|
|
849
|
+
try {
|
|
850
|
+
return fs.readFileSync(resolvedPath);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
if (error?.code === "ENOENT") {
|
|
853
|
+
throw new Error(`SSH identity file not found: ${resolvedPath}`);
|
|
854
|
+
}
|
|
855
|
+
throw new Error(`Failed to read SSH identity file "${resolvedPath}": ${error?.message || "Unknown error"}`);
|
|
897
856
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
857
|
+
}
|
|
858
|
+
resolveIdentityPath(identityPath) {
|
|
859
|
+
const trimmed = identityPath.trim();
|
|
860
|
+
if (!trimmed) {
|
|
861
|
+
return trimmed;
|
|
862
|
+
}
|
|
863
|
+
if (trimmed.startsWith("~")) {
|
|
864
|
+
const homeDir = os.homedir();
|
|
865
|
+
return path.resolve(homeDir, trimmed.slice(1));
|
|
866
|
+
}
|
|
867
|
+
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
868
|
+
}
|
|
869
|
+
loadDefaultIdentityFiles(host) {
|
|
870
|
+
const homeDir = os.homedir();
|
|
871
|
+
const candidatePaths = [
|
|
872
|
+
path.join(homeDir, ".ssh", "id_ed25519"),
|
|
873
|
+
path.join(homeDir, ".ssh", "id_rsa"),
|
|
874
|
+
path.join(homeDir, ".ssh", "id_ecdsa"),
|
|
875
|
+
path.join(homeDir, ".ssh", "id_dsa")
|
|
876
|
+
];
|
|
877
|
+
if (host) {
|
|
878
|
+
const configKeys = this.getIdentityFilesFromConfig(host);
|
|
879
|
+
quickLog(`[SSH Config] Found ${configKeys.length} keys in config for host ${host}
|
|
880
|
+
`);
|
|
881
|
+
for (const keyPath of configKeys) {
|
|
882
|
+
let resolvedKeyPath = keyPath;
|
|
883
|
+
if (!path.isAbsolute(keyPath)) {
|
|
884
|
+
if (keyPath.startsWith("~")) {
|
|
885
|
+
resolvedKeyPath = keyPath.replace(/^~/, homeDir);
|
|
886
|
+
} else {
|
|
887
|
+
resolvedKeyPath = path.join(homeDir, ".ssh", keyPath);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (!candidatePaths.includes(resolvedKeyPath)) {
|
|
891
|
+
candidatePaths.unshift(resolvedKeyPath);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const keys = [];
|
|
896
|
+
for (const candidate of candidatePaths) {
|
|
897
|
+
try {
|
|
898
|
+
if (fs.existsSync(candidate)) {
|
|
899
|
+
quickLog(`[SSH Auth] Loading key from: ${candidate}
|
|
900
|
+
`);
|
|
901
|
+
keys.push(fs.readFileSync(candidate));
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return keys;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Parse ~/.ssh/config to find IdentityFile entries for the given host.
|
|
910
|
+
* Supports standard SSH config patterns including wildcards (*, ?) and quoted paths.
|
|
911
|
+
*/
|
|
912
|
+
getIdentityFilesFromConfig(targetHost) {
|
|
913
|
+
const homeDir = os.homedir();
|
|
914
|
+
const configPath = path.join(homeDir, ".ssh", "config");
|
|
915
|
+
const identityFiles = [];
|
|
916
|
+
if (!fs.existsSync(configPath)) {
|
|
917
|
+
return identityFiles;
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
921
|
+
const lines = content.split("\n");
|
|
922
|
+
let inMatchingHost = false;
|
|
923
|
+
for (const line of lines) {
|
|
924
|
+
const trimmed = line.trim();
|
|
925
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
926
|
+
const parts = trimmed.split(/\s+/);
|
|
927
|
+
const key = parts[0].toLowerCase();
|
|
928
|
+
if (key === "host") {
|
|
929
|
+
const patterns = parts.slice(1);
|
|
930
|
+
inMatchingHost = patterns.some((pattern) => {
|
|
929
931
|
try {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
932
|
+
const regex = this.convertGlobToRegex(pattern);
|
|
933
|
+
return regex.test(targetHost);
|
|
934
|
+
} catch {
|
|
935
|
+
return false;
|
|
934
936
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
+
});
|
|
938
|
+
} else if (inMatchingHost && key === "identityfile") {
|
|
939
|
+
const keyMatch = trimmed.match(/^\S+/);
|
|
940
|
+
if (keyMatch) {
|
|
941
|
+
let fileValue = trimmed.substring(keyMatch[0].length).trim();
|
|
942
|
+
if (fileValue.startsWith('"') && fileValue.endsWith('"') || fileValue.startsWith("'") && fileValue.endsWith("'")) {
|
|
943
|
+
fileValue = fileValue.slice(1, -1);
|
|
937
944
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Parse ~/.ssh/config to find IdentityFile entries for the given host.
|
|
943
|
-
* Supports standard SSH config patterns including wildcards (*, ?) and quoted paths.
|
|
944
|
-
*/
|
|
945
|
-
getIdentityFilesFromConfig(targetHost) {
|
|
946
|
-
const homeDir = os.homedir();
|
|
947
|
-
const configPath = path.join(homeDir, '.ssh', 'config');
|
|
948
|
-
const identityFiles = [];
|
|
949
|
-
if (!fs.existsSync(configPath)) {
|
|
950
|
-
return identityFiles;
|
|
951
|
-
}
|
|
952
|
-
try {
|
|
953
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
954
|
-
const lines = content.split('\n');
|
|
955
|
-
let inMatchingHost = false;
|
|
956
|
-
for (const line of lines) {
|
|
957
|
-
const trimmed = line.trim();
|
|
958
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
959
|
-
continue;
|
|
960
|
-
// Split by whitespace to get key and arguments
|
|
961
|
-
const parts = trimmed.split(/\s+/);
|
|
962
|
-
const key = parts[0].toLowerCase();
|
|
963
|
-
if (key === 'host') {
|
|
964
|
-
// Check if this Host block matches our target
|
|
965
|
-
// Patterns can be separated by whitespace
|
|
966
|
-
const patterns = parts.slice(1);
|
|
967
|
-
inMatchingHost = patterns.some(pattern => {
|
|
968
|
-
// Convert SSH glob pattern to regex
|
|
969
|
-
try {
|
|
970
|
-
const regex = this.convertGlobToRegex(pattern);
|
|
971
|
-
return regex.test(targetHost);
|
|
972
|
-
}
|
|
973
|
-
catch {
|
|
974
|
-
return false;
|
|
975
|
-
}
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
else if (inMatchingHost && key === 'identityfile') {
|
|
979
|
-
// Found an IdentityFile for a matching host.
|
|
980
|
-
// Extract the value robustly (handling potential quotes and spaces)
|
|
981
|
-
// Find where the key ends and value begins in the original line
|
|
982
|
-
const keyMatch = trimmed.match(/^\S+/);
|
|
983
|
-
if (keyMatch) {
|
|
984
|
-
let fileValue = trimmed.substring(keyMatch[0].length).trim();
|
|
985
|
-
// Remove surrounding quotes if present
|
|
986
|
-
if ((fileValue.startsWith('"') && fileValue.endsWith('"')) ||
|
|
987
|
-
(fileValue.startsWith("'") && fileValue.endsWith("'"))) {
|
|
988
|
-
fileValue = fileValue.slice(1, -1);
|
|
989
|
-
}
|
|
990
|
-
if (fileValue) {
|
|
991
|
-
identityFiles.push(fileValue);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
945
|
+
if (fileValue) {
|
|
946
|
+
identityFiles.push(fileValue);
|
|
995
947
|
}
|
|
948
|
+
}
|
|
996
949
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
950
|
+
}
|
|
951
|
+
} catch (e) {
|
|
952
|
+
quickLog(`[SSH Config] Failed to parse config file: ${e}
|
|
953
|
+
`);
|
|
1001
954
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1020
|
-
return new Promise((resolve, reject) => {
|
|
1021
|
-
this._client.sftp((err, sftp) => {
|
|
1022
|
-
if (err) {
|
|
1023
|
-
reject(err);
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
this.sftpClient = sftp;
|
|
1027
|
-
resolve();
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
});
|
|
955
|
+
return identityFiles;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Convert standard SSH config glob pattern to RegExp.
|
|
959
|
+
* Supports * (wildcard) and ? (single char).
|
|
960
|
+
*/
|
|
961
|
+
convertGlobToRegex(pattern) {
|
|
962
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
963
|
+
const regexString = "^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
964
|
+
return new RegExp(regexString);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Initialize SFTP session
|
|
968
|
+
*/
|
|
969
|
+
async initializeSFTP() {
|
|
970
|
+
if (!this._client) {
|
|
971
|
+
throw new Error("SSH client not connected");
|
|
1031
972
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
973
|
+
return new Promise((resolve, reject) => {
|
|
974
|
+
this._client.sftp((err, sftp) => {
|
|
975
|
+
if (err) {
|
|
976
|
+
reject(err);
|
|
977
|
+
} else {
|
|
978
|
+
this.sftpClient = sftp;
|
|
979
|
+
resolve();
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Detect the shell type on the remote system
|
|
986
|
+
*/
|
|
987
|
+
async detectShellType() {
|
|
988
|
+
try {
|
|
989
|
+
const result = await this.executeCommand("echo $SHELL");
|
|
990
|
+
const shellPath = result.stdout.trim();
|
|
991
|
+
if (shellPath.includes("bash")) return "bash";
|
|
992
|
+
if (shellPath.includes("zsh")) return "zsh";
|
|
993
|
+
if (shellPath.includes("fish")) return "fish";
|
|
994
|
+
return "bash";
|
|
995
|
+
} catch {
|
|
996
|
+
return "bash";
|
|
1050
997
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
catch {
|
|
1065
|
-
return 'linux';
|
|
1066
|
-
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Detect the operating system type
|
|
1001
|
+
*/
|
|
1002
|
+
async detectOSType() {
|
|
1003
|
+
try {
|
|
1004
|
+
const result = await this.executeCommand("uname -s");
|
|
1005
|
+
const osName = result.stdout.trim().toLowerCase();
|
|
1006
|
+
if (osName.includes("darwin")) return "macos";
|
|
1007
|
+
if (osName.includes("linux")) return "linux";
|
|
1008
|
+
return "linux";
|
|
1009
|
+
} catch {
|
|
1010
|
+
return "linux";
|
|
1067
1011
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
console.warn('Failed to inject shell integration:', error);
|
|
1079
|
-
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Inject shell integration script
|
|
1015
|
+
*/
|
|
1016
|
+
async injectShellIntegration() {
|
|
1017
|
+
try {
|
|
1018
|
+
const script = this.getShellIntegrationScript();
|
|
1019
|
+
await this.executeCommand(script);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.warn("Failed to inject shell integration:", error);
|
|
1080
1022
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
export
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Get shell integration script based on shell type
|
|
1026
|
+
*/
|
|
1027
|
+
getShellIntegrationScript() {
|
|
1028
|
+
const sessionId = this.sessionId;
|
|
1029
|
+
if (this.shellType === "bash") {
|
|
1030
|
+
return `
|
|
1031
|
+
export CENTAURUS_SUBSHELL=1
|
|
1032
|
+
export CENTAURUS_SESSION_ID="${sessionId}"
|
|
1033
|
+
_centaurus_pwd_hook() {
|
|
1034
|
+
echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
|
|
1035
|
+
}
|
|
1036
|
+
export PROMPT_COMMAND="_centaurus_pwd_hook; \${PROMPT_COMMAND}"
|
|
1094
1037
|
`.trim();
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
export
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
precmd_functions+=(_centaurus_pwd_hook)
|
|
1038
|
+
} else if (this.shellType === "zsh") {
|
|
1039
|
+
return `
|
|
1040
|
+
export CENTAURUS_SUBSHELL=1
|
|
1041
|
+
export CENTAURUS_SESSION_ID="${sessionId}"
|
|
1042
|
+
_centaurus_pwd_hook() {
|
|
1043
|
+
echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
|
|
1044
|
+
}
|
|
1045
|
+
precmd_functions+=(_centaurus_pwd_hook)
|
|
1104
1046
|
`.trim();
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
set -x
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
end
|
|
1047
|
+
} else if (this.shellType === "fish") {
|
|
1048
|
+
return `
|
|
1049
|
+
set -x CENTAURUS_SUBSHELL 1
|
|
1050
|
+
set -x CENTAURUS_SESSION_ID "${sessionId}"
|
|
1051
|
+
function _centaurus_pwd_hook --on-event fish_prompt
|
|
1052
|
+
echo "__CENTAURUS_PWD_${sessionId}__:"(pwd)
|
|
1053
|
+
end
|
|
1113
1054
|
`.trim();
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
export
|
|
1118
|
-
export CENTAURUS_SESSION_ID="${sessionId}"
|
|
1055
|
+
}
|
|
1056
|
+
return `
|
|
1057
|
+
export CENTAURUS_SUBSHELL=1
|
|
1058
|
+
export CENTAURUS_SESSION_ID="${sessionId}"
|
|
1119
1059
|
`.trim();
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Resolve a path to an absolute path
|
|
1063
|
+
*/
|
|
1064
|
+
resolveAbsolutePath(path2) {
|
|
1065
|
+
if (path2.startsWith("/")) {
|
|
1066
|
+
return path2;
|
|
1120
1067
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
*/
|
|
1124
|
-
resolveAbsolutePath(path) {
|
|
1125
|
-
if (path.startsWith('/')) {
|
|
1126
|
-
return path;
|
|
1127
|
-
}
|
|
1128
|
-
if (path.startsWith('~')) {
|
|
1129
|
-
return path;
|
|
1130
|
-
}
|
|
1131
|
-
return `${this.currentWorkingDirectory}/${path}`;
|
|
1068
|
+
if (path2.startsWith("~")) {
|
|
1069
|
+
return path2;
|
|
1132
1070
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
return entries;
|
|
1071
|
+
return `${this.currentWorkingDirectory}/${path2}`;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Parse directory listing output (ls -la)
|
|
1075
|
+
*/
|
|
1076
|
+
parseDirectoryListing(output) {
|
|
1077
|
+
const entries = [];
|
|
1078
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
1079
|
+
for (const line of lines) {
|
|
1080
|
+
if (line.startsWith("total") || line.endsWith(" .") || line.endsWith(" ..")) continue;
|
|
1081
|
+
const match = line.match(/^([drwx-]+)\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\S+\s+\S+\s+\S+\s+(.+)$/);
|
|
1082
|
+
if (match) {
|
|
1083
|
+
const permissions = match[1];
|
|
1084
|
+
const size = parseInt(match[2], 10);
|
|
1085
|
+
const name = match[3];
|
|
1086
|
+
entries.push({
|
|
1087
|
+
name,
|
|
1088
|
+
type: permissions.startsWith("d") ? "directory" : "file",
|
|
1089
|
+
size,
|
|
1090
|
+
permissions
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1157
1093
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1094
|
+
return entries;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Create a new instance of this handler
|
|
1098
|
+
*/
|
|
1099
|
+
createNew() {
|
|
1100
|
+
const newHandler = new SSHHandler();
|
|
1101
|
+
if (this.onPasswordRequest) {
|
|
1102
|
+
newHandler.setPasswordRequestCallback(this.onPasswordRequest);
|
|
1103
|
+
}
|
|
1104
|
+
if (this.onDisconnectCallback) {
|
|
1105
|
+
newHandler.setDisconnectCallback(this.onDisconnectCallback);
|
|
1170
1106
|
}
|
|
1107
|
+
return newHandler;
|
|
1108
|
+
}
|
|
1171
1109
|
}
|
|
1110
|
+
export {
|
|
1111
|
+
SSHHandler
|
|
1112
|
+
};
|
|
1172
1113
|
//# sourceMappingURL=ssh-handler.js.map
|