From 4eeeeac9d2313b80d17c60e7fbe9af3ffe9514f3 Mon Sep 17 00:00:00 2001 From: axisrow Date: Thu, 1 Oct 2026 01:39:22 +0800 Subject: [PATCH] fix(daemon): print usage on bare `ccstatusline daemon` instead of starting a server Bare `daemon` now prints the usage line and exits 1, matching the unknown-verb branch (issue #51). The detached foreground server host spawned by `daemon start` moves to the new `daemon serve` subcommand. Co-Authored-By: Claude Code --- src/ccstatusline.ts | 2 +- src/daemon/__tests__/daemon-cli.test.ts | 31 +++++++++++++++++++++++++ src/daemon/lifecycle.ts | 12 +++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/daemon/__tests__/daemon-cli.test.ts diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index fc517201e..07471ecf5 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -160,7 +160,7 @@ async function main() { } // Daemon modes (#16 transport, #17 lifecycle): the foreground server host - // runs on a bare `daemon`; `daemon start|stop|status|restart` coordinate + // runs on `daemon serve`; `daemon start|stop|status|restart` coordinate // the shared background renderer. Checked before the TTY split like // --serve: the daemon is started detached, without a TTY and without a // piped payload. Imported lazily so the per-repaint render path never diff --git a/src/daemon/__tests__/daemon-cli.test.ts b/src/daemon/__tests__/daemon-cli.test.ts new file mode 100644 index 000000000..0dccf1088 --- /dev/null +++ b/src/daemon/__tests__/daemon-cli.test.ts @@ -0,0 +1,31 @@ +import { + describe, + expect, + it, + vi +} from 'vitest'; + +// CLI behavior of a bare `ccstatusline daemon` (issue #51): it must print +// the usage line and exit non-zero instead of starting a foreground server. +// Run in-process (argv pinned, process.exit mocked) so the assertion covers +// the parsing branch without spawning a child. +describe('daemon CLI usage', () => { + it('prints usage and exits non-zero on a bare `daemon`', async () => { + const originalArgv = process.argv; + process.argv = ['bun', 'ccstatusline', 'daemon']; + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => { + throw new Error(`exit:${code ?? 0}`); + }); + try { + const { runDaemonCommand } = await import('../lifecycle'); + await expect(runDaemonCommand()).rejects.toThrow('exit:1'); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('usage: ccstatusline daemon [start|stop|status|restart|install|uninstall]'); + } finally { + exitSpy.mockRestore(); + errorSpy.mockRestore(); + process.argv = originalArgv; + } + }); +}); diff --git a/src/daemon/lifecycle.ts b/src/daemon/lifecycle.ts index ddcf7e3a9..2eab270e7 100644 --- a/src/daemon/lifecycle.ts +++ b/src/daemon/lifecycle.ts @@ -549,7 +549,7 @@ export async function ensureDaemon(options: LifecycleOptions = {}): Promise | undefined): string { /** * `ccstatusline daemon [start|stop|status|restart|install|uninstall]` (#17, - * #19). Bare `daemon` stays the foreground server host from #16. + * #19). Bare `daemon` prints usage; `daemon serve` is the foreground server + * host from #16 (spawned detached by `daemon start`). */ export async function runDaemonCommand(): Promise { const daemonArgIndex = process.argv.indexOf('daemon'); const subcommand = process.argv[daemonArgIndex + 1] ?? ''; if (subcommand === '') { + console.error('usage: ccstatusline daemon [start|stop|status|restart|install|uninstall]'); + process.exit(1); + } + + if (subcommand === 'serve') { const { runDaemonServer } = await import('./server'); await runDaemonServer(); return;