Add log level handling

This commit is contained in:
Andras Schmelczer 2025-10-21 21:56:28 +01:00
parent 450e62dd25
commit 70c7bdcade
3 changed files with 167 additions and 45 deletions

View file

@ -1,6 +1,7 @@
import { test } from "node:test"; import { test } from "node:test";
import * as assert from "node:assert/strict"; import * as assert from "node:assert/strict";
import { parseArgs } from "./args"; import { parseArgs } from "./args";
import { LogLevel } from "sync-client";
test("parseArgs - parse basic arguments", () => { test("parseArgs - parse basic arguments", () => {
const args = parseArgs([ const args = parseArgs([
@ -134,3 +135,96 @@ test("parseArgs - throws on missing vault name", () => {
]); ]);
}, /--vault-name/); }, /--vault-name/);
}); });
test("parseArgs - default log level is INFO", () => {
const args = parseArgs([
"node",
"cli.js",
"-l",
"/path/to/vault",
"-r",
"https://sync.example.com",
"-t",
"mytoken",
"-v",
"default"
]);
assert.equal(args.logLevel, LogLevel.INFO);
});
test("parseArgs - parse DEBUG log level", () => {
const args = parseArgs([
"node",
"cli.js",
"-l",
"/path/to/vault",
"-r",
"https://sync.example.com",
"-t",
"mytoken",
"-v",
"default",
"--log-level",
"DEBUG"
]);
assert.equal(args.logLevel, LogLevel.DEBUG);
});
test("parseArgs - parse ERROR log level", () => {
const args = parseArgs([
"node",
"cli.js",
"-l",
"/path/to/vault",
"-r",
"https://sync.example.com",
"-t",
"mytoken",
"-v",
"default",
"--log-level",
"ERROR"
]);
assert.equal(args.logLevel, LogLevel.ERROR);
});
test("parseArgs - log level is case insensitive", () => {
const args = parseArgs([
"node",
"cli.js",
"-l",
"/path/to/vault",
"-r",
"https://sync.example.com",
"-t",
"mytoken",
"-v",
"default",
"--log-level",
"debug"
]);
assert.equal(args.logLevel, LogLevel.DEBUG);
});
test("parseArgs - throws on invalid log level", () => {
assert.throws(() => {
parseArgs([
"node",
"cli.js",
"-l",
"/path/to/vault",
"-r",
"https://sync.example.com",
"-t",
"mytoken",
"-v",
"default",
"--log-level",
"INVALID"
]);
}, /Invalid log level/);
});

View file

@ -1,5 +1,6 @@
import { Command } from "commander"; import { Command } from "commander";
import packageJson from "../package.json"; import packageJson from "../package.json";
import { LogLevel } from "sync-client";
export interface CliArgs { export interface CliArgs {
remoteUri: string; remoteUri: string;
@ -10,6 +11,7 @@ export interface CliArgs {
maxFileSizeMB?: number; maxFileSizeMB?: number;
ignorePatterns?: string[]; ignorePatterns?: string[];
webSocketRetryIntervalMs?: number; webSocketRetryIntervalMs?: number;
logLevel: LogLevel;
} }
export function parseArgs(argv: string[]): CliArgs { export function parseArgs(argv: string[]): CliArgs {
@ -21,20 +23,7 @@ export function parseArgs(argv: string[]): CliArgs {
"VaultLink Local CLI - Sync your vault to the local filesystem" "VaultLink Local CLI - Sync your vault to the local filesystem"
) )
.version(packageJson.version) .version(packageJson.version)
.exitOverride((err) => { .option("-l, --local-path <path>", "Local directory path to sync")
// Let help and version exit normally
if (
err.code === "commander.helpDisplayed" ||
err.code === "commander.version"
) {
process.exit(0);
}
throw err;
})
.requiredOption(
"-l, --local-path <path>",
"Local directory path to sync"
)
.option("-r, --remote-uri <uri>", "Remote server URI") .option("-r, --remote-uri <uri>", "Remote server URI")
.option("-t, --token <token>", "Authentication token") .option("-t, --token <token>", "Authentication token")
.option("-v, --vault-name <name>", "Vault name") .option("-v, --vault-name <name>", "Vault name")
@ -57,6 +46,11 @@ export function parseArgs(argv: string[]): CliArgs {
"[OPTIONAL] WebSocket retry interval in milliseconds", "[OPTIONAL] WebSocket retry interval in milliseconds",
parseInt parseInt
) )
.option(
"--log-level <level>",
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
"INFO"
)
.addHelpText( .addHelpText(
"after", "after",
` `
@ -64,40 +58,65 @@ Examples:
$ vaultlink -l ./my-vault -r https://sync.example.com -t mytoken -v default $ vaultlink -l ./my-vault -r https://sync.example.com -t mytoken -v default
$ vaultlink -l ./my-vault -r https://sync.example.com -t mytoken -v default \\ $ vaultlink -l ./my-vault -r https://sync.example.com -t mytoken -v default \\
--ignore-pattern ".git/**" --ignore-pattern "*.tmp" --ignore-pattern ".git/**" --ignore-pattern "*.tmp"
$ vaultlink -l ./my-vault -r https://sync.example.com -t mytoken -v default \\
--log-level DEBUG
` `
); );
program.parse(argv); program.parse(argv);
const options = program.opts<{ /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
localPath: string; const opts = program.opts();
remoteUri?: string; const localPath = opts.localPath as string | undefined;
token?: string; const remoteUri = opts.remoteUri as string | undefined;
vaultName?: string; const token = opts.token as string | undefined;
syncConcurrency?: number; const vaultName = opts.vaultName as string | undefined;
maxFileSizeMb?: number; const syncConcurrency = opts.syncConcurrency as number | undefined;
ignorePattern?: string[]; const maxFileSizeMb = opts.maxFileSizeMb as number | undefined;
websocketRetryIntervalMs?: number; const ignorePattern = opts.ignorePattern as string[] | undefined;
}>(); const websocketRetryIntervalMs = opts.websocketRetryIntervalMs as
| number
| undefined;
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
if (options.remoteUri === undefined) { if (localPath === undefined) {
throw new Error(
"required option '-l, --local-path <path>' not specified"
);
}
if (remoteUri === undefined) {
throw new Error("required option '--remote-uri <uri>' not specified"); throw new Error("required option '--remote-uri <uri>' not specified");
} }
if (options.token === undefined) { if (token === undefined) {
throw new Error("required option '--token <token>' not specified"); throw new Error("required option '--token <token>' not specified");
} }
if (options.vaultName === undefined) { if (vaultName === undefined) {
throw new Error("required option '--vault-name <name>' not specified"); throw new Error("required option '--vault-name <name>' not specified");
} }
// Validate and parse log level
const logLevelUpper = logLevelStr.toUpperCase();
const validLogLevels = Object.values(LogLevel);
const isLogLevel = (value: string): value is LogLevel => {
return (validLogLevels as readonly string[]).includes(value);
};
if (!isLogLevel(logLevelUpper)) {
throw new Error(
`Invalid log level '${logLevelStr}'. Valid values are: ${validLogLevels.join(", ")}`
);
}
const logLevel = logLevelUpper;
return { return {
localPath: options.localPath, localPath,
remoteUri: options.remoteUri ?? "", remoteUri,
token: options.token ?? "", token,
vaultName: options.vaultName ?? "", vaultName,
syncConcurrency: options.syncConcurrency, syncConcurrency,
maxFileSizeMB: options.maxFileSizeMb, maxFileSizeMB: maxFileSizeMb,
ignorePatterns: options.ignorePattern, ignorePatterns: ignorePattern,
webSocketRetryIntervalMs: options.websocketRetryIntervalMs webSocketRetryIntervalMs: websocketRetryIntervalMs,
logLevel
}; };
} }

