diff --git a/README.md b/README.md index d69d036..16c01f8 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ agentctl list # List all sessions (including stopped, last 7 days) agentctl list -a -# Peek at recent output from a session +# Peek at recent output from a session (alias: logs) agentctl peek +agentctl logs # Launch a new Claude Code session agentctl launch -p "Read the spec and implement phase 2" @@ -61,7 +62,7 @@ agentctl stop agentctl resume "fix the failing tests" ``` -Session IDs support prefix matching — `agentctl peek abc123` matches any session starting with `abc123`. +Session IDs support prefix matching — `agentctl peek abc123` (or `agentctl logs abc123`) matches any session starting with `abc123`. ### Parallel Multi-Adapter Launch @@ -144,7 +145,7 @@ agentctl status [options] --adapter Adapter to use --json Output as JSON -agentctl peek [options] +agentctl peek|logs [options] -n, --lines Number of recent messages (default: 20) --adapter Adapter to use diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..8d80783 --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,47 @@ +import { execFile } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +function run(args: string[]): Promise<{ stdout: string; stderr: string }> { + return new Promise((resolve) => { + execFile( + process.execPath, + ["--import", "tsx", "src/cli.ts", ...args], + { env: { ...process.env, AGENTCTL_NO_DAEMON: "1" }, timeout: 10_000 }, + (_err, stdout, stderr) => { + // commander exits 0 for --help; treat non-zero as content too + resolve({ stdout, stderr }); + }, + ); + }); +} + +describe("CLI logs command", () => { + it("appears in top-level --help", async () => { + const { stdout } = await run(["--help"]); + expect(stdout).toContain("logs"); + expect(stdout).toContain("peek"); + }); + + it("logs --help shows default 50 lines", async () => { + const { stdout } = await run(["logs", "--help"]); + expect(stdout).toContain("50"); + expect(stdout).toContain("--lines"); + expect(stdout).toContain("--adapter"); + }); + + it("peek --help shows default 20 lines", async () => { + const { stdout } = await run(["peek", "--help"]); + expect(stdout).toContain("20"); + expect(stdout).toContain("--lines"); + }); + + it("logs errors on missing session (same as peek)", async () => { + const { stderr } = await run(["logs", "nonexistent"]); + expect(stderr).toContain("Session not found"); + }); + + it("peek errors on missing session", async () => { + const { stderr } = await run(["peek", "nonexistent"]); + expect(stderr).toContain("Session not found"); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index e120b25..536c31f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -381,57 +381,71 @@ program process.exit(1); }); +// Shared handler for peek/logs +async function peekAction( + id: string, + opts: { lines: string; adapter?: string }, +): Promise { + const daemonRunning = await ensureDaemon(); + + if (daemonRunning) { + try { + const output = await client.call("session.peek", { + id, + lines: Number.parseInt(opts.lines, 10), + }); + console.log(output); + return; + } catch { + // Daemon failed — fall through to direct adapter lookup + } + } + + // Direct fallback: try specified adapter, or search all adapters + if (opts.adapter) { + const adapter = getAdapter(opts.adapter); + try { + const output = await adapter.peek(id, { + lines: Number.parseInt(opts.lines, 10), + }); + console.log(output); + return; + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + } + + for (const adapter of getAllAdapters()) { + try { + const output = await adapter.peek(id, { + lines: Number.parseInt(opts.lines, 10), + }); + console.log(output); + return; + } catch { + // Try next adapter + } + } + console.error(`Session not found: ${id}`); + process.exit(1); +} + // peek program .command("peek ") - .description("Peek at recent output from a session") + .description("Peek at recent output from a session (alias: logs)") .option("-n, --lines ", "Number of recent messages", "20") .option("--adapter ", "Adapter to use") - .action(async (id: string, opts) => { - const daemonRunning = await ensureDaemon(); + .action(peekAction); - if (daemonRunning) { - try { - const output = await client.call("session.peek", { - id, - lines: Number.parseInt(opts.lines, 10), - }); - console.log(output); - return; - } catch { - // Daemon failed — fall through to direct adapter lookup - } - } - - // Direct fallback: try specified adapter, or search all adapters - if (opts.adapter) { - const adapter = getAdapter(opts.adapter); - try { - const output = await adapter.peek(id, { - lines: Number.parseInt(opts.lines, 10), - }); - console.log(output); - return; - } catch (err) { - console.error((err as Error).message); - process.exit(1); - } - } - - for (const adapter of getAllAdapters()) { - try { - const output = await adapter.peek(id, { - lines: Number.parseInt(opts.lines, 10), - }); - console.log(output); - return; - } catch { - // Try next adapter - } - } - console.error(`Session not found: ${id}`); - process.exit(1); - }); +// logs — alias for peek with higher default line count +program + .command("logs ") + .description("Show recent session output (alias for peek, default 50 lines)") + .option("-n, --lines ", "Number of recent messages", "50") + .option("--adapter ", "Adapter to use") + .action(peekAction); // stop program