Files
dosh/vscode-extension/extension.js
T
DuProcess d471d9576a
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
Expand VS Code generated SSH config paths
2026-07-16 22:38:52 -04:00

281 lines
7.8 KiB
JavaScript

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 generatedPath = expandHomePath(
config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh'),
os.homedir(),
process.platform
);
const mainConfig = path.join(sshDir, 'config');
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 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 generatedPath = expandHomePath(
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,
sshBlock,
sshConfigPathWord,
shellQuote,
sshIncludeTarget,
sshConfigWord,
upsertBlock,
windowsCommandWord
};