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 <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Sonnet 5.5 committed 2026-10-12 14:06:19 +02:00
1 parent 3c08971ff8
commit 5b8c73827e
2 files changed
+27 -1

No files matched your search

+7 -1
View File
@@ -307,7 +307,7 @@ export function buildRemoteFolderListCommand(dir: string, extensions: readonly s
const patterns = [...new Set(extensions.map((ext) => ext.toLowerCase()))] const patterns = [...new Set(extensions.map((ext) => ext.toLowerCase()))]
.map((ext) => `*.${ext.replace(/[a-z]/g, (c) => `[${c}${c.toUpperCase()}]`)}`) .map((ext) => `*.${ext.replace(/[a-z]/g, (c) => `[${c}${c.toUpperCase()}]`)}`)
.join('|'); .join('|');
return [ const script = [
`d=${shellescape(dir)}`, `d=${shellescape(dir)}`,
"case $d in '~/'*) d=$HOME/${d#??} ;; esac", "case $d in '~/'*) d=$HOME/${d#??} ;; esac",
"printf '\\0'", "printf '\\0'",
@@ -327,6 +327,12 @@ export function buildRemoteFolderListCommand(dir: string, extensions: readonly s
' printf \'f|%s|%s|%s\\0\' "${sm% *}" "${sm#* }" "$b"', ' printf \'f|%s|%s|%s\\0\' "${sm% *}" "${sm#* }" "$b"',
'done', 'done',
].join('\n'); ].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)}`;
} }
/** /**
+20
View File
@@ -542,6 +542,26 @@ describe('remote folder listing script (real sh)', () => {
expect(byName['sub dir']).toMatchObject({ kind: 'directory' }); 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', () => { 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); for (let i = 0; i < 5; i++) touch(join(root, `f${i}.png`), 'x', 1_700_000_000);
const capped = run(root, 3); const capped = run(root, 3);