Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ Most API commands support:
- `bluebubbles server info`
API: `GET /api/v1/server/info`
- `bluebubbles server logs [--count <n>]`
Local process manager, no API endpoint
- `bluebubbles server logs --source api [--count <n>]`
API: `GET /api/v1/server/logs`
- `bluebubbles server logs --source local [--count <n>]`
Local process manager log file
- `bluebubbles server open`
Local process manager, no API endpoint
- `bluebubbles server start`
Expand Down
4 changes: 2 additions & 2 deletions docs/local-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
81 changes: 49 additions & 32 deletions src/commands/local-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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 <source>", "Log source (api|local)"),
)
.option("--log-path <path>", "Override the local log file path")
.option("--count <number>", "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)"),
Expand Down Expand Up @@ -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 <source>", "Log source (local|api)", "local"),
)
.option("--log-path <path>", "Override the local log file path")
.option("--count <number>", "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"));
}
6 changes: 5 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 4 additions & 7 deletions src/lib/local-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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",
Expand Down
Loading