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
6 changes: 5 additions & 1 deletion dev/cli/src/commands/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ export async function logs(args: string[], root: string) {
return;
}

if (svc.type === 'infra') {
if (svc.type === 'infra' && svc.devCommand?.startsWith('docker compose')) {
const proc = Bun.spawn(
['docker', 'compose', '-f', 'dev/docker-compose.yml', 'logs', '-f', svc.name],
{ stdout: 'inherit', stderr: 'inherit', cwd: root }
);
await proc.exited;
} else if (svc.type === 'infra') {
ui.warn(
`"${svc.name}" is not a Docker Compose service (runs: ${svc.devCommand ?? 'n/a'}).\n It does not produce persistent logs that can be tailed.`
);
} else {
ui.warn(
`Log tailing for running dev servers is not yet supported.\n Start the service with 'pnpm kilo dev up ${name}' to see its output.`
Expand Down
45 changes: 45 additions & 0 deletions dev/cli/test/logs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from 'bun:test';
import { services } from '../src/services/registry';

/**
* The `logs` command tails `docker compose logs -f <name>` for infra services.
* Only infra services whose devCommand starts with "docker compose" actually
* exist in docker-compose.yml. Non-compose infra services (e.g. migrations)
* must NOT be passed to `docker compose logs` — they'd fail silently or error.
*
* These tests validate the invariants that the logs command relies on.
*/
describe('logs command invariants', () => {
const infraServices = services.filter(s => s.type === 'infra');
const composeInfra = infraServices.filter(s => s.devCommand?.startsWith('docker compose'));
const nonComposeInfra = infraServices.filter(s => !s.devCommand?.startsWith('docker compose'));

test('migrations is an infra service with a non-compose devCommand', () => {
const migrations = services.find(s => s.name === 'migrations');
expect(migrations).toBeDefined();
expect(migrations!.type).toBe('infra');
expect(migrations!.devCommand).toBe('pnpm drizzle migrate');
expect(migrations!.devCommand!.startsWith('docker compose')).toBe(false);
});

test('postgres and redis are infra services with docker compose devCommands', () => {
for (const name of ['postgres', 'redis']) {
const svc = services.find(s => s.name === name);
expect(svc).toBeDefined();
expect(svc!.type).toBe('infra');
expect(svc!.devCommand!.startsWith('docker compose')).toBe(true);
}
});

test('at least one infra service is non-compose (guard against regression)', () => {
expect(nonComposeInfra.length).toBeGreaterThanOrEqual(1);
});

test('compose infra services have names matching docker-compose service names', () => {
// The logs command uses svc.name as the compose service name.
// Compose infra devCommands should contain `-d <name>` referencing the same service.
for (const svc of composeInfra) {
expect(svc.devCommand).toContain(svc.name);
}
});
});