diff --git a/vscode-extension/extension.js b/vscode-extension/extension.js index f65b5fd..668698e 100644 --- a/vscode-extension/extension.js +++ b/vscode-extension/extension.js @@ -86,7 +86,7 @@ async function configureHost() { 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)); + ensureInclude(mainConfig, sshIncludeTarget(mainConfig, generatedPath, process.platform)); upsertBlock(generatedPath, alias, block); return { alias, generatedPath }; } @@ -123,6 +123,18 @@ function ensureInclude(configPath, includeFile) { 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 raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''; const begin = `# BEGIN DOSH ${alias}`; @@ -189,6 +201,18 @@ function sshConfigWord(value, platform = process.platform) { 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 windowsCommandWord(value) { let out = '"'; let backslashes = 0; @@ -218,7 +242,9 @@ module.exports = { activate, deactivate, sshBlock, + sshConfigPathWord, shellQuote, + sshIncludeTarget, sshConfigWord, windowsCommandWord }; diff --git a/vscode-extension/extension.test.js b/vscode-extension/extension.test.js index 1dba3ec..4e4fc81 100644 --- a/vscode-extension/extension.test.js +++ b/vscode-extension/extension.test.js @@ -4,6 +4,8 @@ const assert = require('node:assert/strict'); const { shellQuote, sshBlock, + sshConfigPathWord, + sshIncludeTarget, sshConfigWord, windowsCommandWord } = require('./extension'); @@ -48,3 +50,36 @@ test('ssh block quotes proxy command for the target platform', () => { ); assert.doesNotMatch(block, /'C:\\Program Files\\Dosh\\dosh\.exe'/); }); + +test('ssh include target stays relative for generated config beside main config', () => { + assert.equal( + sshIncludeTarget('/Users/palav/.ssh/config', '/Users/palav/.ssh/config.dosh', 'darwin'), + 'config.dosh' + ); +}); + +test('ssh include target uses absolute path for custom generated config location', () => { + assert.equal( + sshIncludeTarget( + '/Users/palav/.ssh/config', + '/Users/palav/Library/Application Support/Dosh/config.dosh', + 'darwin' + ), + '"/Users/palav/Library/Application Support/Dosh/config.dosh"' + ); +}); + +test('ssh include target handles Windows custom generated config paths', () => { + assert.equal( + sshIncludeTarget( + 'C:\\Users\\palav\\.ssh\\config', + 'C:\\Users\\palav\\AppData\\Roaming\\Dosh Remote\\config.dosh', + 'win32' + ), + '"C:/Users/palav/AppData/Roaming/Dosh Remote/config.dosh"' + ); + assert.equal( + sshConfigPathWord('C:\\Users\\palav\\.ssh\\config.dosh', 'win32'), + 'C:/Users/palav/.ssh/config.dosh' + ); +});