From 9d34013467caa9f84f4c27156bfeefd618090753 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:28:52 +0200 Subject: [PATCH 1/3] Fix SSH Include glob parsing on Windows --- .../core/ssh/config/sshConfigParser.test.ts | 98 +++++++++++++++++-- .../main/core/ssh/config/sshConfigParser.ts | 9 +- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts index 8c4cd10929..f0b5efb5f3 100644 --- a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts +++ b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts @@ -8,6 +8,12 @@ import { parseSshConfigFileAt, } from './sshConfigParser'; +const itWindows = process.platform === 'win32' ? it : it.skip; +const pathSuffix = (suffix: string) => { + const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('/', '[\\\\/]'); + return new RegExp(`[\\\\/]${escapedSuffix}$`); +}; + describe('parseSshConfigContent', () => { it('lists concrete SSH config aliases with proxy and forwarding preview fields', () => { const hosts = parseSshConfigContent(` @@ -35,8 +41,8 @@ Host plain hostname: 'dev.internal', user: 'alice', port: 2222, - identityFile: expect.stringContaining('/.ssh/dev_ed25519'), - identityAgent: expect.stringContaining('/.1password/agent.sock'), + identityFile: expect.stringMatching(pathSuffix('.ssh/dev_ed25519')), + identityAgent: expect.stringMatching(pathSuffix('.1password/agent.sock')), proxyCommand: 'cloudflared access ssh --hostname %h', forwardAgent: true, forwardAgentValue: '$SSH_AUTH_SOCK', @@ -46,8 +52,8 @@ Host plain hostname: 'dev.internal', user: 'alice', port: 2222, - identityFile: expect.stringContaining('/.ssh/dev_ed25519'), - identityAgent: expect.stringContaining('/.1password/agent.sock'), + identityFile: expect.stringMatching(pathSuffix('.ssh/dev_ed25519')), + identityAgent: expect.stringMatching(pathSuffix('.1password/agent.sock')), proxyCommand: 'cloudflared access ssh --hostname %h', forwardAgent: true, forwardAgentValue: '$SSH_AUTH_SOCK', @@ -130,6 +136,86 @@ Host included-alias ]); }); + it('expands quoted and unquoted Include glob patterns', async () => { + const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-quoted-')); + await mkdir(join(dir, 'team config')); + await mkdir(join(dir, 'conf.d')); + await writeFile( + join(dir, 'config'), + ` +Include "team config/*.conf" conf.d/*.conf +`, + 'utf-8' + ); + await writeFile( + join(dir, 'team config', 'primary.conf'), + ` +Host quoted-include + HostName quoted.internal +`, + 'utf-8' + ); + await writeFile( + join(dir, 'conf.d', 'secondary.conf'), + ` +Host unquoted-include + HostName unquoted.internal +`, + 'utf-8' + ); + + expect(await parseSshConfigFileAt(join(dir, 'config'))).toEqual([ + { + host: 'unquoted-include', + hostname: 'unquoted.internal', + }, + { + host: 'quoted-include', + hostname: 'quoted.internal', + }, + ]); + }); + + itWindows('expands Include glob patterns with Windows path separators', async () => { + const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-windows-')); + await mkdir(join(dir, 'absolute')); + await mkdir(join(dir, 'relative')); + await writeFile( + join(dir, 'config'), + ` +Include relative\\*.conf "${join(dir, 'absolute', '*.conf')}" +`, + 'utf-8' + ); + await writeFile( + join(dir, 'absolute', 'team.conf'), + ` +Host absolute-windows-include + HostName absolute.internal +`, + 'utf-8' + ); + await writeFile( + join(dir, 'relative', 'team.conf'), + ` +Host relative-windows-include + HostName relative.internal +`, + 'utf-8' + ); + + expect(await parseSshConfigFileAt(join(dir, 'config'))).toEqual([ + { + host: 'absolute-windows-include', + hostname: 'absolute.internal', + }, + { + host: 'relative-windows-include', + hostname: 'relative.internal', + }, + ]); + }); + it('applies the same included snippet at each Include occurrence', async () => { const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-repeat-')); await writeFile( @@ -159,13 +245,13 @@ Host second host: 'first', hostname: 'first.internal', user: 'shared', - identityAgent: expect.stringContaining('/.ssh/shared-agent.sock'), + identityAgent: expect.stringMatching(pathSuffix('.ssh/shared-agent.sock')), }, { host: 'second', hostname: 'second.internal', user: 'shared', - identityAgent: expect.stringContaining('/.ssh/shared-agent.sock'), + identityAgent: expect.stringMatching(pathSuffix('.ssh/shared-agent.sock')), }, ]); }); diff --git a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts index 3ce3048e84..145303d22f 100644 --- a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts +++ b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts @@ -19,13 +19,13 @@ function stripQuotes(value: string): string { } /** - * Expands a leading `~` or `~/` to the user's home directory. + * Expands a leading `~`, `~/`, or `~\` to the user's home directory. */ function expandTilde(filePath: string): string { if (filePath === '~') { return homedir(); } - if (filePath.startsWith('~/')) { + if (filePath.startsWith('~/') || filePath.startsWith('~\\')) { return join(homedir(), filePath.slice(2)); } return filePath; @@ -83,7 +83,10 @@ async function resolveIncludePaths(value: string, configDir: string): Promise Date: Tue, 30 Jun 2026 07:37:40 +0200 Subject: [PATCH 2/3] fix(ssh): preserve POSIX tilde include escaping --- .../core/ssh/config/sshConfigParser.test.ts | 27 +++++++++++++++++++ .../main/core/ssh/config/sshConfigParser.ts | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts index f0b5efb5f3..969fd0d377 100644 --- a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts +++ b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts @@ -9,6 +9,7 @@ import { } from './sshConfigParser'; const itWindows = process.platform === 'win32' ? it : it.skip; +const itPosix = process.platform === 'win32' ? it.skip : it; const pathSuffix = (suffix: string) => { const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('/', '[\\\\/]'); return new RegExp(`[\\\\/]${escapedSuffix}$`); @@ -216,6 +217,32 @@ Host relative-windows-include ]); }); + itPosix('preserves backslash escaping in tilde Include patterns', async () => { + const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-posix-')); + await writeFile( + join(dir, 'config'), + ` +Include ~\\*.conf +`, + 'utf-8' + ); + await writeFile( + join(dir, '~*.conf'), + ` +Host posix-escaped-include + HostName posix.internal +`, + 'utf-8' + ); + + expect(await parseSshConfigFileAt(join(dir, 'config'))).toEqual([ + { + host: 'posix-escaped-include', + hostname: 'posix.internal', + }, + ]); + }); + it('applies the same included snippet at each Include occurrence', async () => { const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-repeat-')); await writeFile( diff --git a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts index 145303d22f..bd74915876 100644 --- a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts +++ b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.ts @@ -19,13 +19,13 @@ function stripQuotes(value: string): string { } /** - * Expands a leading `~`, `~/`, or `~\` to the user's home directory. + * Expands a leading `~`, `~/`, or Windows `~\` to the user's home directory. */ function expandTilde(filePath: string): string { if (filePath === '~') { return homedir(); } - if (filePath.startsWith('~/') || filePath.startsWith('~\\')) { + if (filePath.startsWith('~/') || (process.platform === 'win32' && filePath.startsWith('~\\'))) { return join(homedir(), filePath.slice(2)); } return filePath; From 03ca005723b878ab422fefc8e31c0d231270ab48 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:58:38 +0200 Subject: [PATCH 3/3] test(ssh): clarify include glob assertions --- .../core/ssh/config/sshConfigParser.test.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts index 969fd0d377..8c881071df 100644 --- a/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts +++ b/apps/emdash-desktop/src/main/core/ssh/config/sshConfigParser.test.ts @@ -165,16 +165,20 @@ Host unquoted-include 'utf-8' ); - expect(await parseSshConfigFileAt(join(dir, 'config'))).toEqual([ - { - host: 'unquoted-include', - hostname: 'unquoted.internal', - }, - { - host: 'quoted-include', - hostname: 'quoted.internal', - }, - ]); + const hosts = await parseSshConfigFileAt(join(dir, 'config')); + expect(hosts).toHaveLength(2); + expect(hosts).toEqual( + expect.arrayContaining([ + { + host: 'unquoted-include', + hostname: 'unquoted.internal', + }, + { + host: 'quoted-include', + hostname: 'quoted.internal', + }, + ]) + ); }); itWindows('expands Include glob patterns with Windows path separators', async () => { @@ -217,7 +221,7 @@ Host relative-windows-include ]); }); - itPosix('preserves backslash escaping in tilde Include patterns', async () => { + itPosix('treats ~\\ as a literal path prefix on POSIX', async () => { const dir = await mkdtemp(join(tmpdir(), 'emdash-ssh-config-posix-')); await writeFile( join(dir, 'config'),