From 5b8c73827e756660f9a0de6df683bf2d0d06f37a Mon Sep 17 00:00:00 2001 From: Randalix Date: Mon, 12 Oct 2026 14:03:35 +0200 Subject: [PATCH] fix(preview): list a remote folder under an explicit sh sshd runs the command in the remote user's login shell. On Hufflepuff cd is an alias that echoes the directory, so the listing script captured the folder path twice and zsh aborted on the glob: every folder grid over ssh failed with "remote host unreachable: no matches found". Wrap the script in sh -c, as the transcript read already does. Co-Authored-By: Claude Sonnet 5.5 --- src/remote-files.ts | 8 +++++++- test/routes/folder-grid-routes.test.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/remote-files.ts b/src/remote-files.ts index 1188e7d2..5be230f4 100644 --- a/src/remote-files.ts +++ b/src/remote-files.ts @@ -307,7 +307,7 @@ export function buildRemoteFolderListCommand(dir: string, extensions: readonly s const patterns = [...new Set(extensions.map((ext) => ext.toLowerCase()))] .map((ext) => `*.${ext.replace(/[a-z]/g, (c) => `[${c}${c.toUpperCase()}]`)}`) .join('|'); - return [ + const script = [ `d=${shellescape(dir)}`, "case $d in '~/'*) d=$HOME/${d#??} ;; esac", "printf '\\0'", @@ -327,6 +327,12 @@ export function buildRemoteFolderListCommand(dir: string, extensions: readonly s ' printf \'f|%s|%s|%s\\0\' "${sm% *}" "${sm#* }" "$b"', 'done', ].join('\n'); + // ⚠️ sshd runs the command in the remote user's LOGIN shell, rc aliases included. + // On Hufflepuff `cd` is an alias that echoes the new directory (`$(cd -P "$d" && + // pwd -P)` came back as two lines, then zsh aborted on the glob: "no matches found: + // /x\n/x/*"), so no folder listing worked over ssh. An explicit `sh` reads no rc + // file — the same wrapper the transcript read uses. + return `sh -c ${shellescape(script)}`; } /** diff --git a/test/routes/folder-grid-routes.test.ts b/test/routes/folder-grid-routes.test.ts index 010b381e..e2575f5c 100644 --- a/test/routes/folder-grid-routes.test.ts +++ b/test/routes/folder-grid-routes.test.ts @@ -542,6 +542,26 @@ describe('remote folder listing script (real sh)', () => { expect(byName['sub dir']).toMatchObject({ kind: 'directory' }); }); + it('is not thrown off by a login shell whose `cd` prints the directory (Hufflepuff: alias cd=folderprompt)', () => { + // sshd hands the command to the remote user's LOGIN shell. There `cd` is an + // alias that echoes the new directory, so `r=$(cd -P "$d" && pwd -P)` captured + // the path twice and the listing failed ("zsh: no matches found: /x\n/x/*"). + // The script has to run under an explicit `sh`, which reads no rc file. + const dir = join(root, 'frames'); + mkdirSync(dir); + touch(join(dir, 'a.png'), 'x', 1_700_000_000); + const stdout = execFileSync( + 'bash', + ['-c', `cd() { builtin cd "$@" && pwd; }; ${buildRemoteFolderListCommand(dir, ['png'], 100)}`], + { encoding: 'utf8', env: { ...process.env, HOME: root } } + ); + const listing = parseRemoteFolderListOutput(stdout); + expect(listing).not.toBeNull(); + expect(listing).not.toBe('not-a-folder'); + expect((listing as { dir: string }).dir).toBe(dir); + expect((listing as { entries: { name: string }[] }).entries.map((e) => e.name)).toEqual(['a.png']); + }); + it('caps entries; tells a missing folder (null) from a file', () => { for (let i = 0; i < 5; i++) touch(join(root, `f${i}.png`), 'x', 1_700_000_000); const capped = run(root, 3);