Skip to content

Commit 4675789

Browse files
fix: write CLI diagnostics to stderr so stdout carries results only
1 parent 16fd139 commit 4675789

11 files changed

Lines changed: 626 additions & 705 deletions

File tree

‎packages/cli/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@
3030
"dist"
3131
],
3232
"scripts": {
33-
"dev": "node ./src/cli-entry.ts --help",
33+
"dev": "node ./src/entry.ts --help",
3434
"build": "tsdown",
3535
"test": "vitest"
3636
},
3737
"dependencies": {
38+
"ansis": "^4.3.1",
3839
"citty": "^0.2.2",
39-
"consola": "^3.4.2",
4040
"tokenx": "^1.3.0"
4141
}
4242
}

‎packages/cli/src/conversion.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import type { InputSource } from './types.ts'
44
import * as fsp from 'node:fs/promises'
55
import * as path from 'node:path'
66
import process from 'node:process'
7-
import { consola } from 'consola'
87
import { estimateTokenCount } from 'tokenx'
98
import { decodeStream, encode, encodeLines } from '../../toon/src/index.ts'
109
import { jsonStreamFromEvents } from './json-from-events.ts'
10+
import * as log from './log.ts'
1111
import { formatInputLabel, readInput, readLinesFromSource } from './utils.ts'
1212

1313
export async function encodeToToon(config: {
@@ -51,20 +51,19 @@ export async function encodeToToon(config: {
5151
if (config.output) {
5252
const relativeInputPath = formatInputLabel(config.input)
5353
const relativeOutputPath = path.relative(process.cwd(), config.output)
54-
consola.success(`Encoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
54+
log.success(`Encoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
5555
}
5656

57-
console.log()
58-
consola.info(`Token estimates: ~${jsonTokens} (JSON) → ~${toonTokens} (TOON)`)
59-
consola.success(`Saved ~${diff} tokens (-${percent}%)`)
57+
log.info(`Token estimates: ~${jsonTokens} (JSON) → ~${toonTokens} (TOON)`)
58+
log.success(`Saved ~${diff} tokens (-${percent}%)`)
6059
}
6160
else {
6261
await writeStream(encodeLines(data, encodeOptions), { outputPath: config.output, separator: '\n' })
6362

6463
if (config.output) {
6564
const relativeInputPath = formatInputLabel(config.input)
6665
const relativeOutputPath = path.relative(process.cwd(), config.output)
67-
consola.success(`Encoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
66+
log.success(`Encoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
6867
}
6968
}
7069
}
@@ -90,7 +89,7 @@ export async function decodeToJson(config: {
9089
if (config.output) {
9190
const relativeInputPath = formatInputLabel(config.input)
9291
const relativeOutputPath = path.relative(process.cwd(), config.output)
93-
consola.success(`Decoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
92+
log.success(`Decoded \`${relativeInputPath}\` → \`${relativeOutputPath}\``)
9493
}
9594
}
9695

‎packages/cli/src/index.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type { InputSource } from './types.ts'
33
import * as path from 'node:path'
44
import process from 'node:process'
55
import { defineCommand } from 'citty'
6-
import { consola } from 'consola'
76
import { DEFAULT_DELIMITER } from '../../toon/src/index.ts'
87
import { assertValidDelimiter } from '../../toon/src/shared/validation.ts'
98
import pkg from '../package.json' with { type: 'json' }
109
import { decodeToJson, encodeToToon } from './conversion.ts'
1110
import { formatError } from './format-error.ts'
11+
import * as log from './log.ts'
1212
import { detectMode } from './utils.ts'
1313

1414
const { name, version } = pkg
@@ -76,17 +76,17 @@ export const mainCommand: CommandDef<ArgsDef> = defineCommand({
7676
: { type: 'file', path: path.resolve(input) }
7777
const outputPath = args.output ? path.resolve(args.output) : undefined
7878

79-
const indentSize = Number.parseInt(args.indent || '2', 10)
80-
if (Number.isNaN(indentSize) || indentSize < 0) {
81-
throw new Error(`Invalid indent value: ${args.indent}`)
82-
}
79+
try {
80+
const indentSize = Number.parseInt(args.indent || '2', 10)
81+
if (Number.isNaN(indentSize) || indentSize < 0) {
82+
throw new Error(`Invalid indent value: ${args.indent}`)
83+
}
8384

84-
const delimiter = args.delimiter || DEFAULT_DELIMITER
85-
assertValidDelimiter(delimiter)
85+
const delimiter = args.delimiter || DEFAULT_DELIMITER
86+
assertValidDelimiter(delimiter)
8687

87-
const mode = detectMode(inputSource, args.encode, args.decode)
88+
const mode = detectMode(inputSource, args.encode, args.decode)
8889

89-
try {
9090
if (mode === 'encode') {
9191
await encodeToToon({
9292
input: inputSource,
@@ -106,7 +106,7 @@ export const mainCommand: CommandDef<ArgsDef> = defineCommand({
106106
}
107107
}
108108
catch (error) {
109-
consola.error(formatError(error, { isVerbose: args.verbose === true }))
109+
log.error(formatError(error, { isVerbose: args.verbose === true }))
110110
// `process.exit` would discard whatever stdout has still buffered, which
111111
// truncates a piped conversion partway through
112112
process.exitCode = 1

‎packages/cli/src/log.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as ansis from 'ansis'
2+
3+
// Every level writes to stderr, so stdout carries results only
4+
5+
export function error(message: string): void {
6+
console.error(`${ansis.red('✖')} ${message}`)
7+
}
8+
9+
export function warn(message: string): void {
10+
console.error(`${ansis.yellow('⚠')} ${message}`)
11+
}
12+
13+
export function info(message: string): void {
14+
console.error(`${ansis.cyan('●')} ${message}`)
15+
}
16+
17+
export function success(message: string): void {
18+
console.error(`${ansis.green('✔')} ${message}`)
19+
}
20+
21+
/** Separates report blocks – on stderr, like every level above. */
22+
export function blankLine(): void {
23+
console.error('')
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { encode } from '../../toon/src/index.ts'
3+
import { version } from '../package.json' with { type: 'json' }
4+
import { runCliProcess, useTemporaryDirectories } from './utils.ts'
5+
6+
const createDirectory = useTemporaryDirectories()
7+
8+
// In-process runs observe neither citty's builtin flags, which `runMain` owns,
9+
// nor the exit code the shell sees.
10+
describe('toon CLI as a child process', () => {
11+
it('prints its version', async () => {
12+
const { stdout, exitCode } = await runCliProcess(['--version'])
13+
14+
expect(stdout).toBe(`${version}\n`)
15+
expect(exitCode).toBe(0)
16+
})
17+
18+
it('encodes a file and exits successfully', async () => {
19+
const data = { items: ['alpha', 'beta'] }
20+
const directory = createDirectory({ 'input.json': JSON.stringify(data) })
21+
22+
const { stdout, exitCode } = await runCliProcess(['input.json'], { cwd: directory })
23+
24+
expect(exitCode).toBe(0)
25+
expect(stdout).toBe(`${encode(data)}\n`)
26+
})
27+
28+
it('exits with a failure status for a missing input', async () => {
29+
const directory = createDirectory()
30+
31+
const { stdout, stderr, exitCode } = await runCliProcess(['nonexistent.json'], { cwd: directory })
32+
33+
expect(exitCode).toBe(1)
34+
expect(stdout).toBe('')
35+
expect(stderr).toContain('nonexistent.json')
36+
})
37+
})

0 commit comments

Comments
 (0)