fix(launcher): address code review findings in stats sync

- merge-catalog: stop dropping anilist_id on every new anime insert
  (bun:sqlite .get() returns null, so the old !== undefined guard always
  fired); the guard was dead anyway since a colliding id is caught earlier
- sync-command: throw instead of fail() inside runHostSync so the finally
  block runs and temp dirs holding snapshot data are cleaned up on failure
- ssh: shell-quote the user-supplied --remote-cmd in the probe, and reject
  option-like (-prefixed) SSH hosts that ssh/scp would parse as flags
- sync-db: close the remote handle if opening the local DB throws
- sync-shared: treat EPERM from process.kill(pid, 0) as alive, not dead
- cli-parser: trim the sync host before the mode-exclusivity check
- tests: cover anilist_id preservation, anilist-based anime matching, and
  the SSH host/shell-quote guards
This commit is contained in:
2026-07-08 23:51:46 -07:00
parent be31f96f02
commit 58d54311d8
8 changed files with 131 additions and 21 deletions
+22 -3
View File
@@ -5,12 +5,24 @@ export interface RemoteRunResult {
stdout: string;
}
/**
* ssh/scp have no `--` terminator for the destination, so a host that starts
* with `-` (e.g. `-oProxyCommand=...`) is parsed as an option. Reject those
* before spawning.
*/
export function assertSafeSshHost(host: string): void {
if (host.startsWith('-')) {
throw new Error(`Refusing to use SSH host that looks like an option: ${host}`);
}
}
/**
* Run a command on the SSH host. stdin/stderr stay attached to the terminal
* so key passphrase / password prompts and remote progress output work;
* stdout is captured for the caller.
*/
export function runSsh(host: string, remoteCommand: string): RemoteRunResult {
assertSafeSshHost(host);
const result = spawnSync('ssh', [host, remoteCommand], {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'inherit'],
@@ -43,11 +55,18 @@ export function shellQuote(value: string): string {
* configured command first and fall back to the default install location.
*/
export function resolveRemoteSubminerCommand(host: string, preferred: string | null): string {
const candidates = preferred ? [preferred] : ['subminer', '~/.local/bin/subminer'];
// Trusted defaults stay unquoted so the remote shell expands `~`; the
// user-supplied override is shell-quoted to prevent command injection.
const candidates: Array<{ value: string; probe: string }> = preferred
? [{ value: preferred, probe: shellQuote(preferred) }]
: [
{ value: 'subminer', probe: 'subminer' },
{ value: '~/.local/bin/subminer', probe: '~/.local/bin/subminer' },
];
for (const candidate of candidates) {
const probe = runSsh(host, `command -v ${candidate} >/dev/null 2>&1`);
const probe = runSsh(host, `command -v ${candidate.probe} >/dev/null 2>&1`);
if (probe.status === 0) {
return candidate;
return candidate.value;
}
}
throw new Error(