const fs = require('fs'); const os = require('os'); const path = require('path'); let vscode; try { vscode = require('vscode'); } catch (_) { vscode = undefined; } function requireVscode() { if (!vscode) { throw new Error('VS Code API is only available inside VS Code'); } return vscode; } function activate(context) { const api = requireVscode(); context.subscriptions.push( api.commands.registerCommand('dosh.openRemote', openRemote), api.commands.registerCommand('dosh.configureHost', configureHostCommand), api.commands.registerCommand('dosh.showSshConfig', showSshConfig) ); } async function openRemote() { const api = requireVscode(); const configured = await configureHost(); if (!configured) { return; } const remotePath = await api.window.showInputBox({ title: 'Remote path', prompt: 'Path to open on the remote host', value: '~' }); if (remotePath === undefined) { return; } const suffix = remotePath ? `/${remotePath.replace(/^\/+/, '')}` : ''; await api.commands.executeCommand( 'vscode.openFolder', api.Uri.parse(`vscode-remote://ssh-remote+${configured.alias}${suffix}`), { forceNewWindow: true } ); } async function configureHostCommand() { const api = requireVscode(); const configured = await configureHost(); if (configured) { api.window.showInformationMessage(`Dosh Remote-SSH host ready: ${configured.alias}`); } } async function configureHost() { const api = requireVscode(); const host = await api.window.showInputBox({ title: 'Dosh host', prompt: 'Dosh host alias, e.g. palav', ignoreFocusOut: true }); if (!host) { return undefined; } const user = await api.window.showInputBox({ title: 'Remote SSH user', prompt: 'Optional. Leave blank to let SSH config decide.', ignoreFocusOut: true }); const config = api.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), platform: process.platform }); const sshDir = path.join(os.homedir(), '.ssh'); fs.mkdirSync(sshDir, { recursive: true }); const mainConfig = path.join(sshDir, 'config'); const generatedPath = resolveGeneratedSshConfigPath( mainConfig, config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh'), os.homedir(), process.platform ); ensureInclude(mainConfig, sshIncludeTarget(mainConfig, generatedPath, process.platform)); upsertBlock(generatedPath, alias, block); return { alias, generatedPath }; } function sshBlock(options) { const platform = options.platform || process.platform; let proxy = sshConfigWord(options.executable, platform); if (options.doshPort > 0) { proxy += ` --dosh-port ${options.doshPort}`; } proxy += ` proxy-stdio ${sshConfigWord(options.host, platform)} %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 dir = path.dirname(configPath); if (dir && dir !== '.') { fs.mkdirSync(dir, { recursive: true }); } 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 sshIncludeTarget(mainConfigPath, generatedPath, platform = process.platform) { const pathApi = platform === 'win32' ? path.win32 : path; const mainDir = pathApi.resolve(pathApi.dirname(mainConfigPath)); const generatedAbsolute = pathApi.resolve(mainDir, generatedPath); const generatedDir = pathApi.resolve(pathApi.dirname(generatedAbsolute)); const includePath = normalizeCase(generatedDir, platform) === normalizeCase(mainDir, platform) ? pathApi.basename(generatedAbsolute) : generatedAbsolute; return sshConfigPathWord(includePath, platform); } function resolveGeneratedSshConfigPath( mainConfigPath, configuredPath, home = os.homedir(), platform = process.platform ) { const pathApi = platform === 'win32' ? path.win32 : path; const mainDir = pathApi.resolve(pathApi.dirname(mainConfigPath)); const expanded = expandHomePath(configuredPath, home, platform); return pathApi.resolve(mainDir, expanded); } function upsertBlock(configPath, alias, block) { const dir = path.dirname(configPath); if (dir && dir !== '.') { fs.mkdirSync(dir, { recursive: true }); } 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 api = requireVscode(); const config = api.workspace.getConfiguration('dosh'); const sshDir = path.join(os.homedir(), '.ssh'); const mainConfig = path.join(sshDir, 'config'); const generatedPath = resolveGeneratedSshConfigPath( mainConfig, config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh'), os.homedir(), process.platform ); if (!fs.existsSync(generatedPath)) { api.window.showWarningMessage('No generated Dosh SSH config yet.'); return; } const doc = await api.workspace.openTextDocument(generatedPath); await api.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 sshConfigWord(value, platform = process.platform) { if (platform === 'win32') { return windowsCommandWord(value); } return shellQuote(value); } function sshConfigPathWord(value, platform = process.platform) { const normalized = platform === 'win32' ? value.replace(/\\/g, '/') : value; if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(normalized)) { return normalized; } return `"${normalized.replace(/["\\]/g, '\\$&')}"`; } function normalizeCase(value, platform) { return platform === 'win32' ? value.toLowerCase() : value; } function expandHomePath(value, home = os.homedir(), platform = process.platform) { if (value === '~') { return home; } if (value.startsWith('~/') || value.startsWith('~\\')) { const pathApi = platform === 'win32' ? path.win32 : path; return pathApi.join(home, value.slice(2)); } return value; } function windowsCommandWord(value) { let out = '"'; let backslashes = 0; for (const ch of value) { if (ch === '\\') { backslashes += 1; continue; } if (ch === '"') { out += '\\'.repeat(backslashes * 2 + 1); out += '"'; backslashes = 0; continue; } out += '\\'.repeat(backslashes); backslashes = 0; out += ch; } out += '\\'.repeat(backslashes * 2); out += '"'; return out; } function deactivate() {} module.exports = { activate, deactivate, expandHomePath, ensureInclude, resolveGeneratedSshConfigPath, sshBlock, sshConfigPathWord, shellQuote, sshIncludeTarget, sshConfigWord, upsertBlock, windowsCommandWord };