/** * Syntax highlighting utilities for CLI output. * * @module */ import chalk from "chalk"; import type { Highlighter, BundledLanguage } from "shiki"; let highlighterInstance: Highlighter | null = null; /** * Get or initialize the Shiki highlighter. */ async function getHighlighter(): Promise { if (!highlighterInstance) { const { createHighlighter } = await import("shiki"); highlighterInstance = await createHighlighter({ themes: ["github-dark"], langs: ["bash", "javascript", "typescript", "json", "env", "yaml"], }); } return highlighterInstance; } /** * Highlight a code snippet with Shiki and convert to terminal colors. */ export async function highlightCode( code: string, lang: BundledLanguage = "bash", ): Promise { try { const highlighter = await getHighlighter(); const html = highlighter.codeToHtml(code, { lang, theme: "github-dark", }); // Convert HTML colors to chalk ANSI codes return htmlToChalk(html); } catch { // Fallback to plain text if highlighting fails return code; } } /** * Convert Shiki HTML output to chalk-colored terminal output. */ function htmlToChalk(html: string): string { // Map Shiki's github-dark theme colors to chalk const colorMap: Record string> = { "#79c0ff": chalk.blue, // variable, function "#d2a8ff": chalk.magentaBright, // type, class "#ffa657": chalk.yellow, // string "#a5d6ff": chalk.cyan, // property "#ffbfb7": chalk.redBright, // error "#f0883e": chalk.hex("#f0883e"), // number "#7ee787": chalk.green, // success "#e5edf9": chalk.white, // text "#8b949e": chalk.gray, // comment "#f778ba": chalk.magentaBright, // special "#ff7b72": chalk.red, // keyword }; let result = html; // Remove HTML tags but keep content result = result.replace(/]*>/g, ""); result = result.replace(/<\/code>/g, ""); // Convert to chalk const spanRegex = /([^<]*)<\/span>/g; result = result.replace(spanRegex, (_, color, content) => { const chalkFn = colorMap[color]; return chalkFn ? chalkFn(content) : content; }); // Remove any remaining HTML tags result = result.replace(/<[^>]+>/g, ""); return result; } /** * Highlight an .env file with syntax highlighting. * Shows KEY in blue, = in gray, VALUE in yellow. */ export function highlightEnvLine(line: string): string { const eqIndex = line.indexOf("="); if (eqIndex === -1) { // Comment or invalid line return line.startsWith("#") ? chalk.gray(line) : chalk.red(line); } const key = line.slice(0, eqIndex).trim(); const value = line.slice(eqIndex + 1).trim(); // Color the KEY const coloredKey = chalk.cyan.bold(key); // Color the value (handle quotes) let coloredValue = value; if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { const quote = value[0]; const innerValue = value.slice(1, -1); coloredValue = `${chalk.gray(quote)}${chalk.yellow(innerValue)}${chalk.gray( quote, )}`; } else { coloredValue = chalk.yellow(value); } return `${coloredKey}${chalk.gray("=")}${coloredValue}`; } /** * Highlight multiple .env lines. */ export function highlightEnvBlock(content: string): string { return content .split("\n") .map((line) => highlightEnvLine(line)) .join("\n"); } /** * Create a visual diff between two values. */ export function createDiff( oldValue: string, newValue: string, ): { removed: string; added: string; lineDiff: string; } { // Simple character-level diff visualization const removed = chalk.red(`- ${oldValue}`); const added = chalk.green(`+ ${newValue}`); // Create a line-by-line comparison const oldLines = oldValue.split("\n"); const newLines = newValue.split("\n"); const maxLines = Math.max(oldLines.length, newLines.length); const lineDiff: string[] = []; for (let i = 0; i < maxLines; i++) { const oldLine = oldLines[i] ?? ""; const newLine = newLines[i] ?? ""; if (oldLine === newLine) { lineDiff.push(chalk.gray(` ${oldLine}`)); } else { if (oldLine) { lineDiff.push(chalk.red(`- ${oldLine}`)); } if (newLine) { lineDiff.push(chalk.green(`+ ${newLine}`)); } } } return { removed, added, lineDiff: lineDiff.join("\n"), }; }