feat: add CLI commands to handle multiple remote configuration

This commit is contained in:
vorotamoroz
2026-05-20 05:10:42 +01:00
parent 67da3964e5
commit 1167b41340
6 changed files with 673 additions and 1 deletions

View File

@@ -74,6 +74,12 @@ livesync-cli [database-path] [command] [args...]
- `pull <src> <dst>`: Pull a file `<src>` from the database into local file `<dst>`.
- `cat <src>`: Read a file from the database and write to stdout.
- `put <dst>`: Read from stdin and write to the database path `<dst>`.
- `remote-add <name> <connstr>`: Add a remote configuration from a connection string.
- `remote-rm <remote-id>`: Remove a remote configuration by ID.
- `remote-ls`: List remote configurations (`id`, `name`, `active/inactive`, redacted URI).
- `remote-export <remote-id>`: Export the stored connection string by remote ID.
- `remote-set <remote-id> <connstr>`: Replace the stored connection string by remote ID.
- `remote-activate <remote-id>`: Activate a remote configuration by ID.
- `init-settings [file]`: Create a default settings file.
### Examples
@@ -252,6 +258,14 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json rm /
# Resolve conflict by keeping a specific revision
livesync-cli /path/to/your-local-database --settings /path/to/settings.json resolve /vault/path/file.md 3-abcdef
# Add/list/activate/remove remote configurations
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-add main "sls+https://user:pass@example.com/db"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-ls
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-export remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-set remote-abc123 "sls+p2p://room-abc?passphrase=secret"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-activate remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-rm remote-abc123
```
### Configuration

View File

@@ -3,6 +3,8 @@ import * as path from "path";
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
import { configURIBase } from "@lib/common/models/shared.const";
import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSettings } from "@lib/common/types";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import type { CLICommandContext, CLIOptions } from "./types";
import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelativePath } from "./utils";
@@ -10,6 +12,10 @@ import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
function redactConnectionString(uri: string): string {
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
}
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
const { databasePath, core, settingsPath } = context;
@@ -469,5 +475,206 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
return await performFullScan(core as any, log, errorManager, false, true);
}
if (options.command === "remote-add") {
if (options.commandArgs.length < 2) {
throw new Error("remote-add requires two arguments: <name> <connstr>");
}
const name = options.commandArgs[0].trim();
const connectionString = options.commandArgs[1].trim();
if (!name) {
throw new Error("remote-add requires a non-empty name");
}
if (!connectionString) {
throw new Error("remote-add requires a non-empty connection string");
}
const parsed = ConnectionStringParser.parse(connectionString);
const canonicalUri = ConnectionStringParser.serialize(parsed);
const id = createRemoteConfigurationId();
let activated = false;
await core.services.setting.updateSettings((currentSettings) => {
currentSettings.remoteConfigurations ||= {};
currentSettings.remoteConfigurations[id] = {
id,
name,
uri: canonicalUri,
isEncrypted: false,
};
if (!currentSettings.activeConfigurationId) {
currentSettings.activeConfigurationId = id;
const applied = activateRemoteConfiguration(currentSettings, id);
activated = applied !== false;
}
return currentSettings;
}, true);
if (activated) {
await core.services.control.applySettings();
}
process.stdout.write(`${id}\t${name}\t${redactConnectionString(canonicalUri)}\n`);
return true;
}
if (options.command === "remote-rm") {
if (options.commandArgs.length < 1) {
throw new Error("remote-rm requires one argument: <remote-id>");
}
const id = options.commandArgs[0].trim();
if (!id) {
throw new Error("remote-rm requires a non-empty remote-id");
}
const current = core.services.setting.currentSettings();
if (!current.remoteConfigurations?.[id]) {
process.stderr.write(`[Info] Remote configuration not found: ${id}\n`);
return false;
}
let switchedActive = false;
await core.services.setting.updateSettings((currentSettings) => {
const configs = currentSettings.remoteConfigurations || {};
delete configs[id];
currentSettings.remoteConfigurations = configs;
if (currentSettings.activeConfigurationId === id) {
const nextActiveId = Object.keys(configs)[0] || "";
currentSettings.activeConfigurationId = nextActiveId;
switchedActive = nextActiveId !== "";
if (nextActiveId !== "") {
activateRemoteConfiguration(currentSettings, nextActiveId);
}
}
if (currentSettings.P2P_ActiveRemoteConfigurationId === id) {
currentSettings.P2P_ActiveRemoteConfigurationId = "";
}
return currentSettings;
}, true);
if (switchedActive) {
await core.services.control.applySettings();
}
console.error(`[Command] remote-rm ${id}`);
return true;
}
if (options.command === "remote-ls") {
const settings = core.services.setting.currentSettings();
const configs = Object.values(settings.remoteConfigurations || {});
configs.sort((a, b) => a.name.localeCompare(b.name));
if (configs.length === 0) {
process.stderr.write("[Info] No remote configurations found.\n");
return true;
}
const lines = configs.map((config) => {
const status = config.id === settings.activeConfigurationId ? "active" : "inactive";
return `${config.id}\t${config.name}\t${status}\t${redactConnectionString(config.uri)}`;
});
process.stdout.write(lines.join("\n") + "\n");
return true;
}
if (options.command === "remote-export") {
if (options.commandArgs.length < 1) {
throw new Error("remote-export requires one argument: <remote-id>");
}
const id = options.commandArgs[0].trim();
if (!id) {
throw new Error("remote-export requires a non-empty remote-id");
}
const config = core.services.setting.currentSettings().remoteConfigurations?.[id];
if (!config) {
process.stderr.write(`[Info] Remote configuration not found: ${id}\n`);
return false;
}
process.stdout.write(`${config.uri}\n`);
return true;
}
if (options.command === "remote-set") {
if (options.commandArgs.length < 2) {
throw new Error("remote-set requires two arguments: <remote-id> <connstr>");
}
const id = options.commandArgs[0].trim();
const connectionString = options.commandArgs[1].trim();
if (!id) {
throw new Error("remote-set requires a non-empty remote-id");
}
if (!connectionString) {
throw new Error("remote-set requires a non-empty connection string");
}
const parsed = ConnectionStringParser.parse(connectionString);
const canonicalUri = ConnectionStringParser.serialize(parsed);
let switchedActive = false;
await core.services.setting.updateSettings((currentSettings) => {
const config = currentSettings.remoteConfigurations?.[id];
if (!config) {
return currentSettings;
}
config.uri = canonicalUri;
if (currentSettings.activeConfigurationId === id) {
const activated = activateRemoteConfiguration(currentSettings, id);
switchedActive = activated !== false;
if (activated) {
return activated;
}
}
return currentSettings;
}, true);
const updated = core.services.setting.currentSettings().remoteConfigurations?.[id];
if (!updated) {
process.stderr.write(`[Info] Remote configuration not found: ${id}\n`);
return false;
}
if (switchedActive) {
await core.services.control.applySettings();
}
console.error(`[Command] remote-set ${id}`);
return true;
}
if (options.command === "remote-activate") {
if (options.commandArgs.length < 1) {
throw new Error("remote-activate requires one argument: <remote-id>");
}
const id = options.commandArgs[0].trim();
if (!id) {
throw new Error("remote-activate requires a non-empty remote-id");
}
let switched = false;
await core.services.setting.updateSettings((currentSettings) => {
const activated = activateRemoteConfiguration(currentSettings, id);
if (activated) {
switched = true;
return activated;
}
return currentSettings;
}, true);
if (!switched) {
process.stderr.write(`[Info] Failed to activate remote configuration: ${id}\n`);
return false;
}
await core.services.control.applySettings();
console.error(`[Command] remote-activate ${id}`);
return true;
}
throw new Error(`Unsupported command: ${options.command}`);
}

View File

@@ -1,12 +1,19 @@
import * as processSetting from "@lib/API/processSetting";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { configURIBase } from "@lib/common/models/shared.const";
import { DEFAULT_SETTINGS } from "@lib/common/types";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@lib/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
import * as commandUtils from "./utils";
function createCoreMock() {
const liveSettings = {
...DEFAULT_SETTINGS,
remoteConfigurations: {},
activeConfigurationId: "",
P2P_ActiveRemoteConfigurationId: "",
} as any;
return {
services: {
control: {
@@ -16,6 +23,10 @@ function createCoreMock() {
setting: {
applyExternalSettings: vi.fn(async () => {}),
applyPartial: vi.fn(async () => {}),
currentSettings: vi.fn(() => liveSettings),
updateSettings: vi.fn(async (updater: any) => {
updater(liveSettings);
}),
},
},
serviceModules: {
@@ -56,6 +67,115 @@ async function createSetupURI(passphrase: string): Promise<string> {
return await processSetting.encodeSettingsToSetupURI(settings, passphrase);
}
function captureStdout() {
const writes: string[] = [];
const spy = vi.spyOn(process.stdout, "write").mockImplementation((chunk: any) => {
writes.push(typeof chunk === "string" ? chunk : String(chunk));
return true;
});
return {
spy,
lines: () =>
writes
.join("")
.split("\n")
.map((e) => e.trim())
.filter((e) => e.length > 0),
};
}
function parseAddedRemoteIdFromLines(lines: string[]): string {
// remote-add prints: <id>\t<name>\t<redacted-connstr>
const last = lines.length > 0 ? lines[lines.length - 1] : "";
return last.split("\t")[0] || "";
}
type ProtocolFixture = {
protocol: string;
connectionString: string;
assertProjectedFields: (settings: any) => void;
};
const protocolFixtures: ProtocolFixture[] = [
{
protocol: "couchdb",
connectionString: ConnectionStringParser.serialize({
type: "couchdb",
settings: {
couchDB_URI: "https://db.example.com:5984",
couchDB_USER: "user1",
couchDB_PASSWORD: "pass1",
couchDB_DBNAME: "vault1",
couchDB_CustomHeaders: "",
useJWT: false,
jwtAlgorithm: "",
jwtKey: "",
jwtKid: "",
jwtSub: "",
jwtExpDuration: 5,
useRequestAPI: false,
},
}),
assertProjectedFields: (settings) => {
expect(settings.remoteType).toBe(REMOTE_COUCHDB);
expect(settings.couchDB_URI).toBe("https://db.example.com:5984");
expect(settings.couchDB_USER).toBe("user1");
expect(settings.couchDB_PASSWORD).toBe("pass1");
expect(settings.couchDB_DBNAME).toBe("vault1");
},
},
{
protocol: "s3",
connectionString: ConnectionStringParser.serialize({
type: "s3",
settings: {
accessKey: "ak",
secretKey: "sk",
endpoint: "https://s3.example.com",
bucket: "bucket-1",
region: "ap-northeast-1",
bucketPrefix: "vault/",
useCustomRequestHandler: true,
bucketCustomHeaders: "x-test:1",
forcePathStyle: false,
},
}),
assertProjectedFields: (settings) => {
expect(settings.remoteType).toBe(REMOTE_MINIO);
expect(settings.accessKey).toBe("ak");
expect(settings.secretKey).toBe("sk");
expect(settings.endpoint).toBe("https://s3.example.com");
expect(settings.bucket).toBe("bucket-1");
expect(settings.region).toBe("ap-northeast-1");
},
},
{
protocol: "p2p",
connectionString: ConnectionStringParser.serialize({
type: "p2p",
settings: {
P2P_Enabled: false,
P2P_roomID: "room-abc",
P2P_passphrase: "pass-123",
P2P_relays: "wss://relay.example",
P2P_AppID: "self-hosted-livesync",
P2P_AutoStart: true,
P2P_AutoBroadcast: false,
P2P_turnServers: "turn:turn.example:3478",
P2P_turnUsername: "turn-user",
P2P_turnCredential: "turn-pass",
},
}),
assertProjectedFields: (settings) => {
expect(settings.remoteType).toBe(REMOTE_P2P);
expect(settings.P2P_roomID).toBe("room-abc");
expect(settings.P2P_passphrase).toBe("pass-123");
expect(settings.P2P_relays).toBe("wss://relay.example");
expect(settings.P2P_AppID).toBe("self-hosted-livesync");
},
},
];
describe("runCommand abnormal cases", () => {
const context = {
databasePath: "/tmp/vault",
@@ -202,4 +322,254 @@ describe("runCommand abnormal cases", () => {
expect(core.services.setting.applyExternalSettings).not.toHaveBeenCalled();
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
it("remote-add stores canonical URI and prints the created id", async () => {
const core = createCoreMock();
const stdout = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const result = await runCommand(makeOptions("remote-add", ["my-remote", "sls+https://example.com/db"]), {
...context,
core,
});
expect(result).toBe(true);
const settings = core.services.setting.currentSettings();
const ids = Object.keys(settings.remoteConfigurations);
expect(ids.length).toBe(1);
expect(settings.remoteConfigurations[ids[0]].name).toBe("my-remote");
expect(settings.remoteConfigurations[ids[0]].uri).toContain("sls+https://example.com/db");
expect(settings.activeConfigurationId).toBe(ids[0]);
expect(stdout).toHaveBeenCalled();
});
it("remote-activate switches active remote and applies settings", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
settings.remoteConfigurations.r2 = {
id: "r2",
name: "R2",
uri: "sls+https://example.com/db2",
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-activate", ["r2"]), {
...context,
core,
});
expect(result).toBe(true);
expect(settings.activeConfigurationId).toBe("r2");
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
});
it("remote-rm removes active remote and promotes first remaining", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
settings.remoteConfigurations.r2 = {
id: "r2",
name: "R2",
uri: "sls+https://example.com/db2",
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-rm", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
expect(settings.remoteConfigurations.r1).toBeUndefined();
expect(settings.activeConfigurationId).toBe("r2");
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
});
it("remote-export prints the exact stored connection string", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db?db=vault",
isEncrypted: false,
};
const stdout = captureStdout();
const result = await runCommand(makeOptions("remote-export", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
const outLines = stdout.lines();
expect(outLines.length > 0 ? outLines[outLines.length - 1] : "").toBe("sls+https://example.com/db?db=vault");
expect(stdout.spy).toHaveBeenCalled();
});
it("remote-set updates URI and applies settings when target is active", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://old.example/db",
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-set", ["r1", "sls+https://new.example/db"]), {
...context,
core,
});
expect(result).toBe(true);
expect(settings.remoteConfigurations.r1.uri).toContain("sls+https://new.example/db");
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
});
it.each(protocolFixtures)(
"remote-activate projects effective settings for $protocol",
async ({ connectionString, assertProjectedFields }) => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://old.example/?db=old",
isEncrypted: false,
};
settings.remoteConfigurations.r2 = {
id: "r2",
name: "R2",
uri: connectionString,
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-activate", ["r2"]), {
...context,
core,
});
expect(result).toBe(true);
expect(settings.activeConfigurationId).toBe("r2");
assertProjectedFields(settings);
}
);
it.each(protocolFixtures)(
"remote-set projects effective settings for active remote ($protocol)",
async ({ connectionString, assertProjectedFields }) => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://old.example/?db=old",
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-set", ["r1", connectionString]), {
...context,
core,
});
expect(result).toBe(true);
assertProjectedFields(settings);
}
);
it.each(protocolFixtures)(
"remote-rm projects promoted active remote effective settings for $protocol",
async ({ connectionString, assertProjectedFields }) => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://old.example/?db=old",
isEncrypted: false,
};
settings.remoteConfigurations.r2 = {
id: "r2",
name: "R2",
uri: connectionString,
isEncrypted: false,
};
settings.activeConfigurationId = "r1";
const result = await runCommand(makeOptions("remote-rm", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
expect(settings.activeConfigurationId).toBe("r2");
assertProjectedFields(settings);
}
);
it.each([
["couchdb", "sls+https://user:pass@example.com:5984/?db=vault"] as const,
[
"s3",
"sls+s3://ak:sk@example.com/?endpoint=https%3A%2F%2Fs3.example.com&bucket=my-bucket&region=ap-northeast-1",
] as const,
[
"p2p",
"sls+p2p://room-abc?passphrase=pass-123&relays=wss%3A%2F%2Frelay.example&appId=self-hosted-livesync",
] as const,
])("remote command round-trip works for %s", async (_protocol, initialConnStr) => {
const core = createCoreMock();
const addOut = captureStdout();
const addResult = await runCommand(makeOptions("remote-add", ["rt", initialConnStr]), {
...context,
core,
});
expect(addResult).toBe(true);
const remoteId = parseAddedRemoteIdFromLines(addOut.lines());
expect(remoteId).not.toBe("");
const export1Out = captureStdout();
const export1Result = await runCommand(makeOptions("remote-export", [remoteId]), {
...context,
core,
});
expect(export1Result).toBe(true);
const export1Lines = export1Out.lines();
const exported1 = export1Lines.length > 0 ? export1Lines[export1Lines.length - 1] : "";
expect(exported1).toBe(ConnectionStringParser.serialize(ConnectionStringParser.parse(initialConnStr)));
const roundTripInput = ConnectionStringParser.serialize(ConnectionStringParser.parse(exported1));
const setResult = await runCommand(makeOptions("remote-set", [remoteId, roundTripInput]), {
...context,
core,
});
expect(setResult).toBe(true);
const export2Out = captureStdout();
const export2Result = await runCommand(makeOptions("remote-export", [remoteId]), {
...context,
core,
});
expect(export2Result).toBe(true);
const export2Lines = export2Out.lines();
const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
expect(exported2).toBe(roundTripInput);
});
});

View File

@@ -20,6 +20,12 @@ export type CLICommand =
| "rm"
| "resolve"
| "mirror"
| "remote-add"
| "remote-rm"
| "remote-ls"
| "remote-export"
| "remote-set"
| "remote-activate"
| "init-settings";
export interface CLIOptions {
@@ -67,5 +73,11 @@ export const VALID_COMMANDS = new Set([
"rm",
"resolve",
"mirror",
"remote-add",
"remote-rm",
"remote-ls",
"remote-export",
"remote-set",
"remote-activate",
"init-settings",
] as const);

View File

@@ -62,6 +62,16 @@ Commands:
rm <path> Mark a file as deleted in local database
resolve <path> <rev> Resolve conflicts by keeping <rev> and deleting others
mirror [vault-path] Mirror database contents to the local file system (vault-path defaults to database-path)
remote-add <name> <connstr>
Add a remote configuration from a connection string
remote-rm <remote-id> Remove a remote configuration by ID
remote-ls List stored remote configurations
remote-export <remote-id>
Export a remote connection string by ID
remote-set <remote-id> <connstr>
Replace a stored remote connection string by ID
remote-activate <remote-id>
Activate a stored remote configuration by ID
Options:
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
@@ -84,6 +94,12 @@ Examples:
livesync-cli ./my-database info notes/hello.md
livesync-cli ./my-database rm notes/hello.md
livesync-cli ./my-database resolve notes/hello.md 3-abcdef
livesync-cli ./my-database remote-add my-remote "sls+https://user:pass@example.com/db"
livesync-cli ./my-database remote-ls
livesync-cli ./my-database remote-export remote-abc123
livesync-cli ./my-database remote-set remote-abc123 "sls+s3://ak:sk@example.com/?endpoint=https%3A%2F%2Fs3.example.com&bucket=mybucket"
livesync-cli ./my-database remote-activate remote-abc123
livesync-cli ./my-database remote-rm remote-abc123
livesync-cli init-settings ./data.json
livesync-cli ./my-database --verbose
`);
@@ -229,6 +245,9 @@ export async function main() {
options.command === "cat" ||
options.command === "cat-rev" ||
options.command === "ls" ||
options.command === "remote-add" ||
options.command === "remote-ls" ||
options.command === "remote-export" ||
options.command === "p2p-peers" ||
options.command === "info" ||
options.command === "rm" ||

View File

@@ -86,6 +86,56 @@ describe("CLI parseArgs", () => {
expect(parsed.commandArgs).toEqual([]);
});
it("parses remote-add command", () => {
process.argv = [
"node",
"livesync-cli",
"./databasePath",
"remote-add",
"my-remote",
"sls+https://user:pass@example.com/db",
];
const parsed = parseArgs();
expect(parsed.databasePath).toBe("./databasePath");
expect(parsed.command).toBe("remote-add");
expect(parsed.commandArgs).toEqual(["my-remote", "sls+https://user:pass@example.com/db"]);
});
it("parses remote-activate command", () => {
process.argv = ["node", "livesync-cli", "./databasePath", "remote-activate", "remote-abc"];
const parsed = parseArgs();
expect(parsed.databasePath).toBe("./databasePath");
expect(parsed.command).toBe("remote-activate");
expect(parsed.commandArgs).toEqual(["remote-abc"]);
});
it("parses remote-export command", () => {
process.argv = ["node", "livesync-cli", "./databasePath", "remote-export", "remote-abc"];
const parsed = parseArgs();
expect(parsed.databasePath).toBe("./databasePath");
expect(parsed.command).toBe("remote-export");
expect(parsed.commandArgs).toEqual(["remote-abc"]);
});
it("parses remote-set command", () => {
process.argv = [
"node",
"livesync-cli",
"./databasePath",
"remote-set",
"remote-abc",
"sls+p2p://room-1?passphrase=abc",
];
const parsed = parseArgs();
expect(parsed.databasePath).toBe("./databasePath");
expect(parsed.command).toBe("remote-set");
expect(parsed.commandArgs).toEqual(["remote-abc", "sls+p2p://room-1?passphrase=abc"]);
});
it("parses --interval flag with valid integer", () => {
process.argv = ["node", "livesync-cli", "./vault", "--interval", "30"];
const parsed = parseArgs();