Add annotations and fix some.

This commit is contained in:
vorotamoroz
2026-06-18 11:57:33 +01:00
parent 866a49204c
commit 5cb76bba72
15 changed files with 48 additions and 33 deletions
+4 -4
View File
@@ -1,17 +1,17 @@
# Self-hosted LiveSync CLI # Self-hosted LiveSync CLI
Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian. Command-line version of Self-hosted LiveSync plug-in for syncing vaults without Obsidian.
## Features ## Features
- ✅ Sync Obsidian vaults using CouchDB without running Obsidian - ✅ Sync Obsidian vaults using CouchDB without running Obsidian
- ✅ Compatible with Self-hosted LiveSync plugin settings - ✅ Compatible with Self-hosted LiveSync plug-in settings
- ✅ Supports all core sync features (encryption, conflict resolution, etc.) - ✅ Supports all core sync features (encryption, conflict resolution, etc.)
- ✅ Lightweight and headless operation - ✅ Lightweight and headless operation
- ✅ Cross-platform (Windows, macOS, Linux) - ✅ Cross-platform (Windows, macOS, Linux)
## Architecture ## Architecture
This CLI version is built using the same core as the Obsidian plugin: This CLI version is built using the same core as the Obsidian plug-in:
``` ```
CLI Main CLI Main
@@ -290,7 +290,7 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlo
### Configuration ### Configuration
The CLI uses the same settings format as the Obsidian plugin. Create a `.livesync/settings.json` file in your vault directory: The CLI uses the same settings format as the Obsidian plug-in. Create a `.livesync/settings.json` file in your vault directory:
```json ```json
{ {
@@ -60,6 +60,7 @@ export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
async readBinary(p: string): Promise<ArrayBuffer> { async readBinary(p: string): Promise<ArrayBuffer> {
const buffer = await fs.readFile(this.resolvePath(p)); const buffer = await fs.readFile(this.resolvePath(p));
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer; return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
} }
@@ -31,6 +31,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
const buffer = await fs.readFile(this.resolvePath(file.path)); const buffer = await fs.readFile(this.resolvePath(file.path));
// Same correction as read() — ensure stat.size matches actual byte length. // Same correction as read() — ensure stat.size matches actual byte length.
file.stat.size = buffer.length; file.stat.size = buffer.length;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer; return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
} }
+1
View File
@@ -1,6 +1,7 @@
import { path, readline } from "../node-compat"; import { path, readline } from "../node-compat";
export function toArrayBuffer(data: Buffer): ArrayBuffer { export function toArrayBuffer(data: Buffer): ArrayBuffer {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer; return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
} }
+5 -5
View File
@@ -303,7 +303,7 @@ export async function main() {
console.error(`Error: ${databasePath} is not a directory`); console.error(`Error: ${databasePath} is not a directory`);
process.exit(1); process.exit(1);
} }
} catch (error) { } catch {
console.error(`Error: Database directory ${databasePath} does not exist`); console.error(`Error: Database directory ${databasePath} does not exist`);
process.exit(1); process.exit(1);
} }
@@ -324,7 +324,7 @@ export async function main() {
? path.resolve(options.commandArgs[0]) ? path.resolve(options.commandArgs[0])
: options.vaultPath : options.vaultPath
? path.resolve(options.vaultPath) ? path.resolve(options.vaultPath)
: databasePath!; : databasePath;
// Check if vault directory exists // Check if vault directory exists
try { try {
@@ -333,7 +333,7 @@ export async function main() {
console.error(`Error: Vault path ${vaultPath} is not a directory`); console.error(`Error: Vault path ${vaultPath} is not a directory`);
process.exit(1); process.exit(1);
} }
} catch (error) { } catch {
console.error(`Error: Vault directory ${vaultPath} does not exist`); console.error(`Error: Vault directory ${vaultPath} does not exist`);
process.exit(1); process.exit(1);
} }
@@ -415,7 +415,7 @@ export async function main() {
// Force disable IndexedDB adapter in CLI environment // Force disable IndexedDB adapter in CLI environment
data.useIndexedDBAdapter = false; data.useIndexedDBAdapter = false;
return data; return data;
} catch (error) { } catch {
if (options.verbose) { if (options.verbose) {
console.error(`[Settings] File not found, using defaults`); console.error(`[Settings] File not found, using defaults`);
} }
@@ -434,7 +434,7 @@ export async function main() {
() => [], // No add-ons () => [], // No add-ons
(core) => { (core) => {
// Register P2P replicator feature. // Register P2P replicator feature.
const _replicator = useP2PReplicatorFeature(core); useP2PReplicatorFeature(core);
// Add target filter to prevent internal files are handled // Add target filter to prevent internal files are handled
core.services.vault.isTargetFile.addHandler(async (target) => { core.services.vault.isTargetFile.addHandler(async (target) => {
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target)); const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
+5 -1
View File
@@ -1,7 +1,11 @@
/* eslint-disable obsidianmd/no-nodejs-builtins */ // eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
import * as nodeFs from "node:fs"; import * as nodeFs from "node:fs";
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
import * as nodeFsPromises from "node:fs/promises"; import * as nodeFsPromises from "node:fs/promises";
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
import * as nodePath from "node:path"; import * as nodePath from "node:path";
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
import * as nodeReadlinePromises from "node:readline/promises"; import * as nodeReadlinePromises from "node:readline/promises";
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
import type { Stats } from "node:fs"; import type { Stats } from "node:fs";
export { nodeFs as fs, nodeFsPromises as fsPromises, nodePath as path, nodeReadlinePromises as readline, type Stats }; export { nodeFs as fs, nodeFsPromises as fsPromises, nodePath as path, nodeReadlinePromises as readline, type Stats };
+5 -5
View File
@@ -6,7 +6,7 @@ Note: (I vrtmrz have not tested this so much yet).
- 🌐 Runs entirely in the browser - 🌐 Runs entirely in the browser
- 📁 Uses FileSystem API to access your local vault - 📁 Uses FileSystem API to access your local vault
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plugin) - 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plug-in)
- 🚫 No server-side code required!! - 🚫 No server-side code required!!
- 💾 Settings stored in `.livesync/settings.json` within your vault - 💾 Settings stored in `.livesync/settings.json` within your vault
- 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver) - 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver)
@@ -127,7 +127,7 @@ webapp/
1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API 1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API
2. **Managers**: Handle storage events and file watching 2. **Managers**: Handle storage events and file watching
3. **Service Modules**: Integrate with LiveSyncBaseCore 3. **Service Modules**: Integrate with LiveSyncBaseCore
4. **Main**: Application initialization and lifecycle management 4. **Main**: Application initialisation and lifecycle management
### Service Hub ### Service Hub
@@ -154,11 +154,11 @@ Uses `BrowserServiceHub` which provides:
- Settings stored in `.livesync/settings.json` in vault - Settings stored in `.livesync/settings.json` in vault
- Real-time file watching only with FileSystemObserver (Chrome 124+) - Real-time file watching only with FileSystemObserver (Chrome 124+)
## Differences from Obsidian Plugin ## Differences from Obsidian Plug-in
- No Obsidian-specific modules (UI, settings dialog, etc.) - No Obsidian-specific modules (UI, settings dialogue, etc.)
- Simplified configuration - Simplified configuration
- No plugin/theme sync features - No plug-in/theme sync features
- No internal file handling (`.obsidian` folder) - No internal file handling (`.obsidian` folder)
## Development Notes ## Development Notes
-2
View File
@@ -23,7 +23,6 @@ import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const SETTINGS_DIR = ".livesync"; const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json"; const SETTINGS_FILE = "settings.json";
const DB_NAME = "livesync-webapp";
/** /**
* Default settings for the webapp * Default settings for the webapp
@@ -65,7 +64,6 @@ class LiveSyncWebApp {
console.log(`Vault directory: ${this.rootHandle.name}`); console.log(`Vault directory: ${this.rootHandle.name}`);
// Create service context and hub // Create service context and hub
const context = new ServiceContext();
this.serviceHub = new BrowserServiceHub<ServiceContext>(); this.serviceHub = new BrowserServiceHub<ServiceContext>();
// Setup API service // Setup API service
@@ -161,7 +161,6 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
this.observer = new FileSystemObserver(async (records: any[]) => { this.observer = new FileSystemObserver(async (records: any[]) => {
for (const record of records) { for (const record of records) {
const handle = record.root;
const changedHandle = record.changedHandle; const changedHandle = record.changedHandle;
const relativePathComponents = record.relativePathComponents; const relativePathComponents = record.relativePathComponents;
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored" const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
+1 -2
View File
@@ -11,7 +11,6 @@ import { eventHub } from "@lib/hub/hub";
import type { Confirm } from "@lib/interfaces/Confirm"; import type { Confirm } from "@lib/interfaces/Confirm";
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger"; import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
import { storeP2PStatusLine } from "./CommandsShim";
import { import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU, EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus, type PeerStatus,
@@ -87,7 +86,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
this._liveSyncReplicator = replicator; this._liveSyncReplicator = replicator;
this.p2pLogCollector = p2pLogCollector; this.p2pLogCollector = p2pLogCollector;
p2pLogCollector.p2pReplicationLine.onChanged((line) => { p2pLogCollector.p2pReplicationLine.onChanged((line) => {
storeP2PStatusLine.set(line.value); p2pStatusLine.value = line.value;
}); });
} }
@@ -738,7 +738,7 @@ Success: ${successCount}, Errored: ${errored}`;
}); });
// Probably no need to wait, but just in case. // Probably no need to wait, but just in case.
let timeout = 2 * 60 * 1000; // 2 minutes let timeout = 2 * 60 * 1000; // 2 minutes
do { for (;;) {
const status = await remote.db.info(); const status = await remote.db.info();
if ("compact_running" in status && status?.compact_running) { if ("compact_running" in status && status?.compact_running) {
this._notice("Compaction in progress on remote database...", "gc-compact"); this._notice("Compaction in progress on remote database...", "gc-compact");
@@ -751,7 +751,7 @@ Success: ${successCount}, Errored: ${errored}`;
} else { } else {
break; break;
} }
} while (true); }
if (compactResult && "ok" in compactResult) { if (compactResult && "ok" in compactResult) {
this._notice("Compaction on remote database completed successfully.", "gc-compact"); this._notice("Compaction on remote database completed successfully.", "gc-compact");
} else { } else {
+1 -1
Submodule src/lib updated: 0278343fc6...9aeab513b0
@@ -99,9 +99,11 @@ export class DocumentHistoryModal extends Modal {
if (!file && id) { if (!file && id) {
this.file = this.services.path.id2path(id); this.file = this.services.path.id2path(id);
} }
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") { if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
this.showDiff = true; this.showDiff = true;
} }
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") { if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
this.diffOnly = true; this.diffOnly = true;
} }
@@ -552,6 +554,7 @@ export class DocumentHistoryModal extends Modal {
} }
checkbox.addEventListener("input", (evt: Event) => { checkbox.addEventListener("input", (evt: Event) => {
this.showDiff = checkbox.checked; this.showDiff = checkbox.checked;
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null); this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
this.updateDiffNavVisibility(); this.updateDiffNavVisibility();
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
@@ -567,6 +570,7 @@ export class DocumentHistoryModal extends Modal {
} }
checkbox.addEventListener("input", (evt: Event) => { checkbox.addEventListener("input", (evt: Event) => {
this.diffOnly = checkbox.checked; this.diffOnly = checkbox.checked;
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null); this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
}); });
@@ -38,20 +38,20 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
} }
async delete(file: TFile | TFolder, force = false): Promise<void> { async delete(file: TFile | TFolder, force = false): Promise<void> {
// if ("trashFile" in this.app.fileManager) { if ("trashFile" in this.app.fileManager) {
// // eslint-disable-next-line obsidianmd/no-unsupported-api // eslint-disable-next-line obsidianmd/no-unsupported-api
// return await this.app.fileManager.trashFile(file); return await this.app.fileManager.trashFile(file);
// } }
//TODO: need fix // eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
return await this.app.vault.delete(file, force); return await this.app.vault.delete(file, force);
} }
async trash(file: TFile | TFolder, force = false): Promise<void> { async trash(file: TFile | TFolder, force = false): Promise<void> {
// if ("trashFile" in this.app.fileManager) { if ("trashFile" in this.app.fileManager) {
// // eslint-disable-next-line obsidianmd/no-unsupported-api // eslint-disable-next-line obsidianmd/no-unsupported-api
// return await this.app.fileManager.trashFile(file); return await this.app.fileManager.trashFile(file);
// } }
//TODO: need fix // eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
return await this.app.vault.trash(file, force); return await this.app.vault.trash(file, force);
} }
+8
View File
@@ -3,6 +3,14 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## Unreleased
18th June, 2026
### Improved
- File deletion now respects the user's deletion preferences (by utilising the `FileManager.trashFile` API) on Obsidian v1.7.2 or newer, regardless of the plug-in's internal trashbin setting.
## 0.25.76 ## 0.25.76
15th June, 2026 15th June, 2026