View file

@ -3,6 +3,7 @@ import * as fs from "fs/promises";
import { import {
SyncClient, SyncClient,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
LogLevel,
type SyncSettings, type SyncSettings,
type StoredDatabase type StoredDatabase
} from "sync-client"; } from "sync-client";
@ -12,6 +13,13 @@ import { FileWatcher } from "./file-watcher";
import { formatLogLine, colorize, styleText } from "./logger-formatter"; import { formatLogLine, colorize, styleText } from "./logger-formatter";
import packageJson from "../package.json"; import packageJson from "../package.json";
const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
[LogLevel.WARNING]: 2,
[LogLevel.ERROR]: 3
};
async function main(): Promise<void> { async function main(): Promise<void> {
const args = parseArgs(process.argv); const args = parseArgs(process.argv);
const absolutePath = path.resolve(args.localPath); const absolutePath = path.resolve(args.localPath);
@ -34,7 +42,6 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
// Print header with colors
console.log( console.log(
styleText("VaultLink Local CLI", "bold", "cyan") + styleText("VaultLink Local CLI", "bold", "cyan") +
colorize(` v${packageJson.version}`, "dim") colorize(` v${packageJson.version}`, "dim")
@ -112,9 +119,12 @@ async function main(): Promise<void> {
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n" nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
}); });
// Add colored log formatter // Add colored log formatter with level filtering
client.logger.addOnMessageListener((logLine) => { client.logger.addOnMessageListener((logLine) => {
console.log(formatLogLine(logLine)); // Only show messages at or above the configured log level
if (LOG_LEVEL_ORDER[logLine.level] >= LOG_LEVEL_ORDER[args.logLevel]) {
console.log(formatLogLine(logLine));
}
}); });
client.logger.info("Starting sync client"); client.logger.info("Starting sync client");
@ -122,10 +132,7 @@ async function main(): Promise<void> {
const fileWatcher = new FileWatcher(absolutePath, client); const fileWatcher = new FileWatcher(absolutePath, client);
client.addWebSocketStatusChangeListener(() => { client.addWebSocketStatusChangeListener(() => {
const currentSettings = client.getSettings(); client.logger.info("WebSocket status changed");
if (currentSettings.isSyncEnabled) {
client.logger.info("WebSocket status changed");
}
}); });
client.addRemainingSyncOperationsListener((remaining) => { client.addRemainingSyncOperationsListener((remaining) => {
@ -143,6 +150,7 @@ async function main(): Promise<void> {
"yellow" "yellow"
) )
); );
fileWatcher.stop(); fileWatcher.stop();
await client.waitAndStop(); await client.waitAndStop();
console.log(colorize("Shutdown complete", "green")); console.log(colorize("Shutdown complete", "green"));
@ -179,9 +187,9 @@ async function main(): Promise<void> {
console.log(colorize("─".repeat(50), "dim")); console.log(colorize("─".repeat(50), "dim"));
console.log(""); console.log("");
await new Promise<void>(() => { // await new Promise<void>(() => {
// Keep process alive until signal received
}); // });
} catch (error) { } catch (error) {
console.error( console.error(
colorize( colorize(
@ -189,6 +197,7 @@ async function main(): Promise<void> {
"red" "red"
) )
); );
fileWatcher.stop(); fileWatcher.stop();
await client.waitAndStop(); await client.waitAndStop();
process.exit(1); process.exit(1);