@kidd-cli/core 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +21 -5
  2. package/dist/{config-D8e5qxLp.js → config-BiEi8RG2.js} +2 -2
  3. package/dist/{config-D8e5qxLp.js.map → config-BiEi8RG2.js.map} +1 -1
  4. package/dist/{create-store-OHdkm_Yt.js → create-store-CGeHrTcl.js} +2 -2
  5. package/dist/{create-store-OHdkm_Yt.js.map → create-store-CGeHrTcl.js.map} +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +37 -56
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/config.js +2 -2
  11. package/dist/lib/format.d.ts +73 -0
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/lib/format.js +20 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/logger.d.ts +1 -1
  16. package/dist/lib/logger.js +10 -0
  17. package/dist/lib/logger.js.map +1 -1
  18. package/dist/lib/project.d.ts +1 -1
  19. package/dist/lib/project.js +1 -1
  20. package/dist/lib/store.d.ts +1 -1
  21. package/dist/lib/store.js +2 -2
  22. package/dist/{logger-9j49T5da.d.ts → logger-Bm-LRSeQ.d.ts} +17 -1
  23. package/dist/logger-Bm-LRSeQ.d.ts.map +1 -0
  24. package/dist/middleware/auth.d.ts +1 -1
  25. package/dist/middleware/auth.js +3 -3
  26. package/dist/middleware/http.d.ts +1 -1
  27. package/dist/middleware/http.js +1 -1
  28. package/dist/middleware/icons.d.ts +119 -0
  29. package/dist/middleware/icons.d.ts.map +1 -0
  30. package/dist/middleware/icons.js +824 -0
  31. package/dist/middleware/icons.js.map +1 -0
  32. package/dist/{middleware-BWnPSRWR.js → middleware-BewRXb2G.js} +1 -1
  33. package/dist/{middleware-BWnPSRWR.js.map → middleware-BewRXb2G.js.map} +1 -1
  34. package/dist/{project-D0g84bZY.js → project-CoWHMVc8.js} +1 -1
  35. package/dist/{project-D0g84bZY.js.map → project-CoWHMVc8.js.map} +1 -1
  36. package/dist/tally-ioa20iGw.js +220 -0
  37. package/dist/tally-ioa20iGw.js.map +1 -0
  38. package/dist/{types-D-BxshYM.d.ts → types-Boe_1EjY.d.ts} +1 -1
  39. package/dist/{types-D-BxshYM.d.ts.map → types-Boe_1EjY.d.ts.map} +1 -1
  40. package/dist/types-Cp8_uIil.d.ts +160 -0
  41. package/dist/types-Cp8_uIil.d.ts.map +1 -0
  42. package/dist/{types-U73X_oQ_.d.ts → types-s-yUj9Zj.d.ts} +47 -37
  43. package/dist/types-s-yUj9Zj.d.ts.map +1 -0
  44. package/package.json +14 -5
  45. package/dist/logger-9j49T5da.d.ts.map +0 -1
  46. package/dist/types-U73X_oQ_.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","names":["match","match","match"],"sources":["../../src/middleware/icons/definitions.ts","../../src/middleware/icons/install.ts","../../src/middleware/icons/context.ts","../../src/middleware/icons/detect.ts","../../src/middleware/icons/icons.ts"],"sourcesContent":["/**\n * Predefined icon definitions organized by category.\n *\n * Each icon has a Nerd Font glyph and an emoji fallback. The middleware\n * resolves to one or the other based on font detection.\n *\n * @module\n */\n\nimport { match } from 'ts-pattern'\n\nimport type { IconCategory, IconDefinition } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Category constants\n// ---------------------------------------------------------------------------\n\n/**\n * Git-related icons for version control operations.\n *\n * Emoji values use Unicode escape sequences rather than literal emoji\n * characters to avoid encoding issues across editors, terminals, and\n * build tools that may not handle multi-byte characters correctly.\n *\n * Nerd Font sources: nf-dev (Devicons), nf-fa (Font Awesome)\n */\nexport const GIT_ICONS: Readonly<Record<string, IconDefinition>> = Object.freeze({\n branch: { emoji: '\\u{1F500}', nerdFont: '\\uE725' },\n clone: { emoji: '\\u{1F4CB}', nerdFont: '\\uF24D' },\n commit: { emoji: '\\u{1F4DD}', nerdFont: '\\uE729' },\n compare: { emoji: '\\u{1F504}', nerdFont: '\\uE728' },\n fetch: { emoji: '\\u{2B07}\\uFE0F', nerdFont: '\\uEC1D' },\n fork: { emoji: '\\u{1F500}', nerdFont: '\\uF126' },\n git: { emoji: '\\u{1F4BB}', nerdFont: '\\uE702' },\n merge: { emoji: '\\u{1F500}', nerdFont: '\\uE727' },\n pr: { emoji: '\\u{1F4E5}', nerdFont: '\\uE726' },\n tag: { emoji: '\\u{1F3F7}\\uFE0F', nerdFont: '\\uF02B' },\n worktree: { emoji: '\\u{1F333}', nerdFont: '\\uEF81' },\n})\n\n/**\n * DevOps and infrastructure icons.\n *\n * Nerd Font sources: nf-dev (Devicons), nf-fa (Font Awesome)\n */\nexport const DEVOPS_ICONS: Readonly<Record<string, IconDefinition>> = Object.freeze({\n ci: { emoji: '\\u{2699}\\uFE0F', nerdFont: '\\uF013' },\n cloud: { emoji: '\\u{2601}\\uFE0F', nerdFont: '\\uF0C2' },\n deploy: { emoji: '\\u{1F680}', nerdFont: '\\uF135' },\n docker: { emoji: '\\u{1F433}', nerdFont: '\\uF21F' },\n kubernetes: { emoji: '\\u{2638}\\uFE0F', nerdFont: '\\uE81D' },\n server: { emoji: '\\u{1F5A5}\\uFE0F', nerdFont: '\\uF233' },\n terminal: { emoji: '\\u{1F4BB}', nerdFont: '\\uF120' },\n})\n\n/**\n * Status indicator icons.\n *\n * Nerd Font sources: nf-fa (Font Awesome)\n */\nexport const STATUS_ICONS: Readonly<Record<string, IconDefinition>> = Object.freeze({\n error: { emoji: '\\u{274C}', nerdFont: '\\uF05C' },\n info: { emoji: '\\u{2139}\\uFE0F', nerdFont: '\\uF129' },\n pending: { emoji: '\\u{23F3}', nerdFont: '\\uF254' },\n running: { emoji: '\\u{25B6}\\uFE0F', nerdFont: '\\uF04B' },\n stopped: { emoji: '\\u{23F9}\\uFE0F', nerdFont: '\\uF28D' },\n success: { emoji: '\\u{2705}', nerdFont: '\\uF05D' },\n warning: { emoji: '\\u{26A0}\\uFE0F', nerdFont: '\\uF071' },\n})\n\n/**\n * File type and filesystem icons.\n *\n * Nerd Font sources: nf-fa (Font Awesome), nf-dev (Devicons)\n */\nexport const FILES_ICONS: Readonly<Record<string, IconDefinition>> = Object.freeze({\n config: { emoji: '\\u{2699}\\uFE0F', nerdFont: '\\uF013' },\n file: { emoji: '\\u{1F4C4}', nerdFont: '\\uF15B' },\n folder: { emoji: '\\u{1F4C1}', nerdFont: '\\uF07B' },\n javascript: { emoji: '\\u{1F4C4}', nerdFont: '\\uE781' },\n json: { emoji: '\\u{1F4C4}', nerdFont: '\\uE80B' },\n lock: { emoji: '\\u{1F512}', nerdFont: '\\uF023' },\n markdown: { emoji: '\\u{1F4C4}', nerdFont: '\\uE73E' },\n typescript: { emoji: '\\u{1F4C4}', nerdFont: '\\uE8CA' },\n})\n\n// ---------------------------------------------------------------------------\n// Exported helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Merge all category icon records into a single definitions record.\n *\n * @returns A frozen record of all predefined icons.\n */\nexport function createDefaultIcons(): Readonly<Record<string, IconDefinition>> {\n return Object.freeze({\n ...GIT_ICONS,\n ...DEVOPS_ICONS,\n ...STATUS_ICONS,\n ...FILES_ICONS,\n })\n}\n\n/**\n * Retrieve the icon definitions for a specific category.\n *\n * @param category - The icon category to retrieve.\n * @returns The frozen record of icons for that category.\n */\nexport function getIconsByCategory(\n category: IconCategory\n): Readonly<Record<string, IconDefinition>> {\n return match(category)\n .with('git', () => GIT_ICONS)\n .with('devops', () => DEVOPS_ICONS)\n .with('status', () => STATUS_ICONS)\n .with('files', () => FILES_ICONS)\n .exhaustive()\n}\n","/**\n * Nerd Font installation for macOS and Linux.\n *\n * Detects installed system fonts, matches them to available Nerd Font\n * equivalents, and lets the user choose which to install. Supports\n * Homebrew on macOS and direct download on Linux.\n *\n * All shell commands run asynchronously so the spinner can animate\n * and ctrl+c remains responsive.\n *\n * @module\n */\n\nimport { exec } from 'node:child_process'\nimport { mkdir, rm } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { promisify } from 'node:util'\n\nimport type { AsyncResult, Result } from '@kidd-cli/utils/fp'\nimport { attemptAsync, ok } from '@kidd-cli/utils/fp'\nimport { getFonts } from 'font-list'\nimport { match } from 'ts-pattern'\nimport { z } from 'zod'\n\nimport type { IconsCtx } from './context.js'\nimport type { IconsError } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst execAsync = promisify(exec)\n\n/**\n * Zod schema for validating font names before shell interpolation.\n *\n * Restricts to alphanumeric characters and hyphens to prevent command injection.\n *\n * @private\n */\nconst fontNameSchema = z\n .string()\n .regex(/^[A-Za-z0-9-]+$/, 'Font name must be alphanumeric or hyphen')\n\n/**\n * Maps base font family name patterns to their Nerd Font release names.\n *\n * Keys are lowercase patterns matched against installed font names.\n * Values are the exact release archive names on GitHub.\n *\n * @private\n */\nconst FONT_MAP: readonly (readonly [string, string])[] = Object.freeze([\n ['jetbrains mono', 'JetBrainsMono'],\n ['fira code', 'FiraCode'],\n ['fira mono', 'FiraMono'],\n ['cascadia code', 'CascadiaCode'],\n ['cascadia mono', 'CascadiaMono'],\n ['hack', 'Hack'],\n ['source code pro', 'SourceCodePro'],\n ['meslo', 'Meslo'],\n ['inconsolata', 'Inconsolata'],\n ['dejavu sans mono', 'DejaVuSansMono'],\n ['droid sans mono', 'DroidSansMono'],\n ['ubuntu mono', 'UbuntuMono'],\n ['ubuntu sans', 'UbuntuSans'],\n ['roboto mono', 'RobotoMono'],\n ['ibm plex mono', 'IBMPlexMono'],\n ['victor mono', 'VictorMono'],\n ['iosevka', 'Iosevka'],\n ['mononoki', 'Mononoki'],\n ['geist mono', 'GeistMono'],\n ['space mono', 'SpaceMono'],\n ['anonymous pro', 'AnonymousPro'],\n ['overpass', 'Overpass'],\n ['go mono', 'Go-Mono'],\n ['noto', 'Noto'],\n ['commit mono', 'CommitMono'],\n ['monaspace', 'Monaspace'],\n ['intel one mono', 'IntelOneMono'],\n ['zed mono', 'ZedMono'],\n ['comic shanns', 'ComicShannsMono'],\n ['lilex', 'Lilex'],\n ['recursive', 'Recursive'],\n ['hermit', 'Hermit'],\n ['hasklig', 'Hasklig'],\n ['martian mono', 'MartianMono'],\n ['0xproto', '0xProto'],\n ['departure mono', 'DepartureMono'],\n ['atkinson hyperlegible', 'AtkinsonHyperlegibleMono'],\n])\n\n/**\n * Popular Nerd Fonts shown as fallback options when no matches are found.\n *\n * @private\n */\nconst POPULAR_FONTS: readonly string[] = Object.freeze([\n 'JetBrainsMono',\n 'FiraCode',\n 'Hack',\n 'CascadiaCode',\n 'Meslo',\n 'SourceCodePro',\n 'Iosevka',\n 'VictorMono',\n])\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link installNerdFont}.\n */\nexport interface InstallFontOptions {\n readonly ctx: IconsCtx\n readonly font?: string\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Interactively install a Nerd Font on the user's system.\n *\n * Detects installed system fonts, matches them against available Nerd Font\n * equivalents, and presents a selection prompt. If a `font` option is\n * provided, skips detection and installs that font directly after confirmation.\n *\n * @param options - Installation options including context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nexport async function installNerdFont(\n options: InstallFontOptions\n): AsyncResult<boolean, IconsError> {\n const { ctx, font } = options\n\n if (font !== undefined) {\n const parsed = fontNameSchema.safeParse(font)\n\n if (!parsed.success) {\n return iconsError({\n message: `Invalid font name: ${parsed.error.message}`,\n type: 'install_failed',\n })\n }\n\n return installWithConfirmation({ ctx, fontName: parsed.data })\n }\n\n return installWithSelection(ctx)\n}\n\n// ---------------------------------------------------------------------------\n// Private types\n// ---------------------------------------------------------------------------\n\n/**\n * Parameters for functions that operate on a context and font name.\n *\n * @private\n */\ninterface CtxFontParams {\n readonly ctx: IconsCtx\n readonly fontName: string\n}\n\n/**\n * Parameters for functions that operate on a context and slug.\n *\n * @private\n */\ninterface CtxSlugParams {\n readonly ctx: IconsCtx\n readonly slug: string\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers — selection flow\n// ---------------------------------------------------------------------------\n\n/**\n * Run the font selection flow: detect installed fonts, match to Nerd Fonts,\n * and let the user pick.\n *\n * @private\n * @param ctx - The icons context with prompts, spinner, and logger.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installWithSelection(ctx: IconsCtx): AsyncResult<boolean, IconsError> {\n ctx.spinner.start('Detecting installed fonts...')\n const matches = await detectMatchingFonts()\n ctx.spinner.stop('Font detection complete')\n\n const choices = buildFontChoices(matches)\n\n const selected = await ctx.prompts.select({\n message: 'Select a Nerd Font to install',\n options: choices,\n })\n\n if (selected === undefined || typeof selected === 'symbol') {\n return ok(false)\n }\n\n const fontName = String(selected)\n const parsed = fontNameSchema.safeParse(fontName)\n\n if (!parsed.success) {\n return iconsError({\n message: `Invalid font name: ${parsed.error.message}`,\n type: 'install_failed',\n })\n }\n\n const action = await ctx.prompts.select({\n message: 'How would you like to install?',\n options: [\n { label: 'Auto install', value: 'auto' },\n { label: 'Show install commands', value: 'commands' },\n ],\n })\n\n if (action === undefined || typeof action === 'symbol') {\n return ok(false)\n }\n\n return match(String(action))\n .with('auto', () => installFontWithSpinner({ ctx, fontName: parsed.data }))\n .with('commands', () => showInstallCommands({ ctx, fontName: parsed.data }))\n .otherwise(() => ok(false))\n}\n\n/**\n * Confirm and install a specific font by name.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installWithConfirmation({\n ctx,\n fontName,\n}: CtxFontParams): AsyncResult<boolean, IconsError> {\n const confirmed = await ctx.prompts.confirm({\n message: `Nerd Fonts not detected. Install ${fontName} Nerd Font?`,\n })\n\n if (!confirmed) {\n return ok(false)\n }\n\n return installFontWithSpinner({ ctx, fontName })\n}\n\n/**\n * Detect system fonts and match them to available Nerd Font equivalents.\n *\n * @private\n * @returns An array of matched Nerd Font release names.\n */\nasync function detectMatchingFonts(): Promise<readonly string[]> {\n const [error, systemFonts] = await attemptAsync(() => getFonts({ disableQuoting: true }))\n\n if (error || systemFonts === null) {\n return []\n }\n\n const lowerFonts = systemFonts.map((f) => f.toLowerCase())\n\n return FONT_MAP.filter(([pattern]) => lowerFonts.some((f) => f.includes(pattern))).map(\n ([, nerdName]) => nerdName\n )\n}\n\n/**\n * Build the select prompt choices from matched and popular fonts.\n *\n * Matched fonts (based on what's installed) appear first with a hint,\n * followed by popular alternatives that weren't already matched.\n *\n * @private\n * @param matches - Nerd Font names matched from installed system fonts.\n * @returns An array of select options.\n */\nfunction buildFontChoices(\n matches: readonly string[]\n): { readonly value: string; readonly label: string; readonly hint?: string }[] {\n const matchedSet = new Set(matches)\n\n const matchedChoices = matches.map((name) => ({\n hint: 'detected on your system',\n label: `${name} Nerd Font`,\n value: name,\n }))\n\n const popularChoices = POPULAR_FONTS.filter((name) => !matchedSet.has(name)).map((name) => ({\n label: `${name} Nerd Font`,\n value: name,\n }))\n\n return [...matchedChoices, ...popularChoices]\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers — installation\n// ---------------------------------------------------------------------------\n\n/**\n * Print the install commands for the user to run manually.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with false since nothing was installed.\n */\nasync function showInstallCommands({\n ctx,\n fontName,\n}: CtxFontParams): AsyncResult<boolean, IconsError> {\n const slug = fontNameToSlug(fontName)\n const url = `https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${fontName}.zip`\n const fontDir = match(process.platform)\n .with('darwin', () => join(homedir(), 'Library', 'Fonts'))\n .otherwise(() => join(homedir(), '.local', 'share', 'fonts'))\n\n const hasBrew = await checkBrewAvailable()\n\n const lines: readonly string[] = match(process.platform)\n .with('darwin', () =>\n match(hasBrew)\n .with(true, () => [\n '',\n 'Run the following command to install via Homebrew:',\n '',\n ` brew install --cask font-${slug}-nerd-font`,\n '',\n ])\n .with(false, () => [\n '',\n 'Run the following commands to install manually:',\n '',\n ` curl -fsSL -o \"${fontDir}/${fontName}.zip\" \"${url}\"`,\n ` unzip -o \"${fontDir}/${fontName}.zip\" -d \"${fontDir}\"`,\n ` rm -f \"${fontDir}/${fontName}.zip\"`,\n '',\n ])\n .exhaustive()\n )\n .with('linux', () => [\n '',\n 'Run the following commands to install:',\n '',\n ` mkdir -p \"${fontDir}\"`,\n ` curl -fsSL -o \"${fontDir}/${fontName}.zip\" \"${url}\"`,\n ` unzip -o \"${fontDir}/${fontName}.zip\" -d \"${fontDir}\"`,\n ` rm -f \"${fontDir}/${fontName}.zip\"`,\n ' fc-cache -fv',\n '',\n ])\n .otherwise(() => ['', `Download the font from: ${url}`, ''])\n\n return ok(\n lines.reduce((_acc, line) => {\n ctx.logger.info(line)\n return false\n }, false)\n )\n}\n\n/**\n * Install a Nerd Font with spinner feedback.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installFontWithSpinner({\n ctx,\n fontName,\n}: CtxFontParams): AsyncResult<boolean, IconsError> {\n ctx.spinner.start(`Installing ${fontName} Nerd Font...`)\n\n const result = await installFont({ ctx, fontName })\n const [error] = result\n\n if (error) {\n ctx.spinner.stop(`Failed to install ${fontName} Nerd Font`)\n return result\n }\n\n ctx.spinner.stop(`${fontName} Nerd Font installed successfully`)\n return result\n}\n\n/**\n * Install a Nerd Font by name, dispatching to the platform-appropriate method.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installFont({ ctx, fontName }: CtxFontParams): AsyncResult<boolean, IconsError> {\n return match(process.platform)\n .with('darwin', () => installDarwin({ ctx, fontName }))\n .with('linux', () => installLinux({ ctx, fontName }))\n .otherwise(() =>\n Promise.resolve(\n iconsError({ message: `Unsupported platform: ${process.platform}`, type: 'install_failed' })\n )\n )\n}\n\n/**\n * Install a Nerd Font on macOS via Homebrew or direct download.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installDarwin({ ctx, fontName }: CtxFontParams): AsyncResult<boolean, IconsError> {\n const slug = fontNameToSlug(fontName)\n const hasBrew = await checkBrewAvailable()\n\n if (hasBrew) {\n return installViaBrew({ ctx, slug })\n }\n\n return installViaDownload({ ctx, fontName })\n}\n\n/**\n * Install a Nerd Font on Linux via direct download.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installLinux({ ctx, fontName }: CtxFontParams): AsyncResult<boolean, IconsError> {\n return installViaDownload({ ctx, fontName })\n}\n\n/**\n * Check whether Homebrew is available on the system.\n *\n * @private\n * @returns A promise resolving to true when the `brew` command is found.\n */\nasync function checkBrewAvailable(): Promise<boolean> {\n const [error] = await attemptAsync(() => execAsync('command -v brew'))\n return error === null\n}\n\n/**\n * Install a Nerd Font via Homebrew cask.\n *\n * @private\n * @param params - The icons context and cask slug.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installViaBrew({ ctx, slug }: CtxSlugParams): AsyncResult<boolean, IconsError> {\n try {\n ctx.spinner.message(`Installing font-${slug}-nerd-font via Homebrew...`)\n await execAsync(`brew install --cask font-${slug}-nerd-font`)\n return ok(true)\n } catch {\n return iconsError({\n message: `Homebrew installation failed for font-${slug}-nerd-font`,\n type: 'install_failed',\n })\n }\n}\n\n/**\n * Install a Nerd Font by downloading from GitHub releases.\n *\n * Downloads the zip archive, extracts it to the appropriate font directory,\n * and refreshes the font cache on Linux.\n *\n * @private\n * @param params - The icons context and font name.\n * @returns A Result with true on success or an IconsError on failure.\n */\nasync function installViaDownload({\n ctx,\n fontName,\n}: CtxFontParams): AsyncResult<boolean, IconsError> {\n const fontDir = match(process.platform)\n .with('darwin', () => join(homedir(), 'Library', 'Fonts'))\n .otherwise(() => join(homedir(), '.local', 'share', 'fonts'))\n\n try {\n await mkdir(fontDir, { recursive: true })\n\n const url = `https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${fontName}.zip`\n const tmpZip = join(fontDir, `${fontName}.zip`)\n\n ctx.spinner.message(`Downloading ${fontName} Nerd Font...`)\n await execAsync(`curl -fsSL -o \"${tmpZip}\" \"${url}\"`, { timeout: 120_000 })\n\n ctx.spinner.message(`Extracting ${fontName} Nerd Font...`)\n await execAsync(`unzip -o \"${tmpZip}\" -d \"${fontDir}\"`)\n\n await rm(tmpZip, { force: true })\n\n if (process.platform === 'linux') {\n ctx.spinner.message('Refreshing font cache...')\n await execAsync('fc-cache -fv')\n }\n\n return ok(true)\n } catch {\n return iconsError({\n message: `Failed to download and install ${fontName} Nerd Font`,\n type: 'install_failed',\n })\n }\n}\n\n/**\n * Canonical mapping of Nerd Font release names to Homebrew cask slugs.\n *\n * The generic regex-based conversion produces incorrect slugs for\n * abbreviations (e.g. IBM, DejaVu) and compound names. This map\n * provides the correct slugs for all fonts in {@link FONT_MAP}.\n *\n * @private\n */\nconst BREW_SLUG_MAP: Readonly<Record<string, string>> = Object.freeze({\n '0xProto': '0xproto',\n AnonymousPro: 'anonymous-pro',\n AtkinsonHyperlegibleMono: 'atkinson-hyperlegible-mono',\n CascadiaCode: 'cascadia-code',\n CascadiaMono: 'cascadia-mono',\n ComicShannsMono: 'comic-shanns-mono',\n CommitMono: 'commit-mono',\n DejaVuSansMono: 'dejavu-sans-mono',\n DepartureMono: 'departure-mono',\n DroidSansMono: 'droid-sans-mono',\n FiraCode: 'fira-code',\n FiraMono: 'fira-mono',\n GeistMono: 'geist-mono',\n 'Go-Mono': 'go-mono',\n Hack: 'hack',\n Hasklig: 'hasklig',\n Hermit: 'hermit',\n IBMPlexMono: 'ibm-plex-mono',\n Inconsolata: 'inconsolata',\n IntelOneMono: 'intone-mono',\n Iosevka: 'iosevka',\n JetBrainsMono: 'jetbrains-mono',\n Lilex: 'lilex',\n MartianMono: 'martian-mono',\n Meslo: 'meslo-lg',\n Monaspace: 'monaspace',\n Mononoki: 'mononoki',\n Noto: 'noto',\n Overpass: 'overpass',\n Recursive: 'recursive',\n RobotoMono: 'roboto-mono',\n SourceCodePro: 'sauce-code-pro',\n SpaceMono: 'space-mono',\n UbuntuMono: 'ubuntu-mono',\n UbuntuSans: 'ubuntu-sans',\n VictorMono: 'victor-mono',\n ZedMono: 'zed-mono',\n})\n\n/**\n * Convert a font family name to a Homebrew cask slug.\n *\n * Uses the canonical {@link BREW_SLUG_MAP} when available, falling\n * back to a regex-based conversion for unknown font names.\n *\n * @private\n * @param name - The font family name (e.g. 'JetBrainsMono').\n * @returns The slug (e.g. 'jetbrains-mono').\n */\nfunction fontNameToSlug(name: string): string {\n const mapped = BREW_SLUG_MAP[name]\n\n if (mapped !== undefined) {\n return mapped\n }\n\n return name.replaceAll(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()\n}\n\n/**\n * Construct a failure Result tuple with an {@link IconsError}.\n *\n * @private\n * @param error - The icons error.\n * @returns A synchronous Result tuple `[IconsError, null]`.\n */\nfunction iconsError(error: IconsError): Result<never, IconsError> {\n return [error, null] as const\n}\n","/**\n * Factory for the {@link IconsContext} object decorated onto `ctx.icons`.\n *\n * Builds an object that resolves icon names to glyphs based on\n * whether Nerd Fonts are detected on the system.\n *\n * @module\n */\n\nimport type { AsyncResult } from '@kidd-cli/utils/fp'\nimport { match } from 'ts-pattern'\n\nimport type { CliLogger, Prompts, Spinner } from '@/context/types.js'\n\nimport { getIconsByCategory } from './definitions.js'\nimport { installNerdFont } from './install.js'\nimport type { IconCategory, IconDefinition, IconsContext, IconsError } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal context subset needed by the icons context factory.\n */\nexport interface IconsCtx {\n readonly logger: CliLogger\n readonly prompts: Prompts\n readonly spinner: Spinner\n}\n\n/**\n * Options for {@link createIconsContext}.\n */\nexport interface CreateIconsContextOptions {\n readonly ctx: IconsCtx\n readonly icons: Readonly<Record<string, IconDefinition>>\n readonly isInstalled: boolean\n readonly font?: string\n readonly forceSetup?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create an {@link IconsContext} value for `ctx.icons`.\n *\n * The returned object exposes methods for resolving icons (`get`, `has`,\n * `installed`, `setup`, `category`).\n *\n * @param options - Factory options.\n * @returns An IconsContext instance.\n */\nexport function createIconsContext(options: CreateIconsContextOptions): IconsContext {\n const { ctx, icons, font, forceSetup } = options\n // NOTE: Intentional mutable closure — mutating `state.isInstalled` after\n // A successful setup() is required so that existing references to ctx.icons\n // Reflect the updated install status without replacing the object.\n const state = { isInstalled: options.isInstalled }\n\n return Object.freeze({\n category: (cat: IconCategory): Readonly<Record<string, string>> => {\n const categoryIcons = getIconsByCategory(cat)\n return Object.freeze(\n Object.fromEntries(\n Object.entries(categoryIcons).map(([name, def]) => [\n name,\n resolveIcon(icons, name, state.isInstalled, def),\n ])\n )\n )\n },\n get: (name: string): string => resolveIcon(icons, name, state.isInstalled),\n has: (name: string): boolean => name in icons,\n installed: (): boolean =>\n match(forceSetup)\n .with(true, () => false)\n .otherwise(() => state.isInstalled),\n setup: async (): AsyncResult<boolean, IconsError> => {\n const [error, result] = await installNerdFont({ ctx, font })\n\n if (error) {\n return [error, null] as const\n }\n\n if (result) {\n state.isInstalled = true\n }\n\n return [null, result] as const\n },\n })\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a single icon to its appropriate glyph string.\n *\n * @private\n * @param icons - The full icon definitions record.\n * @param name - The icon name to resolve.\n * @param nerdFontsInstalled - Whether Nerd Fonts are available.\n * @param fallbackDef - Optional fallback definition (used for category resolution).\n * @returns The resolved glyph string, or empty string if not found.\n */\nfunction resolveIcon(\n icons: Readonly<Record<string, IconDefinition>>,\n name: string,\n nerdFontsInstalled: boolean,\n fallbackDef?: IconDefinition\n): string {\n const def = icons[name] ?? fallbackDef\n if (def === undefined) {\n return ''\n }\n\n return match(nerdFontsInstalled)\n .with(true, () => def.nerdFont)\n .with(false, () => def.emoji)\n .exhaustive()\n}\n","/**\n * Nerd Font detection using the `font-list` package.\n *\n * Queries the system font catalog and checks whether any installed\n * font family name contains \"Nerd\".\n *\n * @module\n */\n\nimport { attemptAsync } from '@kidd-cli/utils/fp'\nimport { getFonts } from 'font-list'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether Nerd Fonts are installed on the system.\n *\n * Uses the `font-list` package to query installed font families and\n * checks for any family name containing \"Nerd\".\n *\n * @returns A promise that resolves to true when at least one Nerd Font is found.\n */\nexport async function detectNerdFonts(): Promise<boolean> {\n const [error, fonts] = await attemptAsync(() => getFonts({ disableQuoting: true }))\n\n if (error || fonts === null) {\n return false\n }\n\n return fonts.some((font) => /nerd/i.test(font))\n}\n","/**\n * Icons middleware factory.\n *\n * Detects Nerd Font availability, optionally prompts for installation,\n * and decorates `ctx.icons` with an icon resolver.\n *\n * @module\n */\n\nimport { decorateContext } from '@/context/decorate.js'\nimport { middleware } from '@/middleware.js'\nimport type { Middleware } from '@/types.js'\n\nimport type { IconsCtx } from './context.js'\nimport { createIconsContext } from './context.js'\nimport { createDefaultIcons } from './definitions.js'\nimport { detectNerdFonts } from './detect.js'\nimport { installNerdFont } from './install.js'\nimport type { IconsOptions } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create an icons middleware that decorates `ctx.icons`.\n *\n * Detects whether Nerd Fonts are installed. When `autoSetup` is enabled\n * and fonts are missing, prompts the user to install them. Merges any\n * custom icon definitions with the built-in defaults.\n *\n * @param options - Optional middleware configuration.\n * @returns A Middleware instance.\n *\n * @example\n * ```ts\n * import { icons } from '@kidd-cli/core/icons'\n *\n * cli({\n * middleware: [\n * icons({ autoSetup: true, font: 'JetBrainsMono' }),\n * ],\n * })\n * ```\n */\nexport function icons(options?: IconsOptions): Middleware {\n const resolved = resolveOptions(options)\n const frozenIcons = Object.freeze({ ...createDefaultIcons(), ...resolved.icons })\n\n return middleware(async (ctx, next) => {\n const isDetected = await detectNerdFonts()\n const isInstalled = await resolveInstallStatus({ ctx, isDetected, resolved })\n\n const iconsContext = createIconsContext({\n ctx,\n font: resolved.font,\n forceSetup: resolved.forceSetup,\n icons: frozenIcons,\n isInstalled,\n })\n\n decorateContext(ctx, 'icons', iconsContext)\n\n return next()\n })\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolved options with explicit undefined for missing fields.\n *\n * @private\n */\ninterface ResolvedOptions {\n readonly icons: IconsOptions['icons']\n readonly autoSetup: boolean\n readonly font: string | undefined\n readonly forceSetup: boolean\n}\n\n/**\n * Extract options into a resolved shape, avoiding optional chaining.\n *\n * @private\n * @param options - Raw middleware options.\n * @returns Resolved options with defaults applied.\n */\nfunction resolveOptions(options: IconsOptions | undefined): ResolvedOptions {\n if (options === undefined) {\n return { autoSetup: false, font: undefined, forceSetup: false, icons: undefined }\n }\n\n return {\n autoSetup: options.autoSetup === true,\n font: options.font,\n forceSetup: options.forceSetup === true,\n icons: options.icons,\n }\n}\n\n/**\n * Parameters for {@link resolveInstallStatus}.\n *\n * @private\n */\ninterface ResolveInstallStatusParams {\n readonly isDetected: boolean\n readonly resolved: ResolvedOptions\n readonly ctx: IconsCtx\n}\n\n/**\n * Determine final install status, triggering auto-setup if configured.\n *\n * @private\n * @param params - Detection state, resolved options, and middleware context.\n * @returns Whether Nerd Fonts should be considered installed.\n */\nasync function resolveInstallStatus({\n isDetected,\n resolved,\n ctx,\n}: ResolveInstallStatusParams): Promise<boolean> {\n if (isDetected) {\n return true\n }\n\n if (!resolved.autoSetup) {\n return false\n }\n\n const [error, result] = await installNerdFont({\n ctx,\n font: resolved.font,\n })\n\n if (error) {\n ctx.logger.warn(`Auto-setup failed: ${error.message}`)\n }\n\n return result === true\n}\n"],"mappings":"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"}
