Improve settings #168

Merged
schmelczer merged 14 commits from asch/explore into main 2025-11-19 19:53:10 +00:00
5 changed files with 106 additions and 5 deletions
Showing only changes of commit c92aa63d71 - Show all commits

Add healthcheck for client

Andras Schmelczer 2025-11-18 21:49:35 +00:00

View file

@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="andras@schmelczer.dev"
COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js
COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 \
CMD node /app/healthcheck.js /tmp/vaultlink-health.json
WORKDIR /vault
VOLUME ["/vault"]
ENTRYPOINT ["node", "/app/cli.js"]
ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"]
CMD ["--help"]

View file

@ -12,6 +12,7 @@ export interface CliArgs {
ignorePatterns?: string[];
webSocketRetryIntervalMs?: number;
logLevel: LogLevel;
health?: string;
}
export function parseArgs(argv: string[]): CliArgs {
@ -51,6 +52,10 @@ export function parseArgs(argv: string[]): CliArgs {
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
"INFO"
)
.option(
"--health <path>",
"[OPTIONAL] Path to health status file for Docker healthcheck"
)
.addHelpText(
"after",
`
@ -78,6 +83,7 @@ Examples:
| number
| undefined;
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
const health = opts.health as string | undefined;
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
if (localPath === undefined) {
@ -117,6 +123,7 @@ Examples:
maxFileSizeMB: maxFileSizeMb,
ignorePatterns: ignorePattern,
webSocketRetryIntervalMs: websocketRetryIntervalMs,
logLevel
logLevel,
health
};
}

View file

@ -1,5 +1,7 @@
import * as path from "path";
import * as fs from "fs/promises";
import * as fsSync from "fs";
import type { NetworkConnectionStatus } from "sync-client";
import {
SyncClient,
DEFAULT_SETTINGS,
@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher";
import { formatLogLine, colorize, styleText } from "./logger-formatter";
import packageJson from "../package.json";
function writeHealthStatus(
filePath: string,
connectionStatus: NetworkConnectionStatus
): void {
try {
fsSync.writeFileSync(filePath, JSON.stringify(connectionStatus));
} catch (error) {
console.error(
`Failed to write health status to ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
@ -78,6 +93,7 @@ async function main(): Promise<void> {
syncConcurrency:
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB,
ignorePatterns,
webSocketRetryIntervalMs:
args.webSocketRetryIntervalMs ??
@ -119,6 +135,15 @@ async function main(): Promise<void> {
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
});
copilot-pull-request-reviewer[bot] commented 2025-11-18 22:19:54 +00:00 (Migrated from github.com)

The setInterval creates an untracked timer that will continue running indefinitely. Consider storing the timer reference and clearing it during cleanup or process termination to prevent potential resource leaks.

		const healthInterval = setInterval(() => {
			void client.checkConnection().then((status) => {
				writeHealthStatus(healthFile, status);
			});
		}, 30 * 1000); // every 30 seconds
		const clearHealthInterval = () => clearInterval(healthInterval);
		process.on("SIGINT", clearHealthInterval);
		process.on("SIGTERM", clearHealthInterval);
		process.on("exit", clearHealthInterval);
The setInterval creates an untracked timer that will continue running indefinitely. Consider storing the timer reference and clearing it during cleanup or process termination to prevent potential resource leaks. ```suggestion const healthInterval = setInterval(() => { void client.checkConnection().then((status) => { writeHealthStatus(healthFile, status); }); }, 30 * 1000); // every 30 seconds const clearHealthInterval = () => clearInterval(healthInterval); process.on("SIGINT", clearHealthInterval); process.on("SIGTERM", clearHealthInterval); process.on("exit", clearHealthInterval); ```
if (args.health !== undefined) {
const healthFile = args.health;
setInterval(() => {
void client.checkConnection().then((status) => {
writeHealthStatus(healthFile, status);
});
}, 30 * 1000); // every 30 seconds
}
// Add colored log formatter with level filtering
client.logger.addOnMessageListener((logLine) => {
// Only show messages at or above the configured log level
@ -132,7 +157,10 @@ async function main(): Promise<void> {
const fileWatcher = new FileWatcher(absolutePath, client);
client.addWebSocketStatusChangeListener(() => {
client.logger.info("WebSocket status changed");
const isConnected = client.isWebSocketConnected;
client.logger.info(
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
);
});
client.addRemainingSyncOperationsListener((remaining) => {

View file

@ -0,0 +1,59 @@
#!/usr/bin/env node
copilot-pull-request-reviewer[bot] commented 2025-11-18 22:19:54 +00:00 (Migrated from github.com)

The type guard always returns true for any non-null object, failing to validate the actual structure of NetworkConnectionStatus. This will pass invalid objects that lack required properties like isSuccessful, isWebSocketConnected, or serverMessage, leading to runtime errors when those properties are accessed on line 44.

	const obj = value as Record<string, unknown>;
	return (
		typeof obj.isSuccessful === "boolean" &&
		typeof obj.isWebSocketConnected === "boolean" &&
		typeof obj.serverMessage === "string"
	);
The type guard always returns true for any non-null object, failing to validate the actual structure of NetworkConnectionStatus. This will pass invalid objects that lack required properties like isSuccessful, isWebSocketConnected, or serverMessage, leading to runtime errors when those properties are accessed on line 44. ```suggestion const obj = value as Record<string, unknown>; return ( typeof obj.isSuccessful === "boolean" && typeof obj.isWebSocketConnected === "boolean" && typeof obj.serverMessage === "string" ); ```
/**
* Healthcheck script for Docker container
* Checks if the sync client is connected to the server
*/
import * as fs from "fs";
import type { NetworkConnectionStatus } from "sync-client";
function isHealthStatus(value: unknown): value is NetworkConnectionStatus {
if (typeof value !== "object" || value === null) {
return false;
}
return true;
}
function main(): void {
if (process.argv.length < 3) {
console.error("Usage: healthcheck <path-to-health-file>");
process.exit(1);
}
const [, , healthFile] = process.argv;
try {
// Check if health file exists
if (!fs.existsSync(healthFile)) {
console.error(`Health file does not exist: ${healthFile}`);
process.exit(1);
}
// Read and parse health status
const content = fs.readFileSync(healthFile, "utf-8");
const parsed: unknown = JSON.parse(content);
// Validate the parsed object using type guard
if (!isHealthStatus(parsed)) {
throw new Error("Invalid health status format");
}
const status = parsed;
if (!status.isSuccessful || !status.isWebSocketConnected) {
console.error("Not connected to server: " + status.serverMessage);
process.exit(1);
}
console.log("Healthy: Connected to server");
process.exit(0);
} catch (error) {
console.error(
`Health check failed: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}
main();

View file

@ -2,7 +2,10 @@ const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/cli.ts",
entry: {
cli: "./src/cli.ts",
healthcheck: "./src/healthcheck.ts"
},
target: "node",
mode: "production",
optimization: {
@ -21,7 +24,7 @@ module.exports = {
},
output: {
globalObject: "this",
filename: "cli.js",
filename: "[name].js",
path: path.resolve(__dirname, "dist")
},
plugins: [