Add log level handling
This commit is contained in:
parent
450e62dd25
commit
70c7bdcade
3 changed files with 167 additions and 45 deletions
|
|
@ -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/);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue