Releasing 0.25.77 (#968)

Squash commits
This commit is contained in:
vorotamoroz
2026-06-19 17:45:37 +09:00
committed by GitHub
parent c6c4044f3c
commit 62f44e38c0
453 changed files with 29917 additions and 727 deletions
+4 -4
View File
@@ -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 -1
View File
@@ -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
+2 -1
View File
@@ -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;
}
+15 -4
View File
@@ -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
);
}
}
+6 -5
View File
@@ -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;
}
+12 -8
View File
@@ -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;
+29 -16
View File
@@ -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") {
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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;
}
+7 -2
View File
@@ -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) => {
+37 -34
View File
@@ -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
View File
@@ -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 {
+6 -8
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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> => {
+8 -3
View File
@@ -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();
}
}
+4 -3
View File
@@ -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(
+1 -1
View File
@@ -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");
});
+1 -1
View File
@@ -24,7 +24,7 @@
/* Path mapping */
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
+5 -2
View File
@@ -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",
+5 -5
View File
@@ -6,7 +6,7 @@ Note: (I vrtmrz have not tested this so much yet).
- 🌐 Runs entirely in the browser
- 📁 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!!
- 💾 Settings stored in `.livesync/settings.json` within your vault
- 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver)
@@ -127,7 +127,7 @@ webapp/
1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API
2. **Managers**: Handle storage events and file watching
3. **Service Modules**: Integrate with LiveSyncBaseCore
4. **Main**: Application initialization and lifecycle management
4. **Main**: Application initialisation and lifecycle management
### Service Hub
@@ -154,11 +154,11 @@ Uses `BrowserServiceHub` which provides:
- Settings stored in `.livesync/settings.json` in vault
- 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
- No plugin/theme sync features
- No plug-in/theme sync features
- No internal file handling (`.obsidian` folder)
## Development Notes
@@ -1,4 +1,4 @@
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 { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
@@ -28,7 +28,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
path: folder.path,
isFolder: true,
children: [],
parent: parentPath as any,
parent: parentPath as FilePath,
};
}
}
@@ -173,7 +173,11 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
}
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (currentHandle as any).entries()) {
for await (const [name, entry] of (
currentHandle as unknown as {
entries(): AsyncIterable<[string, FileSystemHandle]>;
}
).entries()) {
const entryPath = relativePath ? `${relativePath}/${name}` : name;
if (entry.kind === "directory") {
@@ -195,7 +195,11 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
const folders: string[] = [];
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (dirHandle as any).entries()) {
for await (const [name, entry] of (
dirHandle as unknown as {
entries(): AsyncIterable<[string, FileSystemHandle]>;
}
).entries()) {
const entryPath = basePath ? `${basePath}/${name}` : name;
if (entry.kind === "directory") {
@@ -5,13 +5,24 @@ import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
* Type guard adapter implementation for FileSystem API
*/
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
isFile(file: unknown): file is FSAPIFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
"handle" in file &&
!(file as { isFolder?: boolean }).isFolder
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
isFolder(item: unknown): item is FSAPIFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
(item as { isFolder?: boolean }).isFolder === true &&
"handle" in item
);
}
}
@@ -116,8 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
await this.delete(file, force);
}
trigger(name: string, ...data: any[]): any {
trigger(name: string, ...data: unknown[]): void {
// No-op in webapp version (no event system yet)
return undefined;
}
}
+4 -6
View File
@@ -121,19 +121,17 @@ async function initializeVaultSelector(): Promise<void> {
await renderHistoryList();
}
compatGlobal.addEventListener("load", async () => {
try {
await initializeVaultSelector();
} catch (error) {
compatGlobal.addEventListener("load", () => {
initializeVaultSelector().catch((error) => {
console.error("Failed to initialize vault selector:", error);
setStatus("error", `Initialization failed: ${String(error)}`);
}
});
});
compatGlobal.addEventListener("beforeunload", () => {
void app?.shutdown();
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching
(compatGlobal as any).livesyncApp = {
getApp: () => app,
historyStore,
+14 -12
View File
@@ -23,7 +23,6 @@ import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json";
const DB_NAME = "livesync-webapp";
/**
* Default settings for the webapp
@@ -51,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
class LiveSyncWebApp {
private rootHandle: FileSystemDirectoryHandle;
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
private core: LiveSyncBaseCore<ServiceContext, never> | null = null;
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
constructor(rootHandle: FileSystemDirectoryHandle) {
@@ -65,7 +64,6 @@ class LiveSyncWebApp {
console.log(`Vault directory: ${this.rootHandle.name}`);
// Create service context and hub
const context = new ServiceContext();
this.serviceHub = new BrowserServiceHub<ServiceContext>();
// Setup API service
@@ -99,17 +97,19 @@ class LiveSyncWebApp {
});
// App lifecycle handlers
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
compatGlobal.setTimeout(() => {
compatGlobal.location.reload();
}, 1000);
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
void (async () => {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
compatGlobal.setTimeout(() => {
compatGlobal.location.reload();
}, 1000);
})();
});
// Create LiveSync core
this.core = new LiveSyncBaseCore(
this.core = new LiveSyncBaseCore<ServiceContext, never>(
this.serviceHub,
(core, serviceHub) => {
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
@@ -129,7 +129,7 @@ class LiveSyncWebApp {
// new ModuleReplicatorP2P(core), // Register P2P replicator for CLI (useP2PReplicator is not used here)
new SetupManager(core),
],
() => [], // No add-ons
() => [] as never[], // No add-ons
(core) => {
useOfflineScanner(core);
useRedFlagFeatures(core);
@@ -207,6 +207,7 @@ class LiveSyncWebApp {
}
// Scan the directory to populate file cache
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
if (fileAccess?.fsapiAdapter) {
console.log("[Scanning] Scanning vault directory...");
@@ -225,6 +226,7 @@ class LiveSyncWebApp {
console.log("[Shutdown] Shutting down...");
// Stop file watching
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
if (storageEventManager?.cleanup) {
await storageEventManager.cleanup();
@@ -17,14 +17,25 @@ import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
* FileSystem API-specific type guard adapter
*/
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
isFile(file: unknown): file is FSAPIFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
"handle" in file &&
!(file as { isFolder?: boolean }).isFolder
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
isFolder(item: unknown): item is FSAPIFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
(item as { isFolder?: boolean }).isFolder === true &&
"handle" in item
);
}
}
@@ -144,12 +155,14 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
*/
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
private observer: any = null; // FileSystemObserver type
constructor(private rootHandle: FileSystemDirectoryHandle) {}
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// Use FileSystemObserver if available (Chrome 124+)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing global FileSystemObserver
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
@@ -157,11 +170,12 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
this.observer = new FileSystemObserver(async (records: any[]) => {
for (const record of records) {
const handle = record.root;
const changedHandle = record.changedHandle;
const relativePathComponents = record.relativePathComponents;
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
@@ -193,9 +207,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
};
if (type === "appeared") {
await handlers.onCreate(fileInfo, undefined);
handlers.onCreate(fileInfo, undefined);
} else {
await handlers.onChange(fileInfo, undefined);
handlers.onChange(fileInfo, undefined);
}
}
} else if (type === "disappeared") {
@@ -207,9 +221,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
ctime: Date.now(),
type: "file" as const,
},
handle: null as any,
handle: null as unknown as FileSystemFileHandle, // No handle available for disappeared files
};
await handlers.onDelete(fileInfo, undefined);
handlers.onDelete(fileInfo, undefined);
} else if (type === "moved") {
// Handle as delete + create
// Note: FileSystemObserver provides both old and new paths in some cases
@@ -226,7 +240,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
},
handle: changedHandle,
};
await handlers.onChange(fileInfo, undefined);
handlers.onChange(fileInfo, undefined);
}
}
} catch (error) {
@@ -30,7 +30,7 @@ export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStora
async cleanup() {
// Stop file watching
if (this.fsapiAdapter?.watch) {
await (this.fsapiAdapter.watch as any).stopWatch?.();
await this.fsapiAdapter.watch.stopWatch?.();
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "0.25.76-webapp",
"version": "0.25.77-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
@@ -22,7 +22,7 @@ import { ServiceFileHandler } from "@/serviceModules/FileHandler";
*/
export function initialiseServiceModulesFSAPI(
rootHandle: FileSystemDirectoryHandle,
core: LiveSyncBaseCore<ServiceContext, any>,
core: LiveSyncBaseCore<ServiceContext, never>,
services: InjectableServiceHub<ServiceContext>
): ServiceModules {
const storageAccessManager = new StorageAccessManager();
@@ -67,7 +67,7 @@ export function initialiseServiceModulesFSAPI(
});
// File handler (platform-independent)
const fileHandler = new (ServiceFileHandler as any)({
const fileHandler = new ServiceFileHandler({
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
+35 -8
View File
@@ -28,11 +28,38 @@ function stripPrefix(raw: string): string {
return raw.replace(/^[^:]+:/, "");
}
interface TestCore {
services?: {
replication?: {
databaseQueueCount?: { value: number };
storageApplyingCount?: { value: number };
};
fileProcessing?: {
totalQueued?: { value: number };
batched?: { value: number };
processing?: { value: number };
};
database?: {
localDatabase: {
findAllNormalDocs(options?: { conflicts?: boolean }): AsyncIterable<{
_deleted?: boolean;
deleted?: boolean;
path?: string;
_rev?: string;
_conflicts?: string[];
size?: number;
mtime?: number;
}>;
};
};
};
}
/**
* Poll every 300 ms until all known processing queues are drained, or until
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
*/
async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
async function waitForIdle(core: TestCore, timeoutMs = 60_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const q =
@@ -47,8 +74,8 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
}
function getCore(): any {
const core = (app as any)?.core;
function getCore(): TestCore {
const core = (app as unknown as { core: TestCore | null })?.core;
if (!core) throw new Error("Vault not initialised call livesyncTest.init() first");
return core;
}
@@ -178,10 +205,10 @@ const livesyncTest: LiveSyncTestAPI = {
if (docPath !== vaultPath) continue;
return {
path: docPath,
revision: (doc._rev as string) ?? "",
conflicts: (doc._conflicts as string[]) ?? [],
size: (doc.size as number) ?? 0,
mtime: (doc.mtime as number) ?? 0,
revision: doc._rev ?? "",
conflicts: doc._conflicts ?? [],
size: doc.size ?? 0,
mtime: doc.mtime ?? 0,
};
}
return null;
@@ -201,4 +228,4 @@ const livesyncTest: LiveSyncTestAPI = {
};
// Expose on window for Playwright page.evaluate() calls.
(compatGlobal as any).livesyncTest = livesyncTest;
(compatGlobal as unknown as Record<string, unknown>).livesyncTest = livesyncTest;
+2 -2
View File
@@ -24,9 +24,9 @@
// "baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
"include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"],
"exclude": ["node_modules", "dist"]
}
+10 -5
View File
@@ -32,7 +32,10 @@ function randomId(): string {
}
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
const h = handle as any;
const h = handle as unknown as {
queryPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
requestPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
};
if (typeof h.queryPermission === "function") {
const queried = await h.queryPermission({ mode: "readwrite" });
if (queried === "granted") {
@@ -91,7 +94,7 @@ export class VaultHistoryStore {
async getVaultHistory(): Promise<VaultHistoryItem[]> {
return this.withStore("readonly", async (store) => {
const keys = (await this.requestAsPromise(store.getAllKeys()));
const keys = await this.requestAsPromise(store.getAllKeys());
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
const items: VaultHistoryItem[] = [];
for (let i = 0; i < keys.length; i++) {
@@ -172,15 +175,17 @@ export class VaultHistoryStore {
}
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
const picker = (compatGlobal as any).showDirectoryPicker;
const picker = (compatGlobal as unknown as Record<string, unknown>).showDirectoryPicker as
| ((options?: { mode?: "readwrite" | "read"; startIn?: string }) => Promise<FileSystemDirectoryHandle>)
| undefined;
if (typeof picker !== "function") {
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
}
const handle = (await picker({
const handle = await picker({
mode: "readwrite",
startIn: "documents",
})) as FileSystemDirectoryHandle;
});
const granted = await hasReadWritePermission(handle, true);
if (!granted) {
+5 -3
View File
@@ -3,10 +3,12 @@ import { svelte } from "@sveltejs/vite-plugin-svelte";
import istanbul from "vite-plugin-istanbul";
import path from "node:path";
import { readFileSync } from "node:fs";
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
const enableCoverage = process.env.PW_COVERAGE === "1";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
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"));
const enableCoverage = process.env.PW_COVERAGE === "1";
// https://vite.dev/config/
export default defineConfig({
plugins: [
+2 -4
View File
@@ -17,7 +17,7 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-family: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
@@ -254,7 +254,7 @@ body.livesync-log-visible {
flex: 1;
overflow: auto;
padding: 8px 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
font-size: 12px;
line-height: 1.4;
color: #e2e8f0;
@@ -397,6 +397,4 @@ popup {
align-items: center;
justify-content: center;
backdrop-filter: blur(15px);
border-radius: 10px;
z-index: 10;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "0.25.76-webpeer",
"version": "0.25.77-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
+11 -12
View File
@@ -10,8 +10,7 @@ import {
import { eventHub } from "@lib/hub/hub";
import type { Confirm } from "@lib/interfaces/Confirm";
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
import { storeP2PStatusLine } from "./CommandsShim";
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger";
import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus,
@@ -66,7 +65,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
}
return this.db;
}
_simpleStore!: SimpleStore<any>;
_simpleStore!: SimpleStore<unknown>;
async closeDB() {
if (this.db) {
@@ -83,11 +82,11 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
replicator,
p2pLogCollector,
storeP2PStatusLine: p2pStatusLine,
} = useP2PReplicator({ services: this.services } as any);
} = useP2PReplicator({ services: this.services } as unknown as Parameters<typeof useP2PReplicator>[0]);
this._liveSyncReplicator = replicator;
this.p2pLogCollector = p2pLogCollector;
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
storeP2PStatusLine.set(line.value);
p2pStatusLine.value = line.value;
});
}
@@ -98,15 +97,15 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
() => "p2p-livesync-web-peer"
);
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
const repStore = SimpleStoreIDBv2.open<unknown>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
this.services.setting.settings = _settings as any;
(this.services.setting as InjectableSettingService<any>).saveData.setHandler(async (data) => {
this.services.setting.settings = _settings;
(this.services.setting as InjectableSettingService<ServiceContext>).saveData.setHandler(async (data) => {
await repStore.set("settings", data);
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
});
(this.services.setting as InjectableSettingService<any>).loadData.setHandler(async () => {
(this.services.setting as InjectableSettingService<ServiceContext>).loadData.setHandler(async () => {
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
return settings;
});
@@ -148,7 +147,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
return this;
}
_log(msg: any, level?: any): void {
_log(msg: unknown, level?: LOG_LEVEL): void {
Logger(msg, level);
}
_notice(msg: string, key?: string): void {
@@ -157,7 +156,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
getSettings(): P2PSyncSetting {
return this.settings;
}
simpleStore(): SimpleStore<any> {
simpleStore(): SimpleStore<unknown> {
return this._simpleStore;
}
handleReplicatedDocuments(_docs: EntryDoc[]): Promise<boolean> {
@@ -281,7 +280,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
}
await this.services.setting.applyExternalSettings(remoteConfig, true);
if (yn !== DROP) {
await this.plugin.core.services.appLifecycle.scheduleRestart();
this.plugin.core.services.appLifecycle.scheduleRestart();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
+1 -1
View File
@@ -19,7 +19,7 @@
"moduleDetection": "force",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],