feat: add logs cli and restart hints

This commit is contained in:
Peter Steinberger
2026-01-08 06:48:28 +00:00
parent c9e07616c7
commit d1ceb3aa60
15 changed files with 291 additions and 32 deletions

96
src/cli/logs-cli.ts Normal file
View File

@@ -0,0 +1,96 @@
import { setTimeout as delay } from "node:timers/promises";
import type { Command } from "commander";
import { defaultRuntime } from "../runtime.js";
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
type LogsTailPayload = {
file?: string;
cursor?: number;
size?: number;
lines?: string[];
truncated?: boolean;
reset?: boolean;
};
type LogsCliOptions = {
limit?: string;
maxBytes?: string;
follow?: boolean;
interval?: string;
json?: boolean;
url?: string;
token?: string;
timeout?: string;
expectFinal?: boolean;
};
function parsePositiveInt(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
async function fetchLogs(
opts: LogsCliOptions,
cursor: number | undefined,
): Promise<LogsTailPayload> {
const limit = parsePositiveInt(opts.limit, 200);
const maxBytes = parsePositiveInt(opts.maxBytes, 250_000);
const payload = await callGatewayFromCli("logs.tail", opts, {
cursor,
limit,
maxBytes,
});
if (!payload || typeof payload !== "object") {
throw new Error("Unexpected logs.tail response");
}
return payload as LogsTailPayload;
}
export function registerLogsCli(program: Command) {
const logs = program
.command("logs")
.description("Tail gateway file logs via RPC")
.option("--limit <n>", "Max lines to return", "200")
.option("--max-bytes <n>", "Max bytes to read", "250000")
.option("--follow", "Follow log output", false)
.option("--interval <ms>", "Polling interval in ms", "1000")
.option("--json", "Emit JSON payloads", false);
addGatewayClientOptions(logs);
logs.action(async (opts: LogsCliOptions) => {
const interval = parsePositiveInt(opts.interval, 1000);
let cursor: number | undefined;
let first = true;
while (true) {
const payload = await fetchLogs(opts, cursor);
const lines = Array.isArray(payload.lines) ? payload.lines : [];
if (opts.json) {
defaultRuntime.log(JSON.stringify(payload, null, 2));
} else {
if (first && payload.file) {
defaultRuntime.log(`Log file: ${payload.file}`);
}
for (const line of lines) {
defaultRuntime.log(line);
}
if (payload.truncated) {
defaultRuntime.error("Log tail truncated (increase --max-bytes).");
}
if (payload.reset) {
defaultRuntime.error("Log cursor reset (file rotated).");
}
}
cursor =
typeof payload.cursor === "number" && Number.isFinite(payload.cursor)
? payload.cursor
: cursor;
first = false;
if (!opts.follow) return;
await delay(interval);
}
});
}