Add local CLI #144
3 changed files with 167 additions and 45 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { test } from "node:test";
|
||||
import * as assert from "node:assert/strict";
|
||||
import { parseArgs } from "./args";
|
||||
import { LogLevel } from "sync-client";
|
||||
|
||||
test("parseArgs - parse basic arguments", () => {
|
||||
const args = parseArgs([
|
||||
|
|
@ -134,3 +135,96 @@ test("parseArgs - throws on missing 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 packageJson from "../package.json";
|
||||
import { LogLevel } from "sync-client";
|
||||
|
||||
export interface CliArgs {
|
||||
remoteUri: string;
|
||||
|
|
@ -10,6 +11,7 @@ export interface CliArgs {
|
|||
maxFileSizeMB?: number;
|
||||
ignorePatterns?: string[];
|
||||
webSocketRetryIntervalMs?: number;
|
||||
logLevel: LogLevel;
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
.version(packageJson.version)
|
||||
.exitOverride((err) => {
|
||||
// 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("-l, --local-path <path>", "Local directory path to sync")
|
||||
.option("-r, --remote-uri <uri>", "Remote server URI")
|
||||
.option("-t, --token <token>", "Authentication token")
|
||||
.option("-v, --vault-name <name>", "Vault name")
|
||||
|
|
@ -57,6 +46,11 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||
"[OPTIONAL] WebSocket retry interval in milliseconds",
|
||||
parseInt
|
||||
)
|
||||
.option(
|
||||
"--log-level <level>",
|
||||
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
|
||||
"INFO"
|
||||
)
|
||||
.addHelpText(
|
||||
"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 \\
|
||||
--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);
|
||||
|
||||
const options = program.opts<{
|
||||
localPath: string;
|
||||
remoteUri?: string;
|
||||
token?: string;
|
||||
vaultName?: string;
|
||||
syncConcurrency?: number;
|
||||
maxFileSizeMb?: number;
|
||||
ignorePattern?: string[];
|
||||
websocketRetryIntervalMs?: number;
|
||||
}>();
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
|
||||
const opts = program.opts();
|
||||
const localPath = opts.localPath as string | undefined;
|
||||
const remoteUri = opts.remoteUri as string | undefined;
|
||||
const token = opts.token as string | undefined;
|
||||
const vaultName = opts.vaultName as string | undefined;
|
||||
const syncConcurrency = opts.syncConcurrency as number | undefined;
|
||||
const maxFileSizeMb = opts.maxFileSizeMb as number | undefined;
|
||||
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");
|
||||
}
|
||||
if (options.token === undefined) {
|
||||
if (token === undefined) {
|
||||
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");
|
||||
}
|
||||
|
||||
// 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 {
|
||||
localPath: options.localPath,
|
||||
remoteUri: options.remoteUri ?? "",
|
||||
token: options.token ?? "",
|
||||
vaultName: options.vaultName ?? "",
|
||||
syncConcurrency: options.syncConcurrency,
|
||||
maxFileSizeMB: options.maxFileSizeMb,
|
||||
ignorePatterns: options.ignorePattern,
|
||||
webSocketRetryIntervalMs: options.websocketRetryIntervalMs
|
||||
localPath,
|
||||
remoteUri,
|
||||
token,
|
||||
vaultName,
|
||||
syncConcurrency,
|
||||
maxFileSizeMB: maxFileSizeMb,
|
||||
ignorePatterns: ignorePattern,
|
||||
webSocketRetryIntervalMs: websocketRetryIntervalMs,
|
||||
logLevel
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as fs from "fs/promises";
|
|||
import {
|
||||
SyncClient,
|
||||
DEFAULT_SETTINGS,
|
||||
LogLevel,
|
||||
type SyncSettings,
|
||||
type StoredDatabase
|
||||
} from "sync-client";
|
||||
|
|
@ -12,6 +13,13 @@ import { FileWatcher } from "./file-watcher";
|
|||
import { formatLogLine, colorize, styleText } from "./logger-formatter";
|
||||
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> {
|
||||
const args = parseArgs(process.argv);
|
||||
const absolutePath = path.resolve(args.localPath);
|
||||
|
|
@ -34,7 +42,6 @@ async function main(): Promise<void> {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// Print header with colors
|
||||
console.log(
|
||||
styleText("VaultLink Local CLI", "bold", "cyan") +
|
||||
colorize(` v${packageJson.version}`, "dim")
|
||||
|
|
@ -112,9 +119,12 @@ async function main(): Promise<void> {
|
|||
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
|
||||
});
|
||||
|
||||
// Add colored log formatter
|
||||
// Add colored log formatter with level filtering
|
||||
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");
|
||||
|
|
@ -122,10 +132,7 @@ async function main(): Promise<void> {
|
|||
const fileWatcher = new FileWatcher(absolutePath, client);
|
||||
|
||||
client.addWebSocketStatusChangeListener(() => {
|
||||
const currentSettings = client.getSettings();
|
||||
if (currentSettings.isSyncEnabled) {
|
||||
client.logger.info("WebSocket status changed");
|
||||
}
|
||||
client.logger.info("WebSocket status changed");
|
||||
});
|
||||
|
||||
client.addRemainingSyncOperationsListener((remaining) => {
|
||||
|
|
@ -143,6 +150,7 @@ async function main(): Promise<void> {
|
|||
"yellow"
|
||||
)
|
||||
);
|
||||
|
||||
fileWatcher.stop();
|
||||
await client.waitAndStop();
|
||||
console.log(colorize("Shutdown complete", "green"));
|
||||
|
|
@ -179,9 +187,9 @@ async function main(): Promise<void> {
|
|||
console.log(colorize("─".repeat(50), "dim"));
|
||||
console.log("");
|
||||
|
||||
await new Promise<void>(() => {
|
||||
// Keep process alive until signal received
|
||||
});
|
||||
// await new Promise<void>(() => {
|
||||
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
colorize(
|
||||
|
|
@ -189,6 +197,7 @@ async function main(): Promise<void> {
|
|||
"red"
|
||||
)
|
||||
);
|
||||
|
||||
fileWatcher.stop();
|
||||
await client.waitAndStop();
|
||||
process.exit(1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue