From 91d7eb44e110f5f92d57658f829685a4188125e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 18:24:56 +0000 Subject: [PATCH 1/2] Send JSON status from daemon and render table in the CLI client Extracted from PR #177. The status daemon-command previously rendered a cli-table3 table (with ANSI colors via log-update) directly into the socket. Move that rendering to the CLI client: the daemon now emits a newline-delimited JSON array of ScriptStatusEntry objects (or a JSON error object), and the client parses each line and renders the table locally, using log-update for the --interval refresh case. Because connect() no longer auto-pipes socket data to stdout (the status client needs to consume the data itself), pipeToStdout() is extracted as an explicit helper that the start/stop/restart/logs/shutdown commands call. --- src/commands/connect.ts | 11 ++- src/commands/logs.command.ts | 3 +- src/commands/restart.command.ts | 3 +- src/commands/shutdown.command.ts | 3 +- src/commands/start.command.ts | 3 +- src/commands/status.command.ts | 60 ++++++++++++- src/commands/stop.command.ts | 3 +- src/daemon-command/status.daemon-command.ts | 99 +++++++++------------ 8 files changed, 117 insertions(+), 68 deletions(-) diff --git a/src/commands/connect.ts b/src/commands/connect.ts index 4f3ecae..75973a0 100644 --- a/src/commands/connect.ts +++ b/src/commands/connect.ts @@ -20,9 +20,12 @@ export async function connect(): Promise { client.on("connect", () => { resolve(client); }); - client.on("data", (data) => { - //TODO handle stderr/stdin and also write on stderr - process.stdout.write(data); - }); + }); +} + +export function pipeToStdout(client: Socket): void { + client.on("data", (data) => { + //TODO handle stderr/stdin and also write on stderr + process.stdout.write(data); }); } diff --git a/src/commands/logs.command.ts b/src/commands/logs.command.ts index ae33802..8dd91cf 100644 --- a/src/commands/logs.command.ts +++ b/src/commands/logs.command.ts @@ -1,9 +1,10 @@ import { type LogsCommandOptions } from "../daemon-command/logs.daemon-command.js"; -import { connect } from "./connect.js"; +import { connect, pipeToStdout } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; export const logs = async (options: LogsCommandOptions): Promise => { await validConfigOrExit(); const client = await connect(); + pipeToStdout(client); client.write(`logs ${JSON.stringify(options)}`); }; diff --git a/src/commands/restart.command.ts b/src/commands/restart.command.ts index 58661ca..794c0d4 100644 --- a/src/commands/restart.command.ts +++ b/src/commands/restart.command.ts @@ -1,9 +1,10 @@ import { type RestartCommandOptions } from "../daemon-command/restart.daemon-command.js"; -import { connect } from "./connect.js"; +import { connect, pipeToStdout } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; export const restart = async (options: RestartCommandOptions): Promise => { await validConfigOrExit(); const client = await connect(); + pipeToStdout(client); client.write(`restart ${JSON.stringify(options)}`); }; diff --git a/src/commands/shutdown.command.ts b/src/commands/shutdown.command.ts index 2992387..08f8b2e 100644 --- a/src/commands/shutdown.command.ts +++ b/src/commands/shutdown.command.ts @@ -1,8 +1,9 @@ -import { connect } from "./connect.js"; +import { connect, pipeToStdout } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; export const shutdown = async (): Promise => { await validConfigOrExit(); const client = await connect(); + pipeToStdout(client); client.write("shutdown"); }; diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index e81b77a..4631a03 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -1,9 +1,10 @@ import { type StartCommandOptions } from "../daemon-command/start.daemon-command.js"; -import { connect } from "./connect.js"; +import { connect, pipeToStdout } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; export const start = async (options: StartCommandOptions): Promise => { await validConfigOrExit(); const client = await connect(); + pipeToStdout(client); client.write(`start ${JSON.stringify(options)}`); }; diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index fc80d3e..d98e42e 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -1,9 +1,67 @@ -import { type StatusCommandOptions } from "../daemon-command/status.daemon-command.js"; +import CLITable from "cli-table3"; +import colors from "colors"; +import { create as createLogUpdate } from "log-update"; + +import { type ScriptStatus } from "../daemon-command/script.js"; +import { type ScriptStatusEntry, type StatusCommandOptions } from "../daemon-command/status.daemon-command.js"; import { connect } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; +const statusTexts: { [status in ScriptStatus]: string } = { + started: colors.green("Running"), + stopping: colors.red("Stopping"), + stopped: "Stopped", + waiting: colors.yellow("Waiting"), + backoff: colors.red("Backoff"), +}; + +function renderTable(entries: ScriptStatusEntry[]): string { + const table = new CLITable({ + head: [ + colors.blue.bold("ID"), + colors.blue.bold("Script"), + colors.blue.bold("Status"), + colors.blue.bold("CPU"), + colors.blue.bold("Mem"), + colors.bold.blue("PID"), + colors.bold.blue("Restarts"), + ], + style: { compact: true }, + }); + for (const entry of entries) { + table.push([entry.id, entry.name, statusTexts[entry.status], entry.cpu, entry.memory, entry.pid?.toString(), entry.restarts]); + } + return table.toString(); +} + export const status = async (options: StatusCommandOptions): Promise => { await validConfigOrExit(); const client = await connect(); + + const logUpdate = options.interval ? createLogUpdate(process.stdout) : undefined; + let buffer = ""; + + client.on("data", (data: Buffer) => { + buffer += data.toString(); + let newlineIndex: number; + while ((newlineIndex = buffer.indexOf("\n")) !== -1) { + const line = buffer.substring(0, newlineIndex); + buffer = buffer.substring(newlineIndex + 1); + + const parsed = JSON.parse(line); + if (parsed.error) { + console.log(parsed.error); + return; + } + + const output = renderTable(parsed as ScriptStatusEntry[]); + if (logUpdate) { + logUpdate(output); + } else { + console.log(output); + } + } + }); + client.write(`status ${JSON.stringify(options)}`); }; diff --git a/src/commands/stop.command.ts b/src/commands/stop.command.ts index 98786a7..9638ca5 100644 --- a/src/commands/stop.command.ts +++ b/src/commands/stop.command.ts @@ -1,9 +1,10 @@ import { type StopCommandOptions } from "../daemon-command/stop.daemon-command.js"; -import { connect } from "./connect.js"; +import { connect, pipeToStdout } from "./connect.js"; import { validConfigOrExit } from "./validate-config.js"; export const stop = async (options: StopCommandOptions): Promise => { await validConfigOrExit(); const client = await connect(); + pipeToStdout(client); client.write(`stop ${JSON.stringify(options)}`); }; diff --git a/src/daemon-command/status.daemon-command.ts b/src/daemon-command/status.daemon-command.ts index a6b2e9e..f2750f2 100644 --- a/src/daemon-command/status.daemon-command.ts +++ b/src/daemon-command/status.daemon-command.ts @@ -1,6 +1,3 @@ -import CLITable from "cli-table3"; -import colors from "colors"; -import { create as createLogUpdate } from "log-update"; import { type Socket } from "net"; import pidtree from "pidtree"; import pidusage from "pidusage"; @@ -14,13 +11,15 @@ export interface StatusCommandOptions extends ScriptsMatchingPatternOptions { interval: number | undefined; } -const statusTexts: { [status in ScriptStatus]: string } = { - started: colors.green("Started"), - stopping: colors.red("Stopping"), - stopped: "Stopped", - waiting: colors.yellow("Waiting"), - backoff: colors.red("Backoff"), -}; +export interface ScriptStatusEntry { + id: number; + name: string; + status: ScriptStatus; + cpu: string; + memory: string; + pid: number | undefined; + restarts: number; +} async function pidusageRecursive(pid: number): Promise<{ cpu: number; memory: number }> { const pids = await pidtree(pid, { root: true }); @@ -35,63 +34,47 @@ async function pidusageRecursive(pid: number): Promise<{ cpu: number; memory: nu ); } +async function getScriptEntries(daemon: Daemon, patterns: string[]): Promise { + const scriptsToProcess = scriptsMatchingPattern(daemon, { patterns }); + const entries: ScriptStatusEntry[] = []; + for (const script of scriptsToProcess) { + const pid = script.process?.pid; + let cpu = ""; + let memory = ""; + if (pid && script.status == "started") { + try { + const stats = await pidusageRecursive(pid); + cpu = `${Math.round(stats.cpu)}%`; + memory = prettyBytes(stats.memory); + } catch { + // + } + } + entries.push({ + id: script.id, + name: script.name, + status: script.status, + cpu, + memory, + pid, + restarts: script.restartCount, + }); + } + return entries; +} + export async function statusDaemonCommand(daemon: Daemon, socket: Socket, options: StatusCommandOptions): Promise { const scriptsToProcess = scriptsMatchingPattern(daemon, { patterns: options.patterns }); if (!scriptsToProcess.length) { - socket.write("No matching scripts found in dev-pm config\n"); + socket.write(`${JSON.stringify({ error: "No matching scripts found in dev-pm config" })}\n`); socket.end(); return; } - //log-update reads rows/columns from terminal, but in our case it's a socket that doesn't contain those - //inject a high enough number so will refresh more rows and don't wrap too early - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - socket.rows = 1000; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - socket.columns = 200; - - const logUpdate = createLogUpdate(socket); - do { - const table = new CLITable({ - head: [ - colors.blue.bold("ID"), - colors.blue.bold("Script"), - colors.blue.bold("Status"), - colors.blue.bold("CPU"), - colors.blue.bold("Mem"), - colors.bold.blue("PID"), - colors.bold.blue("Restarts"), - ], - style: { compact: true }, - }); - for (const script of scriptsToProcess) { - const pid = script.process?.pid; - let status = statusTexts[script.status]; - if (script.status == "started") { - if (pid) { - status = colors.green("Running"); - } - } - - let cpu = ""; - let memory = ""; - if (pid && script.status == "started") { - try { - const stats = await pidusageRecursive(pid); - cpu = `${Math.round(stats.cpu)}%`; - memory = prettyBytes(stats.memory); - } catch { - // - } - } - table.push([script.id, script.name, status, cpu, memory, pid?.toString(), script.restartCount]); - } - + const entries = await getScriptEntries(daemon, options.patterns); if (!socket.writable) break; - logUpdate(table.toString()); + socket.write(`${JSON.stringify(entries)}\n`); if (options.interval) { await delay(options.interval * 1000); } From 3a7f48e71c00ae8737454e8c298e9aca4e48a5a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 18:31:47 +0000 Subject: [PATCH 2/2] Add changeset for status JSON protocol change --- .changeset/status-json-protocol.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/status-json-protocol.md diff --git a/.changeset/status-json-protocol.md b/.changeset/status-json-protocol.md new file mode 100644 index 0000000..36f7ad8 --- /dev/null +++ b/.changeset/status-json-protocol.md @@ -0,0 +1,5 @@ +--- +"@comet/dev-process-manager": minor +--- + +Internal: the daemon now sends status as structured JSON and the table rendering is done in the CLI client (no change to the `status` output)