Skip to content

feat(packages/core): add icons middleware#56

Merged
zrosenbauer merged 9 commits intomainfrom
feat/icons
Mar 15, 2026
Merged

feat(packages/core): add icons middleware#56
zrosenbauer merged 9 commits intomainfrom
feat/icons

Conversation

@zrosenbauer
Copy link
Member

@zrosenbauer zrosenbauer commented Mar 15, 2026

Summary

  • Add icons middleware that decorates ctx.icons with an icon resolver supporting Nerd Font glyphs and emoji fallbacks
  • 33 predefined icons across 4 categories: git, devops, status, files
  • System font detection via font-list package — matches installed fonts to Nerd Font equivalents
  • Interactive setup flow: font selection prompt → "Auto install" or "Show install commands"
  • All shell commands run async so spinner animates and ctrl+c works
  • Exported as @kidd-cli/core/icons with full module augmentation on Context
  • Includes example app under examples/icons/

API

import { icons } from '@kidd-cli/core/icons'

cli({
  middleware: [icons({ autoSetup: true })],
})

// In handlers:
ctx.icons.get('branch')       // resolve to glyph or emoji
ctx.icons.has('branch')       // check if defined
ctx.icons.installed()         // whether Nerd Fonts detected
ctx.icons.setup()             // interactive install prompt
ctx.icons.category('git')     // Record<string, string> of resolved git icons

Test plan

  • All 536 existing tests pass
  • 21 new tests for icons middleware, context, and detection
  • Typecheck passes
  • Lint passes (0 errors)
  • Build produces new @kidd-cli/core/icons export
  • Example app renders icons correctly in terminal
  • Setup flow shows spinner, font detection, selection prompt

…d install

Add a new `icons` middleware that decorates `ctx.icons` with a callable
icon resolver. Detects whether Nerd Fonts are installed via the `font-list`
package, resolves icon names to Nerd Font glyphs or emoji fallbacks, and
supports interactive font installation.

Features:
- Callable context: `ctx.icons('branch')` / `ctx.icons.get('branch')`
- 33 predefined icons across 4 categories (git, devops, status, files)
- System font detection and matching to Nerd Font equivalents
- Interactive setup with font selection and auto-install or manual commands
- Async shell commands for responsive spinner and ctrl+c support
- Custom icon definitions via middleware options
- Exported as `@kidd-cli/core/icons`

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 15, 2026

🦋 Changeset detected

Latest commit: 0a30f49

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@kidd-cli/core Minor
@kidd-cli/cli Minor
@examples/icons Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new icons middleware under packages/core that provides types, icon definitions (git, devops, status, files), Nerd Font detection, interactive installation logic, and a context factory that decorates ctx.icons with helpers (get, has, category, installed, setup). Exposes the middleware via packages/core exports and tsdown config. Adds comprehensive unit tests for detection, installation, and middleware behavior. Adds an examples/icons demo package with CLI commands (category, show, status, setup), kidd.config, package.json, tsconfig, and an example index CLI wiring the middleware.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding an icons middleware to the core package.
Docstring Coverage ✅ Passed Docstring coverage is 92.59% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description clearly explains the icons middleware feature with API examples, implementation details, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/icons
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/icons/commands/setup.ts`:
- Around line 14-16: The failure branch currently calls ctx.fail(error.message)
but does not stop execution, causing the subsequent check of installed to run
and print "Installation skipped"; update the error handling in the setup command
to return immediately after ctx.fail() (i.e., add a return right after
ctx.fail(error.message)) so that when error is truthy the function exits and
does not continue to the installed check.

In `@examples/icons/commands/status.ts`:
- Around line 14-38: The repeated pattern logging icon categories should be
extracted into a small helper to reduce duplication: create a function (e.g.,
logIconCategory or logCategory) that accepts the category key and a display
title, calls ctx.icons.category(categoryKey), iterates the entries and calls
ctx.logger.info for the header and each glyph line; then replace the four blocks
that use gitIcons/statusIcons/devopsIcons/fileIcons and the repeated
ctx.logger.info calls with calls to this helper (keep using ctx.logger.info and
ctx.icons.category inside the helper to preserve behavior).

In `@packages/core/src/middleware/icons/icons.ts`:
- Around line 133-138: The call to installNerdFont currently discards any thrown
error and just returns false, which hides why auto-setup failed; wrap the await
installNerdFont({ ctx, font: resolved.font }) call in a try/catch, capture the
thrown error and log it (including the font name and context) using the existing
logging facility (e.g., ctx.logger.error or console.error) before returning
false, so failures are visible; keep the returned value behavior (return result
=== true) but ensure the catch logs the error and returns false if
installNerdFont throws.

In `@packages/core/src/middleware/icons/install.ts`:
- Around line 153-158: buildFontChoices returns a ReadonlyArray but
ctx.prompts.select expects a mutable SelectOption<string>[]; update the call
site or the function signature to reconcile types: either change
buildFontChoices' return type to SelectOption<string>[] (remove Readonly
wrappers) or cast the result to a mutable array before passing to
ctx.prompts.select (e.g. convert ReadonlyArray to Array via Array.from or a type
assertion). Locate buildFontChoices and the select invocation (choices /
ctx.prompts.select) and apply the chosen fix so the types align.
- Around line 459-493: In installViaDownload, capture the error thrown by
execAsync when running the curl command and surface timeout-specific details in
the returned IconsError; specifically, wrap the try/catch to capture the thrown
error from execAsync (the execAsync call that runs `curl -fsSL -o "${tmpZip}"
"${url}"`), inspect the error for a timeout indicator (e.g., error.name or
error.code from the underlying exec/timeout library) and return iconsError with
a distinct message/type for timeouts (e.g., "download_timeout") including the
original error message, otherwise return the existing install_failed error with
the original error details so callers can distinguish timeout vs other network
failures.
- Line 319: The code uses lines.map((line) => ctx.logger.info(line)) for side
effects and discards the returned array; replace this with an explicit
side-effect iteration such as a for (const line of lines) {
ctx.logger.info(line) } (or, if you prefer the existing functional style, assign
the map result to a void variable like const _ = lines.map(...)) so intent is
clear; update the occurrence of lines.map(...) around the logger call to one of
these approaches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb0af052-5541-4662-891f-8455f879ad0b

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf8663 and aaac860.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (19)
  • examples/icons/commands/category.ts
  • examples/icons/commands/setup.ts
  • examples/icons/commands/show.ts
  • examples/icons/commands/status.ts
  • examples/icons/index.ts
  • examples/icons/kidd.config.ts
  • examples/icons/package.json
  • examples/icons/tsconfig.json
  • packages/core/package.json
  • packages/core/src/middleware/icons/context.ts
  • packages/core/src/middleware/icons/definitions.ts
  • packages/core/src/middleware/icons/detect.test.ts
  • packages/core/src/middleware/icons/detect.ts
  • packages/core/src/middleware/icons/icons.test.ts
  • packages/core/src/middleware/icons/icons.ts
  • packages/core/src/middleware/icons/index.ts
  • packages/core/src/middleware/icons/install.ts
  • packages/core/src/middleware/icons/types.ts
  • packages/core/tsdown.config.ts

zrosenbauer and others added 2 commits March 14, 2026 21:14
…ntext

Replace Object.assign on a function with a plain frozen object.
ctx.icons is now a regular object with methods (get, has, installed,
setup, category) instead of a callable function with properties bolted on.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add missing return after ctx.fail() in setup example to prevent fallthrough
- Log error on auto-setup failure instead of silently discarding it
- Change buildFontChoices return type to mutable array for clack compatibility

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/icons/commands/setup.ts`:
- Around line 6-9: Replace the two-branch if/else that checks the boolean
installed and calls ctx.logger.success or ctx.logger.info with a ts-pattern
match() on installed; import match from 'ts-pattern' if needed and convert the
branches to match(installed).with(true, () => ctx.logger.success('Nerd Fonts
installed successfully')).with(false, () => ctx.logger.info('Installation
skipped')).exhaustive() so the conditional is exhaustive and follows the
project's conditional style; keep the existing early-return guard clauses
unchanged.

In `@packages/core/src/middleware/icons/icons.ts`:
- Around line 120-124: The function resolveInstallStatus currently takes three
positional args (isDetected, resolved: ResolvedOptions, ctx: IconsCtx); change
its signature to accept a single destructured object parameter ({ isDetected,
resolved, ctx }: { isDetected: boolean; resolved: ResolvedOptions; ctx: IconsCtx
}) and update all call sites to pass an object instead of positional arguments;
ensure any internal references use the new names and update any TypeScript
types/imports accordingly so ResolvedOptions and IconsCtx types are preserved.

In `@packages/core/src/middleware/icons/install.ts`:
- Around line 192-195: Several helper functions (installWithConfirmation,
showInstallCommands, installFontWithSpinner, installFont, installDarwin,
installLinux, installViaBrew, installViaDownload) use positional parameters
instead of the repository's object-destructuring standard; change each function
signature to accept a single object parameter (e.g. { ctx, fontName }: { ctx:
IconsCtx; fontName: string }) and update all internal references to use the
destructured names, then update every call site to pass an object with those
named properties and adjust any related types/overloads to match the new shape;
ensure exported types and JSDoc (if present) are updated accordingly to avoid
breaking type checks.
- Around line 124-131: The font name passed into
installNerdFont/installWithConfirmation is unvalidated and is interpolated into
shell commands (brew install, curl/unzip), allowing command injection; add a Zod
schema for InstallFontOptions that enforces font/slug/fontName to match
/^[A-Za-z0-9-]+$/ and validate options at the entry (InstallFontOptions) so only
safe values reach the shell call sites, then replace direct string interpolation
with the validated value (or use explicit escaping/child_process args) in the
functions that build the brew/curl/unzip commands; additionally convert the
try/catch flows in the affected functions (the blocks around the shell
invocations currently using try/catch) to return Result tuples
(AsyncResult<boolean, IconsError>) per the project's error-handling guideline
instead of throwing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c3c469e5-c76a-4136-822d-966a018fe2ed

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5297f and 3e36f74.

📒 Files selected for processing (5)
  • examples/icons/commands/setup.ts
  • examples/icons/commands/status.ts
  • packages/core/src/middleware/icons/detect.test.ts
  • packages/core/src/middleware/icons/icons.ts
  • packages/core/src/middleware/icons/install.ts

zrosenbauer and others added 3 commits March 14, 2026 21:35
- Replace if/else with ts-pattern match() in setup example
- Convert resolveInstallStatus to use object parameter
- Add Zod validation for font names before shell interpolation
- Convert all 2+ param helpers to use object destructuring

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/middleware/icons/icons.ts`:
- Around line 47-52: The middleware currently calls resolveOptions() and then
merges createDefaultIcons() with resolved.icons on every request, which
preserves a caller-owned icons object by reference and allows future external
mutations to affect middleware behavior; change icons() factory to snapshot and
deep-freeze the merged icon definitions once when the middleware is created:
after calling resolveOptions() (symbol: resolveOptions) perform a one-time merge
of createDefaultIcons() and resolved.icons into a new object, freeze it
(Object.freeze and freeze nested objects) and close over that frozenIcons in the
returned middleware (symbols: middleware, createDefaultIcons, resolved.icons) so
the per-request handler uses the immutable frozenIcons instead of
recomputing/merging on each invocation.

In `@packages/core/src/middleware/icons/install.ts`:
- Around line 322-337: fontNameToSlug produces incorrect Homebrew cask slugs
causing bad brew install commands and broken auto-install paths; replace or
augment its usage in the install flow by validating or mapping slugs before
emitting/using them: update the logic around fontNameToSlug/fontName (where slug
is computed and used in the brew command and auto-install path) to consult a
canonical map of known fontName→brewSlug overrides (e.g.,
JetBrainsMono→jetbrains-mono, DejaVuSansMono→dejavu-sans-mono) or perform an
HTTP HEAD/GET against the composed cask URL to confirm it exists and fall back
to the override map if it 404s; ensure this validation is applied wherever slug
is used (the code computing slug and building the brew command/auto-install
path).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54d1de88-2efc-40af-8d66-12548afd4409

📥 Commits

Reviewing files that changed from the base of the PR and between 3e36f74 and d754616.

📒 Files selected for processing (3)
  • examples/icons/commands/setup.ts
  • packages/core/src/middleware/icons/icons.ts
  • packages/core/src/middleware/icons/install.ts

…back

- Add comprehensive tests for install.ts (font validation, selection flow, confirmation flow)
- Add icons concept documentation at docs/concepts/icons.md
- Use attemptAsync in detect.ts instead of try/catch
- Add JSDoc explaining Unicode escape sequences in definitions.ts

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/concepts/icons.md`:
- Around line 69-77: The docs miss the callable signature for the IconsContext —
update the API table and surrounding text to explicitly document that ctx.icons
is both callable and has methods: describe the callable form ctx.icons(name:
string): string alongside the existing .get(name): string, .has(name): boolean,
.installed(): boolean, .setup(): AsyncResult<boolean, IconsError>, and
.category(cat): Record<string,string>; mention equivalence between
ctx.icons('branch') and ctx.icons.get('branch') and ensure entries at the two
ranges that mention IconsContext/ctx.icons include this callable signature and a
short note about dual usage.
- Line 3: Replace the sentence fragment "The icons system for kidd CLIs." with a
complete present-tense, active-voice sentence that describes what the system
does (for example, start with "Kidd CLIs provide..." or "The icons system
provides..."), ensuring it uses active voice and present tense per the
documentation standards.

In `@packages/core/src/middleware/icons/detect.ts`:
- Line 18: The module-level mutable `cache` object is creating shared state;
remove the top-level `const cache` and stop mutating it from the middleware.
Instead, make detection pure by moving the cache into the request/local scope of
the icon-detection function (e.g., inside the exported middleware or
`detectIcons` function) or accept an injected cache via parameters so each
invocation has isolated state; update any code that currently reads/writes
`cache.value` to use the new local/injected variable or returned result,
ensuring no module-level mutation remains.
- Around line 61-67: The external getFonts() result must be Zod-validated before
use: define a fontSchema = z.array(z.string()) and after calling attemptAsync(()
=> getFonts(...)) validate the fonts by parsing inside attemptAsync (e.g.,
attemptAsync(() => Promise.resolve(fontSchema.parse(fonts)))) handling parse
errors by returning false; replace the direct use of fonts.some(...) with
validated.some((font) => /nerd/i.test(font)) so only a verified string[] from
getFonts is consumed by the detection logic in this module (references:
getFonts, attemptAsync, fontSchema).

In `@packages/core/src/middleware/icons/install.test.ts`:
- Around line 7-12: The mocked exec implementation in the vi.mock block (the
exec mock callback) has an unbraced if statement; update the function inside
vi.mock (the exec: vi.fn(...) mock) to wrap the if (typeof callback ===
'function') branch in curly braces so the callback invocation is enclosed in a
block (i.e., add braces around the if body that calls callback(null, { stdout:
'', stderr: '' })) to satisfy the eslint(curly) rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3c90688e-48ae-40e8-91d8-d742211bd073

📥 Commits

Reviewing files that changed from the base of the PR and between d754616 and 478855f.

⛔ Files ignored due to path filters (1)
  • .changeset/add-icons-middleware.md is excluded by !.changeset/**
📒 Files selected for processing (4)
  • docs/concepts/icons.md
  • packages/core/src/middleware/icons/definitions.ts
  • packages/core/src/middleware/icons/detect.ts
  • packages/core/src/middleware/icons/install.test.ts

…back

- Remove mutable detection cache in favor of pure async function
- Add Zod validation for font-list results at the boundary
- Freeze icon definitions at factory time instead of per-request
- Add canonical BREW_SLUG_MAP for correct Homebrew cask slugs
- Document callable ctx.icons(name) shorthand in IconsContext
- Fix lint violations in install.test.ts

Co-Authored-By: Claude <noreply@anthropic.com>
Remove redundant Zod validation in detect.ts, fix side-effect .map()
in install.ts, correct font option default in docs, add readonly
modifiers to buildFontChoices return type, and add platform-specific
installation tests for darwin/linux/unsupported platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant