mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-27 09:41:34 +00:00
cli: implement daemon startup sequence and CouchDB→local sync
- Add daemon command to help text and --interval/-i flag for polling mode - Capture original sync settings before suspendAllSync() clobbers them - Implement daemon startup: mirror scan → restore settings → applySettings() which triggers the full suspend/resume lifecycle and starts the _changes feed - Guard processSynchroniseResult no-op to non-daemon commands so default handler writes incoming CouchDB changes to the local filesystem - Polling mode: restore settings + clearInterval-safe try/catch error handling - Warn when both liveSync and syncOnStart are false after restore (no-op config) - Fix: only block indefinitely if daemon startup succeeded
This commit is contained in:
+97
-25
@@ -43,7 +43,8 @@ Arguments:
|
||||
database-path Path to the local database directory
|
||||
|
||||
Commands:
|
||||
sync Run one replication cycle and exit
|
||||
daemon (default) Run mirror scan then continuously sync CouchDB <-> local filesystem
|
||||
sync Run one replication cycle and exit
|
||||
p2p-peers <timeout> Show discovered peers as [peer]\t<peer-id>\t<peer-name>
|
||||
p2p-sync <peer> <timeout>
|
||||
Sync with the specified peer-id or peer-name
|
||||
@@ -60,24 +61,30 @@ Commands:
|
||||
rm <path> Mark a file as deleted in local database
|
||||
resolve <path> <rev> Resolve conflicts by keeping <rev> and deleting others
|
||||
mirror [vault-path] Mirror database contents to the local file system (vault-path defaults to database-path)
|
||||
|
||||
Options:
|
||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||
|
||||
Examples:
|
||||
livesync-cli ./my-database sync
|
||||
livesync-cli ./my-database p2p-peers 5
|
||||
livesync-cli ./my-database p2p-sync my-peer-name 15
|
||||
livesync-cli ./my-database p2p-host
|
||||
livesync-cli ./my-database --settings ./custom-settings.json push ./note.md folder/note.md
|
||||
livesync-cli ./my-database pull folder/note.md ./exports/note.md
|
||||
livesync-cli ./my-database pull-rev folder/note.md ./exports/note.old.md 3-abcdef
|
||||
livesync-cli ./my-database setup "obsidian://setuplivesync?settings=..."
|
||||
echo "Hello" | livesync-cli ./my-database put notes/hello.md
|
||||
livesync-cli ./my-database cat notes/hello.md
|
||||
livesync-cli ./my-database cat-rev notes/hello.md 3-abcdef
|
||||
livesync-cli ./my-database ls notes/
|
||||
livesync-cli ./my-database info notes/hello.md
|
||||
livesync-cli ./my-database rm notes/hello.md
|
||||
livesync-cli ./my-database resolve notes/hello.md 3-abcdef
|
||||
livesync-cli init-settings ./data.json
|
||||
livesync-cli ./my-database --verbose
|
||||
livesync-cli ./my-database Run daemon (LiveSync mode)
|
||||
livesync-cli ./my-database --interval 30 Run daemon (polling every 30s)
|
||||
livesync-cli ./my-database sync
|
||||
livesync-cli ./my-database p2p-peers 5
|
||||
livesync-cli ./my-database p2p-sync my-peer-name 15
|
||||
livesync-cli ./my-database p2p-host
|
||||
livesync-cli ./my-database --settings ./custom-settings.json push ./note.md folder/note.md
|
||||
livesync-cli ./my-database pull folder/note.md ./exports/note.md
|
||||
livesync-cli ./my-database pull-rev folder/note.md ./exports/note.old.md 3-abcdef
|
||||
livesync-cli ./my-database setup "obsidian://setuplivesync?settings=..."
|
||||
echo "Hello" | livesync-cli ./my-database put notes/hello.md
|
||||
livesync-cli ./my-database cat notes/hello.md
|
||||
livesync-cli ./my-database cat-rev notes/hello.md 3-abcdef
|
||||
livesync-cli ./my-database ls notes/
|
||||
livesync-cli ./my-database info notes/hello.md
|
||||
livesync-cli ./my-database rm notes/hello.md
|
||||
livesync-cli ./my-database resolve notes/hello.md 3-abcdef
|
||||
livesync-cli init-settings ./data.json
|
||||
livesync-cli ./my-database --verbose
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -94,6 +101,7 @@ export function parseArgs(): CLIOptions {
|
||||
let verbose = false;
|
||||
let debug = false;
|
||||
let force = false;
|
||||
let interval: number | undefined;
|
||||
let command: CLICommand = "daemon";
|
||||
const commandArgs: string[] = [];
|
||||
|
||||
@@ -110,6 +118,21 @@ export function parseArgs(): CLIOptions {
|
||||
settingsPath = args[i];
|
||||
break;
|
||||
}
|
||||
case "--interval":
|
||||
case "-i": {
|
||||
i++;
|
||||
if (!args[i]) {
|
||||
console.error(`Error: Missing value for ${token}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const n = parseInt(args[i], 10);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
console.error(`Error: --interval requires a positive integer, got '${args[i]}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
interval = n;
|
||||
break;
|
||||
}
|
||||
case "--debug":
|
||||
case "-d":
|
||||
// debugging automatically enables verbose logging, as it is intended for debugging issues.
|
||||
@@ -164,6 +187,7 @@ export function parseArgs(): CLIOptions {
|
||||
force,
|
||||
command,
|
||||
commandArgs,
|
||||
interval,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,6 +272,20 @@ export async function main() {
|
||||
infoLog(`Settings: ${settingsPath}`);
|
||||
infoLog("");
|
||||
|
||||
// For daemon and mirror mode, load ignore rules before the core is constructed so that
|
||||
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
||||
const watchEnabled = options.command === "daemon";
|
||||
const vaultPathForIgnoreRules =
|
||||
options.command === "mirror" && options.commandArgs[0]
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: databasePath;
|
||||
let ignoreRules: IgnoreRules | undefined;
|
||||
if (options.command === "daemon" || options.command === "mirror") {
|
||||
ignoreRules = new IgnoreRules(vaultPathForIgnoreRules);
|
||||
await ignoreRules.load();
|
||||
}
|
||||
|
||||
|
||||
// Create service context and hub
|
||||
const context = new NodeServiceContext(databasePath);
|
||||
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
||||
@@ -278,11 +316,14 @@ export async function main() {
|
||||
}
|
||||
console.error(`${prefix} ${message}`);
|
||||
});
|
||||
// Prevent replication result to be processed automatically.
|
||||
serviceHubInstance.replication.processSynchroniseResult.addHandler(async () => {
|
||||
console.error(`[Info] Replication result received, but not processed automatically in CLI mode.`);
|
||||
return await Promise.resolve(true);
|
||||
}, -100);
|
||||
// Prevent replication result from being processed automatically in non-daemon commands.
|
||||
// In daemon mode the default handler must run so changes are applied to the filesystem.
|
||||
if (options.command !== "daemon") {
|
||||
serviceHubInstance.replication.processSynchroniseResult.addHandler(async () => {
|
||||
console.error(`[Info] Replication result received, but not processed automatically in CLI mode.`);
|
||||
return await Promise.resolve(true);
|
||||
}, -100);
|
||||
}
|
||||
|
||||
// Setup settings handlers
|
||||
const settingService = serviceHubInstance.setting;
|
||||
@@ -366,6 +407,25 @@ export async function main() {
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
|
||||
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||
// various code paths persist the clobbered state to disk. We restore on shutdown.
|
||||
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null);
|
||||
|
||||
// Restore settings file on any exit to undo lifecycle mutations.
|
||||
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
|
||||
process.on("exit", () => {
|
||||
if (settingsBackup) {
|
||||
const tmpPath = settingsPath + ".tmp";
|
||||
try {
|
||||
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||
require("fs").renameSync(tmpPath, settingsPath);
|
||||
} catch (err) {
|
||||
console.error("[Settings] Failed to restore settings on exit:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start the core
|
||||
try {
|
||||
infoLog(`[Starting] Initializing LiveSync...`);
|
||||
@@ -375,6 +435,18 @@ export async function main() {
|
||||
console.error(`[Error] Failed to initialize LiveSync`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Capture sync settings before suspendAllSync() clobbers them.
|
||||
// Used by daemon mode to restore the correct sync behaviour after the mirror scan.
|
||||
const settingsBeforeSuspend = core.services.setting.currentSettings();
|
||||
const originalSyncSettings = {
|
||||
liveSync: settingsBeforeSuspend.liveSync,
|
||||
syncOnStart: settingsBeforeSuspend.syncOnStart,
|
||||
periodicReplication: settingsBeforeSuspend.periodicReplication,
|
||||
syncOnSave: settingsBeforeSuspend.syncOnSave,
|
||||
syncOnEditorSave: settingsBeforeSuspend.syncOnEditorSave,
|
||||
syncOnFileOpen: settingsBeforeSuspend.syncOnFileOpen,
|
||||
syncAfterMerge: settingsBeforeSuspend.syncAfterMerge,
|
||||
};
|
||||
await core.services.setting.suspendAllSync();
|
||||
await core.services.control.onReady();
|
||||
|
||||
@@ -400,7 +472,7 @@ export async function main() {
|
||||
infoLog("");
|
||||
}
|
||||
|
||||
const result = await runCommand(options, { databasePath, core, settingsPath });
|
||||
const result = await runCommand(options, { databasePath, core, settingsPath, originalSyncSettings });
|
||||
if (!result) {
|
||||
console.error(`[Error] Command '${options.command}' failed`);
|
||||
process.exitCode = 1;
|
||||
@@ -408,7 +480,7 @@ export async function main() {
|
||||
infoLog(`[Done] Command '${options.command}' completed`);
|
||||
}
|
||||
|
||||
if (options.command === "daemon") {
|
||||
if (options.command === "daemon" && result) {
|
||||
// Keep the process running
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user