mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-25 07:33:57 +00:00
@@ -1,17 +1,17 @@
|
||||
# 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
|
||||
|
||||
- ✅ 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.)
|
||||
- ✅ Lightweight and headless operation
|
||||
- ✅ Cross-platform (Windows, macOS, Linux)
|
||||
|
||||
## 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
|
||||
@@ -290,7 +290,7 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlo
|
||||
|
||||
### 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
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
import { path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Node.js
|
||||
@@ -22,7 +22,7 @@ export class NodeConversionAdapter implements IConversionAdapter<NodeFile, NodeF
|
||||
path: folder.path,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
parent: path.dirname(folder.path) as any,
|
||||
parent: path.dirname(folder.path) as FilePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
|
||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
import { path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Storage adapter implementation for Node.js
|
||||
@@ -60,6 +60,7 @@ export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
|
||||
|
||||
async readBinary(p: string): Promise<ArrayBuffer> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,22 @@ import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
* Type guard adapter implementation for Node.js
|
||||
*/
|
||||
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
|
||||
isFile(file: any): file is NodeFile {
|
||||
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
|
||||
isFile(file: unknown): file is NodeFile {
|
||||
return !!(
|
||||
file &&
|
||||
typeof file === "object" &&
|
||||
"path" in file &&
|
||||
"stat" in file &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is NodeFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true;
|
||||
isFolder(item: unknown): item is NodeFolder {
|
||||
return !!(
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"path" in item &&
|
||||
(item as { isFolder?: boolean }).isFolder === true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { FilePath, UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Node.js
|
||||
@@ -31,6 +31,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
const buffer = await fs.readFile(this.resolvePath(file.path));
|
||||
// Same correction as read() — ensure stat.size matches actual byte 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;
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
|
||||
const stat = await fs.stat(fullPath);
|
||||
return {
|
||||
path: p as any,
|
||||
path: p as FilePath,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: Math.floor(stat.mtimeMs),
|
||||
@@ -92,7 +93,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
|
||||
const stat = await fs.stat(fullPath);
|
||||
return {
|
||||
path: p as any,
|
||||
path: p as FilePath,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: Math.floor(stat.mtimeMs),
|
||||
@@ -117,7 +118,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
// No-op in CLI version (no event system)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { LiveSyncError } from "@lib/common/LSError";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
peerId: string;
|
||||
@@ -21,7 +22,7 @@ export function parseTimeoutSeconds(value: string, commandName: string): number
|
||||
return timeoutSec;
|
||||
}
|
||||
|
||||
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
|
||||
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, never>) {
|
||||
const settings = core.services.setting.currentSettings();
|
||||
if (!settings.P2P_Enabled) {
|
||||
throw new Error("P2P is disabled in settings (P2P_Enabled=false)");
|
||||
@@ -33,7 +34,7 @@ function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
|
||||
settings.P2P_IsHeadless = true;
|
||||
}
|
||||
|
||||
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
|
||||
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||
validateP2PSettings(core);
|
||||
const replicator = await core.services.replicator.getNewReplicator();
|
||||
if (!replicator) {
|
||||
@@ -52,7 +53,7 @@ function getSortedPeers(replicator: LiveSyncTrysteroReplicator): CLIP2PPeer[] {
|
||||
}
|
||||
|
||||
export async function collectPeers(
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer[]> {
|
||||
const replicator = await createReplicator(core);
|
||||
@@ -81,7 +82,7 @@ function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undef
|
||||
}
|
||||
|
||||
export async function syncWithPeer(
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
peerToken: string,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer> {
|
||||
@@ -107,11 +108,14 @@ export async function syncWithPeer(
|
||||
|
||||
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||
throw pullResult.error;
|
||||
throw pullResult.error instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.error);
|
||||
}
|
||||
const pushResult = (await replicator.requestSynchroniseToPeer(targetPeer.peerId)) as any;
|
||||
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
||||
if (!pushResult || pushResult.ok !== true) {
|
||||
throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync");
|
||||
const err = pushResult?.error;
|
||||
throw err instanceof Error
|
||||
? err
|
||||
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
||||
}
|
||||
|
||||
return targetPeer;
|
||||
@@ -120,7 +124,7 @@ export async function syncWithPeer(
|
||||
}
|
||||
}
|
||||
|
||||
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
|
||||
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
return replicator;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
type EntryMilestoneInfo,
|
||||
type EntryDoc,
|
||||
} from "@lib/common/types";
|
||||
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
||||
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
|
||||
@@ -17,7 +19,9 @@ import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./
|
||||
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
|
||||
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
|
||||
|
||||
function redactConnectionString(uri: string): string {
|
||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||
@@ -38,16 +42,20 @@ async function verifyRemoteState(
|
||||
}
|
||||
|
||||
try {
|
||||
let milestone: any;
|
||||
let milestone: EntryMilestoneInfo | false | undefined = undefined;
|
||||
if (settings.remoteType === REMOTE_COUCHDB) {
|
||||
const dbRet = await (replicator as any).connectRemoteCouchDBWithSetting(settings, false, true);
|
||||
const dbRet = await (replicator as LiveSyncCouchDBReplicator).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");
|
||||
milestone = await (replicator as LiveSyncJournalReplicator).client.downloadJson("_00000000-milestone.json");
|
||||
}
|
||||
|
||||
if (milestone) {
|
||||
@@ -62,8 +70,9 @@ async function verifyRemoteState(
|
||||
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`);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
process.stderr.write(`[Verification] Failed to fetch milestone document: ${message}\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +102,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
// 2. Mirror scan to reconcile PouchDB ↔ local filesystem.
|
||||
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
||||
log("Running mirror scan...");
|
||||
const scanOk = await performFullScan(core as any, log, errorManager, false, true);
|
||||
const scanOk = await performFullScan(core, log, errorManager, false, true);
|
||||
if (!scanOk) {
|
||||
console.error("[Daemon] Mirror scan failed, cannot continue");
|
||||
return false;
|
||||
@@ -150,9 +159,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
);
|
||||
}
|
||||
}
|
||||
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
pollTimer = compatGlobal.setTimeout(() => {
|
||||
void poll();
|
||||
}, currentIntervalMs);
|
||||
};
|
||||
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
let pollTimer = compatGlobal.setTimeout(() => {
|
||||
void poll();
|
||||
}, currentIntervalMs);
|
||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||
compatGlobal.clearTimeout(pollTimer);
|
||||
return true;
|
||||
@@ -201,7 +214,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
}
|
||||
const timeoutSec = parseTimeoutSeconds(options.commandArgs[0], "p2p-peers");
|
||||
console.error(`[Command] p2p-peers timeout=${timeoutSec}s`);
|
||||
const peers = await collectPeers(core as any, timeoutSec);
|
||||
const peers = await collectPeers(core, timeoutSec);
|
||||
if (peers.length > 0) {
|
||||
process.stdout.write(peers.map((peer) => `[peer]\t${peer.peerId}\t${peer.name}`).join("\n") + "\n");
|
||||
}
|
||||
@@ -218,14 +231,14 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
}
|
||||
const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "p2p-sync");
|
||||
console.error(`[Command] p2p-sync peer=${peerToken} timeout=${timeoutSec}s`);
|
||||
const peer = await syncWithPeer(core as any, peerToken, timeoutSec);
|
||||
const peer = await syncWithPeer(core, peerToken, timeoutSec);
|
||||
console.error(`[Done] P2P sync completed with ${peer.name} (${peer.peerId})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "p2p-host") {
|
||||
console.error("[Command] p2p-host");
|
||||
await openP2PHost(core as any);
|
||||
await openP2PHost(core);
|
||||
console.error("[Ready] P2P host is running. Press Ctrl+C to stop.");
|
||||
await new Promise(() => {});
|
||||
return true;
|
||||
@@ -438,9 +451,9 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
if (docPath !== targetPath) continue;
|
||||
|
||||
const filename = path.basename(docPath);
|
||||
const conflictsText = (doc._conflicts?.length ?? 0) > 0 ? doc._conflicts.join("\n ") : "N/A";
|
||||
const conflictsText = (doc._conflicts?.length ?? 0) > 0 ? doc._conflicts?.join("\n ") : "N/A";
|
||||
const children = "children" in doc ? doc.children : [];
|
||||
const rawDoc = await core.services.database.localDatabase.getRaw<any>(doc._id, {
|
||||
const rawDoc = await core.services.database.localDatabase.getRaw<EntryDoc>(doc._id, {
|
||||
revs_info: true,
|
||||
});
|
||||
const pastRevisions = (rawDoc._revs_info ?? [])
|
||||
@@ -512,7 +525,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
if (revision === revisionToKeep) {
|
||||
continue;
|
||||
}
|
||||
const resolved = await core.services.conflict.resolveByDeletingRevision(targetPath, revision, "CLI");
|
||||
const resolved = await core.services.conflict.resolveByDeletingRevision(targetPath, revision ?? "", "CLI");
|
||||
if (!resolved) {
|
||||
process.stderr.write(`[Info] Failed to delete revision ${revision} for ${targetPath}\n`);
|
||||
return false;
|
||||
@@ -525,7 +538,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
console.error("[Command] mirror");
|
||||
const log = (msg: unknown) => console.error(`[Mirror] ${msg}`);
|
||||
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
||||
return await performFullScan(core as any, log, errorManager, false, true);
|
||||
return await performFullScan(core, log, errorManager, false, true);
|
||||
}
|
||||
|
||||
if (options.command === "remote-add") {
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface CLIOptions {
|
||||
export interface CLICommandContext {
|
||||
databasePath: string;
|
||||
vaultPath: string;
|
||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||
core: LiveSyncBaseCore<ServiceContext, never>;
|
||||
settingsPath: string;
|
||||
originalSyncSettings: Pick<
|
||||
ObsidianLiveSyncSettings,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { path, readline } from "../node-compat";
|
||||
import { path, readline } from "@/apps/cli/node-compat";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
// eslint-disable -- This is the entry point for the CLI application.
|
||||
import * as polyfill from "werift";
|
||||
import { main } from "./main";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
|
||||
const rtcPolyfillCtor = (polyfill as any).RTCPeerConnection;
|
||||
if (typeof (globalThis as any).RTCPeerConnection === "undefined" && typeof rtcPolyfillCtor === "function") {
|
||||
if (
|
||||
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
|
||||
typeof rtcPolyfillCtor === "function"
|
||||
) {
|
||||
// Fill only the standard WebRTC global in Node CLI runtime.
|
||||
(globalThis as any).RTCPeerConnection = rtcPolyfillCtor;
|
||||
(compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection = rtcPolyfillCtor;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -25,42 +25,45 @@ type PurgeMultiResult = {
|
||||
};
|
||||
type PurgeMultiParam = [docId: string, rev$$1: string];
|
||||
function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
|
||||
return db
|
||||
.get("_local/purges")
|
||||
.then(function (doc: any) {
|
||||
for (const [docId, rev$$1] of docs) {
|
||||
const purgeSeq = doc.purgeSeq + 1;
|
||||
doc.purges.push({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq,
|
||||
});
|
||||
//@ts-ignore : missing type def
|
||||
if (doc.purges.length > db.purged_infos_limit) {
|
||||
return (
|
||||
db
|
||||
.get("_local/purges")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Internal method patching.
|
||||
.then(function (doc: any) {
|
||||
for (const [docId, rev$$1] of docs) {
|
||||
const purgeSeq = doc.purgeSeq + 1;
|
||||
doc.purges.push({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq,
|
||||
});
|
||||
//@ts-ignore : missing type def
|
||||
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
||||
if (doc.purges.length > db.purged_infos_limit) {
|
||||
//@ts-ignore : missing type def
|
||||
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
||||
}
|
||||
doc.purgeSeq = purgeSeq;
|
||||
}
|
||||
doc.purgeSeq = purgeSeq;
|
||||
}
|
||||
return doc;
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
_id: "_local/purges",
|
||||
purges: docs.map(([docId, rev$$1], idx) => ({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq: idx,
|
||||
})),
|
||||
purgeSeq: docs.length,
|
||||
};
|
||||
})
|
||||
.then(function (doc) {
|
||||
return db.put(doc);
|
||||
});
|
||||
return doc;
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
_id: "_local/purges",
|
||||
purges: docs.map(([docId, rev$$1], idx) => ({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq: idx,
|
||||
})),
|
||||
purgeSeq: docs.length,
|
||||
};
|
||||
})
|
||||
.then(function (doc) {
|
||||
return db.put(doc);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+16
-14
@@ -184,6 +184,7 @@ export function parseArgs(): CLIOptions {
|
||||
break;
|
||||
default: {
|
||||
if (!databasePath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Set checking
|
||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||
command = token as CLICommand;
|
||||
break;
|
||||
@@ -195,6 +196,7 @@ export function parseArgs(): CLIOptions {
|
||||
databasePath = token;
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Set checking
|
||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||
command = token as CLICommand;
|
||||
break;
|
||||
@@ -232,15 +234,15 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
||||
const targetPath = options.settingsPath
|
||||
? path.resolve(options.settingsPath)
|
||||
: options.commandArgs[0]
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: path.resolve(process.cwd(), "data.json");
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: path.resolve(process.cwd(), "data.json");
|
||||
|
||||
if (!options.force) {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
throw new Error(`Settings file already exists: ${targetPath} (use --force to overwrite)`);
|
||||
} catch (ex: any) {
|
||||
if (!(ex && ex?.code === "ENOENT")) {
|
||||
} catch (ex) {
|
||||
if (!(ex && (ex as { code?: string })?.code === "ENOENT")) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
@@ -303,7 +305,7 @@ export async function main() {
|
||||
console.error(`Error: ${databasePath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.error(`Error: Database directory ${databasePath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -323,8 +325,8 @@ export async function main() {
|
||||
options.command === "mirror" && options.commandArgs[0]
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: options.vaultPath
|
||||
? path.resolve(options.vaultPath)
|
||||
: databasePath!;
|
||||
? path.resolve(options.vaultPath)
|
||||
: databasePath;
|
||||
|
||||
// Check if vault directory exists
|
||||
try {
|
||||
@@ -333,7 +335,7 @@ export async function main() {
|
||||
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -415,7 +417,7 @@ export async function main() {
|
||||
// Force disable IndexedDB adapter in CLI environment
|
||||
data.useIndexedDBAdapter = false;
|
||||
return data;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
if (options.verbose) {
|
||||
console.error(`[Settings] File not found, using defaults`);
|
||||
}
|
||||
@@ -427,14 +429,14 @@ export async function main() {
|
||||
// Create LiveSync core
|
||||
const core = new LiveSyncBaseCore(
|
||||
serviceHubInstance,
|
||||
(core: LiveSyncBaseCore<NodeServiceContext, any>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
|
||||
(core: LiveSyncBaseCore<NodeServiceContext, never>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
|
||||
return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled);
|
||||
},
|
||||
(core) => [],
|
||||
() => [], // No add-ons
|
||||
(core) => {
|
||||
// Register P2P replicator feature.
|
||||
const _replicator = useP2PReplicatorFeature(core);
|
||||
useP2PReplicatorFeature(core);
|
||||
// Add target filter to prevent internal files are handled
|
||||
core.services.vault.isTargetFile.addHandler(async (target) => {
|
||||
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
|
||||
@@ -458,8 +460,8 @@ export async function main() {
|
||||
if (rules.shouldIgnore(targetPath)) {
|
||||
return false;
|
||||
}
|
||||
// undefined = pass through to next handler in chain
|
||||
return undefined;
|
||||
// At least this handler think it is a target file, but other handlers may still veto it.
|
||||
return true;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -557,7 +559,7 @@ export async function main() {
|
||||
|
||||
if (options.command === "daemon" && result) {
|
||||
// Keep the process running
|
||||
await new Promise(() => { });
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
await core.services.control.onUnload();
|
||||
}
|
||||
|
||||
@@ -13,18 +13,29 @@ import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
|
||||
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
import { fsPromises as fs, path, type Stats } from "../node-compat";
|
||||
import { fsPromises as fs, path, type Stats } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* CLI-specific type guard adapter
|
||||
*/
|
||||
class CLITypeGuardAdapter implements IStorageEventTypeGuardAdapter<NodeFile, NodeFolder> {
|
||||
isFile(file: any): file is NodeFile {
|
||||
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
|
||||
isFile(file: unknown): file is NodeFile {
|
||||
return !!(
|
||||
file &&
|
||||
typeof file === "object" &&
|
||||
"path" in file &&
|
||||
"stat" in file &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is NodeFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true;
|
||||
isFolder(item: unknown): item is NodeFolder {
|
||||
return !!(
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"path" in item &&
|
||||
(item as { isFolder?: boolean }).isFolder === true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +110,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
private basePath: string,
|
||||
private ignoreRules?: IgnoreRules,
|
||||
private watchEnabled: boolean = false
|
||||
) { }
|
||||
) {}
|
||||
|
||||
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
||||
return {
|
||||
|
||||
@@ -1,13 +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";
|
||||
// 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";
|
||||
// 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";
|
||||
// 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";
|
||||
// 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";
|
||||
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 };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.25.76-cli",
|
||||
"version": "0.25.77-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { IgnoreRules } from "./IgnoreRules";
|
||||
*/
|
||||
export function initialiseServiceModulesCLI(
|
||||
basePath: string,
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
services: InjectableServiceHub<ServiceContext>,
|
||||
ignoreRules?: IgnoreRules,
|
||||
watchEnabled: boolean = false
|
||||
@@ -81,7 +81,7 @@ export function initialiseServiceModulesCLI(
|
||||
});
|
||||
|
||||
// File handler (platform-independent)
|
||||
const fileHandler = new (ServiceFileHandler as any)({
|
||||
const fileHandler = new ServiceFileHandler({
|
||||
API: services.API,
|
||||
databaseFileAccess: databaseFileAccess,
|
||||
conflict: services.conflict,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { minimatch } from "minimatch";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
|
||||
import type { IVaultService } from "@lib/services/base/IService";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
||||
|
||||
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
||||
const NODE_KV_VALUES_KEY = "values";
|
||||
@@ -178,7 +178,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
||||
implements IKeyValueDBService
|
||||
{
|
||||
private _kvDB: KeyValueDatabase | undefined;
|
||||
private _simpleStore: SimpleStore<any> | undefined;
|
||||
private _simpleStore: SimpleStore<unknown> | undefined;
|
||||
private filePath: string;
|
||||
private _log = createInstanceLogFunction("NodeKeyValueDBService");
|
||||
|
||||
@@ -248,7 +248,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
||||
if (!(await this.openKeyValueDB())) {
|
||||
return false;
|
||||
}
|
||||
this._simpleStore = this.openSimpleStore<any>("os");
|
||||
this._simpleStore = this.openSimpleStore<unknown>("os");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
||||
get: async (key: string): Promise<T> => {
|
||||
return await getDB().get(`${prefix}${key}`);
|
||||
},
|
||||
set: async (key: string, value: any): Promise<void> => {
|
||||
set: async (key: string, value: unknown): Promise<void> => {
|
||||
await getDB().set(`${prefix}${key}`, value);
|
||||
},
|
||||
delete: async (key: string): Promise<void> => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
|
||||
type LocalStorageShape = {
|
||||
getItem(key: string): string | null;
|
||||
@@ -83,8 +84,12 @@ function createNodeLocalStorageShim(): LocalStorageShape {
|
||||
}
|
||||
|
||||
export function ensureGlobalNodeLocalStorage() {
|
||||
if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") {
|
||||
(globalThis as any).localStorage = createNodeLocalStorageShim();
|
||||
const g = compatGlobal as unknown as Record<string, unknown>;
|
||||
if (
|
||||
!("localStorage" in g) ||
|
||||
typeof (g.localStorage as Record<string, unknown> | undefined)?.getItem !== "function"
|
||||
) {
|
||||
g.localStorage = createNodeLocalStorageShim();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
import { NodeSettingService } from "./NodeSettingService";
|
||||
import { DatabaseService } from "@lib/services/base/DatabaseService";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
import { path as nodePath } from "../node-compat";
|
||||
import { path as nodePath } from "@/apps/cli/node-compat";
|
||||
import type { KeyValueDBService } from "@lib/services/base/KeyValueDBService";
|
||||
|
||||
export class NodeServiceContext extends ServiceContext {
|
||||
databasePath: string;
|
||||
@@ -197,10 +198,10 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
|
||||
path,
|
||||
API,
|
||||
config,
|
||||
keyValueDB: keyValueDB as any,
|
||||
keyValueDB: keyValueDB as unknown as KeyValueDBService<T>,
|
||||
control,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- (Forcibly )
|
||||
super(context, serviceInstancesToInit as any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ export class BackgroundCliProcess {
|
||||
if (this.combined.includes(needle)) return;
|
||||
const status = await Promise.race([
|
||||
this.child.status.then((s) => ({ type: "status" as const, status: s })),
|
||||
new Promise<{ type: "tick" }>((resolve) =>
|
||||
setTimeout(() => resolve({ type: "tick" }), 100)
|
||||
),
|
||||
new Promise<{ type: "tick" }>((resolve) => setTimeout(() => resolve({ type: "tick" }), 100)),
|
||||
]);
|
||||
if (status.type === "status") {
|
||||
throw new Error(
|
||||
|
||||
@@ -132,7 +132,7 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
||||
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
|
||||
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
|
||||
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
|
||||
assert(typeof data.chunks === "number" && (data.chunks) >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assert(typeof data.chunks === "number" && data.chunks >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
|
||||
console.log("[PASS] info output format matched");
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
/* Path mapping */
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
|
||||
@@ -2,9 +2,12 @@ import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
// https://vite.dev/config/
|
||||
const defaultExternal = [
|
||||
"obsidian",
|
||||
|
||||
Reference in New Issue
Block a user