mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
fix(launcher): resolve remote Bun path for sync
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
type: added
|
||||
area: launcher
|
||||
|
||||
- Added `subminer sync <host>` to merge immersion stats and watch history between two machines over SSH. Each side snapshots its database (`VACUUM INTO`), snapshots are exchanged with `scp`, and each machine merges the other's data as an insert-only union keyed on session UUIDs / video keys / series title keys, so re-syncing is idempotent and nothing is double-counted. Lifetime totals and daily/monthly rollups are updated incrementally (history older than the session retention window is preserved); remote-only historical rollups are copied only when they do not conflict with retained local session history. `subminer sync --snapshot <file>` and `subminer sync --merge <file>` expose the underlying steps for manual transfers. The command refuses to run while the stats daemon or an mpv session is active (`--force` overrides), keeps that guard in place through local/remote merges, reports remote stderr on failures, and aborts on stats schema version mismatches.
|
||||
- Added `subminer sync <host>` to merge immersion stats and watch history between two machines over SSH. Each side snapshots its database (`VACUUM INTO`), snapshots are exchanged with `scp`, and each machine merges the other's data as an insert-only union keyed on session UUIDs / video keys / series title keys, so re-syncing is idempotent and nothing is double-counted. Lifetime totals and daily/monthly rollups are updated incrementally (history older than the session retention window is preserved); remote-only historical rollups are copied only when they do not conflict with retained local session history. `subminer sync --snapshot <file>` and `subminer sync --merge <file>` expose the underlying steps for manual transfers. The command refuses to run while the stats daemon or an mpv session is active (`--force` overrides), keeps that guard in place through local/remote merges, supplies standard SubMiner and Bun paths to non-interactive SSH commands, verifies the remote launcher starts, reports remote stderr on failures, and aborts on stats schema version mismatches.
|
||||
|
||||
@@ -90,7 +90,7 @@ subminer sync macbook --remote-cmd ~/bin/subminer # custom remote launcher path
|
||||
|
||||
How it works: each side takes a consistent snapshot of its database (`VACUUM INTO`), the snapshots are exchanged over `scp`, and each machine merges the other's snapshot into its own database. The merge is an insert-only union keyed on stable identifiers (session UUIDs, video keys, series title keys, word/kanji identity), so it is safe to re-run at any time — syncing twice changes nothing, and nothing is ever overwritten or summed twice. Lifetime totals and rollup charts are updated incrementally, so history older than the session retention window is preserved on both sides.
|
||||
|
||||
Close SubMiner (and stop the background stats daemon, `subminer stats -s`) on both machines before syncing; the command refuses to run while a SubMiner process may be writing the database (`--force` overrides). Both machines must be on the same SubMiner version — the sync aborts on a stats schema mismatch.
|
||||
Close SubMiner (and stop the background stats daemon, `subminer stats -s`) on both machines before syncing; the command refuses to run while a SubMiner process may be writing the database (`--force` overrides). Both machines must be on the same SubMiner version — the sync aborts on a stats schema mismatch. Remote sync checks standard SubMiner and Bun locations (`~/.local/bin`, `~/.bun/bin`, Homebrew, `/usr/local/bin`, `/usr/bin`, and `/bin`) even when the non-interactive SSH shell omits them from `PATH`.
|
||||
|
||||
Two lower-level modes are used internally over SSH and also work standalone for manual transfers (e.g. via a USB drive):
|
||||
|
||||
|
||||
+1
-1
@@ -197,7 +197,7 @@ This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blan
|
||||
- `subminer logs -e`: export a sanitized ZIP of today's local-date logs, or the most recent logs when no current-day log exists. The exported copy masks common PII and secrets; on-disk logs are unchanged.
|
||||
- `subminer config`: config file helpers (`path`, `show`).
|
||||
- `subminer mpv`: mpv helpers (`status`, `socket`, `idle`).
|
||||
- `subminer sync <host>`: sync immersion stats and watch history with another machine over SSH. The host is the SSH destination (`user@host` or an SSH config alias). Use `--snapshot <file>` to write a consistent local stats DB snapshot, `--merge <file>` to merge a snapshot into the local stats DB, and `--force` to skip the running stats/mpv safety check. Advanced options: `--db <file>` overrides the local stats DB path, and `--remote-cmd <cmd>` overrides the `subminer` command used on the remote host.
|
||||
- `subminer sync <host>`: sync immersion stats and watch history with another machine over SSH. The host is the SSH destination (`user@host` or an SSH config alias). Remote launcher checks include standard SubMiner and Bun paths even when SSH omits them from `PATH`. Use `--snapshot <file>` to write a consistent local stats DB snapshot, `--merge <file>` to merge a snapshot into the local stats DB, and `--force` to skip the running stats/mpv safety check. Advanced options: `--db <file>` overrides the local stats DB path, and `--remote-cmd <cmd>` overrides the `subminer` command used on the remote host.
|
||||
- `subminer dictionary <path>`: generates a Yomitan-importable character dictionary ZIP from a file/directory target.
|
||||
- Use `subminer dictionary --candidates <path>` and `subminer dictionary --select <id> <path>` to correct AniList character-dictionary matches for a whole series.
|
||||
- `subminer texthooker`: texthooker-only shortcut (same behavior as `--texthooker`). A _texthooker_ is a web page that displays the current subtitle line as selectable text, so browser-based dictionary extensions and other tools can read along with playback.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertSafeSshHost, runScp, shellQuote } from './ssh.js';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { assertSafeSshHost, resolveRemoteSubminerCommand, runScp, shellQuote } from './ssh.js';
|
||||
|
||||
test('assertSafeSshHost rejects option-like hosts', () => {
|
||||
assert.throws(() => assertSafeSshHost('-oProxyCommand=touch pwned'), /looks like an option/);
|
||||
@@ -29,3 +32,30 @@ test('runScp rejects option-like remote host components', () => {
|
||||
/SSH host that looks like an option/,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveRemoteSubminerCommand verifies the launcher under the remote runtime PATH', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-ssh-test-'));
|
||||
const sshPath = path.join(tempDir, 'ssh');
|
||||
const originalPath = process.env.PATH;
|
||||
fs.writeFileSync(
|
||||
sshPath,
|
||||
`#!/bin/sh
|
||||
case "$2" in
|
||||
*'$HOME/.local/bin'*'$HOME/.bun/bin'*'/opt/homebrew/bin'*'/usr/local/bin'*'/usr/bin'*'/bin'*'subminer --help'*) exit 0 ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
try {
|
||||
process.env.PATH = `${tempDir}:${originalPath ?? ''}`;
|
||||
assert.equal(
|
||||
resolveRemoteSubminerCommand('macbook', null),
|
||||
'PATH="$HOME/.local/bin:$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" subminer',
|
||||
);
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
+13
-9
@@ -71,23 +71,27 @@ export function shellQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", `'\\''`)}'`;
|
||||
}
|
||||
|
||||
const REMOTE_RUNTIME_PATH =
|
||||
'PATH="$HOME/.local/bin:$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"';
|
||||
|
||||
/**
|
||||
* Non-interactive SSH shells often miss ~/.local/bin in PATH, so probe the
|
||||
* configured command first and fall back to the default install location.
|
||||
* Non-interactive SSH shells often miss user-installed launchers and Bun.
|
||||
* Probe the launcher under the same deterministic PATH used by sync itself.
|
||||
*/
|
||||
export function resolveRemoteSubminerCommand(host: string, preferred: string | null): string {
|
||||
// Trusted defaults stay unquoted so the remote shell expands `~`; the
|
||||
// Trusted defaults stay unquoted so the remote shell expands `~`; a
|
||||
// user-supplied override is shell-quoted to prevent command injection.
|
||||
const candidates: Array<{ value: string; probe: string }> = preferred
|
||||
? [{ value: preferred, probe: shellQuote(preferred) }]
|
||||
const candidates: Array<{ value: string; invocation: string }> = preferred
|
||||
? [{ value: preferred, invocation: shellQuote(preferred) }]
|
||||
: [
|
||||
{ value: 'subminer', probe: 'subminer' },
|
||||
{ value: '~/.local/bin/subminer', probe: '~/.local/bin/subminer' },
|
||||
{ value: 'subminer', invocation: 'subminer' },
|
||||
{ value: '~/.local/bin/subminer', invocation: '~/.local/bin/subminer' },
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const probe = runSsh(host, `command -v ${candidate.probe} >/dev/null 2>&1`);
|
||||
const command = `${REMOTE_RUNTIME_PATH} ${candidate.invocation}`;
|
||||
const probe = runSsh(host, `${command} --help >/dev/null 2>&1`);
|
||||
if (probe.status === 0) {
|
||||
return candidate.value;
|
||||
return command;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user