mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 10:37:06 +00:00
Merge branch 'main' into fix_911
This commit is contained in:
+17
-2
@@ -61,6 +61,9 @@ livesync-cli [database-path] [command] [args...]
|
|||||||
|
|
||||||
- `database-path`: Path to the directory where `.livesync` folder and `settings.json` are (or will be) located.
|
- `database-path`: Path to the directory where `.livesync` folder and `settings.json` are (or will be) located.
|
||||||
- Note: In previous versions, this was referred to as the "vault" path. Now it is clearly distinguished from the actual vault (the directory containing your `.md` files).
|
- Note: In previous versions, this was referred to as the "vault" path. Now it is clearly distinguished from the actual vault (the directory containing your `.md` files).
|
||||||
|
- `--vault <path>` / `-V <path>`: (daemon/mirror only) Path to the vault directory containing `.md` files.
|
||||||
|
- Allows the PouchDB database directory and the actual vault directory to be different locations.
|
||||||
|
- For `mirror` command, the positional `[vault-path]` argument takes precedence over `--vault`.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
@@ -80,6 +83,10 @@ livesync-cli [database-path] [command] [args...]
|
|||||||
- `remote-export <remote-id>`: Export the stored connection string by remote ID.
|
- `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-set <remote-id> <connstr>`: Replace the stored connection string by remote ID.
|
||||||
- `remote-activate <remote-id>`: Activate a remote configuration by 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.
|
- `init-settings [file]`: Create a default settings file.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
@@ -259,13 +266,19 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json rm /
|
|||||||
# Resolve conflict by keeping a specific revision
|
# 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
|
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-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-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-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-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-activate remote-abc123
|
||||||
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-rm 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
|
### Configuration
|
||||||
@@ -312,6 +325,7 @@ Options:
|
|||||||
--verbose, -v Enable verbose logging
|
--verbose, -v Enable verbose logging
|
||||||
--debug, -d Enable debug logging (includes verbose)
|
--debug, -d Enable debug logging (includes verbose)
|
||||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||||
|
--vault <path>, -V <path> (daemon/mirror) Path to vault directory, decoupled from database-path
|
||||||
--help, -h Show this help message
|
--help, -h Show this help message
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
@@ -332,7 +346,8 @@ Commands:
|
|||||||
info <path> Show file metadata including current and past revisions, conflicts, and chunk list
|
info <path> Show file metadata including current and past revisions, conflicts, and chunk list
|
||||||
rm <path> Mark file as deleted in local database
|
rm <path> Mark file as deleted in local database
|
||||||
resolve <path> <rev> Resolve conflict by keeping the specified revision
|
resolve <path> <rev> Resolve conflict by keeping the specified revision
|
||||||
mirror [vaultPath] Mirror database contents to the local file system (vaultPath defaults to database-path)
|
mirror [vaultPath] Mirror database contents to the local file system
|
||||||
|
(vaultPath positional arg > --vault flag > database-path)
|
||||||
```
|
```
|
||||||
|
|
||||||
Run via npm script:
|
Run via npm script:
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import * as fs from "fs/promises";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
|
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
|
||||||
import { configURIBase } from "@lib/common/models/shared.const";
|
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 { ConnectionStringParser } from "@lib/common/ConnectionString";
|
||||||
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
|
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
|
||||||
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
||||||
@@ -16,6 +23,51 @@ function redactConnectionString(uri: string): string {
|
|||||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
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> {
|
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
|
||||||
const { databasePath, core, settingsPath } = context;
|
const { databasePath, core, settingsPath } = context;
|
||||||
|
|
||||||
@@ -646,7 +698,6 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
console.error(`[Command] remote-set ${id}`);
|
console.error(`[Command] remote-set ${id}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.command === "remote-activate") {
|
if (options.command === "remote-activate") {
|
||||||
if (options.commandArgs.length < 1) {
|
if (options.commandArgs.length < 1) {
|
||||||
throw new Error("remote-activate requires one argument: <remote-id>");
|
throw new Error("remote-activate requires one argument: <remote-id>");
|
||||||
@@ -676,5 +727,126 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
return true;
|
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}`);
|
throw new Error(`Unsupported command: ${options.command}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,34 @@ function createCoreMock() {
|
|||||||
updater(liveSettings);
|
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: {
|
serviceModules: {
|
||||||
fileHandler: {
|
fileHandler: {
|
||||||
@@ -572,4 +600,139 @@ describe("runCommand abnormal cases", () => {
|
|||||||
const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
|
const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
|
||||||
expect(exported2).toBe(roundTripInput);
|
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,10 +26,15 @@ export type CLICommand =
|
|||||||
| "remote-export"
|
| "remote-export"
|
||||||
| "remote-set"
|
| "remote-set"
|
||||||
| "remote-activate"
|
| "remote-activate"
|
||||||
|
| "mark-resolved"
|
||||||
|
| "unlock-remote"
|
||||||
|
| "lock-remote"
|
||||||
|
| "remote-status"
|
||||||
| "init-settings";
|
| "init-settings";
|
||||||
|
|
||||||
export interface CLIOptions {
|
export interface CLIOptions {
|
||||||
databasePath?: string;
|
databasePath?: string;
|
||||||
|
vaultPath?: string;
|
||||||
settingsPath?: string;
|
settingsPath?: string;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
@@ -79,5 +84,9 @@ export const VALID_COMMANDS = new Set([
|
|||||||
"remote-export",
|
"remote-export",
|
||||||
"remote-set",
|
"remote-set",
|
||||||
"remote-activate",
|
"remote-activate",
|
||||||
|
"mark-resolved",
|
||||||
|
"unlock-remote",
|
||||||
|
"lock-remote",
|
||||||
|
"remote-status",
|
||||||
"init-settings",
|
"init-settings",
|
||||||
] as const);
|
] as const);
|
||||||
|
|||||||
+47
-10
@@ -61,7 +61,7 @@ Commands:
|
|||||||
info <path> Show detailed metadata for a file (ID, revision, conflicts, chunks)
|
info <path> Show detailed metadata for a file (ID, revision, conflicts, chunks)
|
||||||
rm <path> Mark a file as deleted in local database
|
rm <path> Mark a file as deleted in local database
|
||||||
resolve <path> <rev> Resolve conflicts by keeping <rev> and deleting others
|
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)
|
mirror [vault-path] Mirror database contents to the local file system (vault-path takes precedence over --vault; defaults to vault from --vault / database-path)
|
||||||
remote-add <name> <connstr>
|
remote-add <name> <connstr>
|
||||||
Add a remote configuration from a connection string
|
Add a remote configuration from a connection string
|
||||||
remote-rm <remote-id> Remove a remote configuration by ID
|
remote-rm <remote-id> Remove a remote configuration by ID
|
||||||
@@ -72,8 +72,18 @@ Commands:
|
|||||||
Replace a stored remote connection string by ID
|
Replace a stored remote connection string by ID
|
||||||
remote-activate <remote-id>
|
remote-activate <remote-id>
|
||||||
Activate a stored remote configuration by 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:
|
Options:
|
||||||
|
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
|
||||||
|
(defaults to database-path; allows separate PouchDB and vault dirs)
|
||||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -100,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-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-activate remote-abc123
|
||||||
livesync-cli ./my-database remote-rm 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 init-settings ./data.json
|
||||||
livesync-cli ./my-database --verbose
|
livesync-cli ./my-database --verbose
|
||||||
`);
|
`);
|
||||||
@@ -114,6 +128,7 @@ export function parseArgs(): CLIOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let databasePath: string | undefined;
|
let databasePath: string | undefined;
|
||||||
|
let vaultPath: string | undefined;
|
||||||
let settingsPath: string | undefined;
|
let settingsPath: string | undefined;
|
||||||
let verbose = false;
|
let verbose = false;
|
||||||
let debug = false;
|
let debug = false;
|
||||||
@@ -125,6 +140,16 @@ export function parseArgs(): CLIOptions {
|
|||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const token = args[i];
|
const token = args[i];
|
||||||
switch (token) {
|
switch (token) {
|
||||||
|
case "--vault":
|
||||||
|
case "-V": {
|
||||||
|
i++;
|
||||||
|
if (!args[i]) {
|
||||||
|
console.error(`Error: Missing value for ${token}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
vaultPath = args[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "--settings":
|
case "--settings":
|
||||||
case "-s": {
|
case "-s": {
|
||||||
i++;
|
i++;
|
||||||
@@ -198,6 +223,7 @@ export function parseArgs(): CLIOptions {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
databasePath,
|
databasePath,
|
||||||
|
vaultPath,
|
||||||
settingsPath,
|
settingsPath,
|
||||||
verbose,
|
verbose,
|
||||||
debug,
|
debug,
|
||||||
@@ -251,7 +277,11 @@ export async function main() {
|
|||||||
options.command === "p2p-peers" ||
|
options.command === "p2p-peers" ||
|
||||||
options.command === "info" ||
|
options.command === "info" ||
|
||||||
options.command === "rm" ||
|
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;
|
const infoLog = avoidStdoutNoise ? console.error : console.log;
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
setGlobalLogFunction((msg, level) => {
|
setGlobalLogFunction((msg, level) => {
|
||||||
@@ -290,16 +320,23 @@ export async function main() {
|
|||||||
: path.join(databasePath, SETTINGS_FILE);
|
: path.join(databasePath, SETTINGS_FILE);
|
||||||
configureNodeLocalStorage(path.join(databasePath, ".livesync", "runtime", "local-storage.json"));
|
configureNodeLocalStorage(path.join(databasePath, ".livesync", "runtime", "local-storage.json"));
|
||||||
|
|
||||||
infoLog(`Self-hosted LiveSync CLI`);
|
// Resolve vault path: mirror positional argument takes priority,
|
||||||
infoLog(`Database Path: ${databasePath}`);
|
// then --vault flag, otherwise fall back to databasePath.
|
||||||
infoLog(`Settings: ${settingsPath}`);
|
// For daemon mode, enable chokidar file watching so the _changes feed picks up events.
|
||||||
infoLog("");
|
// mirror runs a single full scan and doesn't need continuous watching.
|
||||||
|
|
||||||
// For daemon and mirror mode, load ignore rules before the core is constructed so that
|
|
||||||
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
|
||||||
const watchEnabled = options.command === "daemon";
|
const watchEnabled = options.command === "daemon";
|
||||||
const vaultPath =
|
const vaultPath =
|
||||||
options.command === "mirror" && options.commandArgs[0] ? path.resolve(options.commandArgs[0]) : databasePath;
|
options.command === "mirror" && options.commandArgs[0]
|
||||||
|
? path.resolve(options.commandArgs[0])
|
||||||
|
: options.vaultPath
|
||||||
|
? path.resolve(options.vaultPath)
|
||||||
|
: databasePath!;
|
||||||
|
|
||||||
|
infoLog(`Self-hosted LiveSync CLI`);
|
||||||
|
infoLog(`Database Path: ${databasePath}`);
|
||||||
|
infoLog(`Vault Path: ${vaultPath}`);
|
||||||
|
infoLog(`Settings: ${settingsPath}`);
|
||||||
|
infoLog("");
|
||||||
let ignoreRules: IgnoreRules | undefined;
|
let ignoreRules: IgnoreRules | undefined;
|
||||||
if (options.command === "daemon" || options.command === "mirror") {
|
if (options.command === "daemon" || options.command === "mirror") {
|
||||||
ignoreRules = new IgnoreRules(vaultPath);
|
ignoreRules = new IgnoreRules(vaultPath);
|
||||||
|
|||||||
@@ -25,16 +25,18 @@
|
|||||||
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
|
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
|
||||||
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
|
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
|
||||||
"test:e2e:mirror": "bash test/test-mirror-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",
|
"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",
|
"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: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: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: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: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": "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: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": {},
|
"dependencies": {},
|
||||||
"devDependencies": {}
|
"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"
|
||||||
@@ -11,6 +11,12 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
|||||||
|
|
||||||
- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911).
|
- 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
|
||||||
|
|
||||||
|
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
|
||||||
|
|
||||||
## 0.25.73
|
## 0.25.73
|
||||||
|
|
||||||
4th June, 2026
|
4th June, 2026
|
||||||
|
|||||||
Reference in New Issue
Block a user