From f8056b03b6d4fad56642b2e4774dc12200befee8 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:42:45 -0400 Subject: [PATCH] Add VS Code Remote SSH integration --- README.md | 25 +++++ vscode-extension/README.md | 15 +++ vscode-extension/extension.js | 170 ++++++++++++++++++++++++++++++++++ vscode-extension/package.json | 69 ++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 vscode-extension/README.md create mode 100644 vscode-extension/extension.js create mode 100644 vscode-extension/package.json diff --git a/README.md b/README.md index d51c94c..0fb9f5e 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ dosh status HOST # show remote tmux sessions and Dosh service status dosh restart HOST # restart dosh-server and show service status dosh doctor HOST # check config and connectivity dosh recover HOST # clear cached attach state and re-check +dosh proxy-stdio HOST 127.0.0.1 22 +dosh vscode HOST [PATH] # configure/open VS Code Remote-SSH through Dosh ``` Examples: @@ -72,11 +74,34 @@ dosh cat server:tmp/file.txt dosh forward server -L 8080:127.0.0.1:80 dosh forward server -D 1080 dosh forward server -R 2222:127.0.0.1:22 +dosh proxy-stdio server 127.0.0.1 22 +dosh vscode server /home/me/project ``` Agent forwarding is opt-in with `-A` and must be enabled on the server. File copy is enabled by `allow_file_transfer = true` on the server. +## VS Code + +Dosh can carry VS Code Remote-SSH without replacing VS Code's SSH workflow. + +```sh +dosh vscode setup server +dosh vscode server /home/me/project +``` + +This writes a managed SSH config entry using: + +```sshconfig +ProxyCommand dosh proxy-stdio server %h %p +``` + +VS Code still uses Remote-SSH and the normal remote VS Code Server. Dosh carries +the SSH byte stream over its encrypted reconnecting transport. + +The optional extension in `vscode-extension/` provides the same setup from the +Command Palette. + ## Library Dosh can also be used as a Rust transport for application protocols that need diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..8ba095f --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,15 @@ +# Dosh Remote + +Open VS Code Remote-SSH windows through Dosh. + +The extension writes a managed SSH config entry like: + +```sshconfig +Host dosh-palav + HostName 127.0.0.1 + HostKeyAlias palav + ProxyCommand dosh proxy-stdio palav %h %p +``` + +VS Code still uses Remote-SSH, so server install, extensions, terminals, and +normal SSH behavior stay intact. Dosh carries the SSH TCP byte stream. diff --git a/vscode-extension/extension.js b/vscode-extension/extension.js new file mode 100644 index 0000000..bfaf4ea --- /dev/null +++ b/vscode-extension/extension.js @@ -0,0 +1,170 @@ +const vscode = require('vscode'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function activate(context) { + context.subscriptions.push( + vscode.commands.registerCommand('dosh.openRemote', openRemote), + vscode.commands.registerCommand('dosh.configureHost', configureHostCommand), + vscode.commands.registerCommand('dosh.showSshConfig', showSshConfig) + ); +} + +async function openRemote() { + const configured = await configureHost(); + if (!configured) { + return; + } + const remotePath = await vscode.window.showInputBox({ + title: 'Remote path', + prompt: 'Path to open on the remote host', + value: '~' + }); + if (remotePath === undefined) { + return; + } + const suffix = remotePath ? `/${remotePath.replace(/^\/+/, '')}` : ''; + await vscode.commands.executeCommand( + 'vscode.openFolder', + vscode.Uri.parse(`vscode-remote://ssh-remote+${configured.alias}${suffix}`), + { forceNewWindow: true } + ); +} + +async function configureHostCommand() { + const configured = await configureHost(); + if (configured) { + vscode.window.showInformationMessage(`Dosh Remote-SSH host ready: ${configured.alias}`); + } +} + +async function configureHost() { + const host = await vscode.window.showInputBox({ + title: 'Dosh host', + prompt: 'Dosh host alias, e.g. palav', + ignoreFocusOut: true + }); + if (!host) { + return undefined; + } + const user = await vscode.window.showInputBox({ + title: 'Remote SSH user', + prompt: 'Optional. Leave blank to let SSH config decide.', + ignoreFocusOut: true + }); + const config = vscode.workspace.getConfiguration('dosh'); + const alias = `dosh-${safeAlias(host)}`; + const block = sshBlock({ + alias, + host, + user: user || undefined, + executable: config.get('executable') || 'dosh', + targetHost: config.get('targetHost') || '127.0.0.1', + targetPort: Number(config.get('targetPort') || 22), + doshPort: Number(config.get('doshPort') || 0) + }); + const sshDir = path.join(os.homedir(), '.ssh'); + fs.mkdirSync(sshDir, { recursive: true }); + const generatedPath = config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh'); + const mainConfig = path.join(sshDir, 'config'); + ensureInclude(mainConfig, path.basename(generatedPath)); + upsertBlock(generatedPath, alias, block); + return { alias, generatedPath }; +} + +function sshBlock(options) { + let proxy = shellQuote(options.executable); + if (options.doshPort > 0) { + proxy += ` --dosh-port ${options.doshPort}`; + } + proxy += ` proxy-stdio ${shellQuote(options.host)} %h %p`; + const lines = [ + `# BEGIN DOSH ${options.alias}`, + `Host ${options.alias}`, + ` HostName ${options.targetHost}`, + ` Port ${options.targetPort}`, + ` HostKeyAlias ${options.host}`, + options.user ? ` User ${options.user}` : undefined, + ' ClearAllForwardings yes', + ' ServerAliveInterval 15', + ' ServerAliveCountMax 3', + ` ProxyCommand ${proxy}`, + `# END DOSH ${options.alias}`, + '' + ].filter(Boolean); + return lines.join('\n'); +} + +function ensureInclude(configPath, includeFile) { + const raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''; + if (raw.split(/\r?\n/).some((line) => line.trim().toLowerCase() === `include ${includeFile}`.toLowerCase())) { + return; + } + fs.writeFileSync(configPath, `Include ${includeFile}\n${raw ? `\n${raw}` : ''}`); +} + +function upsertBlock(configPath, alias, block) { + const raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''; + const begin = `# BEGIN DOSH ${alias}`; + const end = `# END DOSH ${alias}`; + const lines = raw.split(/\r?\n/); + const out = []; + let skipping = false; + let replaced = false; + for (const line of lines) { + if (line.trim() === begin) { + out.push(block.trimEnd()); + skipping = true; + replaced = true; + continue; + } + if (skipping) { + if (line.trim() === end) { + skipping = false; + } + continue; + } + if (line.length > 0 || out.length > 0) { + out.push(line); + } + } + if (!replaced) { + if (out.length > 0 && out[out.length - 1] !== '') { + out.push(''); + } + out.push(block.trimEnd()); + } + fs.writeFileSync(configPath, `${out.join('\n').trimEnd()}\n`); +} + +async function showSshConfig() { + const config = vscode.workspace.getConfiguration('dosh'); + const sshDir = path.join(os.homedir(), '.ssh'); + const generatedPath = config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh'); + if (!fs.existsSync(generatedPath)) { + vscode.window.showWarningMessage('No generated Dosh SSH config yet.'); + return; + } + const doc = await vscode.workspace.openTextDocument(generatedPath); + await vscode.window.showTextDocument(doc); +} + +function safeAlias(value) { + const alias = value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''); + return alias || 'host'; +} + +function shellQuote(value) { + if (/^[a-zA-Z0-9_./:-]+$/.test(value)) { + return value; + } + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function deactivate() {} + +module.exports = { + activate, + deactivate +}; diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..1f69ca6 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,69 @@ +{ + "name": "dosh-vscode", + "displayName": "Dosh Remote", + "description": "Open VS Code Remote-SSH windows through Dosh.", + "version": "0.1.0", + "publisher": "palav", + "license": "MIT", + "engines": { + "vscode": "^1.90.0" + }, + "categories": [ + "Other" + ], + "extensionDependencies": [ + "ms-vscode-remote.remote-ssh" + ], + "activationEvents": [ + "onCommand:dosh.openRemote", + "onCommand:dosh.configureHost", + "onCommand:dosh.showSshConfig" + ], + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "dosh.openRemote", + "title": "Dosh: Open Remote Folder" + }, + { + "command": "dosh.configureHost", + "title": "Dosh: Configure Remote-SSH Host" + }, + { + "command": "dosh.showSshConfig", + "title": "Dosh: Show Generated SSH Config" + } + ], + "configuration": { + "title": "Dosh Remote", + "properties": { + "dosh.executable": { + "type": "string", + "default": "dosh", + "description": "Path to the local dosh executable." + }, + "dosh.targetHost": { + "type": "string", + "default": "127.0.0.1", + "description": "Host opened from the Dosh server side for VS Code's SSH connection." + }, + "dosh.targetPort": { + "type": "number", + "default": 22, + "description": "SSH port opened from the Dosh server side." + }, + "dosh.doshPort": { + "type": "number", + "default": 0, + "description": "Optional Dosh UDP port override. 0 uses Dosh config." + }, + "dosh.generatedSshConfig": { + "type": "string", + "default": "", + "description": "Path for generated SSH config. Empty means ~/.ssh/config.dosh." + } + } + } + } +}