refactor(sync): collapse 11 sync args into forwarded token array

- Replace syncHost/syncSnapshotPath/… with syncCliTokens: string[] passed verbatim to --sync-cli sync
- Move sync CLI validation from launcher to parseSyncCliTokens (app-side single owner)
- Extract resolveImmersionDbPath to shared db-path.ts importable by both launcher and app
- Delete driver.ts; inline types into libsql-driver.ts and hard-wire openLibsqlSyncDb
- Drop OpenSyncDb injection from mergeSnapshotIntoDb and createDbSnapshot
This commit is contained in:
2026-07-12 22:31:59 -07:00
parent 344a8b44c0
commit 806c56a99b
38 changed files with 473 additions and 798 deletions
+1 -11
View File
@@ -37,17 +37,7 @@ function createContext(): LauncherCommandContext {
useRofi: false,
history: false,
sync: false,
syncHost: '',
syncSnapshotPath: '',
syncMergePath: '',
syncDirection: 'both',
syncRemoteCmd: '',
syncDbPath: '',
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
syncUi: false,
logLevel: 'info',
logRotation: 7,
+31 -71
View File
@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import type { Args } from '../types.js';
import type { LauncherCommandContext } from './context.js';
import { buildSyncCliArgv, runSyncCommand, type SyncCommandDeps } from './sync-command.js';
import { runSyncCommand, type SyncCommandDeps } from './sync-command.js';
function makeContext(
overrides: Partial<Args>,
@@ -11,17 +11,7 @@ function makeContext(
return {
args: {
sync: true,
syncHost: '',
syncSnapshotPath: '',
syncMergePath: '',
syncDirection: 'both',
syncRemoteCmd: '',
syncDbPath: '',
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
logLevel: 'warn',
...overrides,
} as Args,
@@ -35,24 +25,6 @@ function makeContext(
} as unknown as LauncherCommandContext;
}
function makeArgs(overrides: Partial<Parameters<typeof buildSyncCliArgv>[0]>) {
return {
syncHost: '',
syncSnapshotPath: '',
syncMergePath: '',
syncDirection: 'both' as const,
syncRemoteCmd: '',
syncDbPath: '',
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
logLevel: 'warn' as const,
...overrides,
};
}
test('runSyncCommand proxies sync argv to the app in --sync-cli mode', async () => {
const spawned: Array<{ appPath: string; appArgs: string[] }> = [];
const deps: Partial<SyncCommandDeps> = {
@@ -62,7 +34,7 @@ test('runSyncCommand proxies sync argv to the app in --sync-cli mode', async ()
};
assert.equal(
await runSyncCommand(makeContext({ syncHost: 'media-box', syncJson: true }), deps),
await runSyncCommand(makeContext({ syncCliTokens: ['media-box', '--json'] }), deps),
true,
);
assert.deepEqual(spawned, [
@@ -76,19 +48,32 @@ test('runSyncCommand proxies sync argv to the app in --sync-cli mode', async ()
assert.equal(spawned.length, 1);
});
test('buildSyncCliArgv forwards every sync option', () => {
assert.deepEqual(
buildSyncCliArgv(
makeArgs({
syncHost: 'media-box',
syncDirection: 'pull',
syncRemoteCmd: '/opt/SubMiner.AppImage',
syncDbPath: '/tmp/db.sqlite',
syncForce: true,
syncJson: true,
logLevel: 'debug',
}),
),
test('runSyncCommand forwards tokens verbatim and appends the effective log level', async () => {
const spawned: string[][] = [];
const deps: Partial<SyncCommandDeps> = {
runAppCommand: (_appPath, appArgs) => {
spawned.push(appArgs);
},
};
await runSyncCommand(
makeContext({
syncCliTokens: [
'media-box',
'--pull',
'--remote-cmd',
'/opt/SubMiner.AppImage',
'--db',
'/tmp/db.sqlite',
'--force',
'--json',
],
logLevel: 'debug',
}),
deps,
);
assert.deepEqual(spawned, [
[
'--sync-cli',
'sync',
@@ -103,32 +88,7 @@ test('buildSyncCliArgv forwards every sync option', () => {
'--log-level',
'debug',
],
);
assert.deepEqual(
buildSyncCliArgv(makeArgs({ syncSnapshotPath: '/tmp/out.sqlite' })),
['--sync-cli', 'sync', '--snapshot', '/tmp/out.sqlite', '--log-level', 'warn'],
);
assert.deepEqual(
buildSyncCliArgv(makeArgs({ syncHost: 'media-box', syncCheck: true })),
['--sync-cli', 'sync', 'media-box', '--check', '--log-level', 'warn'],
);
assert.deepEqual(
buildSyncCliArgv(makeArgs({ syncMakeTemp: true })),
['--sync-cli', 'sync', '--make-temp', '--log-level', 'warn'],
);
assert.deepEqual(
buildSyncCliArgv(makeArgs({ syncRemoveTempPath: '/tmp/subminer-sync-x' })),
['--sync-cli', 'sync', '--remove-temp', '/tmp/subminer-sync-x', '--log-level', 'warn'],
);
assert.deepEqual(
buildSyncCliArgv(makeArgs({ syncMergePath: '/tmp/in.sqlite', syncForce: true })),
['--sync-cli', 'sync', '--merge', '/tmp/in.sqlite', '--force', '--log-level', 'warn'],
);
]);
});
test('runSyncCommand fails with a clear message when the app binary is missing', async () => {
@@ -142,7 +102,7 @@ test('runSyncCommand fails with a clear message when the app binary is missing',
};
await assert.rejects(
() => runSyncCommand(makeContext({ syncHost: 'media-box' }, null), deps),
() => runSyncCommand(makeContext({ syncCliTokens: ['media-box'] }, null), deps),
/SubMiner app binary not found \(sync runs inside the app\)/,
);
});
+12 -40
View File
@@ -1,6 +1,6 @@
import { SYNC_CLI_FLAG } from '../../src/core/services/stats-sync/cli-args.js';
import { fail } from '../log.js';
import { runAppCommandInteractive } from '../mpv.js';
import type { Args } from '../types.js';
import type { LauncherCommandContext } from './context.js';
export interface SyncCommandDeps {
@@ -13,46 +13,12 @@ const defaultSyncCommandDeps: SyncCommandDeps = {
fail,
};
type SyncArgs = Pick<
Args,
| 'syncHost'
| 'syncSnapshotPath'
| 'syncMergePath'
| 'syncDirection'
| 'syncRemoteCmd'
| 'syncDbPath'
| 'syncForce'
| 'syncJson'
| 'syncCheck'
| 'syncMakeTemp'
| 'syncRemoveTempPath'
| 'logLevel'
>;
/** Rebuild the app's --sync-cli argv from the launcher's parsed sync args. */
export function buildSyncCliArgv(args: SyncArgs): string[] {
const argv = ['--sync-cli', 'sync'];
if (args.syncHost) argv.push(args.syncHost);
if (args.syncSnapshotPath) argv.push('--snapshot', args.syncSnapshotPath);
if (args.syncMergePath) argv.push('--merge', args.syncMergePath);
if (args.syncMakeTemp) argv.push('--make-temp');
if (args.syncRemoveTempPath) argv.push('--remove-temp', args.syncRemoveTempPath);
if (args.syncDirection === 'push') argv.push('--push');
if (args.syncDirection === 'pull') argv.push('--pull');
if (args.syncCheck) argv.push('--check');
if (args.syncRemoteCmd) argv.push('--remote-cmd', args.syncRemoteCmd);
if (args.syncDbPath) argv.push('--db', args.syncDbPath);
if (args.syncForce) argv.push('--force');
if (args.syncJson) argv.push('--json');
argv.push('--log-level', args.logLevel);
return argv;
}
/**
* `subminer sync` is a thin proxy: the sync engine only executes inside the
* SubMiner app (--sync-cli mode, libsql), so the launcher and the app cannot
* drift apart. The launcher contributes its parser/help and app discovery;
* the child owns the terminal and its exit code becomes the launcher's.
* SubMiner app (--sync-cli mode, libsql). The launcher contributes its
* parser/help and app discovery; the app's parseSyncCliTokens owns validation,
* so its errors reach the terminal through the child's inherited stdio. The
* child owns the terminal and its exit code becomes the launcher's.
*/
export async function runSyncCommand(
context: LauncherCommandContext,
@@ -68,6 +34,12 @@ export async function runSyncCommand(
);
return true; // fail() never returns; this only satisfies control-flow analysis
}
deps.runAppCommand(context.appPath, buildSyncCliArgv(context.args));
deps.runAppCommand(context.appPath, [
SYNC_CLI_FLAG,
'sync',
...context.args.syncCliTokens,
'--log-level',
context.args.logLevel,
]);
return true;
}