feat(cli): formalize dev-friendly result output

This commit is contained in:
Andrey Avtomonov 2026-05-12 01:38:59 +02:00
parent dbb40d53b1
commit 41ccf9f12a
3 changed files with 52 additions and 3 deletions

View file

@ -20,6 +20,12 @@ describe('resolveOutputMode', () => {
expect(resolveOutputMode({ explicit: 'pretty', json: true, io: ioWith(true), env: {} })).toBe('json');
});
it('prefers explicit JSON over every other output setting', () => {
expect(resolveOutputMode({ json: true, explicit: 'pretty', io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe(
'json',
);
});
it('throws on unknown explicit value', () => {
expect(() => resolveOutputMode({ explicit: 'fancy', io: ioWith(true), env: {} })).toThrow(/Invalid --output/);
});
@ -34,6 +40,12 @@ describe('resolveOutputMode', () => {
expect(() => resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'fancy' } })).toThrow(/Invalid KTX_OUTPUT/);
});
it('rejects invalid KTX_OUTPUT values', () => {
expect(() => resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'verbose' } })).toThrow(
'Invalid KTX_OUTPUT value: verbose. Expected one of pretty, plain, json.',
);
});
it('returns plain when CI is set to a truthy value', () => {
expect(resolveOutputMode({ io: ioWith(true), env: { CI: 'true' } })).toBe('plain');
expect(resolveOutputMode({ io: ioWith(true), env: { CI: '1' } })).toBe('plain');

View file

@ -42,6 +42,16 @@ export function printList<Row extends object>(args: PrintListArgs<Row>): void {
}
}
export interface KtxJsonResultEnvelope<T> {
kind: string;
data: T;
meta?: Record<string, unknown>;
}
export function writeJsonResult<T>(io: KtxCliIo, envelope: KtxJsonResultEnvelope<T>): void {
io.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
}
function isEmpty(value: unknown): boolean {
return value === undefined || value === null || value === '';
}
@ -61,12 +71,11 @@ function printListPlain<Row extends object>(args: PrintListArgs<Row>): void {
}
function printListJson<Row extends object>(args: PrintListArgs<Row>): void {
const envelope = {
writeJsonResult(args.io, {
kind: 'list',
data: { items: args.rows },
meta: { command: args.command },
};
args.io.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
});
}
function pluralize(count: number, singular: string): string {

View file

@ -400,6 +400,7 @@ joins: []
expect(code).toBe(0);
const parsed = JSON.parse(listIo.stdout());
expect(listIo.stderr()).toBe('');
expect(parsed.kind).toBe('list');
expect(parsed.meta).toEqual({ command: 'sl list' });
expect(parsed.data.items).toHaveLength(1);
@ -412,6 +413,33 @@ joins: []
});
});
it('prints sl list JSON as a single result envelope', async () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
const writeIo = makeIo();
await runKtxSl(
{ command: 'write', projectDir, connectionId: 'warehouse', sourceName: 'orders', yaml: ORDERS_YAML },
writeIo.io,
);
const listIo = makeIo();
await expect(
runKtxSl({ command: 'list', projectDir, connectionId: 'warehouse', json: true }, listIo.io),
).resolves.toBe(0);
expect(listIo.stderr()).toBe('');
expect(JSON.parse(listIo.stdout())).toMatchObject({
kind: 'list',
data: {
items: expect.any(Array),
},
meta: {
command: 'sl list',
},
});
});
it('emits sl list with grouping and Clack-style framing when output=pretty', async () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });