From 0b27748c2e70757cc7aaaf6e5d3a7c89d6b6b9cd Mon Sep 17 00:00:00 2001 From: "Ramin B." Date: Mon, 9 Feb 2026 14:22:56 -0500 Subject: [PATCH 1/3] feat: add ascii-renderer skill Shape vector algorithm for high-quality ASCII art generation. Features: - Text-to-ASCII with customizable fonts - Image-to-ASCII conversion with sharp edge detection - Adjustable contrast, size, and invert options - Built-in demo mode Based on Alex Harri's ASCII rendering research. Co-Authored-By: Claude Opus 4.5 --- README.md | 19 + skills/ascii-renderer/SKILL.md | 139 ++++++ skills/ascii-renderer/scripts/render.ts | 544 ++++++++++++++++++++++++ 3 files changed, 702 insertions(+) create mode 100644 skills/ascii-renderer/SKILL.md create mode 100755 skills/ascii-renderer/scripts/render.ts diff --git a/README.md b/README.md index 2a1f77e..ba26b4d 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,25 @@ Error prevention and best practices enforcement for AI agent-assisted coding. - Model configuration recommendations (Claude 3.5+, GPT-4+, extended thinking) - 60KB+ of reference documentation +### [ascii-renderer](./skills/ascii-renderer) + +Generate ASCII art from images or text using shape vector rendering. + +**Use when:** + +- Creating ASCII text banners for terminals, READMEs, or CLI tools +- Converting images to ASCII art with high-quality edge detection +- Building terminal splash screens or headers +- Generating retro/terminal aesthetic text + +**Features:** + +- Text-to-ASCII with customizable fonts (Arial Black, Impact, etc.) +- Image-to-ASCII conversion with sharp edge detection +- Shape vector algorithm for superior quality vs traditional brightness mapping +- Adjustable contrast, size, and invert options +- Built-in demo mode + --- ## Installation diff --git a/skills/ascii-renderer/SKILL.md b/skills/ascii-renderer/SKILL.md new file mode 100644 index 0000000..ea99a34 --- /dev/null +++ b/skills/ascii-renderer/SKILL.md @@ -0,0 +1,139 @@ +--- +name: ascii-renderer +description: Generate ASCII art from images or text using shape vector rendering. Creates sharp-edged ASCII art banners, converts photos to terminal art, and renders text with customizable fonts. +--- + +# ASCII Art Renderer + +Convert images or text to ASCII art using the **shape vector algorithm** - a technique that produces sharp edges instead of blurry pixelated results. + +Based on [Alex Harri's ASCII rendering research](https://alexharri.com/blog/ascii-rendering). + +## When to Use This Skill + +Activate this skill when: +- User wants to create ASCII text banners (e.g., "make ASCII art saying HELLO") +- Converting images to ASCII art for terminals or READMEs +- Generating retro/terminal aesthetic text +- Building CLI tool splash screens or headers + +## When NOT to Use This Skill + +Skip this skill when: +- User needs simple block letters (figlet/toilet may be simpler) +- Color ASCII art is required (this produces monochrome output) +- User wants emoji or unicode art (this uses standard ASCII characters) + +## Quick Start + +### Text to ASCII + +```bash +# Basic text banner +bun run scripts/render.ts --text "DUPE.COM" + +# Filled letters (inverted) +bun run scripts/render.ts --text "HELLO" --invert --cols 80 + +# Custom font and size +bun run scripts/render.ts --text "WOW" --font "Impact" --cols 60 --rows 12 +``` + +### Image to ASCII + +```bash +# Convert image +bun run scripts/render.ts photo.png --cols 80 --rows 40 + +# Demo sphere (no image needed) +bun run scripts/render.ts --demo +``` + +## Command Reference + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--text` | `-t` | - | Text to render as ASCII art | +| `--cols` | `-c` | 80 | Output width in characters | +| `--rows` | `-r` | auto | Output height (auto-calculated for text) | +| `--invert` | `-i` | false | Invert colors (filled vs outlined) | +| `--font` | `-f` | Arial Black | Font for text rendering | +| `--contrast` | - | 1.5 | Contrast enhancement (higher = sharper) | +| `--demo` | `-d` | false | Render demo sphere | +| `--help` | `-h` | - | Show help | + +## Examples + +### Text Banner (Outlined) + +```bash +bun run scripts/render.ts --text "DUPE" --cols 60 +``` + +``` +@@@@@TTTTTTTT@@@@TTT@@@@TTT@@FTTTTTTT#@@FTTTTTTTT%@@@@@ +@@@@@ `#@@ #@@@ #@i ,w: %@i ,;;;;;]@@@@@ +@@@@@ ]@@i `@@ #@@@ #@i `@@i ]@i J@@@@@@@@@@@ +@@@@@ ]@@D @@ #@@@ #@i `T^ W@i @@@@@@ +@@@@@ ]@@P @@ #@@@ #@i ,cssw#@@i ,=====@@@@@@ +@@@@@ J#P' s@@. J#@t ,@@i `@@@@@@@i `@@@###@@@@@ +@@@@@ ,@@@W. ,W@@i `@@@@@@@i ]@@@@@ +@@@@@=======#@@@@@@==www=@@@@#===@@@@@@@#========#@@@@@ +``` + +### Text Banner (Filled/Inverted) + +```bash +bun run scripts/render.ts --text "DUPE" --cols 60 --invert +``` + +``` + =======w: a=== a=== a======ww ,=========: + @@@P#@@@w &@@@ J@@@ ]@@@PP#@@# `@@@@PPPPP' + @@@i `@@@i &@@D J@@@ ]@@@,,W@@@ ,@@@lwwwwc + @@@i @@@i &@@D ]@@@ ]@@@@@@@P' `@@@@PPPPP + @@@l,;w@@@' ]@@@c.,W@@@ ]@@@ ,@@@l.,.,.. + @@@@@@@@P' T@@@@@@@P' ]@@@ `@@@@@@@@@i +``` + +## How It Works: Shape Vectors + +Traditional ASCII converters map pixel brightness → single character. This creates blurry results. + +**Shape vectors** capture character *geometry* using 6 sampling regions per cell: + +``` +┌─────────────┐ +│ ●₀ ●₁ │ ← Top samples +│ ●₂ ●₃ │ ← Middle samples +│ ●₄ ●₅ │ ← Bottom samples +└─────────────┘ +``` + +Each ASCII character has a unique 6D "shape signature". The algorithm finds the best-matching character for each image cell based on geometric similarity, not just brightness. + +This produces sharp edges that follow the actual boundaries in the image. + +## Requirements + +- **Runtime:** Bun (or Node.js with modifications) +- **Dependencies:** `sharp` for image processing + +```bash +bun add sharp +``` + +## Available Fonts + +Font availability depends on your system. Common options: +- Arial Black (default, bold and clean) +- Impact (condensed, strong) +- Helvetica Bold +- Georgia +- Times New Roman +- Courier New + +## Further Reading + +- [Original blog post](https://alexharri.com/blog/ascii-rendering) - Deep dive into the algorithm +- Shape vector math and K-d tree optimization details in the source code diff --git a/skills/ascii-renderer/scripts/render.ts b/skills/ascii-renderer/scripts/render.ts new file mode 100755 index 0000000..b2fa4f1 --- /dev/null +++ b/skills/ascii-renderer/scripts/render.ts @@ -0,0 +1,544 @@ +#!/usr/bin/env bun +/** + * ASCII Art Renderer using Shape Vector Algorithm + * + * Converts images or text to ASCII art with sharp edge detection. + * + * Usage: + * bun run render.ts [--cols N] [--rows N] [--contrast N] + * bun run render.ts --text "HELLO" [--font "Arial Black"] [--cols N] + * + * Examples: + * bun run render.ts photo.png + * bun run render.ts photo.jpg --cols 120 --rows 60 + * bun run render.ts --text "DUPE.COM" --cols 80 + * bun run render.ts --text "HELLO" --font "Impact" --cols 60 + */ + +import { parseArgs } from "util"; +import { readFileSync, existsSync } from "fs"; + +// ============================================================================ +// Character Shape Vectors +// ============================================================================ + +interface CharacterShape { + character: string; + shapeVector: number[]; // 6D vector [topLeft, topRight, midLeft, midRight, botLeft, botRight] +} + +// Pre-computed shape vectors for ASCII characters +// Values represent ink density in each of 6 sampling regions +const CHARACTER_SHAPES: CharacterShape[] = [ + { character: " ", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }, + { character: ".", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.2, 0.0] }, + { character: "'", shapeVector: [0.3, 0.0, 0.0, 0.0, 0.0, 0.0] }, + { character: "`", shapeVector: [0.0, 0.3, 0.0, 0.0, 0.0, 0.0] }, + { character: ",", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.3] }, + { character: ":", shapeVector: [0.0, 0.0, 0.3, 0.0, 0.3, 0.0] }, + { character: ";", shapeVector: [0.0, 0.0, 0.3, 0.0, 0.2, 0.3] }, + { character: "-", shapeVector: [0.0, 0.0, 0.8, 0.8, 0.0, 0.0] }, + { character: "=", shapeVector: [0.0, 0.0, 0.8, 0.8, 0.8, 0.8] }, + { character: "+", shapeVector: [0.0, 0.4, 0.8, 0.8, 0.0, 0.4] }, + { character: "*", shapeVector: [0.3, 0.3, 0.5, 0.5, 0.3, 0.3] }, + { character: "~", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.0, 0.0] }, + { character: "^", shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, + { character: "\"", shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, + { character: "|", shapeVector: [0.4, 0.4, 0.4, 0.4, 0.4, 0.4] }, + { character: "/", shapeVector: [0.0, 0.7, 0.4, 0.4, 0.7, 0.0] }, + { character: "\\", shapeVector: [0.7, 0.0, 0.4, 0.4, 0.0, 0.7] }, + { character: "(", shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, + { character: ")", shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, + { character: "[", shapeVector: [0.5, 0.5, 0.5, 0.0, 0.5, 0.5] }, + { character: "]", shapeVector: [0.5, 0.5, 0.0, 0.5, 0.5, 0.5] }, + { character: "{", shapeVector: [0.3, 0.5, 0.6, 0.0, 0.3, 0.5] }, + { character: "}", shapeVector: [0.5, 0.3, 0.0, 0.6, 0.5, 0.3] }, + { character: "<", shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, + { character: ">", shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, + { character: "i", shapeVector: [0.3, 0.0, 0.5, 0.0, 0.5, 0.0] }, + { character: "l", shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, + { character: "I", shapeVector: [0.5, 0.5, 0.4, 0.4, 0.5, 0.5] }, + { character: "!", shapeVector: [0.4, 0.0, 0.4, 0.0, 0.3, 0.0] }, + { character: "t", shapeVector: [0.5, 0.5, 0.5, 0.0, 0.3, 0.4] }, + { character: "f", shapeVector: [0.3, 0.5, 0.5, 0.3, 0.5, 0.0] }, + { character: "r", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.0] }, + { character: "n", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.5] }, + { character: "u", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: "v", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.3, 0.3] }, + { character: "x", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, + { character: "z", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, + { character: "c", shapeVector: [0.0, 0.0, 0.5, 0.4, 0.5, 0.4] }, + { character: "o", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: "a", shapeVector: [0.0, 0.0, 0.5, 0.6, 0.5, 0.6] }, + { character: "e", shapeVector: [0.0, 0.0, 0.6, 0.5, 0.5, 0.4] }, + { character: "s", shapeVector: [0.0, 0.0, 0.5, 0.4, 0.4, 0.5] }, + { character: "J", shapeVector: [0.4, 0.5, 0.0, 0.5, 0.5, 0.4] }, + { character: "L", shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, + { character: "C", shapeVector: [0.4, 0.5, 0.5, 0.0, 0.4, 0.5] }, + { character: "U", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.4, 0.4] }, + { character: "O", shapeVector: [0.4, 0.4, 0.5, 0.5, 0.4, 0.4] }, + { character: "0", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5] }, + { character: "Q", shapeVector: [0.4, 0.4, 0.5, 0.5, 0.5, 0.6] }, + { character: "Y", shapeVector: [0.5, 0.5, 0.3, 0.3, 0.3, 0.0] }, + { character: "X", shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, + { character: "Z", shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, + { character: "m", shapeVector: [0.0, 0.0, 0.7, 0.7, 0.6, 0.6] }, + { character: "w", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.7, 0.7] }, + { character: "q", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.6] }, + { character: "p", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.5] }, + { character: "d", shapeVector: [0.0, 0.5, 0.5, 0.5, 0.5, 0.5] }, + { character: "b", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: "k", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: "h", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: "A", shapeVector: [0.4, 0.4, 0.6, 0.6, 0.5, 0.5] }, + { character: "V", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.3, 0.3] }, + { character: "T", shapeVector: [0.6, 0.6, 0.3, 0.3, 0.3, 0.0] }, + { character: "N", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: "H", shapeVector: [0.5, 0.5, 0.6, 0.6, 0.5, 0.5] }, + { character: "K", shapeVector: [0.5, 0.5, 0.6, 0.5, 0.5, 0.5] }, + { character: "D", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, + { character: "P", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.0] }, + { character: "R", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.5] }, + { character: "B", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, + { character: "E", shapeVector: [0.6, 0.6, 0.6, 0.4, 0.6, 0.6] }, + { character: "F", shapeVector: [0.6, 0.6, 0.6, 0.4, 0.5, 0.0] }, + { character: "G", shapeVector: [0.5, 0.6, 0.5, 0.4, 0.5, 0.6] }, + { character: "S", shapeVector: [0.5, 0.6, 0.5, 0.5, 0.6, 0.5] }, + { character: "#", shapeVector: [0.7, 0.7, 0.8, 0.8, 0.7, 0.7] }, + { character: "%", shapeVector: [0.7, 0.5, 0.5, 0.5, 0.5, 0.7] }, + { character: "&", shapeVector: [0.5, 0.5, 0.6, 0.6, 0.6, 0.7] }, + { character: "M", shapeVector: [0.7, 0.7, 0.6, 0.6, 0.6, 0.6] }, + { character: "W", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.7, 0.7] }, + { character: "8", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: "$", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: "@", shapeVector: [0.8, 0.8, 0.8, 0.8, 0.8, 0.7] }, +]; + +// ============================================================================ +// Vector Math +// ============================================================================ + +function euclideanDistance(a: number[], b: number[]): number { + let sum = 0; + for (let i = 0; i < a.length; i++) { + sum += (a[i] - b[i]) ** 2; + } + return Math.sqrt(sum); +} + +// ============================================================================ +// Cached Character Lookup +// ============================================================================ + +const BITS = 5; +const RANGE = 2 ** BITS; +const cache = new Map(); + +function generateCacheKey(vector: number[]): number { + let key = 0; + for (const value of vector) { + const quantized = Math.min(RANGE - 1, Math.floor(value * RANGE)); + key = (key << BITS) | quantized; + } + return key; +} + +function findBestCharacter(samplingVector: number[]): string { + const key = generateCacheKey(samplingVector); + + if (cache.has(key)) { + return cache.get(key)!; + } + + let bestChar = " "; + let bestDistance = Infinity; + + for (const { character, shapeVector } of CHARACTER_SHAPES) { + const dist = euclideanDistance(samplingVector, shapeVector); + if (dist < bestDistance) { + bestDistance = dist; + bestChar = character; + } + } + + cache.set(key, bestChar); + return bestChar; +} + +// ============================================================================ +// Image Sampling +// ============================================================================ + +interface ImageData { + width: number; + height: number; + data: Uint8Array | number[]; +} + +function sampleCircle( + imageData: ImageData, + cx: number, + cy: number, + radius: number +): number { + let sum = 0; + let count = 0; + + const minY = Math.max(0, Math.floor(cy - radius)); + const maxY = Math.min(imageData.height - 1, Math.ceil(cy + radius)); + const minX = Math.max(0, Math.floor(cx - radius)); + const maxX = Math.min(imageData.width - 1, Math.ceil(cx + radius)); + + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + if ((x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2) { + const idx = (y * imageData.width + x) * 4; + const r = imageData.data[idx] ?? 0; + const g = imageData.data[idx + 1] ?? 0; + const b = imageData.data[idx + 2] ?? 0; + // Relative luminance + sum += 0.299 * r + 0.587 * g + 0.114 * b; + count++; + } + } + } + + return count > 0 ? sum / count / 255 : 0; +} + +function sampleCell( + imageData: ImageData, + cellX: number, + cellY: number, + cellWidth: number, + cellHeight: number +): number[] { + const samplingVector: number[] = []; + + // 6 sampling positions (2x3 grid within cell) + const positions = [ + [0.25, 0.17], + [0.75, 0.17], // Top + [0.25, 0.5], + [0.75, 0.5], // Middle + [0.25, 0.83], + [0.75, 0.83], // Bottom + ]; + + const radius = Math.min(cellWidth, cellHeight) * 0.15; + + for (const [px, py] of positions) { + const centerX = cellX + px * cellWidth; + const centerY = cellY + py * cellHeight; + samplingVector.push(sampleCircle(imageData, centerX, centerY, radius)); + } + + return samplingVector; +} + +// ============================================================================ +// Contrast Enhancement +// ============================================================================ + +function enhanceContrast(samplingVector: number[], exponent: number): number[] { + const maxVal = Math.max(...samplingVector); + if (maxVal === 0) return samplingVector; + + return samplingVector.map((v) => { + const norm = v / maxVal; + return Math.pow(norm, exponent) * maxVal; + }); +} + +// ============================================================================ +// Renderer +// ============================================================================ + +interface RenderOptions { + cols: number; + rows: number; + contrastExponent?: number; + invert?: boolean; +} + +function renderToAscii(imageData: ImageData, options: RenderOptions): string { + const { cols, rows, contrastExponent = 2, invert = false } = options; + const cellWidth = imageData.width / cols; + const cellHeight = imageData.height / rows; + + const lines: string[] = []; + + for (let row = 0; row < rows; row++) { + let line = ""; + for (let col = 0; col < cols; col++) { + const x = col * cellWidth; + const y = row * cellHeight; + + let samplingVector = sampleCell(imageData, x, y, cellWidth, cellHeight); + + // Invert if needed (for light backgrounds) + if (invert) { + samplingVector = samplingVector.map((v) => 1 - v); + } + + // Apply contrast enhancement + samplingVector = enhanceContrast(samplingVector, contrastExponent); + + line += findBestCharacter(samplingVector); + } + lines.push(line); + } + + return lines.join("\n"); +} + +// ============================================================================ +// PNG Decoding (using sharp if available, fallback to raw decode) +// ============================================================================ + +async function loadImage(filePath: string): Promise { + try { + // Try using sharp for comprehensive format support + const sharp = await import("sharp"); + const image = sharp.default(filePath); + const metadata = await image.metadata(); + const { data, info } = await image + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }); + + return { + width: info.width, + height: info.height, + data: new Uint8Array(data), + }; + } catch { + // Fallback: basic PNG decoder + console.error( + "Note: Install 'sharp' for better image format support (bun add sharp)" + ); + throw new Error( + "Could not load image. Please install sharp: bun add sharp" + ); + } +} + +// ============================================================================ +// Text to Image Rendering +// ============================================================================ + +async function renderTextToImage( + text: string, + options: { + font?: string; + fontSize?: number; + bold?: boolean; + } = {} +): Promise { + const { font = "Arial Black", fontSize = 120, bold = true } = options; + + // Calculate dimensions based on text length + // Approximate: each character is roughly 0.6x the font size in width + const estimatedWidth = Math.ceil(text.length * fontSize * 0.7) + fontSize; + const estimatedHeight = Math.ceil(fontSize * 1.5); + + // Create SVG with the text + const fontWeight = bold ? "bold" : "normal"; + const svg = ` + + + ${escapeXml(text)} + + `; + + try { + const sharp = await import("sharp"); + + // Convert SVG to PNG buffer, then to raw pixels + const { data, info } = await sharp + .default(Buffer.from(svg)) + .png() + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }); + + return { + width: info.width, + height: info.height, + data: new Uint8Array(data), + }; + } catch (err) { + throw new Error( + "Failed to render text. Make sure sharp is installed: bun add sharp" + ); + } +} + +function escapeXml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// ============================================================================ +// Demo: Generate Test Pattern +// ============================================================================ + +function generateTestPattern(width: number, height: number): ImageData { + const data = new Uint8Array(width * height * 4); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = (y * width + x) * 4; + + // Create a gradient sphere + const cx = width / 2; + const cy = height / 2; + const radius = Math.min(width, height) * 0.4; + + const dx = x - cx; + const dy = y - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < radius) { + // Inside sphere - gradient based on distance and angle for 3D effect + const normalZ = Math.sqrt(1 - (dist / radius) ** 2); + const lightDir = { x: -0.5, y: -0.5, z: 0.7 }; + const normalX = dx / radius; + const normalY = dy / radius; + + const dot = normalX * lightDir.x + normalY * lightDir.y + normalZ * lightDir.z; + const brightness = Math.max(0, Math.min(255, dot * 255)); + + data[idx] = brightness; + data[idx + 1] = brightness; + data[idx + 2] = brightness; + data[idx + 3] = 255; + } else { + // Outside sphere - white background + data[idx] = 255; + data[idx + 1] = 255; + data[idx + 2] = 255; + data[idx + 3] = 255; + } + } + } + + return { width, height, data }; +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + const { values, positionals } = parseArgs({ + args: Bun.argv.slice(2), + options: { + cols: { type: "string", short: "c", default: "80" }, + rows: { type: "string", short: "r" }, + contrast: { type: "string", default: "1.5" }, + invert: { type: "boolean", short: "i", default: false }, + demo: { type: "boolean", short: "d", default: false }, + text: { type: "string", short: "t" }, + font: { type: "string", short: "f", default: "Arial Black" }, + help: { type: "boolean", short: "h", default: false }, + }, + allowPositionals: true, + }); + + if (values.help) { + console.log(` +ASCII Art Renderer - Shape Vector Algorithm + +Usage: + bun run render.ts [options] + bun run render.ts --text "YOUR TEXT" [options] + bun run render.ts --demo [options] + +Options: + -c, --cols Output columns (default: 80) + -r, --rows Output rows (auto-calculated for text, default 40 for images) + --contrast Contrast exponent (default: 1.5, higher = more contrast) + -i, --invert Invert colors (for light images on dark terminal) + -t, --text Render text instead of an image + -f, --font Font for text rendering (default: "Arial Black") + -d, --demo Render a demo sphere + -h, --help Show this help + +Text Examples: + bun run render.ts --text "DUPE.COM" + bun run render.ts --text "HELLO" --cols 60 + bun run render.ts --text "WOW" --font "Impact" --cols 40 + +Image Examples: + bun run render.ts photo.png + bun run render.ts photo.jpg --cols 120 --rows 60 + bun run render.ts --demo --cols 60 --rows 30 + +Available Fonts (system-dependent): + Arial Black, Impact, Helvetica Bold, Georgia, Times New Roman, + Courier New, Verdana, Comic Sans MS +`); + return; + } + + const cols = parseInt(values.cols!, 10); + const contrastExponent = parseFloat(values.contrast!); + + let imageData: ImageData; + let rows: number; + + if (values.text) { + // Render text to image + const text = values.text; + imageData = await renderTextToImage(text, { + font: values.font, + fontSize: 150, + bold: true, + }); + + // Auto-calculate rows to maintain aspect ratio for text + // Terminal characters are roughly 2:1 height:width ratio + const aspectRatio = imageData.height / imageData.width; + rows = values.rows + ? parseInt(values.rows, 10) + : Math.max(5, Math.ceil(cols * aspectRatio * 0.5)); + } else if (values.demo || positionals.length === 0) { + // Generate demo sphere + console.log("Rendering demo sphere...\n"); + imageData = generateTestPattern(400, 400); + rows = values.rows ? parseInt(values.rows, 10) : 40; + } else { + const imagePath = positionals[0]; + if (!existsSync(imagePath)) { + console.error(`Error: File not found: ${imagePath}`); + process.exit(1); + } + imageData = await loadImage(imagePath); + rows = values.rows ? parseInt(values.rows, 10) : 40; + } + + const ascii = renderToAscii(imageData, { + cols, + rows, + contrastExponent, + invert: values.invert, + }); + + console.log(ascii); +} + +main().catch(console.error); From dd0ab09b174011d796a2e5fa62af82d1dcf777ec Mon Sep 17 00:00:00 2001 From: "Ramin B." Date: Mon, 9 Feb 2026 14:33:01 -0500 Subject: [PATCH 2/3] chore: add biome for linting and formatting - Add biome.json with 2 spaces, single quotes, no semicolons - Add package.json with sharp dependency and lint scripts - Format all JS/TS files with biome - Disable noNonNullAssertion and useIterableCallbackReturn rules Co-Authored-By: Claude Opus 4.5 --- biome.json | 42 ++ bun.lock | 93 +++ package.json | 16 + skills/ascii-renderer/scripts/render.ts | 474 +++++++-------- skills/nomistakes/metadata.json | 8 +- skills/nomistakes/scripts/migrate-to-biome.js | 549 +++++++++--------- skills/nomistakes/scripts/validate-skill.js | 219 +++---- 7 files changed, 787 insertions(+), 614 deletions(-) create mode 100644 biome.json create mode 100644 bun.lock create mode 100644 package.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c5a82a7 --- /dev/null +++ b/biome.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off" + }, + "suspicious": { + "useIterableCallbackReturn": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a20e32d --- /dev/null +++ b/bun.lock @@ -0,0 +1,93 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "skills", + "dependencies": { + "sharp": "^0.34.5", + }, + "devDependencies": { + "@biomejs/biome": "^2.3.14", + }, + }, + }, + "packages": { + "@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..86a5fb3 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "skills", + "type": "module", + "private": true, + "scripts": { + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write ." + }, + "dependencies": { + "sharp": "^0.34.5" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.14" + } +} diff --git a/skills/ascii-renderer/scripts/render.ts b/skills/ascii-renderer/scripts/render.ts index b2fa4f1..9d0b491 100755 --- a/skills/ascii-renderer/scripts/render.ts +++ b/skills/ascii-renderer/scripts/render.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun + /** * ASCII Art Renderer using Shape Vector Algorithm * @@ -15,154 +16,154 @@ * bun run render.ts --text "HELLO" --font "Impact" --cols 60 */ -import { parseArgs } from "util"; -import { readFileSync, existsSync } from "fs"; +import { existsSync } from 'node:fs' +import { parseArgs } from 'node:util' // ============================================================================ // Character Shape Vectors // ============================================================================ interface CharacterShape { - character: string; - shapeVector: number[]; // 6D vector [topLeft, topRight, midLeft, midRight, botLeft, botRight] + character: string + shapeVector: number[] // 6D vector [topLeft, topRight, midLeft, midRight, botLeft, botRight] } // Pre-computed shape vectors for ASCII characters // Values represent ink density in each of 6 sampling regions const CHARACTER_SHAPES: CharacterShape[] = [ - { character: " ", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }, - { character: ".", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.2, 0.0] }, + { character: ' ', shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }, + { character: '.', shapeVector: [0.0, 0.0, 0.0, 0.0, 0.2, 0.0] }, { character: "'", shapeVector: [0.3, 0.0, 0.0, 0.0, 0.0, 0.0] }, - { character: "`", shapeVector: [0.0, 0.3, 0.0, 0.0, 0.0, 0.0] }, - { character: ",", shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.3] }, - { character: ":", shapeVector: [0.0, 0.0, 0.3, 0.0, 0.3, 0.0] }, - { character: ";", shapeVector: [0.0, 0.0, 0.3, 0.0, 0.2, 0.3] }, - { character: "-", shapeVector: [0.0, 0.0, 0.8, 0.8, 0.0, 0.0] }, - { character: "=", shapeVector: [0.0, 0.0, 0.8, 0.8, 0.8, 0.8] }, - { character: "+", shapeVector: [0.0, 0.4, 0.8, 0.8, 0.0, 0.4] }, - { character: "*", shapeVector: [0.3, 0.3, 0.5, 0.5, 0.3, 0.3] }, - { character: "~", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.0, 0.0] }, - { character: "^", shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, - { character: "\"", shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, - { character: "|", shapeVector: [0.4, 0.4, 0.4, 0.4, 0.4, 0.4] }, - { character: "/", shapeVector: [0.0, 0.7, 0.4, 0.4, 0.7, 0.0] }, - { character: "\\", shapeVector: [0.7, 0.0, 0.4, 0.4, 0.0, 0.7] }, - { character: "(", shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, - { character: ")", shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, - { character: "[", shapeVector: [0.5, 0.5, 0.5, 0.0, 0.5, 0.5] }, - { character: "]", shapeVector: [0.5, 0.5, 0.0, 0.5, 0.5, 0.5] }, - { character: "{", shapeVector: [0.3, 0.5, 0.6, 0.0, 0.3, 0.5] }, - { character: "}", shapeVector: [0.5, 0.3, 0.0, 0.6, 0.5, 0.3] }, - { character: "<", shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, - { character: ">", shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, - { character: "i", shapeVector: [0.3, 0.0, 0.5, 0.0, 0.5, 0.0] }, - { character: "l", shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, - { character: "I", shapeVector: [0.5, 0.5, 0.4, 0.4, 0.5, 0.5] }, - { character: "!", shapeVector: [0.4, 0.0, 0.4, 0.0, 0.3, 0.0] }, - { character: "t", shapeVector: [0.5, 0.5, 0.5, 0.0, 0.3, 0.4] }, - { character: "f", shapeVector: [0.3, 0.5, 0.5, 0.3, 0.5, 0.0] }, - { character: "r", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.0] }, - { character: "n", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.5] }, - { character: "u", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, - { character: "v", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.3, 0.3] }, - { character: "x", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, - { character: "z", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, - { character: "c", shapeVector: [0.0, 0.0, 0.5, 0.4, 0.5, 0.4] }, - { character: "o", shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, - { character: "a", shapeVector: [0.0, 0.0, 0.5, 0.6, 0.5, 0.6] }, - { character: "e", shapeVector: [0.0, 0.0, 0.6, 0.5, 0.5, 0.4] }, - { character: "s", shapeVector: [0.0, 0.0, 0.5, 0.4, 0.4, 0.5] }, - { character: "J", shapeVector: [0.4, 0.5, 0.0, 0.5, 0.5, 0.4] }, - { character: "L", shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, - { character: "C", shapeVector: [0.4, 0.5, 0.5, 0.0, 0.4, 0.5] }, - { character: "U", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.4, 0.4] }, - { character: "O", shapeVector: [0.4, 0.4, 0.5, 0.5, 0.4, 0.4] }, - { character: "0", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5] }, - { character: "Q", shapeVector: [0.4, 0.4, 0.5, 0.5, 0.5, 0.6] }, - { character: "Y", shapeVector: [0.5, 0.5, 0.3, 0.3, 0.3, 0.0] }, - { character: "X", shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, - { character: "Z", shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, - { character: "m", shapeVector: [0.0, 0.0, 0.7, 0.7, 0.6, 0.6] }, - { character: "w", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.7, 0.7] }, - { character: "q", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.6] }, - { character: "p", shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.5] }, - { character: "d", shapeVector: [0.0, 0.5, 0.5, 0.5, 0.5, 0.5] }, - { character: "b", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, - { character: "k", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, - { character: "h", shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, - { character: "A", shapeVector: [0.4, 0.4, 0.6, 0.6, 0.5, 0.5] }, - { character: "V", shapeVector: [0.5, 0.5, 0.5, 0.5, 0.3, 0.3] }, - { character: "T", shapeVector: [0.6, 0.6, 0.3, 0.3, 0.3, 0.0] }, - { character: "N", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, - { character: "H", shapeVector: [0.5, 0.5, 0.6, 0.6, 0.5, 0.5] }, - { character: "K", shapeVector: [0.5, 0.5, 0.6, 0.5, 0.5, 0.5] }, - { character: "D", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, - { character: "P", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.0] }, - { character: "R", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.5] }, - { character: "B", shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, - { character: "E", shapeVector: [0.6, 0.6, 0.6, 0.4, 0.6, 0.6] }, - { character: "F", shapeVector: [0.6, 0.6, 0.6, 0.4, 0.5, 0.0] }, - { character: "G", shapeVector: [0.5, 0.6, 0.5, 0.4, 0.5, 0.6] }, - { character: "S", shapeVector: [0.5, 0.6, 0.5, 0.5, 0.6, 0.5] }, - { character: "#", shapeVector: [0.7, 0.7, 0.8, 0.8, 0.7, 0.7] }, - { character: "%", shapeVector: [0.7, 0.5, 0.5, 0.5, 0.5, 0.7] }, - { character: "&", shapeVector: [0.5, 0.5, 0.6, 0.6, 0.6, 0.7] }, - { character: "M", shapeVector: [0.7, 0.7, 0.6, 0.6, 0.6, 0.6] }, - { character: "W", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.7, 0.7] }, - { character: "8", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, - { character: "$", shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, - { character: "@", shapeVector: [0.8, 0.8, 0.8, 0.8, 0.8, 0.7] }, -]; + { character: '`', shapeVector: [0.0, 0.3, 0.0, 0.0, 0.0, 0.0] }, + { character: ',', shapeVector: [0.0, 0.0, 0.0, 0.0, 0.0, 0.3] }, + { character: ':', shapeVector: [0.0, 0.0, 0.3, 0.0, 0.3, 0.0] }, + { character: ';', shapeVector: [0.0, 0.0, 0.3, 0.0, 0.2, 0.3] }, + { character: '-', shapeVector: [0.0, 0.0, 0.8, 0.8, 0.0, 0.0] }, + { character: '=', shapeVector: [0.0, 0.0, 0.8, 0.8, 0.8, 0.8] }, + { character: '+', shapeVector: [0.0, 0.4, 0.8, 0.8, 0.0, 0.4] }, + { character: '*', shapeVector: [0.3, 0.3, 0.5, 0.5, 0.3, 0.3] }, + { character: '~', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.0, 0.0] }, + { character: '^', shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, + { character: '"', shapeVector: [0.4, 0.4, 0.0, 0.0, 0.0, 0.0] }, + { character: '|', shapeVector: [0.4, 0.4, 0.4, 0.4, 0.4, 0.4] }, + { character: '/', shapeVector: [0.0, 0.7, 0.4, 0.4, 0.7, 0.0] }, + { character: '\\', shapeVector: [0.7, 0.0, 0.4, 0.4, 0.0, 0.7] }, + { character: '(', shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, + { character: ')', shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, + { character: '[', shapeVector: [0.5, 0.5, 0.5, 0.0, 0.5, 0.5] }, + { character: ']', shapeVector: [0.5, 0.5, 0.0, 0.5, 0.5, 0.5] }, + { character: '{', shapeVector: [0.3, 0.5, 0.6, 0.0, 0.3, 0.5] }, + { character: '}', shapeVector: [0.5, 0.3, 0.0, 0.6, 0.5, 0.3] }, + { character: '<', shapeVector: [0.0, 0.5, 0.5, 0.0, 0.0, 0.5] }, + { character: '>', shapeVector: [0.5, 0.0, 0.0, 0.5, 0.5, 0.0] }, + { character: 'i', shapeVector: [0.3, 0.0, 0.5, 0.0, 0.5, 0.0] }, + { character: 'l', shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, + { character: 'I', shapeVector: [0.5, 0.5, 0.4, 0.4, 0.5, 0.5] }, + { character: '!', shapeVector: [0.4, 0.0, 0.4, 0.0, 0.3, 0.0] }, + { character: 't', shapeVector: [0.5, 0.5, 0.5, 0.0, 0.3, 0.4] }, + { character: 'f', shapeVector: [0.3, 0.5, 0.5, 0.3, 0.5, 0.0] }, + { character: 'r', shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.0] }, + { character: 'n', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.5] }, + { character: 'u', shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: 'v', shapeVector: [0.0, 0.0, 0.5, 0.5, 0.3, 0.3] }, + { character: 'x', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, + { character: 'z', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.6] }, + { character: 'c', shapeVector: [0.0, 0.0, 0.5, 0.4, 0.5, 0.4] }, + { character: 'o', shapeVector: [0.0, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: 'a', shapeVector: [0.0, 0.0, 0.5, 0.6, 0.5, 0.6] }, + { character: 'e', shapeVector: [0.0, 0.0, 0.6, 0.5, 0.5, 0.4] }, + { character: 's', shapeVector: [0.0, 0.0, 0.5, 0.4, 0.4, 0.5] }, + { character: 'J', shapeVector: [0.4, 0.5, 0.0, 0.5, 0.5, 0.4] }, + { character: 'L', shapeVector: [0.5, 0.0, 0.5, 0.0, 0.5, 0.5] }, + { character: 'C', shapeVector: [0.4, 0.5, 0.5, 0.0, 0.4, 0.5] }, + { character: 'U', shapeVector: [0.5, 0.5, 0.5, 0.5, 0.4, 0.4] }, + { character: 'O', shapeVector: [0.4, 0.4, 0.5, 0.5, 0.4, 0.4] }, + { character: '0', shapeVector: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5] }, + { character: 'Q', shapeVector: [0.4, 0.4, 0.5, 0.5, 0.5, 0.6] }, + { character: 'Y', shapeVector: [0.5, 0.5, 0.3, 0.3, 0.3, 0.0] }, + { character: 'X', shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, + { character: 'Z', shapeVector: [0.6, 0.6, 0.4, 0.4, 0.6, 0.6] }, + { character: 'm', shapeVector: [0.0, 0.0, 0.7, 0.7, 0.6, 0.6] }, + { character: 'w', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.7, 0.7] }, + { character: 'q', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.5, 0.6] }, + { character: 'p', shapeVector: [0.0, 0.0, 0.6, 0.6, 0.6, 0.5] }, + { character: 'd', shapeVector: [0.0, 0.5, 0.5, 0.5, 0.5, 0.5] }, + { character: 'b', shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: 'k', shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: 'h', shapeVector: [0.5, 0.0, 0.5, 0.5, 0.5, 0.5] }, + { character: 'A', shapeVector: [0.4, 0.4, 0.6, 0.6, 0.5, 0.5] }, + { character: 'V', shapeVector: [0.5, 0.5, 0.5, 0.5, 0.3, 0.3] }, + { character: 'T', shapeVector: [0.6, 0.6, 0.3, 0.3, 0.3, 0.0] }, + { character: 'N', shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: 'H', shapeVector: [0.5, 0.5, 0.6, 0.6, 0.5, 0.5] }, + { character: 'K', shapeVector: [0.5, 0.5, 0.6, 0.5, 0.5, 0.5] }, + { character: 'D', shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, + { character: 'P', shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.0] }, + { character: 'R', shapeVector: [0.6, 0.5, 0.6, 0.5, 0.5, 0.5] }, + { character: 'B', shapeVector: [0.6, 0.5, 0.6, 0.5, 0.6, 0.5] }, + { character: 'E', shapeVector: [0.6, 0.6, 0.6, 0.4, 0.6, 0.6] }, + { character: 'F', shapeVector: [0.6, 0.6, 0.6, 0.4, 0.5, 0.0] }, + { character: 'G', shapeVector: [0.5, 0.6, 0.5, 0.4, 0.5, 0.6] }, + { character: 'S', shapeVector: [0.5, 0.6, 0.5, 0.5, 0.6, 0.5] }, + { character: '#', shapeVector: [0.7, 0.7, 0.8, 0.8, 0.7, 0.7] }, + { character: '%', shapeVector: [0.7, 0.5, 0.5, 0.5, 0.5, 0.7] }, + { character: '&', shapeVector: [0.5, 0.5, 0.6, 0.6, 0.6, 0.7] }, + { character: 'M', shapeVector: [0.7, 0.7, 0.6, 0.6, 0.6, 0.6] }, + { character: 'W', shapeVector: [0.6, 0.6, 0.6, 0.6, 0.7, 0.7] }, + { character: '8', shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: '$', shapeVector: [0.6, 0.6, 0.6, 0.6, 0.6, 0.6] }, + { character: '@', shapeVector: [0.8, 0.8, 0.8, 0.8, 0.8, 0.7] }, +] // ============================================================================ // Vector Math // ============================================================================ function euclideanDistance(a: number[], b: number[]): number { - let sum = 0; + let sum = 0 for (let i = 0; i < a.length; i++) { - sum += (a[i] - b[i]) ** 2; + sum += (a[i] - b[i]) ** 2 } - return Math.sqrt(sum); + return Math.sqrt(sum) } // ============================================================================ // Cached Character Lookup // ============================================================================ -const BITS = 5; -const RANGE = 2 ** BITS; -const cache = new Map(); +const BITS = 5 +const RANGE = 2 ** BITS +const cache = new Map() function generateCacheKey(vector: number[]): number { - let key = 0; + let key = 0 for (const value of vector) { - const quantized = Math.min(RANGE - 1, Math.floor(value * RANGE)); - key = (key << BITS) | quantized; + const quantized = Math.min(RANGE - 1, Math.floor(value * RANGE)) + key = (key << BITS) | quantized } - return key; + return key } function findBestCharacter(samplingVector: number[]): string { - const key = generateCacheKey(samplingVector); + const key = generateCacheKey(samplingVector) if (cache.has(key)) { - return cache.get(key)!; + return cache.get(key)! } - let bestChar = " "; - let bestDistance = Infinity; + let bestChar = ' ' + let bestDistance = Infinity for (const { character, shapeVector } of CHARACTER_SHAPES) { - const dist = euclideanDistance(samplingVector, shapeVector); + const dist = euclideanDistance(samplingVector, shapeVector) if (dist < bestDistance) { - bestDistance = dist; - bestChar = character; + bestDistance = dist + bestChar = character } } - cache.set(key, bestChar); - return bestChar; + cache.set(key, bestChar) + return bestChar } // ============================================================================ @@ -170,40 +171,40 @@ function findBestCharacter(samplingVector: number[]): string { // ============================================================================ interface ImageData { - width: number; - height: number; - data: Uint8Array | number[]; + width: number + height: number + data: Uint8Array | number[] } function sampleCircle( imageData: ImageData, cx: number, cy: number, - radius: number + radius: number, ): number { - let sum = 0; - let count = 0; + let sum = 0 + let count = 0 - const minY = Math.max(0, Math.floor(cy - radius)); - const maxY = Math.min(imageData.height - 1, Math.ceil(cy + radius)); - const minX = Math.max(0, Math.floor(cx - radius)); - const maxX = Math.min(imageData.width - 1, Math.ceil(cx + radius)); + const minY = Math.max(0, Math.floor(cy - radius)) + const maxY = Math.min(imageData.height - 1, Math.ceil(cy + radius)) + const minX = Math.max(0, Math.floor(cx - radius)) + const maxX = Math.min(imageData.width - 1, Math.ceil(cx + radius)) for (let y = minY; y <= maxY; y++) { for (let x = minX; x <= maxX; x++) { if ((x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2) { - const idx = (y * imageData.width + x) * 4; - const r = imageData.data[idx] ?? 0; - const g = imageData.data[idx + 1] ?? 0; - const b = imageData.data[idx + 2] ?? 0; + const idx = (y * imageData.width + x) * 4 + const r = imageData.data[idx] ?? 0 + const g = imageData.data[idx + 1] ?? 0 + const b = imageData.data[idx + 2] ?? 0 // Relative luminance - sum += 0.299 * r + 0.587 * g + 0.114 * b; - count++; + sum += 0.299 * r + 0.587 * g + 0.114 * b + count++ } } } - return count > 0 ? sum / count / 255 : 0; + return count > 0 ? sum / count / 255 : 0 } function sampleCell( @@ -211,9 +212,9 @@ function sampleCell( cellX: number, cellY: number, cellWidth: number, - cellHeight: number + cellHeight: number, ): number[] { - const samplingVector: number[] = []; + const samplingVector: number[] = [] // 6 sampling positions (2x3 grid within cell) const positions = [ @@ -223,17 +224,17 @@ function sampleCell( [0.75, 0.5], // Middle [0.25, 0.83], [0.75, 0.83], // Bottom - ]; + ] - const radius = Math.min(cellWidth, cellHeight) * 0.15; + const radius = Math.min(cellWidth, cellHeight) * 0.15 for (const [px, py] of positions) { - const centerX = cellX + px * cellWidth; - const centerY = cellY + py * cellHeight; - samplingVector.push(sampleCircle(imageData, centerX, centerY, radius)); + const centerX = cellX + px * cellWidth + const centerY = cellY + py * cellHeight + samplingVector.push(sampleCircle(imageData, centerX, centerY, radius)) } - return samplingVector; + return samplingVector } // ============================================================================ @@ -241,13 +242,13 @@ function sampleCell( // ============================================================================ function enhanceContrast(samplingVector: number[], exponent: number): number[] { - const maxVal = Math.max(...samplingVector); - if (maxVal === 0) return samplingVector; + const maxVal = Math.max(...samplingVector) + if (maxVal === 0) return samplingVector return samplingVector.map((v) => { - const norm = v / maxVal; - return Math.pow(norm, exponent) * maxVal; - }); + const norm = v / maxVal + return norm ** exponent * maxVal + }) } // ============================================================================ @@ -255,41 +256,41 @@ function enhanceContrast(samplingVector: number[], exponent: number): number[] { // ============================================================================ interface RenderOptions { - cols: number; - rows: number; - contrastExponent?: number; - invert?: boolean; + cols: number + rows: number + contrastExponent?: number + invert?: boolean } function renderToAscii(imageData: ImageData, options: RenderOptions): string { - const { cols, rows, contrastExponent = 2, invert = false } = options; - const cellWidth = imageData.width / cols; - const cellHeight = imageData.height / rows; + const { cols, rows, contrastExponent = 2, invert = false } = options + const cellWidth = imageData.width / cols + const cellHeight = imageData.height / rows - const lines: string[] = []; + const lines: string[] = [] for (let row = 0; row < rows; row++) { - let line = ""; + let line = '' for (let col = 0; col < cols; col++) { - const x = col * cellWidth; - const y = row * cellHeight; + const x = col * cellWidth + const y = row * cellHeight - let samplingVector = sampleCell(imageData, x, y, cellWidth, cellHeight); + let samplingVector = sampleCell(imageData, x, y, cellWidth, cellHeight) // Invert if needed (for light backgrounds) if (invert) { - samplingVector = samplingVector.map((v) => 1 - v); + samplingVector = samplingVector.map((v) => 1 - v) } // Apply contrast enhancement - samplingVector = enhanceContrast(samplingVector, contrastExponent); + samplingVector = enhanceContrast(samplingVector, contrastExponent) - line += findBestCharacter(samplingVector); + line += findBestCharacter(samplingVector) } - lines.push(line); + lines.push(line) } - return lines.join("\n"); + return lines.join('\n') } // ============================================================================ @@ -299,27 +300,25 @@ function renderToAscii(imageData: ImageData, options: RenderOptions): string { async function loadImage(filePath: string): Promise { try { // Try using sharp for comprehensive format support - const sharp = await import("sharp"); - const image = sharp.default(filePath); - const metadata = await image.metadata(); + const sharp = await import('sharp') + const image = sharp.default(filePath) + const _metadata = await image.metadata() const { data, info } = await image .raw() .ensureAlpha() - .toBuffer({ resolveWithObject: true }); + .toBuffer({ resolveWithObject: true }) return { width: info.width, height: info.height, data: new Uint8Array(data), - }; + } } catch { // Fallback: basic PNG decoder console.error( - "Note: Install 'sharp' for better image format support (bun add sharp)" - ); - throw new Error( - "Could not load image. Please install sharp: bun add sharp" - ); + "Note: Install 'sharp' for better image format support (bun add sharp)", + ) + throw new Error('Could not load image. Please install sharp: bun add sharp') } } @@ -330,20 +329,20 @@ async function loadImage(filePath: string): Promise { async function renderTextToImage( text: string, options: { - font?: string; - fontSize?: number; - bold?: boolean; - } = {} + font?: string + fontSize?: number + bold?: boolean + } = {}, ): Promise { - const { font = "Arial Black", fontSize = 120, bold = true } = options; + const { font = 'Arial Black', fontSize = 120, bold = true } = options // Calculate dimensions based on text length // Approximate: each character is roughly 0.6x the font size in width - const estimatedWidth = Math.ceil(text.length * fontSize * 0.7) + fontSize; - const estimatedHeight = Math.ceil(fontSize * 1.5); + const estimatedWidth = Math.ceil(text.length * fontSize * 0.7) + fontSize + const estimatedHeight = Math.ceil(fontSize * 1.5) // Create SVG with the text - const fontWeight = bold ? "bold" : "normal"; + const fontWeight = bold ? 'bold' : 'normal' const svg = ` @@ -358,10 +357,10 @@ async function renderTextToImage( fill="black" >${escapeXml(text)} - `; + ` try { - const sharp = await import("sharp"); + const sharp = await import('sharp') // Convert SVG to PNG buffer, then to raw pixels const { data, info } = await sharp @@ -369,27 +368,27 @@ async function renderTextToImage( .png() .raw() .ensureAlpha() - .toBuffer({ resolveWithObject: true }); + .toBuffer({ resolveWithObject: true }) return { width: info.width, height: info.height, data: new Uint8Array(data), - }; - } catch (err) { + } + } catch (_err) { throw new Error( - "Failed to render text. Make sure sharp is installed: bun add sharp" - ); + 'Failed to render text. Make sure sharp is installed: bun add sharp', + ) } } function escapeXml(text: string): string { return text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') } // ============================================================================ @@ -397,46 +396,47 @@ function escapeXml(text: string): string { // ============================================================================ function generateTestPattern(width: number, height: number): ImageData { - const data = new Uint8Array(width * height * 4); + const data = new Uint8Array(width * height * 4) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const idx = (y * width + x) * 4; + const idx = (y * width + x) * 4 // Create a gradient sphere - const cx = width / 2; - const cy = height / 2; - const radius = Math.min(width, height) * 0.4; + const cx = width / 2 + const cy = height / 2 + const radius = Math.min(width, height) * 0.4 - const dx = x - cx; - const dy = y - cy; - const dist = Math.sqrt(dx * dx + dy * dy); + const dx = x - cx + const dy = y - cy + const dist = Math.sqrt(dx * dx + dy * dy) if (dist < radius) { // Inside sphere - gradient based on distance and angle for 3D effect - const normalZ = Math.sqrt(1 - (dist / radius) ** 2); - const lightDir = { x: -0.5, y: -0.5, z: 0.7 }; - const normalX = dx / radius; - const normalY = dy / radius; - - const dot = normalX * lightDir.x + normalY * lightDir.y + normalZ * lightDir.z; - const brightness = Math.max(0, Math.min(255, dot * 255)); - - data[idx] = brightness; - data[idx + 1] = brightness; - data[idx + 2] = brightness; - data[idx + 3] = 255; + const normalZ = Math.sqrt(1 - (dist / radius) ** 2) + const lightDir = { x: -0.5, y: -0.5, z: 0.7 } + const normalX = dx / radius + const normalY = dy / radius + + const dot = + normalX * lightDir.x + normalY * lightDir.y + normalZ * lightDir.z + const brightness = Math.max(0, Math.min(255, dot * 255)) + + data[idx] = brightness + data[idx + 1] = brightness + data[idx + 2] = brightness + data[idx + 3] = 255 } else { // Outside sphere - white background - data[idx] = 255; - data[idx + 1] = 255; - data[idx + 2] = 255; - data[idx + 3] = 255; + data[idx] = 255 + data[idx + 1] = 255 + data[idx + 2] = 255 + data[idx + 3] = 255 } } } - return { width, height, data }; + return { width, height, data } } // ============================================================================ @@ -447,17 +447,17 @@ async function main() { const { values, positionals } = parseArgs({ args: Bun.argv.slice(2), options: { - cols: { type: "string", short: "c", default: "80" }, - rows: { type: "string", short: "r" }, - contrast: { type: "string", default: "1.5" }, - invert: { type: "boolean", short: "i", default: false }, - demo: { type: "boolean", short: "d", default: false }, - text: { type: "string", short: "t" }, - font: { type: "string", short: "f", default: "Arial Black" }, - help: { type: "boolean", short: "h", default: false }, + cols: { type: 'string', short: 'c', default: '80' }, + rows: { type: 'string', short: 'r' }, + contrast: { type: 'string', default: '1.5' }, + invert: { type: 'boolean', short: 'i', default: false }, + demo: { type: 'boolean', short: 'd', default: false }, + text: { type: 'string', short: 't' }, + font: { type: 'string', short: 'f', default: 'Arial Black' }, + help: { type: 'boolean', short: 'h', default: false }, }, allowPositionals: true, - }); + }) if (values.help) { console.log(` @@ -491,44 +491,44 @@ Image Examples: Available Fonts (system-dependent): Arial Black, Impact, Helvetica Bold, Georgia, Times New Roman, Courier New, Verdana, Comic Sans MS -`); - return; +`) + return } - const cols = parseInt(values.cols!, 10); - const contrastExponent = parseFloat(values.contrast!); + const cols = parseInt(values.cols!, 10) + const contrastExponent = parseFloat(values.contrast!) - let imageData: ImageData; - let rows: number; + let imageData: ImageData + let rows: number if (values.text) { // Render text to image - const text = values.text; + const text = values.text imageData = await renderTextToImage(text, { font: values.font, fontSize: 150, bold: true, - }); + }) // Auto-calculate rows to maintain aspect ratio for text // Terminal characters are roughly 2:1 height:width ratio - const aspectRatio = imageData.height / imageData.width; + const aspectRatio = imageData.height / imageData.width rows = values.rows ? parseInt(values.rows, 10) - : Math.max(5, Math.ceil(cols * aspectRatio * 0.5)); + : Math.max(5, Math.ceil(cols * aspectRatio * 0.5)) } else if (values.demo || positionals.length === 0) { // Generate demo sphere - console.log("Rendering demo sphere...\n"); - imageData = generateTestPattern(400, 400); - rows = values.rows ? parseInt(values.rows, 10) : 40; + console.log('Rendering demo sphere...\n') + imageData = generateTestPattern(400, 400) + rows = values.rows ? parseInt(values.rows, 10) : 40 } else { - const imagePath = positionals[0]; + const imagePath = positionals[0] if (!existsSync(imagePath)) { - console.error(`Error: File not found: ${imagePath}`); - process.exit(1); + console.error(`Error: File not found: ${imagePath}`) + process.exit(1) } - imageData = await loadImage(imagePath); - rows = values.rows ? parseInt(values.rows, 10) : 40; + imageData = await loadImage(imagePath) + rows = values.rows ? parseInt(values.rows, 10) : 40 } const ascii = renderToAscii(imageData, { @@ -536,9 +536,9 @@ Available Fonts (system-dependent): rows, contrastExponent, invert: values.invert, - }); + }) - console.log(ascii); + console.log(ascii) } -main().catch(console.error); +main().catch(console.error) diff --git a/skills/nomistakes/metadata.json b/skills/nomistakes/metadata.json index 92275bb..9510d0b 100644 --- a/skills/nomistakes/metadata.json +++ b/skills/nomistakes/metadata.json @@ -10,13 +10,7 @@ "https://www.typescriptlang.org/docs/", "https://effect.website" ], - "compatibility": [ - "claude-code", - "opencode", - "cursor", - "goose", - "amp" - ], + "compatibility": ["claude-code", "opencode", "cursor", "goose", "amp"], "tags": [ "error-prevention", "best-practices", diff --git a/skills/nomistakes/scripts/migrate-to-biome.js b/skills/nomistakes/scripts/migrate-to-biome.js index b150f58..94d1953 100755 --- a/skills/nomistakes/scripts/migrate-to-biome.js +++ b/skills/nomistakes/scripts/migrate-to-biome.js @@ -2,18 +2,18 @@ /** * Migrate from ESLint/Prettier to Biome - * + * * This script automates the migration to Biome (biomejs.dev), a fast Rust-based * linter and formatter that replaces ESLint + Prettier with a single tool. - * + * * Benefits: * - 50-100x faster than ESLint * - Zero config TypeScript support * - Single tool for linting + formatting * - Compatible with most ESLint rules - * + * * Usage: node scripts/migrate-to-biome.js - * + * * What it does: * 1. Checks for existing ESLint/Prettier config * 2. Installs Biome as dev dependency @@ -23,9 +23,9 @@ * 6. Creates backup of removed configs */ -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); +const fs = require('node:fs') +const path = require('node:path') +const { execSync } = require('node:child_process') // ANSI color codes const colors = { @@ -36,31 +36,31 @@ const colors = { blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m', -}; +} function log(message, color = 'reset') { - const colorCode = colors[color] ?? colors.reset; - console.log(`${colorCode}${message}${colors.reset}`); + const colorCode = colors[color] ?? colors.reset + console.log(`${colorCode}${message}${colors.reset}`) } function step(message) { - log(`\n→ ${message}`, 'cyan'); + log(`\n→ ${message}`, 'cyan') } function success(message) { - log(` ✓ ${message}`, 'green'); + log(` ✓ ${message}`, 'green') } function warning(message) { - log(` ⚠ ${message}`, 'yellow'); + log(` ⚠ ${message}`, 'yellow') } function error(message) { - log(` ✗ ${message}`, 'red'); + log(` ✗ ${message}`, 'red') } function info(message) { - log(` ${message}`, 'gray'); + log(` ${message}`, 'gray') } // Files to check for existing linter/formatter config @@ -83,7 +83,7 @@ const CONFIG_FILES = { 'prettier.config.js', 'prettier.config.cjs', ], -}; +} // Packages to remove const PACKAGES_TO_REMOVE = [ @@ -93,231 +93,231 @@ const PACKAGES_TO_REMOVE = [ 'eslint-config-prettier', 'eslint-plugin-prettier', 'prettier', -]; +] function findRootDir() { - let dir = process.cwd(); - let iterations = 0; - const MAX_ITERATIONS = 100; + let dir = process.cwd() + let iterations = 0 + const MAX_ITERATIONS = 100 // Look for package.json with iteration limit to prevent infinite loops while (dir !== '/' && iterations < MAX_ITERATIONS) { if (fs.existsSync(path.join(dir, 'package.json'))) { - return dir; + return dir } - dir = path.dirname(dir); - iterations++; + dir = path.dirname(dir) + iterations++ } - return process.cwd(); + return process.cwd() } function findExistingConfigs(rootDir) { const found = { eslint: [], prettier: [], - }; - + } + // Check for ESLint configs - CONFIG_FILES.eslint.forEach(file => { - const filePath = path.join(rootDir, file); + CONFIG_FILES.eslint.forEach((file) => { + const filePath = path.join(rootDir, file) if (fs.existsSync(filePath)) { - found.eslint.push(file); + found.eslint.push(file) } - }); - + }) + // Check for Prettier configs - CONFIG_FILES.prettier.forEach(file => { - const filePath = path.join(rootDir, file); + CONFIG_FILES.prettier.forEach((file) => { + const filePath = path.join(rootDir, file) if (fs.existsSync(filePath)) { - found.prettier.push(file); + found.prettier.push(file) } - }); - + }) + // Check package.json for embedded configs - const pkgPath = path.join(rootDir, 'package.json'); + const pkgPath = path.join(rootDir, 'package.json') if (fs.existsSync(pkgPath)) { try { - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) if (pkg && typeof pkg === 'object' && !Array.isArray(pkg)) { if (pkg.eslintConfig) { - found.eslint.push('package.json (eslintConfig field)'); + found.eslint.push('package.json (eslintConfig field)') } if (pkg.prettier) { - found.prettier.push('package.json (prettier field)'); + found.prettier.push('package.json (prettier field)') } } } catch (e) { - warning(`Could not parse package.json: ${e.message}`); + warning(`Could not parse package.json: ${e.message}`) } } - return found; + return found } function backupConfigs(rootDir, configs) { - const backupDir = path.join(rootDir, '.biome-migration-backup'); - + const backupDir = path.join(rootDir, '.biome-migration-backup') + if (!fs.existsSync(backupDir)) { - fs.mkdirSync(backupDir, { recursive: true }); - } - - const backedUp = []; - - [...configs.eslint, ...configs.prettier].forEach(file => { - if (file.includes('package.json')) return; // Handle separately - - const srcPath = path.join(rootDir, file); - const destPath = path.join(backupDir, file); - + fs.mkdirSync(backupDir, { recursive: true }) + } + + const backedUp = [] + + ;[...configs.eslint, ...configs.prettier].forEach((file) => { + if (file.includes('package.json')) return // Handle separately + + const srcPath = path.join(rootDir, file) + const destPath = path.join(backupDir, file) + if (fs.existsSync(srcPath)) { - fs.copyFileSync(srcPath, destPath); - backedUp.push(file); + fs.copyFileSync(srcPath, destPath) + backedUp.push(file) } - }); - - return { backupDir, backedUp }; + }) + + return { backupDir, backedUp } } function generateBiomeConfig() { return { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true + $schema: 'https://biomejs.dev/schemas/1.9.4/schema.json', + vcs: { + enabled: true, + clientKind: 'git', + useIgnoreFile: true, }, - "files": { - "ignoreUnknown": false, - "ignore": [ - "node_modules", - "dist", - "build", - ".next", - ".nuxt", - "coverage", - "*.min.js" - ] + files: { + ignoreUnknown: false, + ignore: [ + 'node_modules', + 'dist', + 'build', + '.next', + '.nuxt', + 'coverage', + '*.min.js', + ], }, - "formatter": { - "enabled": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 100, - "attributePosition": "auto" + formatter: { + enabled: true, + formatWithErrors: false, + indentStyle: 'space', + indentWidth: 2, + lineEnding: 'lf', + lineWidth: 100, + attributePosition: 'auto', }, - "organizeImports": { - "enabled": true + organizeImports: { + enabled: true, }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "correctness": { - "noUnusedVariables": "error", - "noUnusedImports": "error" + linter: { + enabled: true, + rules: { + recommended: true, + correctness: { + noUnusedVariables: 'error', + noUnusedImports: 'error', }, - "style": { - "useConst": "error", - "useTemplate": "error" + style: { + useConst: 'error', + useTemplate: 'error', }, - "suspicious": { - "noExplicitAny": "warn", - "noConsoleLog": "warn" - } - } + suspicious: { + noExplicitAny: 'warn', + noConsoleLog: 'warn', + }, + }, }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "trailingCommas": "all", - "semicolons": "always", - "arrowParentheses": "always", - "bracketSpacing": true, - "bracketSameLine": false - } + javascript: { + formatter: { + quoteStyle: 'single', + jsxQuoteStyle: 'double', + quoteProperties: 'asNeeded', + trailingCommas: 'all', + semicolons: 'always', + arrowParentheses: 'always', + bracketSpacing: true, + bracketSameLine: false, + }, }, - "json": { - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 + json: { + formatter: { + enabled: true, + indentStyle: 'space', + indentWidth: 2, + lineWidth: 100, }, - "linter": { - "enabled": true - } - } - }; + linter: { + enabled: true, + }, + }, + } } function updatePackageJson(rootDir) { - const pkgPath = path.join(rootDir, 'package.json'); + const pkgPath = path.join(rootDir, 'package.json') - let pkg; + let pkg try { - pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) } catch (e) { - throw new Error(`Failed to parse package.json: ${e.message}`); + throw new Error(`Failed to parse package.json: ${e.message}`) } // Validate pkg structure if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) { - throw new Error('Invalid package.json structure'); + throw new Error('Invalid package.json structure') } // Remove old config fields - const removedFields = []; + const removedFields = [] if (pkg.eslintConfig) { - delete pkg.eslintConfig; - removedFields.push('eslintConfig'); + delete pkg.eslintConfig + removedFields.push('eslintConfig') } if (pkg.prettier) { - delete pkg.prettier; - removedFields.push('prettier'); + delete pkg.prettier + removedFields.push('prettier') } // Update scripts - const oldScripts = { ...(pkg.scripts || {}) }; - pkg.scripts = pkg.scripts || {}; + const oldScripts = { ...(pkg.scripts || {}) } + pkg.scripts = pkg.scripts || {} // Replace lint/format scripts const scriptUpdates = { - 'lint': 'biome lint .', - 'format': 'biome format --write .', + lint: 'biome lint .', + format: 'biome format --write .', 'format:check': 'biome format .', - 'check': 'biome check .', + check: 'biome check .', 'check:fix': 'biome check --write .', - }; + } Object.entries(scriptUpdates).forEach(([key, value]) => { - pkg.scripts[key] = value; - }); + pkg.scripts[key] = value + }) try { - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`) } catch (e) { - throw new Error(`Failed to write package.json: ${e.message}`); + throw new Error(`Failed to write package.json: ${e.message}`) } - return { removedFields, oldScripts }; + return { removedFields, oldScripts } } function detectPackageManager(rootDir) { if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'))) { - return 'pnpm'; + return 'pnpm' } if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) { - return 'yarn'; + return 'yarn' } if (fs.existsSync(path.join(rootDir, 'bun.lockb'))) { - return 'bun'; + return 'bun' } - return 'npm'; + return 'npm' } function installBiome(rootDir, packageManager) { @@ -326,42 +326,41 @@ function installBiome(rootDir, packageManager) { yarn: 'yarn add --dev --exact @biomejs/biome', pnpm: 'pnpm add --save-dev --save-exact @biomejs/biome', bun: 'bun add --dev --exact @biomejs/biome', - }; + } - const command = commands[packageManager]; + const command = commands[packageManager] if (!command) { - throw new Error(`Unknown package manager: ${packageManager}`); + throw new Error(`Unknown package manager: ${packageManager}`) } try { - execSync(command, { cwd: rootDir, stdio: 'inherit' }); + execSync(command, { cwd: rootDir, stdio: 'inherit' }) } catch (e) { - throw new Error(`Failed to install Biome: ${e.message}`); + throw new Error(`Failed to install Biome: ${e.message}`) } } function removeOldPackages(rootDir, packageManager) { - const pkgPath = path.join(rootDir, 'package.json'); + const pkgPath = path.join(rootDir, 'package.json') - let pkg; + let pkg try { - pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) } catch (e) { - throw new Error(`Failed to parse package.json: ${e.message}`); + throw new Error(`Failed to parse package.json: ${e.message}`) } // Validate pkg structure if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) { - throw new Error('Invalid package.json structure'); + throw new Error('Invalid package.json structure') } - const toRemove = PACKAGES_TO_REMOVE.filter(name => { - return (pkg.devDependencies && pkg.devDependencies[name]) || - (pkg.dependencies && pkg.dependencies[name]); - }); + const toRemove = PACKAGES_TO_REMOVE.filter((name) => { + return pkg.devDependencies?.[name] || pkg.dependencies?.[name] + }) if (toRemove.length === 0) { - return []; + return [] } const commands = { @@ -369,171 +368,179 @@ function removeOldPackages(rootDir, packageManager) { yarn: `yarn remove ${toRemove.join(' ')}`, pnpm: `pnpm remove ${toRemove.join(' ')}`, bun: `bun remove ${toRemove.join(' ')}`, - }; + } - const command = commands[packageManager]; + const command = commands[packageManager] if (!command) { - throw new Error(`Unknown package manager: ${packageManager}`); + throw new Error(`Unknown package manager: ${packageManager}`) } try { - execSync(command, { cwd: rootDir, stdio: 'inherit' }); + execSync(command, { cwd: rootDir, stdio: 'inherit' }) } catch (e) { - throw new Error(`Failed to remove packages: ${e.message}`); + throw new Error(`Failed to remove packages: ${e.message}`) } - return toRemove; + return toRemove } function removeConfigFiles(rootDir, configs) { - const removed = []; - - [...configs.eslint, ...configs.prettier].forEach(file => { - if (file.includes('package.json')) return; // Already handled - - const filePath = path.join(rootDir, file); + const removed = [] + + ;[...configs.eslint, ...configs.prettier].forEach((file) => { + if (file.includes('package.json')) return // Already handled + + const filePath = path.join(rootDir, file) if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - removed.push(file); + fs.unlinkSync(filePath) + removed.push(file) } - }); - - return removed; + }) + + return removed } async function main() { - log('\n╔═══════════════════════════════════════╗', 'cyan'); - log('║ Biome Migration Script ║', 'cyan'); - log('║ ESLint + Prettier → Biome ║', 'cyan'); - log('╚═══════════════════════════════════════╝\n', 'cyan'); - - const rootDir = findRootDir(); - log(`Working directory: ${rootDir}\n`, 'gray'); - + log('\n╔═══════════════════════════════════════╗', 'cyan') + log('║ Biome Migration Script ║', 'cyan') + log('║ ESLint + Prettier → Biome ║', 'cyan') + log('╚═══════════════════════════════════════╝\n', 'cyan') + + const rootDir = findRootDir() + log(`Working directory: ${rootDir}\n`, 'gray') + // Step 1: Check existing configs - step('Checking for existing ESLint/Prettier configuration...'); - const configs = findExistingConfigs(rootDir); - + step('Checking for existing ESLint/Prettier configuration...') + const configs = findExistingConfigs(rootDir) + if (configs.eslint.length === 0 && configs.prettier.length === 0) { - warning('No ESLint or Prettier configuration found.'); - warning('Biome will be installed with default configuration.'); + warning('No ESLint or Prettier configuration found.') + warning('Biome will be installed with default configuration.') } else { if (configs.eslint.length > 0) { - success(`Found ESLint config: ${configs.eslint.join(', ')}`); + success(`Found ESLint config: ${configs.eslint.join(', ')}`) } if (configs.prettier.length > 0) { - success(`Found Prettier config: ${configs.prettier.join(', ')}`); + success(`Found Prettier config: ${configs.prettier.join(', ')}`) } } - + // Step 2: Backup existing configs if (configs.eslint.length > 0 || configs.prettier.length > 0) { - step('Creating backup of existing configurations...'); - const { backupDir, backedUp } = backupConfigs(rootDir, configs); - success(`Backed up ${backedUp.length} file(s) to ${path.relative(rootDir, backupDir)}/`); - backedUp.forEach(file => info(` - ${file}`)); + step('Creating backup of existing configurations...') + const { backupDir, backedUp } = backupConfigs(rootDir, configs) + success( + `Backed up ${backedUp.length} file(s) to ${path.relative(rootDir, backupDir)}/`, + ) + backedUp.forEach((file) => info(` - ${file}`)) } - + // Step 3: Detect package manager - step('Detecting package manager...'); - const packageManager = detectPackageManager(rootDir); - success(`Using ${packageManager}`); - + step('Detecting package manager...') + const packageManager = detectPackageManager(rootDir) + success(`Using ${packageManager}`) + // Step 4: Install Biome - step('Installing @biomejs/biome...'); + step('Installing @biomejs/biome...') try { - installBiome(rootDir, packageManager); - success('Biome installed successfully'); + installBiome(rootDir, packageManager) + success('Biome installed successfully') } catch (err) { - error('Failed to install Biome'); - error(err.message); - process.exit(1); + error('Failed to install Biome') + error(err.message) + process.exit(1) } - + // Step 5: Generate biome.json - step('Generating biome.json configuration...'); - const biomeConfig = generateBiomeConfig(); - const biomeConfigPath = path.join(rootDir, 'biome.json'); + step('Generating biome.json configuration...') + const biomeConfig = generateBiomeConfig() + const biomeConfigPath = path.join(rootDir, 'biome.json') try { - fs.writeFileSync(biomeConfigPath, JSON.stringify(biomeConfig, null, 2) + '\n'); - success('Created biome.json'); + fs.writeFileSync( + biomeConfigPath, + `${JSON.stringify(biomeConfig, null, 2)}\n`, + ) + success('Created biome.json') } catch (e) { - error(`Failed to write biome.json: ${e.message}`); - process.exit(1); + error(`Failed to write biome.json: ${e.message}`) + process.exit(1) } - + // Step 6: Update package.json - step('Updating package.json scripts...'); - const { removedFields, oldScripts } = updatePackageJson(rootDir); - success('Updated package.json scripts'); + step('Updating package.json scripts...') + const { removedFields } = updatePackageJson(rootDir) + success('Updated package.json scripts') if (removedFields.length > 0) { - info(`Removed fields: ${removedFields.join(', ')}`); + info(`Removed fields: ${removedFields.join(', ')}`) } - + // Step 7: Remove old packages - step('Removing ESLint/Prettier packages...'); + step('Removing ESLint/Prettier packages...') try { - const removed = removeOldPackages(rootDir, packageManager); + const removed = removeOldPackages(rootDir, packageManager) if (removed.length > 0) { - success(`Removed ${removed.length} package(s):`); - removed.forEach(pkg => info(` - ${pkg}`)); + success(`Removed ${removed.length} package(s):`) + removed.forEach((pkg) => info(` - ${pkg}`)) } else { - info('No old packages to remove'); + info('No old packages to remove') } } catch (err) { - warning('Failed to remove old packages'); - warning('You can manually remove them later'); - info(err.message); + warning('Failed to remove old packages') + warning('You can manually remove them later') + info(err.message) } - + // Step 8: Remove old config files if (configs.eslint.length > 0 || configs.prettier.length > 0) { - step('Removing old configuration files...'); - const removed = removeConfigFiles(rootDir, configs); + step('Removing old configuration files...') + const removed = removeConfigFiles(rootDir, configs) if (removed.length > 0) { - success(`Removed ${removed.length} config file(s):`); - removed.forEach(file => info(` - ${file}`)); + success(`Removed ${removed.length} config file(s):`) + removed.forEach((file) => info(` - ${file}`)) } } - + // Summary - log('\n╔═══════════════════════════════════════╗', 'green'); - log('║ Migration Complete! 🎉 ║', 'green'); - log('╚═══════════════════════════════════════╝\n', 'green'); - - log('Next steps:', 'cyan'); - log(' 1. Review biome.json and adjust rules as needed', 'gray'); - log(' 2. Run: npm run check (or yarn/pnpm/bun check)', 'gray'); - log(' 3. Run: npm run format (or yarn/pnpm/bun format)', 'gray'); - log(' 4. Update your CI/CD scripts to use Biome', 'gray'); - log(' 5. Update editor integrations (see docs/BIOME_MIGRATION.md)', 'gray'); - - log('\nAvailable scripts:', 'cyan'); - log(' npm run lint - Run linter', 'gray'); - log(' npm run format - Format code', 'gray'); - log(' npm run check - Lint + format check', 'gray'); - log(' npm run check:fix - Lint + format with auto-fix', 'gray'); - + log('\n╔═══════════════════════════════════════╗', 'green') + log('║ Migration Complete! 🎉 ║', 'green') + log('╚═══════════════════════════════════════╝\n', 'green') + + log('Next steps:', 'cyan') + log(' 1. Review biome.json and adjust rules as needed', 'gray') + log(' 2. Run: npm run check (or yarn/pnpm/bun check)', 'gray') + log(' 3. Run: npm run format (or yarn/pnpm/bun format)', 'gray') + log(' 4. Update your CI/CD scripts to use Biome', 'gray') + log(' 5. Update editor integrations (see docs/BIOME_MIGRATION.md)', 'gray') + + log('\nAvailable scripts:', 'cyan') + log(' npm run lint - Run linter', 'gray') + log(' npm run format - Format code', 'gray') + log(' npm run check - Lint + format check', 'gray') + log(' npm run check:fix - Lint + format with auto-fix', 'gray') + if (configs.eslint.length > 0 || configs.prettier.length > 0) { - log('\nBackup location:', 'yellow'); - log(' .biome-migration-backup/ (safe to delete after verification)', 'gray'); - } - - log('\nDocumentation:', 'cyan'); - log(' • Migration guide: docs/BIOME_MIGRATION.md', 'gray'); - log(' • Biome docs: https://biomejs.dev', 'gray'); - log(''); + log('\nBackup location:', 'yellow') + log( + ' .biome-migration-backup/ (safe to delete after verification)', + 'gray', + ) + } + + log('\nDocumentation:', 'cyan') + log(' • Migration guide: docs/BIOME_MIGRATION.md', 'gray') + log(' • Biome docs: https://biomejs.dev', 'gray') + log('') } // Run migration -main().catch(err => { - error('\nMigration failed:'); - error(err.message); +main().catch((err) => { + error('\nMigration failed:') + error(err.message) // Only show full stack trace in debug mode to avoid overwhelming users if (process.env.DEBUG) { - console.error(err); + console.error(err) } else { - info('Set DEBUG=1 for full stack trace'); + info('Set DEBUG=1 for full stack trace') } - process.exit(1); -}); + process.exit(1) +}) diff --git a/skills/nomistakes/scripts/validate-skill.js b/skills/nomistakes/scripts/validate-skill.js index 0c3e8f7..d526a0d 100755 --- a/skills/nomistakes/scripts/validate-skill.js +++ b/skills/nomistakes/scripts/validate-skill.js @@ -2,9 +2,9 @@ /** * Validate SKILL.md against agentskills.io specification - * + * * Usage: node scripts/validate-skill.js [path-to-SKILL.md] - * + * * Validates: * - YAML frontmatter structure (starts/ends with ---) * - Required fields: name, description @@ -13,8 +13,8 @@ * - Optional metadata fields format */ -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs') +const path = require('node:path') // ANSI color codes for output const colors = { @@ -24,254 +24,275 @@ const colors = { yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', -}; +} function log(message, color = 'reset') { - const colorCode = colors[color] ?? colors.reset; - console.log(`${colorCode}${message}${colors.reset}`); + const colorCode = colors[color] ?? colors.reset + console.log(`${colorCode}${message}${colors.reset}`) } // Constants -const FRONTMATTER_DELIMITER = '---\n'; +const FRONTMATTER_DELIMITER = '---\n' function validateSkillMd(filePath) { - log('\n=== SKILL.md Validation ===\n', 'cyan'); + log('\n=== SKILL.md Validation ===\n', 'cyan') // Input validation if (!filePath || typeof filePath !== 'string') { - log('✗ Invalid file path provided', 'red'); - process.exit(1); + log('✗ Invalid file path provided', 'red') + process.exit(1) } - const errors = []; - const warnings = []; - const info = []; + const errors = [] + const warnings = [] + const info = [] // Read file with error handling if (!fs.existsSync(filePath)) { - log(`✗ File not found: ${filePath}`, 'red'); - process.exit(1); + log(`✗ File not found: ${filePath}`, 'red') + process.exit(1) } - let content; + let content try { - content = fs.readFileSync(filePath, 'utf8'); + content = fs.readFileSync(filePath, 'utf8') } catch (e) { - log(`✗ Failed to read file: ${e.message}`, 'red'); - process.exit(1); + log(`✗ Failed to read file: ${e.message}`, 'red') + process.exit(1) } - const lines = content.split('\n'); - + const lines = content.split('\n') + // Check line count - const lineCount = lines.length; - info.push(`Line count: ${lineCount}`); + const lineCount = lines.length + info.push(`Line count: ${lineCount}`) if (lineCount > 500) { - warnings.push(`Line count (${lineCount}) exceeds recommended maximum of 500 lines. Consider moving detailed content to references/ directory.`); + warnings.push( + `Line count (${lineCount}) exceeds recommended maximum of 500 lines. Consider moving detailed content to references/ directory.`, + ) } // Extract frontmatter if (!content.startsWith(FRONTMATTER_DELIMITER)) { - errors.push('SKILL.md must start with YAML frontmatter delimiter (---)'); - return printResults(errors, warnings, info); + errors.push('SKILL.md must start with YAML frontmatter delimiter (---)') + return printResults(errors, warnings, info) } - const frontmatterEnd = content.indexOf('\n' + FRONTMATTER_DELIMITER.slice(0, -1), 4); + const frontmatterEnd = content.indexOf( + `\n${FRONTMATTER_DELIMITER.slice(0, -1)}`, + 4, + ) if (frontmatterEnd === -1) { - errors.push('YAML frontmatter must end with --- delimiter'); - return printResults(errors, warnings, info); + errors.push('YAML frontmatter must end with --- delimiter') + return printResults(errors, warnings, info) } - const frontmatter = content.substring(4, frontmatterEnd); - const frontmatterLines = frontmatter.split('\n'); - + const frontmatter = content.substring(4, frontmatterEnd) + const frontmatterLines = frontmatter.split('\n') + // Parse frontmatter (simple YAML parser for our needs) - const metadata = {}; - let currentArray = null; + const metadata = {} + let currentArray = null for (const line of frontmatterLines) { - if (line.trim() === '') continue; + if (line.trim() === '') continue // Array item if (line.trim().startsWith('- ')) { if (currentArray) { - currentArray.push(line.trim().substring(2)); + currentArray.push(line.trim().substring(2)) } - continue; + continue } // Key-value pair - const colonIndex = line.indexOf(':'); + const colonIndex = line.indexOf(':') if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); + const key = line.substring(0, colonIndex).trim() + const value = line.substring(colonIndex + 1).trim() if (value === '') { // Array start - currentArray = []; - metadata[key] = currentArray; + currentArray = [] + metadata[key] = currentArray } else { // Simple value - metadata[key] = value; - currentArray = null; + metadata[key] = value + currentArray = null } } } // Validate required fields if (!metadata.name) { - errors.push('Required field missing: name'); + errors.push('Required field missing: name') } else { // Validate name format (lowercase-hyphenated) if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(metadata.name)) { - errors.push(`Invalid name format: "${metadata.name}". Must be lowercase-hyphenated (e.g., "my-skill-name")`); + errors.push( + `Invalid name format: "${metadata.name}". Must be lowercase-hyphenated (e.g., "my-skill-name")`, + ) } - + // Check name matches directory - const dirName = path.basename(path.dirname(filePath)); + const dirName = path.basename(path.dirname(filePath)) if (dirName !== '.' && dirName !== metadata.name) { - warnings.push(`Skill name "${metadata.name}" does not match directory name "${dirName}"`); + warnings.push( + `Skill name "${metadata.name}" does not match directory name "${dirName}"`, + ) } - - info.push(`Name: ${metadata.name}`); + + info.push(`Name: ${metadata.name}`) } if (!metadata.description) { - errors.push('Required field missing: description'); + errors.push('Required field missing: description') } else { - const descLength = metadata.description.length; + const descLength = metadata.description.length if (descLength < 1 || descLength > 1024) { - errors.push(`Description length (${descLength}) must be between 1 and 1024 characters`); + errors.push( + `Description length (${descLength}) must be between 1 and 1024 characters`, + ) } - info.push(`Description: ${descLength} characters`); - + info.push(`Description: ${descLength} characters`) + // Check for discovery keywords - const keywords = ['when', 'use', 'helps', 'for']; - const hasKeywords = keywords.some(kw => metadata.description.toLowerCase().includes(kw)); + const keywords = ['when', 'use', 'helps', 'for'] + const hasKeywords = keywords.some((kw) => + metadata.description.toLowerCase().includes(kw), + ) if (!hasKeywords) { - warnings.push('Description should include discovery keywords (when to use this skill) to help agents find it'); + warnings.push( + 'Description should include discovery keywords (when to use this skill) to help agents find it', + ) } } // Validate optional fields if (metadata.version) { if (!/^\d+\.\d+\.\d+/.test(metadata.version)) { - warnings.push(`Version "${metadata.version}" should follow semver format (e.g., 1.0.0)`); + warnings.push( + `Version "${metadata.version}" should follow semver format (e.g., 1.0.0)`, + ) } - info.push(`Version: ${metadata.version}`); + info.push(`Version: ${metadata.version}`) } if (metadata.license) { - info.push(`License: ${metadata.license}`); + info.push(`License: ${metadata.license}`) } if (metadata.author) { - info.push(`Author: ${metadata.author}`); + info.push(`Author: ${metadata.author}`) } if (metadata.tags) { if (Array.isArray(metadata.tags)) { - info.push(`Tags: ${metadata.tags.length} (${metadata.tags.join(', ')})`); + info.push(`Tags: ${metadata.tags.length} (${metadata.tags.join(', ')})`) } else { - warnings.push('Tags field should be an array'); + warnings.push('Tags field should be an array') } } if (metadata.compatibility) { if (Array.isArray(metadata.compatibility)) { - info.push(`Compatibility: ${metadata.compatibility.join(', ')}`); + info.push(`Compatibility: ${metadata.compatibility.join(', ')}`) } else { - warnings.push('Compatibility field should be an array'); + warnings.push('Compatibility field should be an array') } } // Check for recommended sections - const contentBody = content.substring(frontmatterEnd + 5); - + const contentBody = content.substring(frontmatterEnd + 5) + const recommendedSections = [ { name: 'When to Use This Skill', pattern: /##\s+When to Use/i }, { name: 'When NOT to Use This Skill', pattern: /##\s+When NOT to Use/i }, - ]; + ] for (const section of recommendedSections) { if (!section.pattern.test(contentBody)) { - warnings.push(`Recommended section missing: "${section.name}"`); + warnings.push(`Recommended section missing: "${section.name}"`) } } // Check for references directory - const referencesDir = path.join(path.dirname(filePath), 'references'); + const referencesDir = path.join(path.dirname(filePath), 'references') if (fs.existsSync(referencesDir)) { try { - const refFiles = fs.readdirSync(referencesDir); + const refFiles = fs.readdirSync(referencesDir) if (refFiles.length > 0) { - info.push(`References: ${refFiles.length} file(s) (${refFiles.join(', ')})`); + info.push( + `References: ${refFiles.length} file(s) (${refFiles.join(', ')})`, + ) } else { - warnings.push('References directory exists but is empty'); + warnings.push('References directory exists but is empty') } } catch (e) { // Log with context for debugging, but continue validation - warnings.push(`Could not read references directory: ${e.message}`); + warnings.push(`Could not read references directory: ${e.message}`) } } // Check for scripts directory - const scriptsDir = path.join(path.dirname(filePath), 'scripts'); + const scriptsDir = path.join(path.dirname(filePath), 'scripts') if (fs.existsSync(scriptsDir)) { try { - const scriptFiles = fs.readdirSync(scriptsDir); + const scriptFiles = fs.readdirSync(scriptsDir) if (scriptFiles.length > 0) { - info.push(`Scripts: ${scriptFiles.length} file(s) (${scriptFiles.join(', ')})`); + info.push( + `Scripts: ${scriptFiles.length} file(s) (${scriptFiles.join(', ')})`, + ) } else { - warnings.push('Scripts directory exists but is empty'); + warnings.push('Scripts directory exists but is empty') } } catch (e) { // Log with context for debugging, but continue validation - warnings.push(`Could not read scripts directory: ${e.message}`); + warnings.push(`Could not read scripts directory: ${e.message}`) } } - printResults(errors, warnings, info); + printResults(errors, warnings, info) } function printResults(errors, warnings, info) { // Print info if (info.length > 0) { - log('\nℹ Info:', 'blue'); - info.forEach(msg => log(` ${msg}`, 'blue')); + log('\nℹ Info:', 'blue') + info.forEach((msg) => log(` ${msg}`, 'blue')) } // Print warnings if (warnings.length > 0) { - log('\n⚠ Warnings:', 'yellow'); - warnings.forEach(msg => log(` ${msg}`, 'yellow')); + log('\n⚠ Warnings:', 'yellow') + warnings.forEach((msg) => log(` ${msg}`, 'yellow')) } // Print errors if (errors.length > 0) { - log('\n✗ Errors:', 'red'); - errors.forEach(msg => log(` ${msg}`, 'red')); - log(`\n❌ Validation FAILED (${errors.length} error(s))\n`, 'red'); - process.exit(1); + log('\n✗ Errors:', 'red') + errors.forEach((msg) => log(` ${msg}`, 'red')) + log(`\n❌ Validation FAILED (${errors.length} error(s))\n`, 'red') + process.exit(1) } if (warnings.length > 0) { - log(`\n⚠️ Validation passed with ${warnings.length} warning(s)\n`, 'yellow'); + log(`\n⚠️ Validation passed with ${warnings.length} warning(s)\n`, 'yellow') } else { - log('\n✓ Validation PASSED\n', 'green'); + log('\n✓ Validation PASSED\n', 'green') } } // Main -const args = process.argv.slice(2); -const skillPath = args[0] || 'SKILL.md'; +const args = process.argv.slice(2) +const skillPath = args[0] || 'SKILL.md' // Wrap main execution in try/catch to ensure graceful exit try { - validateSkillMd(skillPath); + validateSkillMd(skillPath) } catch (e) { - log(`\n✗ Unexpected error during validation: ${e.message}`, 'red'); + log(`\n✗ Unexpected error during validation: ${e.message}`, 'red') if (process.env.DEBUG) { - console.error(e); + console.error(e) } - process.exit(1); + process.exit(1) } From 28937ee169a7cefb3cc797d480f0fa042ae88674 Mon Sep 17 00:00:00 2001 From: "Ramin B." Date: Mon, 9 Feb 2026 14:36:10 -0500 Subject: [PATCH 3/3] fix: rename CommonJS scripts to .cjs extension package.json uses "type": "module" which requires CommonJS scripts to use .cjs extension. Updated all references. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/validate-skills.yml | 5 ++++- README.md | 2 +- skills/nomistakes/README.md | 4 ++-- skills/nomistakes/references/BIOME_MIGRATION.md | 4 ++-- .../scripts/{migrate-to-biome.js => migrate-to-biome.cjs} | 2 +- .../scripts/{validate-skill.js => validate-skill.cjs} | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) rename skills/nomistakes/scripts/{migrate-to-biome.js => migrate-to-biome.cjs} (99%) rename skills/nomistakes/scripts/{validate-skill.js => validate-skill.cjs} (99%) diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml index c75a668..758d436 100644 --- a/.github/workflows/validate-skills.yml +++ b/.github/workflows/validate-skills.yml @@ -63,7 +63,10 @@ jobs: fi # Run skill-specific validator if exists - if [ -f "${skill_dir}scripts/validate-skill.js" ]; then + if [ -f "${skill_dir}scripts/validate-skill.cjs" ]; then + echo " 🔧 Running custom validator..." + node "${skill_dir}scripts/validate-skill.cjs" "${skill_dir}SKILL.md" || exit 1 + elif [ -f "${skill_dir}scripts/validate-skill.js" ]; then echo " 🔧 Running custom validator..." node "${skill_dir}scripts/validate-skill.js" "${skill_dir}SKILL.md" || exit 1 fi diff --git a/README.md b/README.md index ba26b4d..90a2a1a 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ description: What it does and when to use it 1. **Keep SKILL.md under 500 lines** - Move detailed docs to `references/` 2. **Write for agents** - Clear, actionable instructions 3. **Include examples** - Show both ❌ BAD and ✅ GOOD patterns -4. **Test your skill** - Run `node scripts/validate-skill.js` if available +4. **Test your skill** - Run `node scripts/validate-skill.cjs` if available --- diff --git a/skills/nomistakes/README.md b/skills/nomistakes/README.md index 08ca10f..ab330ef 100644 --- a/skills/nomistakes/README.md +++ b/skills/nomistakes/README.md @@ -92,8 +92,8 @@ async function createUser( | Script | Description | |--------|-------------| -| `scripts/validate-skill.js` | Validate SKILL.md against agentskills.io spec | -| `scripts/migrate-to-biome.js` | Automated ESLint/Prettier → Biome migration | +| `scripts/validate-skill.cjs` | Validate SKILL.md against agentskills.io spec | +| `scripts/migrate-to-biome.cjs` | Automated ESLint/Prettier → Biome migration | ## License diff --git a/skills/nomistakes/references/BIOME_MIGRATION.md b/skills/nomistakes/references/BIOME_MIGRATION.md index 4da4a78..7a5947b 100644 --- a/skills/nomistakes/references/BIOME_MIGRATION.md +++ b/skills/nomistakes/references/BIOME_MIGRATION.md @@ -82,7 +82,7 @@ The easiest way to migrate is using the automated script: ```bash # Run migration script -node scripts/migrate-to-biome.js +node scripts/migrate-to-biome.cjs ``` **What the script does:** @@ -581,7 +581,7 @@ Use this checklist to ensure a smooth migration: - [ ] Inform team members about the migration - [ ] **Migration** - - [ ] Run `node scripts/migrate-to-biome.js` + - [ ] Run `node scripts/migrate-to-biome.cjs` - [ ] Review generated `biome.json` - [ ] Test: `npm run check` - [ ] Fix auto-fixable issues: `npm run check:fix` diff --git a/skills/nomistakes/scripts/migrate-to-biome.js b/skills/nomistakes/scripts/migrate-to-biome.cjs similarity index 99% rename from skills/nomistakes/scripts/migrate-to-biome.js rename to skills/nomistakes/scripts/migrate-to-biome.cjs index 94d1953..43156c5 100755 --- a/skills/nomistakes/scripts/migrate-to-biome.js +++ b/skills/nomistakes/scripts/migrate-to-biome.cjs @@ -12,7 +12,7 @@ * - Single tool for linting + formatting * - Compatible with most ESLint rules * - * Usage: node scripts/migrate-to-biome.js + * Usage: node scripts/migrate-to-biome.cjs * * What it does: * 1. Checks for existing ESLint/Prettier config diff --git a/skills/nomistakes/scripts/validate-skill.js b/skills/nomistakes/scripts/validate-skill.cjs similarity index 99% rename from skills/nomistakes/scripts/validate-skill.js rename to skills/nomistakes/scripts/validate-skill.cjs index d526a0d..5c7e270 100755 --- a/skills/nomistakes/scripts/validate-skill.js +++ b/skills/nomistakes/scripts/validate-skill.cjs @@ -3,7 +3,7 @@ /** * Validate SKILL.md against agentskills.io specification * - * Usage: node scripts/validate-skill.js [path-to-SKILL.md] + * Usage: node scripts/validate-skill.cjs [path-to-SKILL.md] * * Validates: * - YAML frontmatter structure (starts/ends with ---)