Skip to content

Remove picocolors dependency in favor of a native util.styleText facade #193

Description

@shouze

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Remove picocolors from package.json dependencies and run
    bun install to update bun.lock.
  8. 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

  • No file in the repository imports picocolors (bun run knip reports it
    as unused, and it is removed from package.json)
  • src/style.ts is the only module importing styleText from node:util
  • All ANSI output (TUI, help text, progress bars, error messages) is
    visually identical to before the migration when tested manually in a
    real terminal
  • NO_COLOR=1 disables all color output; FORCE_COLOR=1 forces color
    output even when stdout is piped (verified both manually and by a test)
  • Golden-master tests in src/style.test.ts pass for every style and every
    composed 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, and
    bun run build.ts all pass

Definition of Done

  • PR merged to main following the standard refactor workflow (branch
    refactor/remove-picocolors, signed commits, behavior-preserving note in
    the PR description)
  • picocolors no longer appears in package.json, bun.lock, or anywhere
    in src/, github-code-search.ts
  • AGENTS.md and .github/skills/documentation.md reflect the new
    src/style.ts convention
  • No follow-up regression reported in manual TUI testing across at least
    one real terminal session, one piped/non-TTY run, and one
    NO_COLOR/FORCE_COLOR override run

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions