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;
}
+4 -44
View File
@@ -136,17 +136,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
statsCleanupLifetime: false,
statsLogLevel: null,
syncTriggered: false,
syncHost: null,
syncSnapshotPath: null,
syncMergePath: null,
syncDirection: 'both',
syncRemoteCmd: null,
syncDbPath: null,
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
syncLogLevel: null,
syncUiTriggered: false,
syncUiLogLevel: null,
@@ -197,17 +187,7 @@ test('applyInvocationsToArgs maps settings invocation to settings window', () =>
statsCleanupLifetime: false,
statsLogLevel: null,
syncTriggered: false,
syncHost: null,
syncSnapshotPath: null,
syncMergePath: null,
syncDirection: 'both',
syncRemoteCmd: null,
syncDbPath: null,
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
syncLogLevel: null,
syncUiTriggered: false,
syncUiLogLevel: null,
@@ -251,17 +231,7 @@ test('applyInvocationsToArgs fails when config invocation has no action', () =>
statsCleanupLifetime: false,
statsLogLevel: null,
syncTriggered: false,
syncHost: null,
syncSnapshotPath: null,
syncMergePath: null,
syncDirection: 'both',
syncRemoteCmd: null,
syncDbPath: null,
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
syncLogLevel: null,
syncUiTriggered: false,
syncUiLogLevel: null,
@@ -303,17 +273,7 @@ test('applyInvocationsToArgs maps texthooker browser-open request', () => {
statsCleanupLifetime: false,
statsLogLevel: null,
syncTriggered: false,
syncHost: null,
syncSnapshotPath: null,
syncMergePath: null,
syncDirection: 'both',
syncRemoteCmd: null,
syncDbPath: null,
syncForce: false,
syncJson: false,
syncCheck: false,
syncMakeTemp: false,
syncRemoveTempPath: '',
syncCliTokens: [],
syncLogLevel: null,
syncUiTriggered: false,
syncUiLogLevel: null,
+2 -22
View File
@@ -200,17 +200,7 @@ export function createDefaultArgs(
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: loggingConfig.level ?? 'warn',
logRotation: loggingConfig.rotation ?? 7,
@@ -279,17 +269,7 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
}
if (invocations.syncTriggered) {
parsed.sync = true;
parsed.syncHost = invocations.syncHost ?? '';
parsed.syncSnapshotPath = invocations.syncSnapshotPath ?? '';
parsed.syncMergePath = invocations.syncMergePath ?? '';
parsed.syncDirection = invocations.syncDirection;
parsed.syncRemoteCmd = invocations.syncRemoteCmd ?? '';
parsed.syncDbPath = invocations.syncDbPath ?? '';
parsed.syncForce = invocations.syncForce;
parsed.syncJson = invocations.syncJson;
parsed.syncCheck = invocations.syncCheck;
parsed.syncMakeTemp = invocations.syncMakeTemp;
parsed.syncRemoveTempPath = invocations.syncRemoveTempPath ?? '';
parsed.syncCliTokens = invocations.syncCliTokens;
if (invocations.syncLogLevel) parsed.logLevel = parseLogLevel(invocations.syncLogLevel);
}
if (invocations.syncUiTriggered) {
+51 -30
View File
@@ -43,42 +43,63 @@ test('parseCliPrograms captures texthooker browser-open flag', () => {
assert.equal(result.invocations.texthookerOpenBrowser, true);
});
test('parseCliPrograms captures one-way sync directions', () => {
test('parseCliPrograms lowers sync options into app-owned CLI tokens', () => {
const push = parseCliPrograms(['sync', 'media-box', '--push'], 'subminer');
assert.equal(push.invocations.syncTriggered, true);
assert.deepEqual(push.invocations.syncCliTokens, ['media-box', '--push']);
const pull = parseCliPrograms(['sync', 'media-box', '--pull'], 'subminer');
assert.equal(push.invocations.syncDirection, 'push');
assert.equal(pull.invocations.syncDirection, 'pull');
});
test('parseCliPrograms rejects conflicting or hostless one-way sync directions', () => {
assert.throws(
() => parseCliPrograms(['sync', 'media-box', '--push', '--pull'], 'subminer'),
/--push and --pull cannot be combined/,
);
assert.throws(
() => parseCliPrograms(['sync', '--snapshot', '/tmp/stats.sqlite', '--push'], 'subminer'),
/--push and --pull require a host/,
);
});
test('parseCliPrograms captures sync --json and --check flags', () => {
const json = parseCliPrograms(['sync', 'media-box', '--json'], 'subminer');
assert.equal(json.invocations.syncJson, true);
assert.equal(json.invocations.syncCheck, false);
assert.deepEqual(pull.invocations.syncCliTokens, ['media-box', '--pull']);
const check = parseCliPrograms(['sync', 'media-box', '--check', '--json'], 'subminer');
assert.equal(check.invocations.syncCheck, true);
assert.equal(check.invocations.syncJson, true);
assert.equal(check.invocations.syncHost, 'media-box');
assert.deepEqual(check.invocations.syncCliTokens, ['media-box', '--check', '--json']);
const full = parseCliPrograms(
[
'sync',
'media-box',
'--remote-cmd',
'/opt/SubMiner.AppImage',
'--db',
'/tmp/db.sqlite',
'--force',
'--log-level',
'debug',
],
'subminer',
);
assert.deepEqual(full.invocations.syncCliTokens, [
'media-box',
'--remote-cmd',
'/opt/SubMiner.AppImage',
'--db',
'/tmp/db.sqlite',
'--force',
]);
assert.equal(full.invocations.syncLogLevel, 'debug');
const snapshot = parseCliPrograms(['sync', '--snapshot', '/tmp/out.sqlite'], 'subminer');
assert.deepEqual(snapshot.invocations.syncCliTokens, ['--snapshot', '/tmp/out.sqlite']);
const merge = parseCliPrograms(['sync', '--merge', '/tmp/in.sqlite'], 'subminer');
assert.deepEqual(merge.invocations.syncCliTokens, ['--merge', '/tmp/in.sqlite']);
const makeTemp = parseCliPrograms(['sync', '--make-temp'], 'subminer');
assert.deepEqual(makeTemp.invocations.syncCliTokens, ['--make-temp']);
const removeTemp = parseCliPrograms(['sync', '--remove-temp', '/tmp/subminer-sync-x'], 'subminer');
assert.deepEqual(removeTemp.invocations.syncCliTokens, ['--remove-temp', '/tmp/subminer-sync-x']);
});
test('parseCliPrograms rejects invalid sync --check combinations', () => {
assert.throws(() => parseCliPrograms(['sync', '--check'], 'subminer'), /--check requires a host/);
assert.throws(
() => parseCliPrograms(['sync', 'media-box', '--check', '--push'], 'subminer'),
/--check cannot be combined/,
);
test('parseCliPrograms leaves sync validation to the app parser', () => {
// Invalid combinations are forwarded; the app's parseSyncCliTokens rejects them.
const invalid = parseCliPrograms(['sync', 'media-box', '--push', '--pull'], 'subminer');
assert.equal(invalid.invocations.syncTriggered, true);
assert.deepEqual(invalid.invocations.syncCliTokens, ['media-box', '--push', '--pull']);
const empty = parseCliPrograms(['sync'], 'subminer');
assert.equal(empty.invocations.syncTriggered, true);
assert.deepEqual(empty.invocations.syncCliTokens, []);
});
test('parseCliPrograms captures sync --ui', () => {
+21 -75
View File
@@ -39,17 +39,7 @@ export interface CliInvocations {
statsCleanupLifetime: boolean;
statsLogLevel: string | null;
syncTriggered: boolean;
syncHost: string | null;
syncSnapshotPath: string | null;
syncMergePath: string | null;
syncDirection: 'both' | 'push' | 'pull';
syncRemoteCmd: string | null;
syncDbPath: string | null;
syncForce: boolean;
syncJson: boolean;
syncCheck: boolean;
syncMakeTemp: boolean;
syncRemoveTempPath: string | null;
syncCliTokens: string[];
syncLogLevel: string | null;
syncUiTriggered: boolean;
syncUiLogLevel: string | null;
@@ -181,17 +171,7 @@ export function parseCliPrograms(
let statsCleanupLifetime = false;
let statsLogLevel: string | null = null;
let syncTriggered = false;
let syncHost: string | null = null;
let syncSnapshotPath: string | null = null;
let syncMergePath: string | null = null;
let syncDirection: 'both' | 'push' | 'pull' = 'both';
let syncRemoteCmd: string | null = null;
let syncDbPath: string | null = null;
let syncForce = false;
let syncJson = false;
let syncCheck = false;
let syncMakeTemp = false;
let syncRemoveTempPath: string | null = null;
let syncCliTokens: string[] = [];
let syncLogLevel: string | null = null;
let syncUiTriggered = false;
let syncUiLogLevel: string | null = null;
@@ -369,49 +349,25 @@ export function parseCliPrograms(
syncUiLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
return;
}
if (push && pull) {
throw new Error('Sync --push and --pull cannot be combined.');
}
if ((push || pull) && !host) {
throw new Error('Sync --push and --pull require a host.');
}
if (check && !host) {
throw new Error('Sync --check requires a host.');
}
if (check && (push || pull || snapshot || merge)) {
throw new Error(
'Sync --check cannot be combined with --push, --pull, --snapshot, or --merge.',
);
}
if ((makeTemp || removeTemp) && (push || pull || check)) {
throw new Error('Sync --make-temp/--remove-temp cannot be combined with other sync options.');
}
const modes = [
Boolean(host),
Boolean(snapshot),
Boolean(merge),
makeTemp,
Boolean(removeTemp),
].filter(Boolean).length;
if (modes === 0) {
throw new Error('Sync requires a host, --snapshot <file>, or --merge <file>.');
}
if (modes > 1) {
throw new Error('Sync host, --snapshot, --merge, --make-temp, and --remove-temp cannot be combined.');
}
// No validation here: the app's parseSyncCliTokens owns the sync rules
// and its error text reaches the terminal through the child's stdio.
const remoteCmd = typeof options.remoteCmd === 'string' ? options.remoteCmd.trim() : '';
const dbPath = typeof options.db === 'string' ? options.db.trim() : '';
const tokens: string[] = [];
if (host) tokens.push(host);
if (snapshot) tokens.push('--snapshot', snapshot);
if (merge) tokens.push('--merge', merge);
if (makeTemp) tokens.push('--make-temp');
if (removeTemp) tokens.push('--remove-temp', removeTemp);
if (push) tokens.push('--push');
if (pull) tokens.push('--pull');
if (check) tokens.push('--check');
if (remoteCmd) tokens.push('--remote-cmd', remoteCmd);
if (dbPath) tokens.push('--db', dbPath);
if (options.force === true) tokens.push('--force');
if (options.json === true) tokens.push('--json');
syncTriggered = true;
syncHost = host || null;
syncSnapshotPath = snapshot || null;
syncMergePath = merge || null;
syncDirection = push ? 'push' : pull ? 'pull' : 'both';
syncRemoteCmd =
typeof options.remoteCmd === 'string' ? options.remoteCmd.trim() || null : null;
syncDbPath = typeof options.db === 'string' ? options.db.trim() || null : null;
syncForce = options.force === true;
syncJson = options.json === true;
syncCheck = check;
syncMakeTemp = makeTemp;
syncRemoveTempPath = removeTemp || null;
syncCliTokens = tokens;
syncLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
});
@@ -527,17 +483,7 @@ export function parseCliPrograms(
statsCleanupLifetime,
statsLogLevel,
syncTriggered,
syncHost,
syncSnapshotPath,
syncMergePath,
syncDirection,
syncRemoteCmd,
syncDbPath,
syncForce,
syncJson,
syncCheck,
syncMakeTemp,
syncRemoveTempPath,
syncCliTokens,
syncLogLevel,
syncUiTriggered,
syncUiLogLevel,
+2 -28
View File
@@ -1,38 +1,12 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Database } from 'bun:sqlite';
import { resolveConfigDir } from '../src/config/path-resolution.js';
import { readLauncherMainConfigObject } from './config/shared-config-reader.js';
import type { HistoryVideoRow } from './history-types.js';
import { resolvePathMaybe } from './util.js';
import { resolveImmersionDbPath } from '../src/core/services/stats-sync/db-path.js';
import {
isReadonlyWalRetryError,
withReadonlyWalRetry,
} from '../src/core/services/stats-sync/wal-retry.js';
export { isReadonlyWalRetryError, withReadonlyWalRetry };
export function resolveImmersionDbPath(): string {
const root = readLauncherMainConfigObject();
const tracking =
root?.immersionTracking &&
typeof root.immersionTracking === 'object' &&
!Array.isArray(root.immersionTracking)
? (root.immersionTracking as Record<string, unknown>)
: null;
const configured = typeof tracking?.dbPath === 'string' ? tracking.dbPath.trim() : '';
if (configured) return resolvePathMaybe(configured);
const configDir = resolveConfigDir({
platform: process.platform,
appDataDir: process.env.APPDATA,
xdgConfigHome: process.env.XDG_CONFIG_HOME,
homeDir: os.homedir(),
existsSync: fs.existsSync,
});
return path.join(configDir, 'immersion.sqlite');
}
export { isReadonlyWalRetryError, resolveImmersionDbPath, withReadonlyWalRetry };
interface RawHistoryRow {
video_id: number;
+1 -11
View File
@@ -31,17 +31,7 @@ function createArgs(): Args {
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,
+1 -11
View File
@@ -603,17 +603,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
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: 'error',
logRotation: 7,
+5 -11
View File
@@ -4,17 +4,11 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Database } from 'bun:sqlite';
import { openLibsqlSyncDb } from '../../src/core/services/stats-sync/libsql-driver.js';
import { createDbSnapshot as createDbSnapshotWith } from '../../src/core/services/stats-sync/shared.js';
import { mergeSnapshotIntoDb as mergeSnapshotIntoDbWith } from '../../src/core/services/stats-sync/merge.js';
// The engine only executes inside the app (libsql driver) in production, so
// these merge tests run through that same binding; bun:sqlite is used only to
// build fixtures and inspect results.
const createDbSnapshot = (dbPath: string, outPath: string) =>
createDbSnapshotWith(openLibsqlSyncDb, dbPath, outPath);
const mergeSnapshotIntoDb = (localDbPath: string, snapshotPath: string) =>
mergeSnapshotIntoDbWith(openLibsqlSyncDb, localDbPath, snapshotPath);
// The engine executes on libsql in production; these merge tests run through
// that same driver. bun:sqlite is used only to build fixtures and inspect
// results.
import { createDbSnapshot } from '../../src/core/services/stats-sync/shared.js';
import { mergeSnapshotIntoDb } from '../../src/core/services/stats-sync/merge.js';
import {
createImmersionDbFixture,
insertFixtureSession,
+2 -11
View File
@@ -114,17 +114,8 @@ export interface Args {
useRofi: boolean;
history: boolean;
sync: boolean;
syncHost: string;
syncSnapshotPath: string;
syncMergePath: string;
syncDirection: 'both' | 'push' | 'pull';
syncRemoteCmd: string;
syncDbPath: string;
syncForce: boolean;
syncJson: boolean;
syncCheck: boolean;
syncMakeTemp: boolean;
syncRemoveTempPath: string;
/** App-owned sync argv tokens forwarded verbatim to `--sync-cli sync`. */
syncCliTokens: string[];
syncUi: boolean;
logLevel: LogLevel;
logRotation: LogRotation;