@@ -60,4 +60,4 @@ function middleware(handler) {
60
60
  //#endregion
61
61
  export { decorateContext as n, middleware as t };
62
62
 
63
- //# sourceMappingURL=middleware-BWnPSRWR.js.map
63
+ //# sourceMappingURL=middleware-BewRXb2G.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware-BWnPSRWR.js","names":[],"sources":["../src/context/decorate.ts","../src/middleware.ts"],"sourcesContent":["import type { Context } from './types.js'\n\n/**\n * Add a typed, immutable property to a context instance.\n *\n * Middleware authors use this to extend ctx with custom properties.\n * Pair with module augmentation on Context for type safety:\n *\n * ```ts\n * declare module '@kidd-cli/core' {\n * interface Context {\n * readonly github: HttpClient\n * }\n * }\n * ```\n *\n * **Note:** This function mutates the context object via\n * `Object.defineProperty`. The added property is non-writable and\n * non-configurable, making it effectively frozen after assignment.\n * Mutation is intentional here — the context is assembled incrementally\n * across middleware, and copying the entire object on each decoration\n * would break the single-reference threading model used by the runner.\n *\n * @param ctx - The context instance to decorate (mutated in place).\n * @param key - The property name.\n * @param value - The property value (frozen after assignment).\n * @returns The same ctx reference, now carrying the new property.\n */\nexport function decorateContext<TKey extends string, TValue>(\n ctx: Context,\n key: TKey,\n value: TValue\n): Context {\n Object.defineProperty(ctx, key, { configurable: false, enumerable: true, value, writable: false })\n return ctx\n}\n","import { withTag } from '@kidd-cli/utils/tag'\n\nimport type { Middleware, MiddlewareEnv, MiddlewareFn } from './types.js'\n\n/**\n * Create a typed middleware that runs before command handlers.\n *\n * Use the generic parameter to declare context variables the middleware provides.\n * The handler's `ctx` type in downstream commands will include these variables.\n *\n * @param handler - The middleware function receiving ctx and next.\n * @returns A Middleware object for use in the cli() or command() middleware stack.\n *\n * @example\n * ```ts\n * const loadUser = middleware<{ Variables: { user: User } }>(async (ctx, next) => {\n * decorateContext(ctx, 'user', await fetchUser())\n * await next()\n * })\n * ```\n */\nexport function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(\n handler: MiddlewareFn<TEnv>\n): Middleware<TEnv> {\n return withTag({ handler }, 'Middleware')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBACd,KACA,KACA,OACS;AACT,QAAO,eAAe,KAAK,KAAK;EAAE,cAAc;EAAO,YAAY;EAAM;EAAO,UAAU;EAAO,CAAC;AAClG,QAAO;;;;;;;;;;;;;;;;;;;;;ACbT,SAAgB,WACd,SACkB;AAClB,QAAO,QAAQ,EAAE,SAAS,EAAE,aAAa"}
1
+ {"version":3,"file":"middleware-BewRXb2G.js","names":[],"sources":["../src/context/decorate.ts","../src/middleware.ts"],"sourcesContent":["import type { Context } from './types.js'\n\n/**\n * Add a typed, immutable property to a context instance.\n *\n * Middleware authors use this to extend ctx with custom properties.\n * Pair with module augmentation on Context for type safety:\n *\n * ```ts\n * declare module '@kidd-cli/core' {\n * interface Context {\n * readonly github: HttpClient\n * }\n * }\n * ```\n *\n * **Note:** This function mutates the context object via\n * `Object.defineProperty`. The added property is non-writable and\n * non-configurable, making it effectively frozen after assignment.\n * Mutation is intentional here — the context is assembled incrementally\n * across middleware, and copying the entire object on each decoration\n * would break the single-reference threading model used by the runner.\n *\n * @param ctx - The context instance to decorate (mutated in place).\n * @param key - The property name.\n * @param value - The property value (frozen after assignment).\n * @returns The same ctx reference, now carrying the new property.\n */\nexport function decorateContext<TKey extends string, TValue>(\n ctx: Context,\n key: TKey,\n value: TValue\n): Context {\n Object.defineProperty(ctx, key, { configurable: false, enumerable: true, value, writable: false })\n return ctx\n}\n","import { withTag } from '@kidd-cli/utils/tag'\n\nimport type { Middleware, MiddlewareEnv, MiddlewareFn } from './types.js'\n\n/**\n * Create a typed middleware that runs before command handlers.\n *\n * Use the generic parameter to declare context variables the middleware provides.\n * The handler's `ctx` type in downstream commands will include these variables.\n *\n * @param handler - The middleware function receiving ctx and next.\n * @returns A Middleware object for use in the cli() or command() middleware stack.\n *\n * @example\n * ```ts\n * const loadUser = middleware<{ Variables: { user: User } }>(async (ctx, next) => {\n * decorateContext(ctx, 'user', await fetchUser())\n * await next()\n * })\n * ```\n */\nexport function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(\n handler: MiddlewareFn<TEnv>\n): Middleware<TEnv> {\n return withTag({ handler }, 'Middleware')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBACd,KACA,KACA,OACS;AACT,QAAO,eAAe,KAAK,KAAK;EAAE,cAAc;EAAO,YAAY;EAAM;EAAO,UAAU;EAAO,CAAC;AAClG,QAAO;;;;;;;;;;;;;;;;;;;;;ACbT,SAAgB,WACd,SACkB;AAClB,QAAO,QAAQ,EAAE,SAAS,EAAE,aAAa"}
@@ -177,4 +177,4 @@ function resolvePath(options) {
177
177
  //#endregion
178
178
  export { getParentRepoRoot as a, findProjectRoot as i, resolveLocalPath as n, isInSubmodule as o, resolvePath as r, resolveGlobalPath as t };
179
179
 
180
- //# sourceMappingURL=project-D0g84bZY.js.map
180
+ //# sourceMappingURL=project-CoWHMVc8.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-D0g84bZY.js","names":["match"],"sources":["../src/lib/project/root.ts","../src/lib/project/paths.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\nimport { attempt } from '@kidd-cli/utils/fp'\n\nimport type { ProjectRoot } from './types.js'\n\nconst GITDIR_RE = /^gitdir:\\s*(.+)$/\nconst MIN_MODULES_PARTS = 2\n\n/**\n * Walk up the directory tree to find the nearest git project root.\n *\n * @param startDir - Directory to start searching from (defaults to cwd).\n * @returns The project root info, or null if no git root is found.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): ProjectRoot | null {\n /**\n * Recursively walk up the directory tree searching for a `.git` marker.\n *\n * @private\n */\n const findRootRecursive = (currentDir: string, visited: Set<string>): ProjectRoot | null => {\n if (visited.has(currentDir)) {\n return null\n }\n const nextVisited = new Set([...visited, currentDir])\n\n const gitPath = join(currentDir, '.git')\n // Race condition: file may have been deleted between existsSync and statSync\n const [checkError, result] = attempt(() => checkGitPath(gitPath, currentDir))\n\n if (!checkError && result) {\n return result\n }\n\n const parent = dirname(currentDir)\n if (parent === currentDir) {\n return null\n }\n return findRootRecursive(parent, nextVisited)\n }\n\n return findRootRecursive(resolve(startDir), new Set())\n}\n\n/**\n * Check whether the current directory is inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns True if the directory is inside a submodule.\n */\nexport function isInSubmodule(startDir?: string): boolean {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot) {\n return false\n }\n return projectRoot.isSubmodule\n}\n\n/**\n * Resolve the parent repository root when inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns The parent repository root path, or null.\n */\nexport function getParentRepoRoot(startDir?: string): string | null {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot || !projectRoot.isSubmodule) {\n return null\n }\n\n const gitFilePath = join(projectRoot.path, '.git')\n const gitFileContent = readGitFile(gitFilePath)\n if (gitFileContent === null) {\n return null\n }\n\n return resolveParentGitDir(projectRoot, gitFileContent)\n}\n\n// ---------------------------------------------------------------------------\n// Private\n// ---------------------------------------------------------------------------\n\n/**\n * Read and trim the contents of a git-related file.\n *\n * @param filePath - The absolute file path to read.\n * @returns The trimmed file content, or null when the file cannot be read.\n * @private\n */\nfunction readGitFile(filePath: string): string | null {\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n if (error || content === null) {\n return null\n }\n return content.trim()\n}\n\n/**\n * Resolve a `.git` file reference to determine if this is a submodule.\n *\n * @private\n */\nfunction resolveGitFileSubmodule(gitPath: string, currentDir: string): ProjectRoot | null {\n const gitFileContent = readGitFile(gitPath)\n if (gitFileContent === null) {\n return { isSubmodule: false, path: currentDir }\n }\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (gitDirMatch && gitDirMatch[1]) {\n const gitDir = resolve(currentDir, gitDirMatch[1])\n const isSubmodule = /[/\\\\]\\.git[/\\\\]modules[/\\\\]/.test(gitDir)\n return { isSubmodule, path: currentDir }\n }\n return null\n}\n\n/**\n * Check whether a `.git` path is a directory or file and resolve accordingly.\n *\n * @private\n */\nfunction checkGitPath(gitPath: string, currentDir: string): ProjectRoot | null {\n if (!existsSync(gitPath)) {\n return null\n }\n\n const stats = statSync(gitPath)\n if (stats.isDirectory()) {\n return { isSubmodule: false, path: currentDir }\n }\n\n if (stats.isFile()) {\n return resolveGitFileSubmodule(gitPath, currentDir)\n }\n\n return null\n}\n\n/**\n * Extract the parent repository root from a resolved git modules path.\n *\n * @private\n */\nfunction resolveParentFromGitDir(resolvedGitDir: string): string | null {\n const gitDirParts = resolvedGitDir.split('/modules/')\n if (gitDirParts.length >= MIN_MODULES_PARTS) {\n const [parentGitDir] = gitDirParts\n if (parentGitDir && parentGitDir.endsWith('.git')) {\n return dirname(parentGitDir)\n }\n }\n return null\n}\n\n/**\n * Resolve the parent repository root from a submodule's gitdir reference.\n *\n * @private\n */\nfunction resolveParentGitDir(projectRoot: ProjectRoot, gitFileContent: string): string | null {\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (!gitDirMatch) {\n return null\n }\n\n const gitDir = gitDirMatch[1] ?? ''\n const resolvedGitDir = resolve(projectRoot.path, gitDir)\n\n if (process.platform === 'win32') {\n return null\n }\n\n return resolveParentFromGitDir(resolvedGitDir)\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { match } from 'ts-pattern'\n\nimport { findProjectRoot } from './root.js'\nimport type { ResolvePathOptions } from './types.js'\n\n/**\n * Resolve a directory path relative to the project root.\n *\n * @param options - Options containing the directory name and optional start directory.\n * @returns The resolved local path, or null if no project root is found.\n */\nexport function resolveLocalPath(options: {\n readonly dirName: string\n readonly startDir?: string\n}): string | null {\n const projectRoot = findProjectRoot(options.startDir)\n if (!projectRoot) {\n return null\n }\n return join(projectRoot.path, options.dirName)\n}\n\n/**\n * Resolve a directory path relative to the user's home directory.\n *\n * @param options - Options containing the directory name.\n * @returns The resolved global path.\n */\nexport function resolveGlobalPath(options: { readonly dirName: string }): string {\n return join(homedir(), options.dirName)\n}\n\n/**\n * Resolve a directory path using the specified source strategy.\n *\n * When source is 'local', resolves relative to the project root.\n * When source is 'global', resolves relative to the home directory.\n * When source is 'resolve' (default), tries local first, falling back to global.\n *\n * @param options - Resolution options with dirName, source, and startDir.\n * @returns The resolved path, or null if local resolution fails with source='local'.\n */\nexport function resolvePath(options: ResolvePathOptions): string | null {\n const { dirName, source = 'resolve', startDir } = options\n return match(source)\n .with('local', (): string | null => resolveLocalPath({ dirName, startDir }))\n .with('global', (): string => resolveGlobalPath({ dirName }))\n .with('resolve', (): string => {\n const localPath = resolveLocalPath({ dirName, startDir })\n if (localPath) {\n return localPath\n }\n return resolveGlobalPath({ dirName })\n })\n .exhaustive()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project-CoWHMVc8.js","names":["match"],"sources":["../src/lib/project/root.ts","../src/lib/project/paths.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\nimport { attempt } from '@kidd-cli/utils/fp'\n\nimport type { ProjectRoot } from './types.js'\n\nconst GITDIR_RE = /^gitdir:\\s*(.+)$/\nconst MIN_MODULES_PARTS = 2\n\n/**\n * Walk up the directory tree to find the nearest git project root.\n *\n * @param startDir - Directory to start searching from (defaults to cwd).\n * @returns The project root info, or null if no git root is found.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): ProjectRoot | null {\n /**\n * Recursively walk up the directory tree searching for a `.git` marker.\n *\n * @private\n */\n const findRootRecursive = (currentDir: string, visited: Set<string>): ProjectRoot | null => {\n if (visited.has(currentDir)) {\n return null\n }\n const nextVisited = new Set([...visited, currentDir])\n\n const gitPath = join(currentDir, '.git')\n // Race condition: file may have been deleted between existsSync and statSync\n const [checkError, result] = attempt(() => checkGitPath(gitPath, currentDir))\n\n if (!checkError && result) {\n return result\n }\n\n const parent = dirname(currentDir)\n if (parent === currentDir) {\n return null\n }\n return findRootRecursive(parent, nextVisited)\n }\n\n return findRootRecursive(resolve(startDir), new Set())\n}\n\n/**\n * Check whether the current directory is inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns True if the directory is inside a submodule.\n */\nexport function isInSubmodule(startDir?: string): boolean {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot) {\n return false\n }\n return projectRoot.isSubmodule\n}\n\n/**\n * Resolve the parent repository root when inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns The parent repository root path, or null.\n */\nexport function getParentRepoRoot(startDir?: string): string | null {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot || !projectRoot.isSubmodule) {\n return null\n }\n\n const gitFilePath = join(projectRoot.path, '.git')\n const gitFileContent = readGitFile(gitFilePath)\n if (gitFileContent === null) {\n return null\n }\n\n return resolveParentGitDir(projectRoot, gitFileContent)\n}\n\n// ---------------------------------------------------------------------------\n// Private\n// ---------------------------------------------------------------------------\n\n/**\n * Read and trim the contents of a git-related file.\n *\n * @param filePath - The absolute file path to read.\n * @returns The trimmed file content, or null when the file cannot be read.\n * @private\n */\nfunction readGitFile(filePath: string): string | null {\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n if (error || content === null) {\n return null\n }\n return content.trim()\n}\n\n/**\n * Resolve a `.git` file reference to determine if this is a submodule.\n *\n * @private\n */\nfunction resolveGitFileSubmodule(gitPath: string, currentDir: string): ProjectRoot | null {\n const gitFileContent = readGitFile(gitPath)\n if (gitFileContent === null) {\n return { isSubmodule: false, path: currentDir }\n }\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (gitDirMatch && gitDirMatch[1]) {\n const gitDir = resolve(currentDir, gitDirMatch[1])\n const isSubmodule = /[/\\\\]\\.git[/\\\\]modules[/\\\\]/.test(gitDir)\n return { isSubmodule, path: currentDir }\n }\n return null\n}\n\n/**\n * Check whether a `.git` path is a directory or file and resolve accordingly.\n *\n * @private\n */\nfunction checkGitPath(gitPath: string, currentDir: string): ProjectRoot | null {\n if (!existsSync(gitPath)) {\n return null\n }\n\n const stats = statSync(gitPath)\n if (stats.isDirectory()) {\n return { isSubmodule: false, path: currentDir }\n }\n\n if (stats.isFile()) {\n return resolveGitFileSubmodule(gitPath, currentDir)\n }\n\n return null\n}\n\n/**\n * Extract the parent repository root from a resolved git modules path.\n *\n * @private\n */\nfunction resolveParentFromGitDir(resolvedGitDir: string): string | null {\n const gitDirParts = resolvedGitDir.split('/modules/')\n if (gitDirParts.length >= MIN_MODULES_PARTS) {\n const [parentGitDir] = gitDirParts\n if (parentGitDir && parentGitDir.endsWith('.git')) {\n return dirname(parentGitDir)\n }\n }\n return null\n}\n\n/**\n * Resolve the parent repository root from a submodule's gitdir reference.\n *\n * @private\n */\nfunction resolveParentGitDir(projectRoot: ProjectRoot, gitFileContent: string): string | null {\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (!gitDirMatch) {\n return null\n }\n\n const gitDir = gitDirMatch[1] ?? ''\n const resolvedGitDir = resolve(projectRoot.path, gitDir)\n\n if (process.platform === 'win32') {\n return null\n }\n\n return resolveParentFromGitDir(resolvedGitDir)\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { match } from 'ts-pattern'\n\nimport { findProjectRoot } from './root.js'\nimport type { ResolvePathOptions } from './types.js'\n\n/**\n * Resolve a directory path relative to the project root.\n *\n * @param options - Options containing the directory name and optional start directory.\n * @returns The resolved local path, or null if no project root is found.\n */\nexport function resolveLocalPath(options: {\n readonly dirName: string\n readonly startDir?: string\n}): string | null {\n const projectRoot = findProjectRoot(options.startDir)\n if (!projectRoot) {\n return null\n }\n return join(projectRoot.path, options.dirName)\n}\n\n/**\n * Resolve a directory path relative to the user's home directory.\n *\n * @param options - Options containing the directory name.\n * @returns The resolved global path.\n */\nexport function resolveGlobalPath(options: { readonly dirName: string }): string {\n return join(homedir(), options.dirName)\n}\n\n/**\n * Resolve a directory path using the specified source strategy.\n *\n * When source is 'local', resolves relative to the project root.\n * When source is 'global', resolves relative to the home directory.\n * When source is 'resolve' (default), tries local first, falling back to global.\n *\n * @param options - Resolution options with dirName, source, and startDir.\n * @returns The resolved path, or null if local resolution fails with source='local'.\n */\nexport function resolvePath(options: ResolvePathOptions): string | null {\n const { dirName, source = 'resolve', startDir } = options\n return match(source)\n .with('local', (): string | null => resolveLocalPath({ dirName, startDir }))\n .with('global', (): string => resolveGlobalPath({ dirName }))\n .with('resolve', (): string => {\n const localPath = resolveLocalPath({ dirName, startDir })\n if (localPath) {\n return localPath\n }\n return resolveGlobalPath({ dirName })\n })\n .exhaustive()\n}\n"],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { z } from "zod";
2
+ import pc from "picocolors";
3
+ import { match } from "ts-pattern";
4
+ //#region src/lib/format/constants.ts
5
+ /**
6
+ * Unicode glyphs used in formatted output, frozen for immutability.
7
+ */
8
+ const GLYPHS = Object.freeze({
9
+ arrow: "›",
10
+ check: "✓",
11
+ corner: "╰",
12
+ cross: "✗",
13
+ dash: "─",
14
+ dot: "·",
15
+ fix: "⚙",
16
+ pipe: "│",
17
+ skip: "○",
18
+ warning: "⚠"
19
+ });
20
+ //#endregion
21
+ //#region src/lib/format/check.ts
22
+ /**
23
+ * Zod schema for validating a check status at the public API boundary.
24
+ *
25
+ * @private
26
+ */
27
+ const CheckStatusSchema = z.enum([
28
+ "pass",
29
+ "fail",
30
+ "warn",
31
+ "skip",
32
+ "fix"
33
+ ]);
34
+ /**
35
+ * Format a single pass/fail/warn check row.
36
+ *
37
+ * @param input - The check data to format.
38
+ * @returns A formatted check string.
39
+ */
40
+ function formatCheck(input) {
41
+ const parsedStatus = CheckStatusSchema.safeParse(input.status);
42
+ if (!parsedStatus.success) return `[Invalid check status: ${parsedStatus.error.issues.map((issue) => issue.message).join(", ")}]`;
43
+ return ` ${match(input.status).with("pass", () => pc.green(GLYPHS.check)).with("fail", () => pc.red(GLYPHS.cross)).with("warn", () => pc.yellow(GLYPHS.warning)).with("skip", () => pc.gray(GLYPHS.skip)).with("fix", () => pc.blue(GLYPHS.fix)).exhaustive()} ${match(input.status).with("fail", () => pc.red(input.name)).with("warn", () => pc.yellow(input.name)).with("skip", () => pc.gray(input.name)).otherwise(() => input.name)}${formatOptionalDetail(input.detail)}${formatOptionalDuration(input.duration)}${formatOptionalHint(input.hint)}`;
44
+ }
45
+ /**
46
+ * Format optional detail text.
47
+ *
48
+ * @private
49
+ * @param detail - Optional detail string.
50
+ * @returns Formatted detail or empty string.
51
+ */
52
+ function formatOptionalDetail(detail) {
53
+ return match(detail).with(void 0, () => "").otherwise((d) => ` ${pc.gray(d)}`);
54
+ }
55
+ /**
56
+ * Format optional duration for a check row.
57
+ *
58
+ * @private
59
+ * @param duration - Optional duration in milliseconds.
60
+ * @returns Formatted duration or empty string.
61
+ */
62
+ function formatOptionalDuration(duration) {
63
+ return match(duration).with(void 0, () => "").otherwise((d) => ` ${pc.gray(`(${formatDurationInline(d)})`)}`);
64
+ }
65
+ /**
66
+ * Format optional hint text.
67
+ *
68
+ * @private
69
+ * @param hint - Optional hint string.
70
+ * @returns Formatted hint or empty string.
71
+ */
72
+ function formatOptionalHint(hint) {
73
+ return match(hint).with(void 0, () => "").otherwise((h) => ` ${pc.dim(`[${h}]`)}`);
74
+ }
75
+ /**
76
+ * Inline duration format for check rows (compact).
77
+ *
78
+ * @private
79
+ * @param ms - Duration in milliseconds.
80
+ * @returns A compact formatted duration string.
81
+ */
82
+ function formatDurationInline(ms) {
83
+ return match(ms).when((v) => v < 1, () => "< 1ms").when((v) => v < 1e3, (v) => `${Math.round(v)}ms`).otherwise((v) => `${(v / 1e3).toFixed(2)}s`);
84
+ }
85
+ //#endregion
86
+ //#region src/lib/format/code-frame.ts
87
+ /**
88
+ * Zod schema for validating a code frame annotation at the public API boundary.
89
+ *
90
+ * @private
91
+ */
92
+ const CodeFrameAnnotationSchema = z.object({
93
+ column: z.number().int().positive(),
94
+ length: z.number().int().positive(),
95
+ line: z.number().int().positive(),
96
+ message: z.string()
97
+ });
98
+ /**
99
+ * Format an annotated code frame (oxlint style).
100
+ *
101
+ * @param input - The code frame data to format.
102
+ * @returns A formatted code frame string.
103
+ */
104
+ function formatCodeFrame(input) {
105
+ const parsed = CodeFrameAnnotationSchema.safeParse(input.annotation);
106
+ if (!parsed.success) return ` ${pc.red("✗")} Invalid code frame annotation: ${parsed.error.issues.map((issue) => issue.message).join(", ")}`;
107
+ const { annotation, filePath, lines, startLine } = input;
108
+ const lastLine = startLine + lines.length - 1;
109
+ if (annotation.line < startLine || annotation.line > lastLine) return `[Invalid annotation: line ${String(annotation.line)} is out of range ${String(startLine)}–${String(lastLine)}]`;
110
+ const gutterWidth = String(lastLine).length;
111
+ const header = ` ${pc.cyan(GLYPHS.arrow)} ${pc.cyan(`${filePath}:${String(annotation.line)}:${String(annotation.column)}`)}`;
112
+ const separator = ` ${" ".repeat(gutterWidth)} ${pc.cyan(GLYPHS.pipe)}`;
113
+ const codeLines = lines.map((line, idx) => {
114
+ const lineNum = startLine + idx;
115
+ return ` ${pc.cyan(String(lineNum).padStart(gutterWidth))} ${pc.cyan(GLYPHS.pipe)} ${line}`;
116
+ });
117
+ const annotationLineIdx = annotation.line - startLine;
118
+ const pointer = " ".repeat(annotation.column - 1) + pc.red("^".repeat(annotation.length));
119
+ const annotationRow = ` ${" ".repeat(gutterWidth)} ${pc.cyan(GLYPHS.pipe)} ${pointer} ${pc.red(annotation.message)}`;
120
+ return [
121
+ header,
122
+ separator,
123
+ ...codeLines.reduce((acc, line, idx) => match(idx === annotationLineIdx).with(true, () => [
124
+ ...acc,
125
+ line,
126
+ annotationRow
127
+ ]).with(false, () => [...acc, line]).exhaustive(), []),
128
+ separator
129
+ ].join("\n");
130
+ }
131
+ //#endregion
132
+ //#region src/lib/format/finding.ts
133
+ /**
134
+ * Format a full finding (oxlint style).
135
+ *
136
+ * @param input - The finding data to format.
137
+ * @returns A formatted finding string.
138
+ */
139
+ function formatFinding(input) {
140
+ const severityLabel = match(input.severity).with("error", () => pc.red("error")).with("warning", () => pc.yellow("warning")).with("hint", () => pc.blue("hint")).exhaustive();
141
+ const categoryText = formatCategory(input.category);
142
+ const ruleText = pc.dim(`[${input.rule}]`);
143
+ const header = ` ${severityLabel}${match(categoryText).with("", () => "").otherwise((text) => ` ${text}`)}: ${input.message} ${ruleText}`;
144
+ const framePart = formatFramePart(input.frame);
145
+ const helpPart = formatHelpPart(input.help);
146
+ return [
147
+ header,
148
+ ...framePart,
149
+ ...helpPart
150
+ ].join("");
151
+ }
152
+ /**
153
+ * Format the optional category text.
154
+ *
155
+ * @private
156
+ * @param category - Optional category string.
157
+ * @returns Formatted category or empty string.
158
+ */
159
+ function formatCategory(category) {
160
+ return match(category).with(void 0, () => "").otherwise((c) => pc.dim(`(${c})`));
161
+ }
162
+ /**
163
+ * Format the optional code frame part.
164
+ *
165
+ * @private
166
+ * @param frame - Optional code frame input.
167
+ * @returns Array with formatted frame line, or empty array.
168
+ */
169
+ function formatFramePart(frame) {
170
+ return match(frame).with(void 0, () => []).otherwise((f) => [`\n${formatCodeFrame(f)}`]);
171
+ }
172
+ /**
173
+ * Format the optional help text part.
174
+ *
175
+ * @private
176
+ * @param help - Optional help string.
177
+ * @returns Array with formatted help line, or empty array.
178
+ */
179
+ function formatHelpPart(help) {
180
+ return match(help).with(void 0, () => []).otherwise((h) => [`\n ${pc.cyan(`${GLYPHS.corner}${GLYPHS.dash}`)} ${pc.cyan("help")}: ${h}`]);
181
+ }
182
+ //#endregion
183
+ //#region src/lib/format/tally.ts
184
+ /**
185
+ * Format a tally block.
186
+ *
187
+ * - `style: 'tally'` renders labeled stat rows aligned in a block.
188
+ * - `style: 'inline'` renders a pipe-separated one-liner.
189
+ *
190
+ * @param input - The tally data to format.
191
+ * @returns A formatted tally string.
192
+ */
193
+ function formatTally(input) {
194
+ return match(input).with({ style: "tally" }, formatTallyBlock).with({ style: "inline" }, formatTallyInline).exhaustive();
195
+ }
196
+ /**
197
+ * Format a tally block with labeled stat rows.
198
+ *
199
+ * @private
200
+ * @param input - The tally block data.
201
+ * @returns A formatted tally block string.
202
+ */
203
+ function formatTallyBlock(input) {
204
+ const maxWidth = input.stats.reduce((max, stat) => Math.max(max, stat.label.length), 0);
205
+ return input.stats.map((stat) => ` ${stat.label.padEnd(maxWidth)} ${stat.value}`).join("\n");
206
+ }
207
+ /**
208
+ * Format a tally inline as a pipe-separated one-liner.
209
+ *
210
+ * @private
211
+ * @param input - The tally inline data.
212
+ * @returns A formatted tally line string.
213
+ */
214
+ function formatTallyInline(input) {
215
+ return ` ${input.stats.join(pc.gray(" | "))}`;
216
+ }
217
+ //#endregion
218
+ export { GLYPHS as a, formatCheck as i, formatFinding as n, formatCodeFrame as r, formatTally as t };
219
+
220
+ //# sourceMappingURL=tally-ioa20iGw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tally-ioa20iGw.js","names":[],"sources":["../src/lib/format/constants.ts","../src/lib/format/check.ts","../src/lib/format/code-frame.ts","../src/lib/format/finding.ts","../src/lib/format/tally.ts"],"sourcesContent":["/**\n * Shape of the Unicode glyphs object.\n */\nexport interface Glyphs {\n readonly check: '\\u2713'\n readonly cross: '\\u2717'\n readonly warning: '\\u26A0'\n readonly dash: '\\u2500'\n readonly dot: '\\u00B7'\n readonly arrow: '\\u203A'\n readonly skip: '\\u25CB'\n readonly fix: '\\u2699'\n readonly pipe: '\\u2502'\n readonly corner: '\\u2570'\n}\n\n/**\n * Unicode glyphs used in formatted output, frozen for immutability.\n */\nexport const GLYPHS: Glyphs = Object.freeze({\n arrow: '\\u203A',\n check: '\\u2713',\n corner: '\\u2570',\n cross: '\\u2717',\n dash: '\\u2500',\n dot: '\\u00B7',\n fix: '\\u2699',\n pipe: '\\u2502',\n skip: '\\u25CB',\n warning: '\\u26A0',\n})\n","import pc from 'picocolors'\nimport { match } from 'ts-pattern'\nimport { z } from 'zod'\n\nimport { GLYPHS } from './constants.js'\nimport type { CheckInput } from './types.js'\n\n/**\n * Zod schema for validating a check status at the public API boundary.\n *\n * @private\n */\nconst CheckStatusSchema = z.enum(['pass', 'fail', 'warn', 'skip', 'fix'])\n\n/**\n * Format a single pass/fail/warn check row.\n *\n * @param input - The check data to format.\n * @returns A formatted check string.\n */\nexport function formatCheck(input: CheckInput): string {\n const parsedStatus = CheckStatusSchema.safeParse(input.status)\n if (!parsedStatus.success) {\n return `[Invalid check status: ${parsedStatus.error.issues.map((issue) => issue.message).join(', ')}]`\n }\n\n const icon = match(input.status)\n .with('pass', () => pc.green(GLYPHS.check))\n .with('fail', () => pc.red(GLYPHS.cross))\n .with('warn', () => pc.yellow(GLYPHS.warning))\n .with('skip', () => pc.gray(GLYPHS.skip))\n .with('fix', () => pc.blue(GLYPHS.fix))\n .exhaustive()\n\n const nameText = match(input.status)\n .with('fail', () => pc.red(input.name))\n .with('warn', () => pc.yellow(input.name))\n .with('skip', () => pc.gray(input.name))\n .otherwise(() => input.name)\n\n const detailText = formatOptionalDetail(input.detail)\n const durationText = formatOptionalDuration(input.duration)\n const hintText = formatOptionalHint(input.hint)\n\n return ` ${icon} ${nameText}${detailText}${durationText}${hintText}`\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format optional detail text.\n *\n * @private\n * @param detail - Optional detail string.\n * @returns Formatted detail or empty string.\n */\nfunction formatOptionalDetail(detail: string | undefined): string {\n return match(detail)\n .with(undefined, () => '')\n .otherwise((d) => ` ${pc.gray(d)}`)\n}\n\n/**\n * Format optional duration for a check row.\n *\n * @private\n * @param duration - Optional duration in milliseconds.\n * @returns Formatted duration or empty string.\n */\nfunction formatOptionalDuration(duration: number | undefined): string {\n return match(duration)\n .with(undefined, () => '')\n .otherwise((d) => ` ${pc.gray(`(${formatDurationInline(d)})`)}`)\n}\n\n/**\n * Format optional hint text.\n *\n * @private\n * @param hint - Optional hint string.\n * @returns Formatted hint or empty string.\n */\nfunction formatOptionalHint(hint: string | undefined): string {\n return match(hint)\n .with(undefined, () => '')\n .otherwise((h) => ` ${pc.dim(`[${h}]`)}`)\n}\n\n/**\n * Inline duration format for check rows (compact).\n *\n * @private\n * @param ms - Duration in milliseconds.\n * @returns A compact formatted duration string.\n */\nfunction formatDurationInline(ms: number): string {\n return match(ms)\n .when(\n (v) => v < 1,\n () => '< 1ms'\n )\n .when(\n (v) => v < 1000,\n (v) => `${Math.round(v)}ms`\n )\n .otherwise((v) => `${(v / 1000).toFixed(2)}s`)\n}\n","import pc from 'picocolors'\nimport { match } from 'ts-pattern'\nimport { z } from 'zod'\n\nimport { GLYPHS } from './constants.js'\nimport type { CodeFrameInput } from './types.js'\n\n/**\n * Zod schema for validating a code frame annotation at the public API boundary.\n *\n * @private\n */\nconst CodeFrameAnnotationSchema = z.object({\n column: z.number().int().positive(),\n length: z.number().int().positive(),\n line: z.number().int().positive(),\n message: z.string(),\n})\n\n/**\n * Format an annotated code frame (oxlint style).\n *\n * @param input - The code frame data to format.\n * @returns A formatted code frame string.\n */\nexport function formatCodeFrame(input: CodeFrameInput): string {\n const parsed = CodeFrameAnnotationSchema.safeParse(input.annotation)\n if (!parsed.success) {\n return ` ${pc.red('✗')} Invalid code frame annotation: ${parsed.error.issues.map((issue) => issue.message).join(', ')}`\n }\n\n const { annotation, filePath, lines, startLine } = input\n\n const lastLine = startLine + lines.length - 1\n if (annotation.line < startLine || annotation.line > lastLine) {\n return `[Invalid annotation: line ${String(annotation.line)} is out of range ${String(startLine)}–${String(lastLine)}]`\n }\n\n const gutterWidth = String(lastLine).length\n\n const header = ` ${pc.cyan(GLYPHS.arrow)} ${pc.cyan(`${filePath}:${String(annotation.line)}:${String(annotation.column)}`)}`\n\n const separator = ` ${' '.repeat(gutterWidth)} ${pc.cyan(GLYPHS.pipe)}`\n\n const codeLines = lines.map((line, idx) => {\n const lineNum = startLine + idx\n const gutter = pc.cyan(String(lineNum).padStart(gutterWidth))\n return ` ${gutter} ${pc.cyan(GLYPHS.pipe)} ${line}`\n })\n\n const annotationLineIdx = annotation.line - startLine\n const pointer = ' '.repeat(annotation.column - 1) + pc.red('^'.repeat(annotation.length))\n const annotationRow = ` ${' '.repeat(gutterWidth)} ${pc.cyan(GLYPHS.pipe)} ${pointer} ${pc.red(annotation.message)}`\n\n const outputLines = codeLines.reduce<readonly string[]>(\n (acc, line, idx) =>\n match(idx === annotationLineIdx)\n .with(true, () => [...acc, line, annotationRow])\n .with(false, () => [...acc, line])\n .exhaustive(),\n []\n )\n\n return [header, separator, ...outputLines, separator].join('\\n')\n}\n","import pc from 'picocolors'\nimport { match } from 'ts-pattern'\n\nimport { formatCodeFrame } from './code-frame.js'\nimport { GLYPHS } from './constants.js'\nimport type { CodeFrameInput, FindingInput } from './types.js'\n\n/**\n * Format a full finding (oxlint style).\n *\n * @param input - The finding data to format.\n * @returns A formatted finding string.\n */\nexport function formatFinding(input: FindingInput): string {\n const severityLabel = match(input.severity)\n .with('error', () => pc.red('error'))\n .with('warning', () => pc.yellow('warning'))\n .with('hint', () => pc.blue('hint'))\n .exhaustive()\n\n const categoryText = formatCategory(input.category)\n const ruleText = pc.dim(`[${input.rule}]`)\n const categorySuffix = match(categoryText)\n .with('', () => '')\n .otherwise((text) => ` ${text}`)\n\n const header = ` ${severityLabel}${categorySuffix}: ${input.message} ${ruleText}`\n\n const framePart = formatFramePart(input.frame)\n const helpPart = formatHelpPart(input.help)\n\n return [header, ...framePart, ...helpPart].join('')\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format the optional category text.\n *\n * @private\n * @param category - Optional category string.\n * @returns Formatted category or empty string.\n */\nfunction formatCategory(category: string | undefined): string {\n return match(category)\n .with(undefined, () => '')\n .otherwise((c) => pc.dim(`(${c})`))\n}\n\n/**\n * Format the optional code frame part.\n *\n * @private\n * @param frame - Optional code frame input.\n * @returns Array with formatted frame line, or empty array.\n */\nfunction formatFramePart(frame: CodeFrameInput | undefined): readonly string[] {\n return match(frame)\n .with(undefined, () => [] as readonly string[])\n .otherwise((f) => [`\\n${formatCodeFrame(f)}`])\n}\n\n/**\n * Format the optional help text part.\n *\n * @private\n * @param help - Optional help string.\n * @returns Array with formatted help line, or empty array.\n */\nfunction formatHelpPart(help: string | undefined): readonly string[] {\n return match(help)\n .with(undefined, () => [] as readonly string[])\n .otherwise((h) => [`\\n ${pc.cyan(`${GLYPHS.corner}${GLYPHS.dash}`)} ${pc.cyan('help')}: ${h}`])\n}\n","import pc from 'picocolors'\nimport { match } from 'ts-pattern'\n\nimport type { TallyBlockInput, TallyInlineInput, TallyInput } from './types.js'\n\n/**\n * Format a tally block.\n *\n * - `style: 'tally'` renders labeled stat rows aligned in a block.\n * - `style: 'inline'` renders a pipe-separated one-liner.\n *\n * @param input - The tally data to format.\n * @returns A formatted tally string.\n */\nexport function formatTally(input: TallyInput): string {\n return match(input)\n .with({ style: 'tally' }, formatTallyBlock)\n .with({ style: 'inline' }, formatTallyInline)\n .exhaustive()\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format a tally block with labeled stat rows.\n *\n * @private\n * @param input - The tally block data.\n * @returns A formatted tally block string.\n */\nfunction formatTallyBlock(input: TallyBlockInput): string {\n const maxWidth = input.stats.reduce((max, stat) => Math.max(max, stat.label.length), 0)\n\n return input.stats.map((stat) => ` ${stat.label.padEnd(maxWidth)} ${stat.value}`).join('\\n')\n}\n\n/**\n * Format a tally inline as a pipe-separated one-liner.\n *\n * @private\n * @param input - The tally inline data.\n * @returns A formatted tally line string.\n */\nfunction formatTallyInline(input: TallyInlineInput): string {\n return ` ${input.stats.join(pc.gray(' | '))}`\n}\n"],"mappings":"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"}
@@ -20,4 +20,4 @@ interface ResolvePathOptions {
20
20
  }
21
21
  //#endregion
22
22
  export { ProjectRoot as n, ResolvePathOptions as r, PathSource as t };
23
- //# sourceMappingURL=types-D-BxshYM.d.ts.map
23
+ //# sourceMappingURL=types-Boe_1EjY.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-D-BxshYM.d.ts","names":[],"sources":["../src/lib/project/types.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAAA,SACN,WAAA;EAAA,SACA,IAAA;AAAA;;;;KAMC,UAAA;AAKZ;;;AAAA,UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,MAAA,GAAS,UAAA;EAAA,SACT,QAAA;AAAA"}
1
+ {"version":3,"file":"types-Boe_1EjY.d.ts","names":[],"sources":["../src/lib/project/types.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAAA,SACN,WAAA;EAAA,SACA,IAAA;AAAA;;;;KAMC,UAAA;AAKZ;;;AAAA,UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,MAAA,GAAS,UAAA;EAAA,SACT,QAAA;AAAA"}
@@ -0,0 +1,160 @@
1
+ //#region src/lib/format/types.d.ts
2
+ /**
3
+ * Status for a single check row (e.g. test file, lint check).
4
+ */
5
+ type CheckStatus = "pass" | "fail" | "warn" | "skip" | "fix";
6
+ /**
7
+ * Severity level for a finding.
8
+ */
9
+ type FindingSeverity = "error" | "warning" | "hint";
10
+ /**
11
+ * Input for a single pass/fail/warn check row.
12
+ */
13
+ interface CheckInput {
14
+ /**
15
+ * Status of the check.
16
+ */
17
+ readonly status: CheckStatus;
18
+ /**
19
+ * Display name (e.g. file path, test name).
20
+ */
21
+ readonly name: string;
22
+ /**
23
+ * Optional detail text shown after the name.
24
+ */
25
+ readonly detail?: string;
26
+ /**
27
+ * Duration in milliseconds.
28
+ */
29
+ readonly duration?: number;
30
+ /**
31
+ * Optional hint shown at the end.
32
+ */
33
+ readonly hint?: string;
34
+ }
35
+ /**
36
+ * A labeled row in a tally-style summary block.
37
+ */
38
+ interface TallyStat {
39
+ /**
40
+ * Row label (e.g. "Tests", "Duration").
41
+ */
42
+ readonly label: string;
43
+ /**
44
+ * Row value — can contain pre-colored text.
45
+ */
46
+ readonly value: string;
47
+ }
48
+ /**
49
+ * Tally block: labeled rows aligned in a block.
50
+ *
51
+ * ```
52
+ * Tests 3 passed | 2 failed (5)
53
+ * Duration 5.63s
54
+ * ```
55
+ */
56
+ interface TallyBlockInput {
57
+ /**
58
+ * Display as a multi-row tally block.
59
+ */
60
+ readonly style: "tally";
61
+ /**
62
+ * One or more labeled stat rows.
63
+ */
64
+ readonly stats: readonly TallyStat[];
65
+ }
66
+ /**
67
+ * Tally inline: pipe-separated one-liner.
68
+ *
69
+ * ```
70
+ * 1 error | 3 warnings | 95 files | in 142ms
71
+ * ```
72
+ */
73
+ interface TallyInlineInput {
74
+ /**
75
+ * Display as a single-line stats footer.
76
+ */
77
+ readonly style: "inline";
78
+ /**
79
+ * Pre-formatted stat segments to join with pipes.
80
+ */
81
+ readonly stats: readonly string[];
82
+ }
83
+ /**
84
+ * Discriminated union for tally output.
85
+ */
86
+ type TallyInput = TallyBlockInput | TallyInlineInput;
87
+ /**
88
+ * Annotation applied to a line in a code frame.
89
+ */
90
+ interface CodeFrameAnnotation {
91
+ /**
92
+ * 1-based line number to annotate.
93
+ */
94
+ readonly line: number;
95
+ /**
96
+ * 1-based column where the annotation starts.
97
+ */
98
+ readonly column: number;
99
+ /**
100
+ * Length of the annotated span.
101
+ */
102
+ readonly length: number;
103
+ /**
104
+ * Message shown on the annotation line.
105
+ */
106
+ readonly message: string;
107
+ }
108
+ /**
109
+ * Input for a standalone annotated code snippet.
110
+ */
111
+ interface CodeFrameInput {
112
+ /**
113
+ * File path displayed above the frame.
114
+ */
115
+ readonly filePath: string;
116
+ /**
117
+ * Source lines to display.
118
+ */
119
+ readonly lines: readonly string[];
120
+ /**
121
+ * 1-based line number of the first line in `lines`.
122
+ */
123
+ readonly startLine: number;
124
+ /**
125
+ * Annotation to render below the target line.
126
+ */
127
+ readonly annotation: CodeFrameAnnotation;
128
+ }
129
+ /**
130
+ * Input for a full finding (lint error/warning).
131
+ */
132
+ interface FindingInput {
133
+ /**
134
+ * Severity of the finding.
135
+ */
136
+ readonly severity: FindingSeverity;
137
+ /**
138
+ * Rule identifier (e.g. "no-unused-vars").
139
+ */
140
+ readonly rule: string;
141
+ /**
142
+ * Optional category (e.g. "correctness", "style").
143
+ */
144
+ readonly category?: string;
145
+ /**
146
+ * Finding message.
147
+ */
148
+ readonly message: string;
149
+ /**
150
+ * Optional code frame showing the problematic code.
151
+ */
152
+ readonly frame?: CodeFrameInput;
153
+ /**
154
+ * Optional help text with a suggested fix.
155
+ */
156
+ readonly help?: string;
157
+ }
158
+ //#endregion
159
+ export { FindingInput as a, TallyInlineInput as c, CodeFrameInput as i, TallyInput as l, CheckStatus as n, FindingSeverity as o, CodeFrameAnnotation as r, TallyBlockInput as s, CheckInput as t, TallyStat as u };
160
+ //# sourceMappingURL=types-Cp8_uIil.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-Cp8_uIil.d.ts","names":[],"sources":["../src/lib/format/types.ts"],"mappings":";;AAGA;;KAAY,WAAA;;;AAKZ;KAAY,eAAA;;;;UAKK,UAAA;EAAA;;;EAAA,SAIN,MAAA,EAAQ,WAAA;;;;WAIR,IAAA;;;;WAIA,MAAA;EAcM;;;EAAA,SAVN,QAAA;EA6BX;;;EAAA,SAzBW,IAAA;AAAA;;;;UAMM,SAAA;EAqCjB;;;EAAA,SAjCW,KAAA;EAyCA;AAMX;;EANW,SArCA,KAAA;AAAA;;AAgDX;;;;;;;UArCiB,eAAA;;;AA2DjB;WAvDW,KAAA;;;;WAIA,KAAA,WAAgB,SAAA;AAAA;;;;;AAyE3B;;;UA/DiB,gBAAA;;;;WAIN,KAAA;;;;WAIA,KAAA;AAAA;;;;KAMC,UAAA,GAAa,eAAA,GAAkB,gBAAA;;;;UAK1B,mBAAA;;;;WAIN,IAAA;;;;WAIA,MAAA;;;;WAIA,MAAA;;;;WAIA,OAAA;AAAA;;;;UAMM,cAAA;;;;WAIN,QAAA;;;;WAIA,KAAA;;;;WAIA,SAAA;;;;WAIA,UAAA,EAAY,mBAAA;AAAA;;;;UAMN,YAAA;;;;WAIN,QAAA,EAAU,eAAA;;;;WAIV,IAAA;;;;WAIA,QAAA;;;;WAIA,OAAA;;;;WAIA,KAAA,GAAQ,cAAA;;;;WAIR,IAAA;AAAA"}