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
2 changes: 1 addition & 1 deletion src/ccstatusline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions src/daemon/__tests__/daemon-cli.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
});
12 changes: 9 additions & 3 deletions src/daemon/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ export async function ensureDaemon(options: LifecycleOptions = {}): Promise<Ensu

/**
* Spawn the daemon host detached so it outlives this process. The child is
* this same entry in `daemon` mode; the config path is passed explicitly so
* this same entry in `daemon serve` mode; the config path is passed explicitly so
* a custom --config on the parent survives the splice in main().
*/
function spawnDetachedDaemon(options: LifecycleOptions): SpawnedDaemon | undefined {
Expand All @@ -562,7 +562,7 @@ function spawnDetachedDaemon(options: LifecycleOptions): SpawnedDaemon | undefin
}
const child: ChildProcess = spawn(
process.execPath,
[entry, '--config', getConfigPath(), 'daemon'],
[entry, '--config', getConfigPath(), 'daemon', 'serve'],
{ detached: true, stdio: 'ignore' }
);
child.unref();
Expand Down Expand Up @@ -681,13 +681,19 @@ function formatCounters(counters: Record<string, number> | 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<void> {
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;
Expand Down
Loading