Remove picocolors dependency in favor of a native util.styleText facade
Context
picocolors is currently the only styling dependency in the codebase
(AGENTS.md: "picocolors is the only styling dependency; do not add chalk or
similar"). It is imported directly in 9 production files: src/render.ts,
src/render/highlight.ts, src/render/summary.ts, src/render/team-pick.ts,
src/tui.ts, src/upgrade.ts, src/api.ts and github-code-search.ts, plus a
few test files (src/render/terminal.test.ts, src/render/team-pick.test.ts)
and src/test-setup.ts (which forces FORCE_COLOR=1 for ANSI-assertion tests).
Since Node.js 20.12 / Bun 1.4, util.styleText (from node:util) ships a
built-in, zero-dependency ANSI styling API with automatic TTY / NO_COLOR /
FORCE_COLOR detection, matching the exact style names picocolors exposes
(dim, bold, italic, underline, and the named colors/background colors).
This removes the last external styling dependency from the project.
The only style names actually used today are: dim, bold, italic,
underline, red, green, yellow, cyan, magenta, white, black,
bgMagenta. Several call sites compose two or three styles by nesting
picocolors calls, e.g. pc.bold(pc.yellow(s)),
pc.bgMagenta(pc.black(pc.bold(" x "))), pc.cyan(pc.underline(url)). bold
and dim share the same SGR reset code (22), so naively replacing nested
picocolors calls with two chained util.styleText() calls is not guaranteed to
be byte-identical — picocolors has internal nesting-safe logic that a naive
facade would lose. util.styleText natively accepts an array of formats in a
single call (styleText(['bold', 'yellow'], text)), which is the correct way to
reproduce these compositions safely.
Typing gap found during implementation: the @types/node version resolved
in this project (25.2.3, pulled transitively via bun-types) does not declare
styleText on the node:util module, even though Bun 1.4's runtime implements
it. bun run/bun test still work at runtime (Bun strips types, it doesn't
type-check), but editors/IDEs would show a type error on the import. An ambient
module augmentation is required to fill this gap until @types/node catches
up.
Decision
Remove picocolors entirely and introduce a single new module, src/style.ts,
as the sole call site for node:util's styleText in the codebase — the
same convention already used by src/render/terminal.ts for Bun-native
terminal APIs (Bun.stringWidth, Bun.stripANSI, Bun.sliceAnsi). All 9
production files import from this facade instead of picocolors directly. A
small ambient .d.ts augmentation ships alongside the facade to type
styleText until upstream @types/node declares it.
Solution
- Add an ambient module augmentation (e.g.
src/style.d.ts) declaring
styleText's signature on node:util, since @types/node 25.2.3 does not
expose it yet.
- Create
src/style.ts:
- Export one function per single style actually in use:
dim, bold,
italic, underline, red, green, yellow, cyan, magenta,
white, black, bgMagenta — each a thin wrapper around
util.styleText('<name>', text).
- Export a
style(names: StyleName[], text: string): string composer for
multi-style call sites, using util.styleText(names, text) in a single
call (not nested wrapper calls), to avoid the bold/dim reset-code
collision and produce a single clean SGR sequence.
- No other module may import
node:util's styleText directly — this
mirrors the existing render/terminal.ts convention and must be
documented as such in AGENTS.md.
- Add
src/style.test.ts:
- For every style name and every composed combination found in the
codebase, assert the exact ANSI byte sequence produced by src/style.ts
matches what picocolors produced before the migration (golden-master
comparison) — this specifically guards against the bold/dim nesting
collision risk described above.
- Test
NO_COLOR, FORCE_COLOR, and non-TTY stdout behavior explicitly.
- Migrate all 9 production files to import from
src/style.ts instead of
picocolors, replacing:
- Single-style calls 1:1 (
pc.dim(x) → style.dim(x), etc.)
- Nested composition calls with the array composer (
pc.bold(pc.yellow(s))
→ style.style(["bold", "yellow"], s))
- In
github-code-search.ts, the existing manual HAS_COLOR gate for
Commander's configureHelp styling hooks keeps its explicit TTY check
but calls into src/style.ts instead of picocolors.
- Update the test files that reference
picocolors directly
(src/render/terminal.test.ts, src/render/team-pick.test.ts) to use the
new facade, and update stale comments in src/render/highlight.test.ts and
src/test-setup.ts.
- Verify
FORCE_COLOR=1 set in src/test-setup.ts still forces ANSI output
under bun test even though stdout is typically piped in that context.
- Remove
picocolors from package.json dependencies and run
bun install to update bun.lock.
- Update documentation:
AGENTS.md (the "picocolors is the only styling
dependency" note) and .github/skills/documentation.md (the "only
picocolors (CLI) and VitePress built-ins" note) to describe the new
src/style.ts convention.
Acceptance Criteria
Definition of Done
Remove
picocolorsdependency in favor of a nativeutil.styleTextfacadeContext
picocolorsis currently the only styling dependency in the codebase(
AGENTS.md: "picocolors is the only styling dependency; do not add chalk orsimilar"). It is imported directly in 9 production files:
src/render.ts,src/render/highlight.ts,src/render/summary.ts,src/render/team-pick.ts,src/tui.ts,src/upgrade.ts,src/api.tsandgithub-code-search.ts, plus afew test files (
src/render/terminal.test.ts,src/render/team-pick.test.ts)and
src/test-setup.ts(which forcesFORCE_COLOR=1for ANSI-assertion tests).Since Node.js 20.12 / Bun 1.4,
util.styleText(fromnode:util) ships abuilt-in, zero-dependency ANSI styling API with automatic TTY /
NO_COLOR/FORCE_COLORdetection, matching the exact style names picocolors exposes(
dim,bold,italic,underline, and the named colors/background colors).This removes the last external styling dependency from the project.
The only style names actually used today are:
dim,bold,italic,underline,red,green,yellow,cyan,magenta,white,black,bgMagenta. Several call sites compose two or three styles by nestingpicocolors calls, e.g.
pc.bold(pc.yellow(s)),pc.bgMagenta(pc.black(pc.bold(" x "))),pc.cyan(pc.underline(url)).boldand
dimshare the same SGR reset code (22), so naively replacing nestedpicocolors calls with two chained
util.styleText()calls is not guaranteed tobe byte-identical — picocolors has internal nesting-safe logic that a naive
facade would lose.
util.styleTextnatively accepts an array of formats in asingle call (
styleText(['bold', 'yellow'], text)), which is the correct way toreproduce these compositions safely.
Typing gap found during implementation: the
@types/nodeversion resolvedin this project (25.2.3, pulled transitively via
bun-types) does not declarestyleTexton thenode:utilmodule, even though Bun 1.4's runtime implementsit.
bun run/bun teststill work at runtime (Bun strips types, it doesn'ttype-check), but editors/IDEs would show a type error on the import. An ambient
module augmentation is required to fill this gap until
@types/nodecatchesup.
Decision
Remove
picocolorsentirely and introduce a single new module,src/style.ts,as the sole call site for
node:util'sstyleTextin the codebase — thesame convention already used by
src/render/terminal.tsfor Bun-nativeterminal APIs (
Bun.stringWidth,Bun.stripANSI,Bun.sliceAnsi). All 9production files import from this facade instead of
picocolorsdirectly. Asmall ambient
.d.tsaugmentation ships alongside the facade to typestyleTextuntil upstream@types/nodedeclares it.Solution
src/style.d.ts) declaringstyleText's signature onnode:util, since@types/node25.2.3 does notexpose it yet.
src/style.ts:dim,bold,italic,underline,red,green,yellow,cyan,magenta,white,black,bgMagenta— each a thin wrapper aroundutil.styleText('<name>', text).style(names: StyleName[], text: string): stringcomposer formulti-style call sites, using
util.styleText(names, text)in a singlecall (not nested wrapper calls), to avoid the bold/dim reset-code
collision and produce a single clean SGR sequence.
node:util'sstyleTextdirectly — thismirrors the existing
render/terminal.tsconvention and must bedocumented as such in
AGENTS.md.src/style.test.ts:codebase, assert the exact ANSI byte sequence produced by
src/style.tsmatches what
picocolorsproduced before the migration (golden-mastercomparison) — this specifically guards against the bold/dim nesting
collision risk described above.
NO_COLOR,FORCE_COLOR, and non-TTY stdout behavior explicitly.src/style.tsinstead ofpicocolors, replacing:pc.dim(x)→style.dim(x), etc.)pc.bold(pc.yellow(s))→
style.style(["bold", "yellow"], s))github-code-search.ts, the existing manualHAS_COLORgate forCommander's
configureHelpstyling hooks keeps its explicit TTY checkbut calls into
src/style.tsinstead ofpicocolors.picocolorsdirectly(
src/render/terminal.test.ts,src/render/team-pick.test.ts) to use thenew facade, and update stale comments in
src/render/highlight.test.tsandsrc/test-setup.ts.FORCE_COLOR=1set insrc/test-setup.tsstill forces ANSI outputunder
bun testeven though stdout is typically piped in that context.picocolorsfrompackage.jsondependenciesand runbun installto updatebun.lock.AGENTS.md(the "picocolors is the only stylingdependency" note) and
.github/skills/documentation.md(the "onlypicocolors (CLI) and VitePress built-ins" note) to describe the new
src/style.tsconvention.Acceptance Criteria
picocolors(bun run knipreports itas unused, and it is removed from
package.json)src/style.tsis the only module importingstyleTextfromnode:utilvisually identical to before the migration when tested manually in a
real terminal
NO_COLOR=1disables all color output;FORCE_COLOR=1forces coloroutput even when stdout is piped (verified both manually and by a test)
src/style.test.tspass for every style and everycomposed combination found in the codebase, proving no regression from
the bold/dim reset-code nesting risk
bun test,bun run lint,bun run format:check,bun run knip, andbun run build.tsall passDefinition of Done
mainfollowing the standard refactor workflow (branchrefactor/remove-picocolors, signed commits, behavior-preserving note inthe PR description)
picocolorsno longer appears inpackage.json,bun.lock, or anywherein
src/,github-code-search.tsAGENTS.mdand.github/skills/documentation.mdreflect the newsrc/style.tsconventionone real terminal session, one piped/non-TTY run, and one
NO_COLOR/FORCE_COLORoverride run