mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-24 21:42:58 +00:00
refactor: inject CLI I/O and diagnostics
This commit is contained in:
+67
-57
@@ -25,14 +25,18 @@ import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
|
||||
import { fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { createNodeStandardIo, fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { writeStderrLine, writeStdoutLine } from "./cliOutput";
|
||||
|
||||
const SETTINGS_FILE = ".livesync/settings.json";
|
||||
ensureGlobalNodeLocalStorage();
|
||||
defaultLoggerEnv.minLogLevel = LOG_LEVEL_DEBUG;
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
function printHelp(standardIo: StandardIo): void {
|
||||
writeStdoutLine(
|
||||
standardIo,
|
||||
`
|
||||
Self-hosted LiveSync CLI
|
||||
|
||||
Usage:
|
||||
@@ -115,14 +119,15 @@ Examples:
|
||||
livesync-cli ./my-database remote-status remote-abc123
|
||||
livesync-cli init-settings ./data.json
|
||||
livesync-cli ./my-database --verbose
|
||||
`);
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function parseArgs(): CLIOptions {
|
||||
export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIOptions {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
||||
printHelp();
|
||||
printHelp(standardIo);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ export function parseArgs(): CLIOptions {
|
||||
case "-V": {
|
||||
i++;
|
||||
if (!args[i]) {
|
||||
console.error(`Error: Missing value for ${token}`);
|
||||
writeStderrLine(standardIo, `Error: Missing value for ${token}`);
|
||||
process.exit(1);
|
||||
}
|
||||
vaultPath = args[i];
|
||||
@@ -153,7 +158,7 @@ export function parseArgs(): CLIOptions {
|
||||
case "-s": {
|
||||
i++;
|
||||
if (!args[i]) {
|
||||
console.error(`Error: Missing value for ${token}`);
|
||||
writeStderrLine(standardIo, `Error: Missing value for ${token}`);
|
||||
process.exit(1);
|
||||
}
|
||||
settingsPath = args[i];
|
||||
@@ -163,12 +168,12 @@ export function parseArgs(): CLIOptions {
|
||||
case "-i": {
|
||||
i++;
|
||||
if (!args[i]) {
|
||||
console.error(`Error: Missing value for ${token}`);
|
||||
writeStderrLine(standardIo, `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]}'`);
|
||||
writeStderrLine(standardIo, `Error: --interval requires a positive integer, got '${args[i]}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
interval = n;
|
||||
@@ -212,12 +217,12 @@ export function parseArgs(): CLIOptions {
|
||||
}
|
||||
|
||||
if (!databasePath && command !== "init-settings") {
|
||||
console.error("Error: database-path is required");
|
||||
writeStderrLine(standardIo, "Error: database-path is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (command === "daemon" && commandArgs.length > 0) {
|
||||
console.error(`Error: Unknown command '${commandArgs[0]}'`);
|
||||
writeStderrLine(standardIo, `Error: Unknown command '${commandArgs[0]}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -234,7 +239,7 @@ export function parseArgs(): CLIOptions {
|
||||
};
|
||||
}
|
||||
|
||||
async function createDefaultSettingsFile(options: CLIOptions) {
|
||||
async function createDefaultSettingsFile(options: CLIOptions, standardIo: StandardIo) {
|
||||
const targetPath = options.settingsPath
|
||||
? path.resolve(options.settingsPath)
|
||||
: options.commandArgs[0]
|
||||
@@ -259,13 +264,13 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
||||
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.writeFile(targetPath, JSON.stringify(settings, null, 2), "utf-8");
|
||||
console.log(`[Done] Created settings file: ${targetPath}`);
|
||||
writeStdoutLine(standardIo, `[Done] Created settings file: ${targetPath}`);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const options = parseArgs();
|
||||
export async function main(standardIo: StandardIo = createNodeStandardIo()) {
|
||||
const options = parseArgs(standardIo);
|
||||
if (options.interval && options.command !== "daemon") {
|
||||
console.error(`Warning: --interval is only used in daemon mode, ignored for '${options.command}'`);
|
||||
writeStderrLine(standardIo, `Warning: --interval is only used in daemon mode, ignored for '${options.command}'`);
|
||||
}
|
||||
const avoidStdoutNoise =
|
||||
options.command === "cat" ||
|
||||
@@ -282,13 +287,13 @@ export async function main() {
|
||||
options.command === "unlock-remote" ||
|
||||
options.command === "lock-remote" ||
|
||||
options.command === "remote-status";
|
||||
const infoLog = avoidStdoutNoise ? console.error : console.log;
|
||||
const infoLog = (...values: readonly unknown[]) => {
|
||||
const writeLine = avoidStdoutNoise ? writeStderrLine : writeStdoutLine;
|
||||
writeLine(standardIo, ...values);
|
||||
};
|
||||
if (options.debug) {
|
||||
setGlobalLogFunction((msg, level) => {
|
||||
console.error(`[${level}] ${typeof msg === "string" ? msg : JSON.stringify(msg)}`);
|
||||
if (msg instanceof Error) {
|
||||
console.error(msg);
|
||||
}
|
||||
writeStderrLine(standardIo, `[${level}]`, msg);
|
||||
});
|
||||
} else {
|
||||
setGlobalLogFunction((msg, level) => {
|
||||
@@ -296,7 +301,7 @@ export async function main() {
|
||||
});
|
||||
}
|
||||
if (options.command === "init-settings") {
|
||||
await createDefaultSettingsFile(options);
|
||||
await createDefaultSettingsFile(options, standardIo);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,11 +311,11 @@ export async function main() {
|
||||
try {
|
||||
const stat = await fs.stat(databasePath);
|
||||
if (!stat.isDirectory()) {
|
||||
console.error(`Error: ${databasePath} is not a directory`);
|
||||
writeStderrLine(standardIo, `Error: ${databasePath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
console.error(`Error: Database directory ${databasePath} does not exist`);
|
||||
writeStderrLine(standardIo, `Error: Database directory ${databasePath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -336,11 +341,11 @@ export async function main() {
|
||||
try {
|
||||
const stat = await fs.stat(vaultPath);
|
||||
if (!stat.isDirectory()) {
|
||||
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
||||
writeStderrLine(standardIo, `Error: Vault path ${vaultPath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
||||
writeStderrLine(standardIo, `Error: Vault directory ${vaultPath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -351,12 +356,18 @@ export async function main() {
|
||||
infoLog("");
|
||||
let ignoreRules: IgnoreRules | undefined;
|
||||
if (options.command === "daemon" || options.command === "mirror") {
|
||||
ignoreRules = new IgnoreRules(vaultPath);
|
||||
ignoreRules = new IgnoreRules(vaultPath, (message, detail) => {
|
||||
if (detail === undefined) {
|
||||
writeStderrLine(standardIo, message);
|
||||
} else {
|
||||
writeStderrLine(standardIo, message, detail);
|
||||
}
|
||||
});
|
||||
await ignoreRules.load();
|
||||
}
|
||||
|
||||
// Create service context and hub
|
||||
const context = new NodeServiceContext(databasePath);
|
||||
const context = new NodeServiceContext(databasePath, standardIo);
|
||||
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
||||
serviceHubInstance.API.addLog.setHandler((message: unknown, level: LOG_LEVEL) => {
|
||||
let levelStr = "";
|
||||
@@ -377,19 +388,19 @@ export async function main() {
|
||||
levelStr = "Urgent";
|
||||
break;
|
||||
default:
|
||||
levelStr = `${level}`;
|
||||
levelStr = String(level);
|
||||
}
|
||||
const prefix = `(${levelStr})`;
|
||||
if (level <= LOG_LEVEL_INFO) {
|
||||
if (!options.verbose) return;
|
||||
}
|
||||
console.error(`${prefix} ${message}`);
|
||||
writeStderrLine(standardIo, prefix, message);
|
||||
});
|
||||
// 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.`);
|
||||
writeStderrLine(standardIo, `[Info] Replication result received, but not processed automatically in CLI mode.`);
|
||||
return await Promise.resolve(true);
|
||||
}, -100);
|
||||
}
|
||||
@@ -402,10 +413,10 @@ export async function main() {
|
||||
try {
|
||||
await fs.writeFile(settingsPath, JSON.stringify(data, null, 2), "utf-8");
|
||||
if (options.verbose) {
|
||||
console.error(`[Settings] Saved to ${settingsPath}`);
|
||||
writeStderrLine(standardIo, `[Settings] Saved to ${settingsPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Settings] Failed to save:`, error);
|
||||
writeStderrLine(standardIo, `[Settings] Failed to save:`, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -414,16 +425,15 @@ export async function main() {
|
||||
async (): Promise<ObsidianLiveSyncSettings | undefined> => {
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
const data = JSON.parse(content);
|
||||
const data = JSON.parse(content) as ObsidianLiveSyncSettings;
|
||||
if (options.verbose) {
|
||||
console.error(`[Settings] Loaded from ${settingsPath}`);
|
||||
writeStderrLine(standardIo, `[Settings] Loaded from ${settingsPath}`);
|
||||
}
|
||||
// Force disable IndexedDB adapter in CLI environment
|
||||
data.useIndexedDBAdapter = false;
|
||||
return data;
|
||||
// Force disable IndexedDB adapter in CLI environment without mutating the loaded settings object.
|
||||
return { ...data, useIndexedDBAdapter: false };
|
||||
} catch {
|
||||
if (options.verbose) {
|
||||
console.error(`[Settings] File not found, using defaults`);
|
||||
writeStderrLine(standardIo, `[Settings] File not found, using defaults`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -473,14 +483,14 @@ export async function main() {
|
||||
|
||||
// Setup signal handlers for graceful shutdown
|
||||
const shutdown = async (signal: string) => {
|
||||
console.log();
|
||||
console.log(`[Shutdown] Received ${signal}, shutting down gracefully...`);
|
||||
writeStdoutLine(standardIo);
|
||||
writeStdoutLine(standardIo, `[Shutdown] Received ${signal}, shutting down gracefully...`);
|
||||
try {
|
||||
await core.services.control.onUnload();
|
||||
console.log(`[Shutdown] Complete`);
|
||||
writeStdoutLine(standardIo, `[Shutdown] Complete`);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(`[Shutdown] Error:`, error);
|
||||
writeStderrLine(standardIo, `[Shutdown] Error:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
@@ -502,7 +512,7 @@ export async function main() {
|
||||
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||
fsSync.renameSync(tmpPath, settingsPath);
|
||||
} catch (err) {
|
||||
console.error("[Settings] Failed to restore settings on exit:", err);
|
||||
writeStderrLine(standardIo, "[Settings] Failed to restore settings on exit:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -513,7 +523,7 @@ export async function main() {
|
||||
|
||||
const loadResult = await core.services.control.onLoad();
|
||||
if (!loadResult) {
|
||||
console.error(`[Error] Failed to initialize LiveSync`);
|
||||
writeStderrLine(standardIo, `[Error] Failed to initialize LiveSync`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Capture sync settings before suspendAllSync() clobbers them.
|
||||
@@ -538,15 +548,15 @@ export async function main() {
|
||||
// Check if configured
|
||||
const settings = core.services.setting.currentSettings();
|
||||
if (!settings.isConfigured) {
|
||||
console.warn(`[Warning] LiveSync is not configured yet`);
|
||||
console.warn(`[Warning] Please edit ${settingsPath} to configure CouchDB connection`);
|
||||
console.warn();
|
||||
console.warn(`Required settings:`);
|
||||
console.warn(` - couchDB_URI: CouchDB server URL`);
|
||||
console.warn(` - couchDB_USER: CouchDB username`);
|
||||
console.warn(` - couchDB_PASSWORD: CouchDB password`);
|
||||
console.warn(` - couchDB_DBNAME: Database name`);
|
||||
console.warn();
|
||||
writeStderrLine(standardIo, `[Warning] LiveSync is not configured yet`);
|
||||
writeStderrLine(standardIo, `[Warning] Please edit ${settingsPath} to configure CouchDB connection`);
|
||||
writeStderrLine(standardIo);
|
||||
writeStderrLine(standardIo, `Required settings:`);
|
||||
writeStderrLine(standardIo, ` - couchDB_URI: CouchDB server URL`);
|
||||
writeStderrLine(standardIo, ` - couchDB_USER: CouchDB username`);
|
||||
writeStderrLine(standardIo, ` - couchDB_PASSWORD: CouchDB password`);
|
||||
writeStderrLine(standardIo, ` - couchDB_DBNAME: Database name`);
|
||||
writeStderrLine(standardIo);
|
||||
} else {
|
||||
infoLog(`[Info] LiveSync is configured and ready`);
|
||||
infoLog(`[Info] Database: ${settings.couchDB_URI}/${settings.couchDB_DBNAME}`);
|
||||
@@ -555,7 +565,7 @@ export async function main() {
|
||||
|
||||
const result = await runCommand(options, { databasePath, vaultPath, core, settingsPath, originalSyncSettings });
|
||||
if (!result) {
|
||||
console.error(`[Error] Command '${options.command}' failed`);
|
||||
writeStderrLine(standardIo, `[Error] Command '${options.command}' failed`);
|
||||
process.exitCode = 1;
|
||||
} else if (options.command !== "daemon") {
|
||||
infoLog(`[Done] Command '${options.command}' completed`);
|
||||
@@ -568,7 +578,7 @@ export async function main() {
|
||||
await core.services.control.onUnload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Error] Failed to start:`, error);
|
||||
writeStderrLine(standardIo, `[Error] Failed to start:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
// To prevent unexpected hanging in webRTC connections.
|
||||
|
||||
Reference in New Issue
Block a user