diff --git a/docs/cli-reference.mdx b/docs/cli-reference.mdx index 1f9e4e9..ac243e1 100644 --- a/docs/cli-reference.mdx +++ b/docs/cli-reference.mdx @@ -39,9 +39,9 @@ Most API commands support: - `bluebubbles server info` API: `GET /api/v1/server/info` - `bluebubbles server logs [--count ]` - Local process manager, no API endpoint -- `bluebubbles server logs --source api [--count ]` API: `GET /api/v1/server/logs` +- `bluebubbles server logs --source local [--count ]` + Local process manager log file - `bluebubbles server open` Local process manager, no API endpoint - `bluebubbles server start` diff --git a/docs/local-server.mdx b/docs/local-server.mdx index dedc819..f388d57 100644 --- a/docs/local-server.mdx +++ b/docs/local-server.mdx @@ -48,10 +48,10 @@ bluebubbles server start --arg --headless --arg --foreground ## Logs -The CLI prefers a configured log file or the log captured from CLI-owned launches. +`bluebubbles server logs` reads live logs from the BlueBubbles API by default. Use `--source local` for the configured or CLI-owned local log file. ```bash bluebubbles server logs --count 100 -bluebubbles server logs --follow +bluebubbles server logs --source local --follow bluebubbles config set logPath /path/to/bluebubbles.log ``` diff --git a/src/commands/local-server.ts b/src/commands/local-server.ts index 98b3b25..d929c35 100644 --- a/src/commands/local-server.ts +++ b/src/commands/local-server.ts @@ -24,6 +24,54 @@ import { } from "~/lib/local-server.js"; import type { CommandOverrides, OutputOptions } from "~/lib/types.js"; +type LogOptions = CommandOverrides & OutputOptions & { + count: number; + follow?: boolean; + config?: string; + source?: string; + logPath?: string; +}; + +async function printRemoteLogs(options: LogOptions): Promise { + if (options.follow) { + throw new CliError("`--follow` is only supported with local log files.", "validation"); + } + const client = await clientFromOptions(options); + const result = await getRemoteLogs(client, options.count); + maybePrint(result.data, options, () => printSuccess(result.data ?? "", false)); +} + +function registerLogsCommand(command: Command): void { + addConnectionOptions( + command + .description("Show BlueBubbles server logs (API by default; local for CLI-managed log files)") + .option("--source ", "Log source (api|local)"), + ) + .option("--log-path ", "Override the local log file path") + .option("--count ", "Number of log lines to show", (value) => Number.parseInt(value, 10), 100) + .option("-f, --follow", "Follow the local log file") + .action(async (options: LogOptions) => { + const source = (options.source ?? (options.follow ? "local" : "api")).toLowerCase(); + if (source === "api") { + await printRemoteLogs(options); + return; + } + if (source !== "local") { + throw new CliError(`Unsupported log source "${options.source}". Use: api, local`, "validation"); + } + + const context = await withConfig(options); + await showLogs({ + statePath: context.statePath, + config: context.config, + defaultLogPath: context.defaultLogPath, + count: options.count, + follow: options.follow, + logPath: options.logPath, + }); + }); +} + export function registerServerLifecycleCommands(serverCommand: Command): void { addConnectionOptions( serverCommand.command("open").description("Open the local BlueBubbles desktop app (local process manager, no API endpoint)"), @@ -127,36 +175,5 @@ export function registerServerLifecycleCommands(serverCommand: Command): void { maybePrint(state, options, () => printSuccess(`Restarted BlueBubbles with PID ${state.pid}`, false)); }); - addConnectionOptions( - serverCommand - .command("logs") - .description("Show server logs (local process manager by default; use --source api for GET /api/v1/server/logs)") - .option("--source ", "Log source (local|api)", "local"), - ) - .option("--log-path ", "Override the local log file path") - .option("--count ", "Number of log lines to show", (value) => Number.parseInt(value, 10), 100) - .option("-f, --follow", "Follow the log file") - .action(async (options: CommandOverrides & OutputOptions & { count: number; follow?: boolean; config?: string; source?: string }) => { - const source = (options.source ?? "local").toLowerCase(); - if (source === "api") { - if (options.follow) { - throw new CliError("`--follow` is only supported with `--source local`.", "validation"); - } - const client = await clientFromOptions(options); - const result = await getRemoteLogs(client, options.count); - maybePrint(result.data, options, () => printSuccess(result.data ?? "", false)); - return; - } - if (source !== "local") { - throw new CliError(`Unsupported log source "${options.source}". Use: local, api`, "validation"); - } - const context = await withConfig(options); - await showLogs({ - statePath: context.statePath, - config: context.config, - count: options.count, - follow: options.follow, - logPath: options.logPath, - }); - }); + registerLogsCommand(serverCommand.command("logs")); } diff --git a/src/index.test.ts b/src/index.test.ts index a2bb263..d3395ea 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -342,6 +342,10 @@ describe("server flows", () => { test("server alert, update, and restart commands work", async () => { expect((await cli(["server", "logs", "--source", "api"])).exitCode).toBe(0); + const defaultLogs = await cli(["server", "logs"]); + expect(defaultLogs.exitCode).toBe(0); + expect(defaultLogs.stdout).toContain("log line 1"); + expect((await cli(["logs"])).exitCode).not.toBe(0); expect((await cli(["server", "alert", "list"])).exitCode).toBe(0); expect((await cli(["server", "alert", "read", "1"])).exitCode).toBe(0); expect((await cli(["server", "update", "check"])).exitCode).toBe(0); @@ -435,7 +439,7 @@ describe("server lifecycle", () => { const freshConfig = path.join(tmpDir, "local-config.json"); const stop = await cli(["server", "stop", "--yes"], { BLUEBUBBLES_CONFIG: freshConfig }); - const logs = await cli(["server", "logs"], { BLUEBUBBLES_CONFIG: freshConfig }); + const logs = await cli(["server", "logs", "--source", "local"], { BLUEBUBBLES_CONFIG: freshConfig }); const status = await cli(["server", "status"], { BLUEBUBBLES_CONFIG: freshConfig }); const expected = process.platform === "darwin" ? 6 : 5; const expectedStatus = process.platform === "darwin" ? 0 : 5; diff --git a/src/lib/local-server.ts b/src/lib/local-server.ts index 31c3cb6..4316107 100644 --- a/src/lib/local-server.ts +++ b/src/lib/local-server.ts @@ -268,25 +268,22 @@ export async function serverStatus(input: { export async function showLogs(input: { statePath: string; config: CliConfig; + defaultLogPath: string; count: number; follow?: boolean; logPath?: string; }): Promise { ensureMacOS(); const state = await readRuntimeState(input.statePath); - const logPath = input.logPath ?? input.config.logPath ?? state?.logPath; + const logPath = input.logPath ?? input.config.logPath ?? state?.logPath ?? input.defaultLogPath; - if (!logPath) { + if (!existsSync(logPath)) { throw new CliError( - "No local BlueBubbles log file is configured yet. Start the server through the CLI or set `bluebubbles config set logPath /path/to/log`.", + `Local log file does not exist: ${logPath}. Use \`bluebubbles logs --source api\` for live server logs, or start the app with \`bluebubbles server start\` to create the local log file.`, "not-found", ); } - if (!existsSync(logPath)) { - throw new CliError(`Log file does not exist: ${logPath}`, "not-found"); - } - if (input.follow) { const child = spawn("tail", ["-n", String(input.count), "-f", logPath], { stdio: "inherit",