From 853784e36100977c0f4b1b02f815990d39ed0062 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 27 Jul 2026 23:51:54 +0100 Subject: [PATCH] Enhance command line utilities and logging functionality - Refactor commandline.js to improve server status checks and OTP authentication handling. - Introduce new functions for checking server status, printing server information, and setting log levels. - Add support for command line flags to manage log levels and server information display. - Implement logging level management in localserver.js, allowing dynamic log level adjustments via API. - Create a new logging.js module to centralize log level validation and application. - Update commandlineutils.js to include utility functions for flag handling and improved CLI output. --- src/commandline.js | 303 +++++++++++++++++++++++++-------- src/commandlineutils.js | 53 ++++-- src/localserver/localserver.js | 26 +++ src/logging.js | 26 +++ 4 files changed, 325 insertions(+), 83 deletions(-) create mode 100644 src/logging.js diff --git a/src/commandline.js b/src/commandline.js index c7b1047..a30336e 100644 --- a/src/commandline.js +++ b/src/commandline.js @@ -1,98 +1,259 @@ import { authenticateWithOtp, checkRunning, - isInfo, - otpCode, + getFlagValue, + hasFlag, printAuthCommand, printerHostInfo, + setLogLevel, startWaiting, stopWaiting, } from "./commandlineutils.js"; +import { VALID_LOG_LEVELS } from "./logging.js"; +import chalk from "chalk"; -export async function handleCommandLine(cli, logger) { - const isCommandLines = otpCode != undefined || isInfo == true; - let runningServer; +async function getRunningServer(cli, logger) { try { startWaiting("Checking if Farm Control Server is running...", logger); - runningServer = await checkRunning(); + const runningServer = await checkRunning(); await stopWaiting(); - if (runningServer == false && isCommandLines == true) { - cli.failure("Farm Control Server is not running."); - cli.blank(); - return { handled: true, result: false }; - } + return runningServer; } catch (err) { await stopWaiting(); cli.failure("Failed to check if Farm Control Server is running:", err); cli.blank(); + return null; + } +} + +async function checkServerRunning(cli, logger) { + const runningServer = await getRunningServer(cli, logger); + if (runningServer == false) { + cli.failure("Farm Control Server is not running."); + cli.blank(); + return false; + } + if (runningServer == null) { + return false; + } + cli.success("Farm Control Server is running."); + return runningServer; +} + +function printServerInfo(cli, runningServer) { + const buildNumber = runningServer.buildNumber ?? runningServer.build ?? "n/a"; + cli.blank(); + cli.sectionHeader("FarmControl Server"); + cli.label( + "Version:", + runningServer.version ? `v${runningServer.version}` : "n/a", + ); + cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); + cli.label("Config:", runningServer.configPath || "n/a"); + cli.label("Data Dir:", runningServer.dataDir || "n/a"); + cli.label("Connected:", cli.yesNo(runningServer.connected == true)); + cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true)); + cli.sectionFooter(); + cli.blank(); + + if (runningServer.authenticated == false && runningServer.connected == true) { + cli.sectionHeader("Authenticate"); + printAuthCommand(cli); + cli.sectionFooter(); + } else if ( + runningServer.connected == true && + runningServer.authenticated == true + ) { + cli.sectionHeader("Host"); + printerHostInfo(runningServer.host, cli); + cli.sectionFooter(); + } + + cli.blank(); +} + +function printHelp(cli) { + cli.sectionHeader("Help"); + cli.label( + chalk.bold.white("Usage: ") + + chalk.bold.cyanBright("farmcontrol-server ") + + chalk.white.dim("[options]"), + ); + cli.sectionFooter(); + const optionsPadding = " ".repeat(9); + cli.label( + chalk.bold.white("Options: ") + + chalk.bold.magentaBright("--info") + + " Outputs server information", + ); + cli.label( + optionsPadding + + chalk.bold.magentaBright("--logLevel") + + " " + + chalk.dim("") + + " Sets log level (" + + chalk.bold.cyan("trace") + + ", " + + chalk.bold.blue("debug") + + ", " + + chalk.bold.green("info") + + ", " + + chalk.bold.yellow("warn") + + ", " + + chalk.bold.red("error") + + ")", + ); + cli.label( + optionsPadding + + chalk.bold.magentaBright("--otp") + + " " + + chalk.dim("") + + " Authenticates with OTP", + ); + cli.sectionFooter(); + cli.blank(); +} + +async function runOtpCommand(cli, logger, otpCode) { + cli.blank(); + + if (otpCode == undefined) { + cli.failure("OTP code is required."); + cli.blank(); return { handled: true, result: false }; } - if (isCommandLines == true) { - cli.success("Farm Control Server is running."); + if (!/^\d{6}$/.test(String(otpCode))) { + cli.failure("OTP code must be a 6-digit number."); + cli.blank(); + return { handled: true, result: false }; } - if (isInfo) { - const buildNumber = - runningServer.buildNumber ?? runningServer.build ?? "n/a"; - cli.blank(); - cli.sectionHeader("FarmControl Server"); - cli.label( - "Version:", - runningServer.version ? `v${runningServer.version}` : "n/a", - ); - cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); - cli.label("Config:", runningServer.configPath || "n/a"); - cli.label("Data Dir:", runningServer.dataDir || "n/a"); - cli.label("Connected:", cli.yesNo(runningServer.connected == true)); - cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true)); - cli.sectionFooter(); - cli.blank(); - if ( - runningServer.authenticated == false && - runningServer.connected == true - ) { - cli.sectionHeader("Authenticate"); - printAuthCommand(cli); - cli.sectionFooter(); - } else if ( - runningServer.connected == true && - runningServer.authenticated == true - ) { - cli.sectionHeader("Host"); - printerHostInfo(runningServer.host, cli); - cli.sectionFooter(); - } - cli.blank(); - return { handled: true, result: runningServer }; + startWaiting("Authenticating with OTP...", logger); + let authenticatedServer; + try { + authenticatedServer = await authenticateWithOtp(otpCode); + await stopWaiting(); + } catch (err) { + await stopWaiting(); + throw err; } - if (otpCode != undefined) { + if (authenticatedServer.valid == false) { + cli.failure("Failed to authenticate!"); + cli.failure(authenticatedServer.error); cli.blank(); - startWaiting("Authenticating with OTP...", logger); - let authenticatedServer; - try { - authenticatedServer = await authenticateWithOtp(otpCode); - await stopWaiting(); - } catch (err) { - await stopWaiting(); - throw err; - } - if (authenticatedServer.valid == false) { - cli.failure("Failed to authenticate!"); - cli.failure(authenticatedServer.error); - } else { - cli.success("Authenticated with OTP."); - cli.blank(); - cli.sectionHeader("Host"); - printerHostInfo(authenticatedServer.host, cli); - cli.sectionFooter(); - cli.blank(); - return { handled: true, result: authenticatedServer }; - } - cli.blank(); - return { handled: true, result: authenticatedServer }; + return authenticatedServer; } - return { handled: false, runningServer }; + cli.success("Authenticated with OTP."); + cli.blank(); + cli.sectionHeader("Host"); + printerHostInfo(authenticatedServer.host, cli); + cli.sectionFooter(); + cli.blank(); + return authenticatedServer; +} + +async function runLogLevelCommand(cli, logger, level) { + cli.blank(); + + if (level == undefined) { + cli.failure("Log level is required."); + cli.blank(); + return { handled: true, result: false }; + } + + const normalized = String(level).toLowerCase(); + if (!VALID_LOG_LEVELS.includes(normalized)) { + cli.failure( + `Invalid log level. Must be one of: ${VALID_LOG_LEVELS.join(", ")}`, + ); + cli.blank(); + return { handled: true, result: false }; + } + + startWaiting("Setting log level...", logger); + let result; + try { + result = await setLogLevel(normalized); + await stopWaiting(); + } catch (err) { + await stopWaiting(); + throw err; + } + + if (result.valid == false) { + cli.failure("Failed to set log level!"); + cli.failure(result.error); + cli.blank(); + return result; + } + + cli.success(`Log level set to ${result.logLevel}.`); + cli.blank(); + return result; +} + +const commands = [ + { + matches: () => hasFlag("info"), + run: async ({ cli, logger }) => { + const runningServer = await checkServerRunning(cli, logger); + if (runningServer == false) { + return { handled: true, result: false }; + } + printServerInfo(cli, runningServer); + return { handled: true, result: true }; + }, + }, + { + matches: () => hasFlag("help") || hasFlag("h"), + run: async ({ cli }) => { + printHelp(cli); + return true; + }, + }, + { + matches: () => + getFlagValue("logLevel") != undefined || hasFlag("logLevel"), + run: async ({ cli, logger }) => { + const runningServer = await checkServerRunning(cli, logger); + if (runningServer == false) { + return { handled: true, result: false }; + } + const result = await runLogLevelCommand( + cli, + logger, + getFlagValue("logLevel"), + ); + return { handled: true, result }; + }, + }, + { + matches: () => getFlagValue("otp") != undefined || hasFlag("otp"), + run: async ({ cli, logger }) => { + const runningServer = await checkServerRunning(cli, logger); + if (runningServer == false) { + return { handled: true, result: false }; + } + const result = await runOtpCommand(cli, logger, getFlagValue("otp")); + return { handled: true, result }; + }, + }, +]; + +export async function handleCommandLine(cli, logger) { + const command = commands.find((entry) => entry.matches()); + + if (!command) { + const runningServer = await getRunningServer(cli, logger); + if (runningServer == null) { + return { handled: true, result: false }; + } + return { handled: false, runningServer }; + } + + const result = await command.run({ cli, logger }); + return { handled: true, result }; } diff --git a/src/commandlineutils.js b/src/commandlineutils.js index ad97c8a..92f1a91 100644 --- a/src/commandlineutils.js +++ b/src/commandlineutils.js @@ -102,7 +102,11 @@ export function createCliLogger(logger) { }), label: withSpinnerStopped((key, value) => { - logger.info(chalk.bold(key), value); + if (value == undefined) { + logger.info(key); + } else { + logger.info(chalk.bold(key), value); + } }), yesNo(value) { @@ -119,23 +123,32 @@ export function createCliLogger(logger) { return cli; } -export function getArgValue(flag) { - const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`)); - if (withEquals) { - return withEquals.slice(flag.length + 1); - } +export function hasFlag(name) { + return ( + process.argv.includes(`--${name}`) || process.argv.includes(`-${name}`) + ); +} - const index = process.argv.indexOf(flag); - if (index !== -1 && index + 1 < process.argv.length) { - return process.argv[index + 1]; +export function getFlagValue(name) { + for (const prefix of [`--${name}`, `-${name}`]) { + const withEquals = process.argv.find((arg) => arg.startsWith(`${prefix}=`)); + if (withEquals) { + return withEquals.slice(prefix.length + 1); + } + + const index = process.argv.indexOf(prefix); + if (index !== -1 && index + 1 < process.argv.length) { + const next = process.argv[index + 1]; + if (!next.startsWith("-")) { + return next; + } + } } return undefined; } export const isHeadless = process.argv.includes("--headless"); -export const isInfo = process.argv.includes("--info"); -export const otpCode = getArgValue("--otp") || undefined; export function printerHostInfo(host, cli) { let ref = host?._reference || "Unknown"; @@ -163,7 +176,7 @@ export function printerHostInfo(host, cli) { export function printAuthCommand(cli) { cli.info( "Run '" + - chalk.bold.white("farmcontrol-server") + + chalk.bold.magentaBright("farmcontrol-server") + " " + chalk.bold.magentaBright("--otp") + " " + @@ -201,3 +214,19 @@ export async function authenticateWithOtp(code) { throw new Error("Failed to authenticate with OTP"); } } + +export async function setLogLevel(level) { + try { + const response = await axios.post( + `http://127.0.0.1:${LOCAL_SERVER_PORT}/logLevel`, + { logLevel: level }, + { timeout: 5000 }, + ); + return response.data; + } catch (err) { + if (err.response?.data) { + return err.response.data; + } + throw new Error("Failed to set log level"); + } +} diff --git a/src/localserver/localserver.js b/src/localserver/localserver.js index b6d4fca..a743e92 100644 --- a/src/localserver/localserver.js +++ b/src/localserver/localserver.js @@ -2,6 +2,7 @@ import express from "express"; import log4js from "log4js"; import { notPrompting } from "../utils.js"; import { getConfigPath, loadConfig } from "../config.js"; +import { applyLogLevel } from "../logging.js"; import { getServerVersionInfo } from "../serverVersion.js"; import { startWaiting, stopWaiting } from "../spinner.js"; @@ -71,6 +72,31 @@ export class LocalServer { res.status(500).json({ error: err.message }); } }); + + this.app.post("/logLevel", async (req, res) => { + logger.debug("Received request to set log level"); + const { logLevel } = req.body; + + if (!logLevel) { + res.status(400).json({ valid: false, error: "logLevel is required" }); + return; + } + + try { + const result = await applyLogLevel(logLevel); + + if (!result.valid) { + res.status(400).json(result); + return; + } + + logger.level = result.logLevel; + res.json(result); + } catch (err) { + logger.error("Failed to set log level:", err); + res.status(500).json({ valid: false, error: err.message }); + } + }); } start() { diff --git a/src/logging.js b/src/logging.js new file mode 100644 index 0000000..b2e3a61 --- /dev/null +++ b/src/logging.js @@ -0,0 +1,26 @@ +import log4js from "log4js"; +import { loadConfig, saveConfig } from "./config.js"; + +export const VALID_LOG_LEVELS = ["trace", "debug", "info", "warn", "error"]; + +export async function applyLogLevel(level) { + const normalized = String(level || "").toLowerCase(); + + if (!VALID_LOG_LEVELS.includes(normalized)) { + return { + valid: false, + error: `Invalid log level. Must be one of: ${VALID_LOG_LEVELS.join(", ")}`, + }; + } + + const config = loadConfig(); + config.logLevel = normalized; + await saveConfig(config); + + log4js.configure({ + appenders: { out: { type: "stdout" } }, + categories: { default: { appenders: ["out"], level: normalized } }, + }); + + return { valid: true, logLevel: normalized }; +}