mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-09 16:00:15 +00:00
Merge remote-tracking branch 'origin/main' into fix_941
This commit is contained in:
+11
-1
@@ -83,6 +83,10 @@ livesync-cli [database-path] [command] [args...]
|
||||
- `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.
|
||||
- `mark-resolved [remote-id]`: Resolve remote synchronisation status.
|
||||
- `unlock-remote [remote-id]`: Unlock the remote database.
|
||||
- `lock-remote [remote-id]`: Lock the remote database.
|
||||
- `remote-status [remote-id]`: Show remote database status.
|
||||
- `init-settings [file]`: Create a default settings file.
|
||||
|
||||
### Examples
|
||||
@@ -262,13 +266,19 @@ 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
|
||||
# Add, list, activate, and 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
|
||||
|
||||
# Lock, unlock, resolve, and view status of remote database
|
||||
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-status remote-abc123
|
||||
livesync-cli /path/to/your-local-database --settings /path/to/settings.json lock-remote remote-abc123
|
||||
livesync-cli /path/to/your-local-database --settings /path/to/settings.json mark-resolved remote-abc123
|
||||
livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlock-remote remote-abc123
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -2,7 +2,14 @@ import * as fs from "fs/promises";
|
||||
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 {
|
||||
DEFAULT_SETTINGS,
|
||||
MILESTONE_DOCID,
|
||||
type FilePathWithPrefix,
|
||||
type ObsidianLiveSyncSettings,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
} 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";
|
||||
@@ -16,6 +23,51 @@ function redactConnectionString(uri: string): string {
|
||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||
}
|
||||
|
||||
async function verifyRemoteState(
|
||||
core: CLICommandContext["core"],
|
||||
settings: ObsidianLiveSyncSettings
|
||||
): Promise<boolean> {
|
||||
const replicator = core.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
process.stderr.write("[Verification] No active replicator found\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!replicator.nodeid) {
|
||||
await replicator.initializeDatabaseForReplication();
|
||||
}
|
||||
|
||||
try {
|
||||
let milestone: any;
|
||||
if (settings.remoteType === REMOTE_COUCHDB) {
|
||||
const dbRet = await (replicator as any).connectRemoteCouchDBWithSetting(settings, false, true);
|
||||
if (typeof dbRet === "string") {
|
||||
process.stderr.write(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
|
||||
return false;
|
||||
}
|
||||
milestone = await dbRet.db.get(MILESTONE_DOCID);
|
||||
} else if (settings.remoteType === REMOTE_MINIO) {
|
||||
milestone = await (replicator as any).client.downloadJson("_00000000-milestone.json");
|
||||
}
|
||||
|
||||
if (milestone) {
|
||||
const isLocked = !!milestone.locked;
|
||||
const isAccepted = !!milestone.accepted_nodes?.includes(replicator.nodeid);
|
||||
process.stderr.write(`[Verification] Remote Database: ${isLocked ? "LOCKED" : "UNLOCKED"}\n`);
|
||||
process.stderr.write(
|
||||
`[Verification] Current Device Node ID (${replicator.nodeid}): ${isAccepted ? "ACCEPTED" : "NOT ACCEPTED"}\n`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
process.stderr.write("[Verification] Milestone document not found on remote.\n");
|
||||
return false;
|
||||
}
|
||||
} catch (e: any) {
|
||||
process.stderr.write(`[Verification] Failed to fetch milestone document: ${e?.message || e}\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
|
||||
const { databasePath, core, settingsPath } = context;
|
||||
|
||||
@@ -646,7 +698,6 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
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>");
|
||||
@@ -676,5 +727,126 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "mark-resolved") {
|
||||
const id = options.commandArgs[0]?.trim();
|
||||
if (id) {
|
||||
let switched = false;
|
||||
await core.services.setting.updateSettings((currentSettings) => {
|
||||
const activated = activateRemoteConfiguration(currentSettings, id);
|
||||
if (activated) {
|
||||
switched = true;
|
||||
return activated;
|
||||
}
|
||||
return currentSettings;
|
||||
}, false);
|
||||
|
||||
if (!switched) {
|
||||
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await core.services.control.applySettings();
|
||||
}
|
||||
|
||||
console.error(`[Command] mark-resolved${id ? ` ${id}` : ""}`);
|
||||
await core.services.replication.markResolved();
|
||||
const settings = core.services.setting.currentSettings();
|
||||
await verifyRemoteState(core, settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "unlock-remote") {
|
||||
const id = options.commandArgs[0]?.trim();
|
||||
if (id) {
|
||||
let switched = false;
|
||||
await core.services.setting.updateSettings((currentSettings) => {
|
||||
const activated = activateRemoteConfiguration(currentSettings, id);
|
||||
if (activated) {
|
||||
switched = true;
|
||||
return activated;
|
||||
}
|
||||
return currentSettings;
|
||||
}, false);
|
||||
|
||||
if (!switched) {
|
||||
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await core.services.control.applySettings();
|
||||
}
|
||||
|
||||
console.error(`[Command] unlock-remote${id ? ` ${id}` : ""}`);
|
||||
await core.services.replication.markUnlocked();
|
||||
const settings = core.services.setting.currentSettings();
|
||||
await verifyRemoteState(core, settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "lock-remote") {
|
||||
const id = options.commandArgs[0]?.trim();
|
||||
if (id) {
|
||||
let switched = false;
|
||||
await core.services.setting.updateSettings((currentSettings) => {
|
||||
const activated = activateRemoteConfiguration(currentSettings, id);
|
||||
if (activated) {
|
||||
switched = true;
|
||||
return activated;
|
||||
}
|
||||
return currentSettings;
|
||||
}, false);
|
||||
|
||||
if (!switched) {
|
||||
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await core.services.control.applySettings();
|
||||
}
|
||||
|
||||
console.error(`[Command] lock-remote${id ? ` ${id}` : ""}`);
|
||||
await core.services.replication.markLocked();
|
||||
const settings = core.services.setting.currentSettings();
|
||||
await verifyRemoteState(core, settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "remote-status") {
|
||||
const id = options.commandArgs[0]?.trim();
|
||||
if (id) {
|
||||
let switched = false;
|
||||
await core.services.setting.updateSettings((currentSettings) => {
|
||||
const activated = activateRemoteConfiguration(currentSettings, id);
|
||||
if (activated) {
|
||||
switched = true;
|
||||
return activated;
|
||||
}
|
||||
return currentSettings;
|
||||
}, false);
|
||||
|
||||
if (!switched) {
|
||||
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await core.services.control.applySettings();
|
||||
}
|
||||
|
||||
console.error(`[Command] remote-status${id ? ` ${id}` : ""}`);
|
||||
const replicator = core.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
process.stderr.write("[Error] No active replicator found\n");
|
||||
return false;
|
||||
}
|
||||
const settings = core.services.setting.currentSettings();
|
||||
const status = await replicator.getRemoteStatus(settings);
|
||||
if (status === false) {
|
||||
process.stderr.write("[Error] Failed to fetch remote status\n");
|
||||
return false;
|
||||
}
|
||||
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported command: ${options.command}`);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,34 @@ function createCoreMock() {
|
||||
updater(liveSettings);
|
||||
}),
|
||||
},
|
||||
replication: {
|
||||
markResolved: vi.fn(async () => {}),
|
||||
markUnlocked: vi.fn(async () => {}),
|
||||
markLocked: vi.fn(async () => {}),
|
||||
},
|
||||
replicator: {
|
||||
getActiveReplicator: vi.fn(() => ({
|
||||
nodeid: "test-node-id",
|
||||
initializeDatabaseForReplication: vi.fn(async () => {}),
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({
|
||||
db: {
|
||||
get: vi.fn(async (id) => {
|
||||
if (id.includes("milestone")) {
|
||||
return {
|
||||
locked: false,
|
||||
accepted_nodes: ["test-node-id"],
|
||||
};
|
||||
}
|
||||
throw new Error("not found");
|
||||
}),
|
||||
},
|
||||
})),
|
||||
getRemoteStatus: vi.fn(async () => ({
|
||||
db_name: "test-db",
|
||||
doc_count: 42,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
},
|
||||
serviceModules: {
|
||||
fileHandler: {
|
||||
@@ -572,4 +600,139 @@ describe("runCommand abnormal cases", () => {
|
||||
const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
|
||||
expect(exported2).toBe(roundTripInput);
|
||||
});
|
||||
|
||||
describe("mark-resolved and unlock-remote commands", () => {
|
||||
it("mark-resolved without args runs on active database", async () => {
|
||||
const core = createCoreMock();
|
||||
const result = await runCommand(makeOptions("mark-resolved", []), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mark-resolved with remote-id temporarily activates it and runs markResolved", 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,
|
||||
};
|
||||
|
||||
const result = await runCommand(makeOptions("mark-resolved", ["r1"]), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
|
||||
expect(settings.activeConfigurationId).toBe("r1");
|
||||
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
|
||||
});
|
||||
|
||||
it("unlock-remote without args runs on active database", async () => {
|
||||
const core = createCoreMock();
|
||||
const result = await runCommand(makeOptions("unlock-remote", []), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unlock-remote with remote-id temporarily activates it and runs markUnlocked", 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,
|
||||
};
|
||||
|
||||
const result = await runCommand(makeOptions("unlock-remote", ["r1"]), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
|
||||
expect(settings.activeConfigurationId).toBe("r1");
|
||||
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
|
||||
});
|
||||
|
||||
it("lock-remote without args runs on active database", async () => {
|
||||
const core = createCoreMock();
|
||||
const result = await runCommand(makeOptions("lock-remote", []), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lock-remote with remote-id temporarily activates it and runs markLocked", 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,
|
||||
};
|
||||
|
||||
const result = await runCommand(makeOptions("lock-remote", ["r1"]), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
|
||||
expect(settings.activeConfigurationId).toBe("r1");
|
||||
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
|
||||
});
|
||||
|
||||
it("remote-status without args outputs status of active remote configuration", async () => {
|
||||
const core = createCoreMock();
|
||||
const stdout = captureStdout();
|
||||
const result = await runCommand(makeOptions("remote-status", []), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
const fullOutput = stdout.spy.mock.calls.map((c) => c[0]).join("");
|
||||
const parsedStatus = JSON.parse(fullOutput);
|
||||
expect(parsedStatus.db_name).toBe("test-db");
|
||||
expect(parsedStatus.doc_count).toBe(42);
|
||||
});
|
||||
|
||||
it("remote-status with remote-id temporarily activates it and outputs status", 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,
|
||||
};
|
||||
const stdout = captureStdout();
|
||||
const result = await runCommand(makeOptions("remote-status", ["r1"]), {
|
||||
...context,
|
||||
core,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
const fullOutput = stdout.spy.mock.calls.map((c) => c[0]).join("");
|
||||
const parsedStatus = JSON.parse(fullOutput);
|
||||
expect(parsedStatus.db_name).toBe("test-db");
|
||||
expect(parsedStatus.doc_count).toBe(42);
|
||||
expect(settings.activeConfigurationId).toBe("r1");
|
||||
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,10 @@ export type CLICommand =
|
||||
| "remote-export"
|
||||
| "remote-set"
|
||||
| "remote-activate"
|
||||
| "mark-resolved"
|
||||
| "unlock-remote"
|
||||
| "lock-remote"
|
||||
| "remote-status"
|
||||
| "init-settings";
|
||||
|
||||
export interface CLIOptions {
|
||||
@@ -80,5 +84,9 @@ export const VALID_COMMANDS = new Set([
|
||||
"remote-export",
|
||||
"remote-set",
|
||||
"remote-activate",
|
||||
"mark-resolved",
|
||||
"unlock-remote",
|
||||
"lock-remote",
|
||||
"remote-status",
|
||||
"init-settings",
|
||||
] as const);
|
||||
|
||||
+17
-1
@@ -72,6 +72,14 @@ Commands:
|
||||
Replace a stored remote connection string by ID
|
||||
remote-activate <remote-id>
|
||||
Activate a stored remote configuration by ID
|
||||
mark-resolved [remote-id]
|
||||
Resolve remote synchronisation status
|
||||
unlock-remote [remote-id]
|
||||
Unlock remote database
|
||||
lock-remote [remote-id]
|
||||
Lock remote database
|
||||
remote-status [remote-id]
|
||||
Show remote database status
|
||||
|
||||
Options:
|
||||
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
|
||||
@@ -102,6 +110,10 @@ Examples:
|
||||
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 ./my-database mark-resolved remote-abc123
|
||||
livesync-cli ./my-database unlock-remote remote-abc123
|
||||
livesync-cli ./my-database lock-remote remote-abc123
|
||||
livesync-cli ./my-database remote-status remote-abc123
|
||||
livesync-cli init-settings ./data.json
|
||||
livesync-cli ./my-database --verbose
|
||||
`);
|
||||
@@ -265,7 +277,11 @@ export async function main() {
|
||||
options.command === "p2p-peers" ||
|
||||
options.command === "info" ||
|
||||
options.command === "rm" ||
|
||||
options.command === "resolve";
|
||||
options.command === "resolve" ||
|
||||
options.command === "mark-resolved" ||
|
||||
options.command === "unlock-remote" ||
|
||||
options.command === "lock-remote" ||
|
||||
options.command === "remote-status";
|
||||
const infoLog = avoidStdoutNoise ? console.error : console.log;
|
||||
if (options.debug) {
|
||||
setGlobalLogFunction((msg, level) => {
|
||||
|
||||
@@ -25,16 +25,18 @@
|
||||
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
|
||||
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:mirror": "bash test/test-mirror-linux.sh",
|
||||
"test:e2e:remote-commands": "bash test/test-remote-commands-linux.sh",
|
||||
"pretest:e2e:all": "npm run build",
|
||||
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:p2p",
|
||||
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:remote-commands",
|
||||
"pretest:e2e:docker:all": "npm run build:docker",
|
||||
"test:e2e:docker:push-pull": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-push-pull-linux.sh",
|
||||
"test:e2e:docker:setup-put-cat": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-setup-put-cat-linux.sh",
|
||||
"test:e2e:docker:mirror": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-mirror-linux.sh",
|
||||
"test:e2e:docker:remote-commands": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-remote-commands-linux.sh",
|
||||
"test:e2e:docker:sync-two-local": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-sync-two-local-databases-linux.sh",
|
||||
"test:e2e:docker:p2p": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-three-nodes-conflict-linux.sh",
|
||||
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror"
|
||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test: CLI remote management commands: remote-status, unlock-remote, and mark-resolved.
|
||||
#
|
||||
# Scenario:
|
||||
# 1. Start CouchDB, create a test database, and perform an initial sync.
|
||||
# 2. Run remote-status and assert that the output contains the database name in JSON format.
|
||||
# 3. Lock the remote database milestone manually using curl, verify status, and run unlock-remote.
|
||||
# Assert that the output of unlock-remote contains the unlocked verification status.
|
||||
# 4. Run mark-resolved and verify it succeeds.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$CLI_DIR"
|
||||
source "$SCRIPT_DIR/test-helpers.sh"
|
||||
display_test_info
|
||||
|
||||
RUN_BUILD="${RUN_BUILD:-1}"
|
||||
TEST_ENV_FILE="${TEST_ENV_FILE:-$CLI_DIR/.test.env}"
|
||||
cli_test_init_cli_cmd
|
||||
|
||||
if [[ ! -f "$TEST_ENV_FILE" ]]; then
|
||||
echo "[ERROR] test env file not found: $TEST_ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
source "$TEST_ENV_FILE"
|
||||
set +a
|
||||
|
||||
DB_SUFFIX="$(date +%s)-$RANDOM"
|
||||
|
||||
COUCHDB_URI="${hostname%/}"
|
||||
COUCHDB_DBNAME="${dbname}-remotes-${DB_SUFFIX}"
|
||||
COUCHDB_USER="${username:-}"
|
||||
COUCHDB_PASSWORD="${password:-}"
|
||||
|
||||
if [[ -z "$COUCHDB_URI" || -z "$COUCHDB_USER" || -z "$COUCHDB_PASSWORD" ]]; then
|
||||
echo "[ERROR] COUCHDB_URI, COUCHDB_USER, and COUCHDB_PASSWORD are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-remote-cmds.XXXXXX")"
|
||||
VAULT_DIR="$WORK_DIR/vault"
|
||||
SETTINGS_FILE="$WORK_DIR/settings.json"
|
||||
mkdir -p "$VAULT_DIR"
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
cli_test_stop_couchdb
|
||||
rm -rf "$WORK_DIR"
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "$RUN_BUILD" == "1" ]]; then
|
||||
echo "[INFO] building CLI"
|
||||
npm run build
|
||||
fi
|
||||
|
||||
echo "[INFO] starting CouchDB and creating test database: $COUCHDB_DBNAME"
|
||||
cli_test_start_couchdb "$COUCHDB_URI" "$COUCHDB_USER" "$COUCHDB_PASSWORD" "$COUCHDB_DBNAME"
|
||||
|
||||
echo "[INFO] preparing settings"
|
||||
cli_test_init_settings_file "$SETTINGS_FILE"
|
||||
echo ".."
|
||||
cli_test_apply_couchdb_settings "$SETTINGS_FILE" "$COUCHDB_URI" "$COUCHDB_USER" "$COUCHDB_PASSWORD" "$COUCHDB_DBNAME" 1
|
||||
echo "..."
|
||||
|
||||
echo "[INFO] initial sync to create milestone document"
|
||||
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" sync >/dev/null
|
||||
|
||||
MILESTONE_ID="_local/obsydian_livesync_milestone"
|
||||
MILESTONE_URL="${COUCHDB_URI}/${COUCHDB_DBNAME}/${MILESTONE_ID}"
|
||||
|
||||
update_milestone() {
|
||||
local locked="$1"
|
||||
local accepted_nodes="$2"
|
||||
local current
|
||||
current="$(cli_test_curl_json --user "${COUCHDB_USER}:${COUCHDB_PASSWORD}" "$MILESTONE_URL")"
|
||||
local updated
|
||||
updated="$(node -e '
|
||||
const doc = JSON.parse(process.argv[1]);
|
||||
doc.locked = process.argv[2] === "true";
|
||||
doc.accepted_nodes = JSON.parse(process.argv[3]);
|
||||
process.stdout.write(JSON.stringify(doc));
|
||||
' "$current" "$locked" "$accepted_nodes")"
|
||||
cli_test_curl_json -X PUT \
|
||||
--user "${COUCHDB_USER}:${COUCHDB_PASSWORD}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$updated" \
|
||||
"$MILESTONE_URL" >/dev/null
|
||||
}
|
||||
|
||||
CMD_LOG="$WORK_DIR/cmd.log"
|
||||
|
||||
echo "[CASE] remote-status outputs valid JSON with CouchDB details"
|
||||
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" remote-status >"$CMD_LOG" 2>&1
|
||||
|
||||
cli_test_assert_contains "$(cat "$CMD_LOG")" \
|
||||
"\"db_name\": \"$COUCHDB_DBNAME\"" \
|
||||
"remote-status should return JSON containing db_name"
|
||||
|
||||
echo "[PASS] remote-status verified"
|
||||
|
||||
echo "[CASE] lock-remote locks and verifies state"
|
||||
# Run lock-remote and verify output contains verification message
|
||||
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" lock-remote >"$CMD_LOG" 2>&1
|
||||
|
||||
cli_test_assert_contains "$(cat "$CMD_LOG")" \
|
||||
"[Verification] Remote Database: LOCKED" \
|
||||
"lock-remote output should show that the remote database is locked"
|
||||
|
||||
echo "[PASS] lock-remote verified"
|
||||
|
||||
echo "[CASE] unlock-remote unlocks and verifies state"
|
||||
# Manually lock milestone
|
||||
update_milestone "true" "[]"
|
||||
|
||||
# Run unlock-remote and verify output contains verification message
|
||||
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" unlock-remote >"$CMD_LOG" 2>&1
|
||||
|
||||
cli_test_assert_contains "$(cat "$CMD_LOG")" \
|
||||
"[Verification] Remote Database: UNLOCKED" \
|
||||
"unlock-remote output should contain verification status"
|
||||
|
||||
echo "[PASS] unlock-remote verified"
|
||||
|
||||
echo "[CASE] mark-resolved resolves and verifies state"
|
||||
# Manually lock milestone
|
||||
update_milestone "true" "[]"
|
||||
|
||||
# Run mark-resolved and verify output contains verification message
|
||||
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" mark-resolved >"$CMD_LOG" 2>&1
|
||||
|
||||
cli_test_assert_contains "$(cat "$CMD_LOG")" \
|
||||
"[Verification] Remote Database: LOCKED" \
|
||||
"mark-resolved output should show that the remote database remains locked"
|
||||
|
||||
cli_test_assert_contains "$(cat "$CMD_LOG")" \
|
||||
"ACCEPTED" \
|
||||
"mark-resolved output should show that the current device node is accepted"
|
||||
|
||||
echo "[PASS] mark-resolved verified"
|
||||
|
||||
echo "[ALL PASS] All remote CLI commands verified successfully"
|
||||
+10
-1
@@ -9,7 +9,16 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan.
|
||||
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941).
|
||||
- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911).
|
||||
|
||||
## Only CLI
|
||||
|
||||
8th June, 2026
|
||||
|
||||
I should also consider the version numbering for the CLI...
|
||||
|
||||
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
|
||||
|
||||
## 0.25.73
|
||||
|
||||
|
||||
Reference in New Issue
Block a user