/** * CLI banner — renders the Coolify "C" logo from SVG data as terminal art. * Uses half-block Unicode characters (▀▄█) with chalk hex colors for * a faithful multi-layer shadow reproduction of the original SVG. * * @module */ import chalk from "chalk"; // ─── SVG source data ───────────────────────────────────────────────────────── // Original SVG: 500×500 viewport, 3 layers of a "C" shape. // Each layer is 3 rectangles (top bar, left bar, bottom bar). const SVG_SIZE = 500; /** The 3 rectangles forming the "C" shape (in SVG coords). */ const C_RECTS = [ { x: 162, y: 97, w: 224, h: 56 }, // top bar { x: 106, y: 153, w: 56, h: 168 }, // left bar { x: 162, y: 321, w: 224, h: 56 }, // bottom bar ]; /** Layers from back (shadow) to front (solid). Higher contrast for dark terms. */ const LAYERS = [ { dx: 35, dy: 35, color: "#3d2570" }, // shadow — visible on dark bg { dx: 17, dy: 17, color: "#6b40c0" }, // mid shadow { dx: 0, dy: 0, color: "#a875ff" }, // foreground — brighter purple ]; // ─── Grid renderer ─────────────────────────────────────────────────────────── const GRID_W = 46; const GRID_H = 46; const CELL = SVG_SIZE / GRID_W; /** * Paint a rectangle onto the grid (higher value = front layer wins). */ function paintRect( grid: number[][], sx: number, sy: number, sw: number, sh: number, layerIdx: number, ): void { const x1 = Math.round(sx / CELL); const y1 = Math.round(sy / CELL); const x2 = Math.round((sx + sw) / CELL); const y2 = Math.round((sy + sh) / CELL); for (let y = y1; y < y2 && y < GRID_H; y++) { for (let x = x1; x < x2 && x < GRID_W; x++) { if (y >= 0 && x >= 0) { grid[y][x] = Math.max(grid[y][x], layerIdx + 1); } } } } /** * Build the pixel grid from SVG layers. * Returns a 2D array where 0=empty, 1=shadow, 2=mid, 3=solid. */ function buildGrid(): number[][] { const grid: number[][] = Array.from({ length: GRID_H }, () => Array(GRID_W).fill(0), ); for (let li = 0; li < LAYERS.length; li++) { const { dx, dy } = LAYERS[li]; for (const rect of C_RECTS) { paintRect(grid, rect.x + dx, rect.y + dy, rect.w, rect.h, li); } } return grid; } /** * Trim empty rows/cols from the grid, returns a compact subgrid. */ function trimGrid(grid: number[][]): number[][] { let minR = grid.length, maxR = 0, minC = grid[0].length, maxC = 0; for (let r = 0; r < grid.length; r++) { for (let c = 0; c < grid[0].length; c++) { if (grid[r][c] > 0) { minR = Math.min(minR, r); maxR = Math.max(maxR, r); minC = Math.min(minC, c); maxC = Math.max(maxC, c); } } } if (minR > maxR) return [[]]; return grid.slice(minR, maxR + 1).map((row) => row.slice(minC, maxC + 1)); } /** * Render grid to terminal lines using half-block characters. * Each terminal line encodes 2 grid rows using ▀▄█ characters. */ function renderHalfBlocks(grid: number[][]): string[] { const colors = ["", ...LAYERS.map((l) => l.color)]; const lines: string[] = []; // Process 2 rows at a time for (let r = 0; r < grid.length; r += 2) { let line = ""; const topRow = grid[r]; const botRow = r + 1 < grid.length ? grid[r + 1] : topRow.map(() => 0); for (let c = 0; c < topRow.length; c++) { const top = topRow[c]; const bot = botRow[c]; if (top === 0 && bot === 0) { line += " "; } else if (top === bot) { // Both same color → full block line += chalk.hex(colors[top])("█"); } else if (top > 0 && bot === 0) { // Only top → upper half block line += chalk.hex(colors[top])("▀"); } else if (top === 0 && bot > 0) { // Only bottom → lower half block line += chalk.hex(colors[bot])("▄"); } else { // Different colors → ▀ with fg=top, bg=bot line += chalk.hex(colors[top]).bgHex(colors[bot])("▀"); } } lines.push(line); } return lines; } // ─── Banner assembly ───────────────────────────────────────────────────────── /** * Render the Coolify "C" logo as terminal art (full size ~15 lines). * Returns an array of styled terminal lines. */ function renderLogo(): string[] { const grid = buildGrid(); const trimmed = trimGrid(grid); return renderHalfBlocks(trimmed); } /** * Get logo lines for external use (e.g. header integration). */ export function getLogoLines(): string[] { return renderLogo(); } /** * Render a smaller version of the logo (~7 lines) for persistent headers. * Uses a coarser grid (24×24 instead of 46×46). */ export function getMiniLogoLines(): string[] { const miniGridW = 24; const miniGridH = 24; const miniCell = SVG_SIZE / miniGridW; const grid: number[][] = Array.from({ length: miniGridH }, () => Array(miniGridW).fill(0), ); for (let li = 0; li < LAYERS.length; li++) { const { dx, dy } = LAYERS[li]; for (const rect of C_RECTS) { const x1 = Math.round((rect.x + dx) / miniCell); const y1 = Math.round((rect.y + dy) / miniCell); const x2 = Math.round((rect.x + dx + rect.w) / miniCell); const y2 = Math.round((rect.y + dy + rect.h) / miniCell); for (let y = y1; y < y2 && y < miniGridH; y++) { for (let x = x1; x < x2 && x < miniGridW; x++) { if (y >= 0 && x >= 0) grid[y][x] = Math.max(grid[y][x], li + 1); } } } } const trimmed = trimGrid(grid); return renderHalfBlocks(trimmed); } /** * Show the full banner with logo + title + info. * Layout: logo on left, text on right. * * @param version - CLI version string * @param subtitle - Optional subtitle line */ export function showBanner( version: string = "0.9.0", subtitle?: string, ): void { const logoLines = renderLogo(); const logoWidth = logoLines.reduce( (max, line) => Math.max(max, stripAnsi(line).length), 0, ); // Text block (appears next to logo, vertically centered) const textLines = [ "", chalk.bold.hex("#8c52ff")(" Coolify CLI"), chalk.gray(` v${version}`), "", chalk.gray(subtitle || " Manage your deployments"), "", ]; // Vertically center text relative to logo const textStart = Math.max(0, Math.floor((logoLines.length - textLines.length) / 2)); const output: string[] = []; const maxLines = Math.max(logoLines.length, textStart + textLines.length); for (let i = 0; i < maxLines; i++) { const logoPart = i < logoLines.length ? logoLines[i] : ""; const logoPadded = logoPart + " ".repeat(Math.max(0, logoWidth - stripAnsi(logoPart).length)); const textIdx = i - textStart; const textPart = textIdx >= 0 && textIdx < textLines.length ? textLines[textIdx] : ""; output.push(` ${logoPadded}${textPart}`); } console.log(output.join("\n")); } /** * Show a compact single-line banner (for non-interactive / piped output). * * @param version - CLI version string */ export function showCompactBanner(version: string = "0.9.0"): void { console.log( `${chalk.hex("#8c52ff")("█")} ${chalk.bold("Coolify CLI")} ${chalk.gray(`v${version}`)}`, ); } /** * Show the appropriate banner based on TTY status. * * @param version - CLI version string * @param subtitle - Optional subtitle for full banner */ export function showAutoBanner( version: string = "0.9.0", subtitle?: string, ): void { if (process.stdout.isTTY) { showBanner(version, subtitle); } else { showCompactBanner(version); } } /** * Strip ANSI escape codes from a string (for measuring visible width). */ function stripAnsi(str: string): string { // eslint-disable-next-line no-control-regex return str.replace(/\x1b\[[0-9;]*m/g, ""); }