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 .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"bracketSameLine": false,
"bracketSpacing": true,
"sortImports": true,
"ignorePatterns": ["**/dist/**", "**/coverage/**"]
"ignorePatterns": ["**/dist/**", "**/coverage/**", "**/CHANGELOG.md"]
}
4 changes: 3 additions & 1 deletion src/checks/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type ExternalCheckSpec = {
docs?: string
/** Extra guard beyond bin presence (e.g. require a tsconfig). */
canRun?: () => boolean
/** Rewrite the tool's captured output before it is printed, e.g. to strip a tool's own hardcoded colouring. */
transformOutput?: (output: string) => string
/**
* Default trailing args scaffolded after `--` (e.g. skott's `src/*.ts` target), surfaced in the `verify:<name>`
* script so a consumer can see and tweak them. Not baked into `runDefault`; only the scaffolded script carries them.
Expand Down Expand Up @@ -90,7 +92,7 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check {
}
const command = appendArgs(selectCommand(spec, resolveMode()), extraArgs)
// quiet: buffer the tool's output and flush only on failure (streamed live under --verbose).
const code = await runCommand(command, { env: envWithLocalBin(), quiet: true })
const code = await runCommand(command, { env: envWithLocalBin(), quiet: true, transform: spec.transformOutput })
// Only on failure — passing runs stay silent to save tokens.
if (code !== 0) console.error(color.dim(externalFailureHint(spec, command)))
return { name: spec.name, ok: code === 0 }
Expand Down
4 changes: 4 additions & 0 deletions src/checks/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'node:fs'

import { withoutRed } from '../shared/color.ts'
import { runComments } from './comments.ts'
import { runComplexity } from './complexity.ts'
import { defineExternalCheck } from './external.ts'
Expand Down Expand Up @@ -85,6 +86,9 @@ export const CHECKS: Check[] = [
bin: 'jscpd',
checkCommand: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src',
devDeps: ['jscpd'],
// jscpd hardcodes red header cells in its stats table (even on a clean run) — reads like a failure. It ignores
// NO_COLOR/FORCE_COLOR, so strip the red foreground from its output; the table renders in the default colour.
transformOutput: withoutRed,
docs: 'https://github.com/kucherenko/jscpd/tree/master/apps/jscpd#config',
}),
]
Expand Down
11 changes: 10 additions & 1 deletion src/orchestrator/runAll.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CHECKS } from '../checks/registry.ts'
import { color } from '../shared/color.ts'
import { color, paintRed } from '../shared/color.ts'
import { resolveMode } from '../shared/mode.ts'
import { installConsoleCapture, runCaptured } from '../shared/output.ts'
import { runCommand } from '../shared/spawn.ts'
Expand Down Expand Up @@ -92,6 +92,15 @@ export async function runAll(opts: RunAllOptions = {}): Promise<number> {
if (run.output) process.stdout.write(run.output)
}

// Under --verbose every check is printed, so a single failure gets buried mid-list; re-print the
// failing checks' output at the end in red so the step that actually failed stands out.
if (loud) {
for (const run of runs.filter((r) => !r.ok)) {
console.log(color.red(color.heading(`\n▶ ${run.name} (failed)`)))
if (run.output) process.stdout.write(paintRed(run.output))
}
}

if (opts.measure) {
const records: MeasureRecord[] = runs.map((r) => ({ script: r.name, code: r.ok ? 0 : 1, durationMs: r.durationMs }))
printMeasureTable(records, Date.now() - startTime)
Expand Down
23 changes: 23 additions & 0 deletions src/shared/color.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'

import { paintRed, withoutRed } from './color.ts'

describe('withoutRed', () => {
it('drops red-foreground codes so text renders in the default colour', () => {
expect(withoutRed('\x1b[31mheader\x1b[39m')).toBe('header\x1b[39m')
})

it('leaves other colours (e.g. a tool table border) untouched', () => {
expect(withoutRed('\x1b[90m│\x1b[39m\x1b[31m cell \x1b[39m')).toBe('\x1b[90m│\x1b[39m cell \x1b[39m')
})
})

describe('paintRed', () => {
it('wraps the text in red', () => {
expect(paintRed('boom')).toBe('\x1b[31mboom\x1b[39m')
})

it("re-asserts red after an embedded foreground reset so the highlight isn't cancelled partway", () => {
expect(paintRed('a\x1b[39mb')).toBe('\x1b[31ma\x1b[31mb\x1b[39m')
})
})
13 changes: 13 additions & 0 deletions src/shared/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ export const color = {
bold: (s: string | number) => `\x1b[1m${s}\x1b[22m`,
heading: (s: string | number) => `\x1b[1m\x1b[4m${s}\x1b[24m\x1b[22m`,
}

/**
* Paint a whole block red, re-asserting red after any foreground-colour reset the text already contains
* (e.g. a tool's own ANSI codes) so the highlight survives to the end instead of being cancelled partway.
*/
export function paintRed(text: string): string {
return `\x1b[31m${text.replaceAll('\x1b[39m', '\x1b[31m')}\x1b[39m`
}

/** Drop red-foreground codes from text so a tool's hardcoded red renders in the terminal's default colour. */
export function withoutRed(text: string): string {
return text.replaceAll('\x1b[31m', '')
}
7 changes: 6 additions & 1 deletion src/shared/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type RunCommandOptions = {
cwd?: string
env?: Record<string, string>
quiet?: boolean
/** Rewrite the command's buffered output before it is emitted (only applies when output is suppressed/buffered). */
transform?: (output: string) => string
}

/**
Expand All @@ -45,7 +47,10 @@ export function runCommand(command: string, opts: RunCommandOptions = {}): Promi
child.on('close', (code) => {
const exitCode = code ?? 1
// When capturing (parallel `verifyx all`), hand all output to the buffer; otherwise flush only on failure.
if (suppress && chunks.length > 0 && (isCapturing() || exitCode !== 0)) emit(Buffer.concat(chunks).toString())
if (suppress && chunks.length > 0 && (isCapturing() || exitCode !== 0)) {
const out = Buffer.concat(chunks).toString()
emit(opts.transform ? opts.transform(out) : out)
}
resolve(exitCode)
})
child.on('error', () => resolve(127))
Expand Down