Monorepo for Aesthetic.Computer aesthetic.computer
at main 1088 lines 42 kB view raw
1#!/usr/bin/env node 2// KidLisp Source Tree Analyzer 3// Usage: ./source-tree.mjs $cow 4// Output: A visual tree representation of KidLisp piece dependencies 5 6import { execSync } from 'child_process'; 7import chalk from 'chalk'; 8import terminalKit from 'terminal-kit'; 9import gradient from 'gradient-string'; 10 11const term = terminalKit.terminal; 12// Shows the embedd// Format piece name with bright gradient effects and $ prefix 13function formatPieceName(pieceName, showDollar = true) { 14 let result = ''; 15 16 // Always add bright gradient $ prefix for piece names 17 if (showDollar) { 18 result += gradient('lime', 'cyan')('$'); 19 } 20 21 // Create brighter gradient colors for better visibility 22 const brightGradientColors = ['#ff1493', '#00ffff', '#ffff00', '#ff4500', '#adff2f', '#ff69b4']; 23 const startColor = brightGradientColors[pieceName.length % brightGradientColors.length]; 24 const endColor = brightGradientColors[(pieceName.length + 2) % brightGradientColors.length]; 25 26 // Apply bright gradient to the piece name 27 const gradientText = gradient(startColor, endColor)(pieceName); 28 return result + gradientText; 29} 30 31import https from 'https'; 32 33// Import KidLisp modules for native syntax highlighting 34let kidlisp, graph, num; 35try { 36 kidlisp = await import('../../system/public/aesthetic.computer/lib/kidlisp.mjs'); 37 graph = await import('../../system/public/aesthetic.computer/lib/graph.mjs'); 38 num = await import('../../system/public/aesthetic.computer/lib/num.mjs'); 39} catch (error) { 40 console.warn("⚠️ Warning: Could not load KidLisp modules, using fallback highlighting"); 41 console.warn("Error:", error.message); 42} 43 44// ANSI terminal colors - using custom RGB instead of default terminal colors 45// Color utilities using chalk for simpler API 46const colors = { 47 // Basic colors using chalk for text output 48 red: chalk.rgb(220, 50, 47), 49 green: chalk.rgb(50, 200, 50), 50 yellow: chalk.rgb(255, 193, 7), 51 blue: chalk.rgb(38, 139, 210), 52 magenta: chalk.rgb(211, 54, 130), 53 cyan: chalk.rgb(42, 161, 152), 54 white: chalk.rgb(253, 246, 227), 55 gray: chalk.rgb(147, 161, 161), 56 dim: chalk.dim, 57 58 // ANSI codes for complex formatting (still needed for syntax highlighting) 59 redCode: '\x1b[38;2;220;50;47m', 60 greenCode: '\x1b[38;2;50;200;50m', 61 yellowCode: '\x1b[38;2;255;193;7m', 62 blueCode: '\x1b[38;2;38;139;210m', 63 magentaCode: '\x1b[38;2;211;54;130m', 64 cyanCode: '\x1b[38;2;42;161;152m', 65 whiteCode: '\x1b[38;2;253;246;227m', 66 grayCode: '\x1b[38;2;147;161;161m', 67 reset: '\x1b[0m', 68}; 69 70// Convert KidLisp's \color\text format to ANSI colored text 71// Convert KidLisp colors to ANSI escape codes with piece recognition 72function convertKidlispColorsToAnsi(coloredString) { 73 if (!coloredString) return ''; 74 75 return coloredString 76 .replace(/\$(\w+)/g, (match, pieceName) => { 77 // Apply character-by-character formatting to piece references 78 return formatPieceName(pieceName, true); 79 }) 80 // Handle KidLisp backslash RGB format: \r,g,b\ 81 .replace(/\\(\d+),(\d+),(\d+)\\/g, (match, r, g, b) => { 82 return `\x1b[38;2;${r};${g};${b}m`; 83 }) 84 // Handle KidLisp backslash RGBA format: \r,g,b,a\ (ignore alpha for terminal) 85 .replace(/\\(\d+),(\d+),(\d+),(\d+)\\/g, (match, r, g, b, a) => { 86 return `\x1b[38;2;${r};${g};${b}m`; 87 }) 88 // Handle KidLisp named colors with backslashes: \colorname\ 89 .replace(/\\([a-zA-Z]+)\\/g, (match, colorName) => { 90 return simpleColorToAnsi(colorName) || ''; 91 }) 92 // Handle curly brace RGB format: {r:123,g:456,b:789} 93 .replace(/\{r:(\d+),g:(\d+),b:(\d+)\}/g, (match, r, g, b) => { 94 return `\x1b[38;2;${r};${g};${b}m`; 95 }) 96 .replace(/\{\/\}/g, '\x1b[0m') 97 .replace(/\{\}/g, '\x1b[0m'); 98} 99 100// Convert RGB values to ANSI color (24-bit true color if supported, fallback to 256-color) 101function rgbToAnsi(r, g, b) { 102 // Calculate brightness to determine if we need special handling 103 const brightness = (r * 0.299 + g * 0.587 + b * 0.114); 104 105 // Use 24-bit true color if supported (most modern terminals) 106 if (process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit') { 107 // For very light colors (white, very light gray), use background color for visibility 108 if (brightness > 240) { 109 return `\x1b[48;2;${r};${g};${b}m\x1b[38;2;0;0;0m`; // light background, dark text 110 } 111 // For very dark colors (black, very dark colors), use brighter version or background 112 else if (brightness < 30) { 113 return `\x1b[48;2;${r};${g};${b}m\x1b[38;2;255;255;255m`; // dark background, light text 114 } 115 // Normal foreground color for everything else 116 else { 117 return `\x1b[38;2;${r};${g};${b}m`; 118 } 119 } 120 121 // Fallback to 256-color approximation 122 if (r === g && g === b) { 123 // Grayscale 124 if (r === 0) return '\x1b[38;5;16m'; // Black 125 if (r === 255) return '\x1b[38;5;231m'; // White 126 const gray = Math.round(r / 255 * 23) + 232; 127 return '\x1b[38;5;' + gray + 'm'; 128 } 129 130 // Color cube (216 colors) 131 const rIndex = Math.round(r / 255 * 5); 132 const gIndex = Math.round(g / 255 * 5); 133 const bIndex = Math.round(b / 255 * 5); 134 const colorIndex = 16 + (36 * rIndex) + (6 * gIndex) + bIndex; 135 return '\x1b[38;5;' + colorIndex + 'm'; 136} 137 138// Simple color name to ANSI fallback 139function simpleColorToAnsi(colorName) { 140 switch (colorName.toLowerCase()) { 141 case "red": return '\x1b[38;2;220;50;47m'; 142 case "green": return '\x1b[38;2;50;200;50m'; 143 case "yellow": return '\x1b[38;2;255;193;7m'; 144 case "blue": return '\x1b[38;2;38;139;210m'; 145 case "magenta": return '\x1b[38;2;211;54;130m'; 146 case "cyan": return '\x1b[38;2;42;161;152m'; 147 case "white": return '\x1b[38;2;253;246;227m'; 148 case "gray": case "grey": return '\x1b[38;2;147;161;161m'; 149 case "orange": return '\x1b[38;2;255;165;0m'; // Orange 150 case "purple": return '\x1b[38;2;211;54;130m'; 151 case "lime": return '\x1b[38;2;50;205;50m'; // Lime green 152 case "limegreen": return '\x1b[38;2;50;205;50m'; 153 case "pink": return '\x1b[38;2;255;192;203m'; // Pink 154 case "salmon": return '\x1b[38;2;250;128;114m'; // Salmon 155 case "mediumseagreen": return '\x1b[38;2;60;179;113m'; 156 case "palegreen": return '\x1b[38;2;152;251;152m'; 157 default: return ''; 158 } 159} 160 161// Track which server is working to use it for all subsequent calls 162let workingServer = null; 163let workingUrl = null; 164 165function printUsage() { 166 console.log("Usage: source-tree.mjs <piece-name>"); 167 console.log(" source-tree.mjs @<handle> [limit]"); 168 console.log(" source-tree.mjs --test-colors"); 169 console.log(" source-tree.mjs --debug-colors"); 170 console.log(" source-tree.mjs --test-css-colors"); 171 console.log(""); 172 console.log("Examples:"); 173 console.log(" source-tree.mjs $cow # Show source tree for $cow piece"); 174 console.log(" source-tree.mjs cow # Show source tree ($ prefix optional)"); 175 console.log(" source-tree.mjs @fifi # List all codes by user @fifi"); 176 console.log(" source-tree.mjs @fifi 200 # List up to 200 codes by @fifi"); 177 console.log(""); 178 console.log("Options:"); 179 console.log(" --test-colors Test terminal color capabilities"); 180 console.log(" --debug-colors Debug color name conversions"); 181 console.log(" --test-css-colors Test CSS color name mappings"); 182 console.log(""); 183 console.log("Shows complete source code tree for each piece by default."); 184 console.log("For user handles, displays all codes with syntax highlighting and timestamps."); 185} 186 187// Function to syntax highlight KidLisp code using native KidLisp highlighting 188function syntaxHighlight(code) { 189 if (!code) return ''; 190 191 try { 192 // If we have KidLisp and graph modules loaded, use native syntax highlighting 193 if (kidlisp && graph && kidlisp.KidLisp) { 194 // Create a KidLisp instance and use buildColoredKidlispString 195 const kidlispInstance = new kidlisp.KidLisp(); 196 kidlispInstance.initializeSyntaxHighlighting(code); 197 const coloredText = kidlispInstance.buildColoredKidlispString(); 198 199 // Convert KidLisp's \color\text format to ANSI codes 200 return convertKidlispColorsToAnsi(coloredText); 201 } else { 202 // Use simple fallback highlighting 203 return simpleHighlight(code); 204 } 205 } catch (error) { 206 console.warn('⚠️ KidLisp native highlighting failed:', error.message); 207 return simpleHighlight(code); 208 } 209} 210 211// Simple fallback highlighting function using chalk for clean color API 212function simpleHighlight(code) { 213 return code 214 // Piece name references (like $39i, $cow, etc.) 215 .replace(/\$([a-zA-Z0-9_-]+)/g, (match, pieceName) => { 216 return formatPieceName(pieceName, true); 217 }) 218 // Comments 219 .replace(/;[^\n]*/g, match => colors.gray(match)) 220 // String literals 221 .replace(/"[^"]*"/g, match => colors.green(match)) 222 // Numbers 223 .replace(/\b\d+(\.\d+)?\b/g, match => colors.cyan(match)) 224 // Keywords and special forms 225 .replace(/\b(define|if|cond|let|lambda|quote|quasiquote|unquote|and|or|not|car|cdr|cons|list|map|filter|fold|apply)\b/g, 226 match => colors.magenta(match)) 227 // Function calls (first item in parentheses) 228 .replace(/\(([a-zA-Z][a-zA-Z0-9-]*)/g, match => { 229 const parts = match.split('('); 230 const funcName = parts[1]; 231 return `(${colors.yellow(funcName)}`; 232 }) 233 // Embed calls 234 .replace(/#embed\s+([a-zA-Z0-9-]+)/g, match => { 235 const parts = match.split(' '); 236 return `${colors.red('#embed')} ${colors.blue(parts[1])}`; 237 }); 238} 239 240async function fetchSourceFromEndpoint(url, cleanName, isProduction = false) { 241 return new Promise((resolve, reject) => { 242 const timeout = setTimeout(() => { 243 reject(new Error(`Request timeout for ${cleanName}`)); 244 }, 3000); 245 246 const options = isProduction ? {} : { rejectUnauthorized: false }; 247 248 https.get(url, options, (res) => { 249 clearTimeout(timeout); 250 let data = ''; 251 252 res.on('data', (chunk) => { 253 data += chunk; 254 }); 255 256 res.on('end', () => { 257 try { 258 const response = JSON.parse(data); 259 260 if (response.error) { 261 reject(new Error(`Piece '$${cleanName}' not found`)); 262 return; 263 } 264 265 if (!response.source) { 266 reject(new Error("Could not parse source code from response")); 267 return; 268 } 269 270 // Return the full response object including metadata 271 resolve(response); 272 } catch (error) { 273 reject(new Error("Could not parse JSON response")); 274 } 275 }); 276 }).on('error', (error) => { 277 clearTimeout(timeout); 278 reject(new Error(`Connection failed: ${error.message}`)); 279 }); 280 }); 281} 282 283// Fetch all codes by user handle 284async function fetchCodesByHandle(handle, limit = 1000) { 285 const cleanHandle = handle.startsWith('@') ? handle.substring(1) : handle; 286 287 // Try localhost first (no SSL issues in dev) 288 const localUrl = `https://localhost:8888/.netlify/functions/store-kidlisp?recent=true&limit=${limit}`; 289 290 try { 291 return await fetchCodesFromEndpoint(localUrl, cleanHandle, limit, false); 292 } catch (localError) { 293 console.log(colors.dim(` Note: localhost unavailable, trying production...`)); 294 // Fallback to production 295 const prodUrl = `https://aesthetic.computer/api/store-kidlisp?recent=true&limit=${limit}`; 296 try { 297 return await fetchCodesFromEndpoint(prodUrl, cleanHandle, limit, true); 298 } catch (prodError) { 299 throw new Error(`Both servers failed: Local: ${localError.message}, Production: ${prodError.message}`); 300 } 301 } 302} 303 304// Helper to fetch codes from an endpoint 305async function fetchCodesFromEndpoint(url, cleanHandle, limit, isProduction) { 306 return new Promise((resolve, reject) => { 307 const timeout = setTimeout(() => { 308 reject(new Error('Request timeout')); 309 }, 10000); 310 311 const options = isProduction ? {} : { rejectUnauthorized: false }; 312 313 https.get(url, options, (res) => { 314 let data = ''; 315 316 res.on('data', (chunk) => { 317 data += chunk; 318 }); 319 320 res.on('end', () => { 321 clearTimeout(timeout); 322 try { 323 const response = JSON.parse(data); 324 325 if (response.error) { 326 reject(new Error(`Failed to fetch codes: ${response.error}`)); 327 return; 328 } 329 330 if (!response.recent) { 331 reject(new Error("Invalid response format")); 332 return; 333 } 334 335 // Filter by handle 336 const userCodes = response.recent.filter(item => 337 item.handle === `@${cleanHandle}` 338 ); 339 340 resolve(userCodes); 341 } catch (error) { 342 reject(new Error("Could not parse JSON response")); 343 } 344 }); 345 }).on('error', (error) => { 346 clearTimeout(timeout); 347 reject(new Error(`Connection failed: ${error.message}`)); 348 }); 349 }); 350} 351 352// Fetch handle from user ID 353async function fetchHandleFromUserId(userId) { 354 if (!userId) return null; 355 356 // URL encode the userId properly - the pipe | in auth0|xxx needs to be encoded 357 const encodedUserId = encodeURIComponent(userId); 358 const url = `https://aesthetic.computer/.netlify/functions/handle?for=${encodedUserId}`; 359 360 return new Promise((resolve, reject) => { 361 const timeout = setTimeout(() => { 362 resolve(null); // Don't fail, just return null 363 }, 3000); 364 365 https.get(url, (res) => { 366 let data = ''; 367 368 res.on('data', (chunk) => { 369 data += chunk; 370 }); 371 372 res.on('end', () => { 373 clearTimeout(timeout); 374 try { 375 const response = JSON.parse(data); 376 if (response.handle) { 377 resolve(`@${response.handle}`); 378 } else { 379 resolve(null); 380 } 381 } catch (error) { 382 console.warn(`Failed to fetch handle for ${userId}:`, error.message); 383 resolve(null); 384 } 385 }); 386 }).on('error', (error) => { 387 clearTimeout(timeout); 388 resolve(null); 389 }); 390 }); 391} 392 393async function fetchSource(pieceName) { 394 const cleanName = pieceName.replace(/^\$/, ''); 395 396 // If we already know which server works, use it directly 397 if (workingServer && workingUrl) { 398 const url = workingUrl.replace(/code=[^&]*/, `code=${cleanName}`); 399 try { 400 const response = await fetchSourceFromEndpoint(url, cleanName, workingServer); 401 // Fetch handle if user ID is present 402 if (response.user) { 403 response.handle = await fetchHandleFromUserId(response.user); 404 } 405 return response; 406 } catch (error) { 407 // Reset and try both servers again 408 workingServer = null; 409 workingUrl = null; 410 } 411 } 412 413 // Try local development server first 414 const localUrl = `https://localhost:8888/.netlify/functions/store-kidlisp?code=${cleanName}`; 415 416 try { 417 const response = await fetchSourceFromEndpoint(localUrl, cleanName, 'local'); 418 workingServer = 'local'; 419 workingUrl = localUrl; 420 // Fetch handle if user ID is present 421 if (response.user) { 422 response.handle = await fetchHandleFromUserId(response.user); 423 } 424 return response; 425 } catch (localError) { 426 // Fallback to production 427 const productionUrl = `https://aesthetic.computer/api/store-kidlisp?code=${cleanName}`; 428 429 try { 430 const response = await fetchSourceFromEndpoint(productionUrl, cleanName, 'production'); 431 workingServer = 'production'; 432 workingUrl = productionUrl; 433 // Fetch handle if user ID is present 434 if (response.user) { 435 response.handle = await fetchHandleFromUserId(response.user); 436 } 437 return response; 438 } catch (prodError) { 439 throw new Error(`Both servers failed: Local: ${localError.message}, Production: ${prodError.message}`); 440 } 441 } 442} 443 444function extractEmbeddedPieces(source) { 445 // Find all $piece references like ($39i ...) or ($r2f ...) 446 const regex = /\(\$[a-zA-Z0-9_-]+/g; 447 const matches = source.match(regex) || []; 448 449 // Extract piece names and remove duplicates 450 const pieces = [...new Set(matches.map(match => match.replace('($', '')))]; 451 return pieces; 452} 453 454// ASCII art generation using toilet command (matching project aesthetic) 455 456// Generate ASCII art for piece names using toilet with beautiful gradients 457function generateAsciiArt(text, isSubtree = false) { 458 try { 459 // Escape the text properly for shell 460 const safeText = text.replace(/\$/g, '\\$'); 461 462 // Use different fonts for main vs subtree items 463 const font = isSubtree ? 'smbraille' : 'smblock'; 464 465 // Generate ASCII art using toilet with appropriate font 466 const result = execSync(`echo "${safeText}" | toilet -f ${font}`, { encoding: 'utf8' }); 467 468 const lines = result.split('\n').filter(line => line.trim() !== ''); 469 470 // Apply gradient effects to the ASCII art 471 if (lines.length > 0) { 472 return lines.map(line => { 473 if (isSubtree) { 474 // Green coloring for subtree items (piece references) 475 return gradient(['lime', 'green'])(line); 476 } else { 477 // Dynamic gradient based on the text content for main titles 478 const gradients = [ 479 gradient(['cyan', 'magenta']), 480 gradient(['#ff6b6b', '#4ecdc4']), 481 gradient(['gold', 'orange']), 482 gradient(['lime', 'cyan']), 483 gradient(['purple', 'pink']) 484 ]; 485 486 const gradientIndex = text.length % gradients.length; 487 return gradients[gradientIndex](line); 488 } 489 }); 490 } 491 492 // Fallback 493 if (isSubtree) { 494 return [gradient('lime', 'green')(text.toUpperCase())]; 495 } else { 496 return [gradient('cyan', 'magenta')(text.toUpperCase())]; 497 } 498 } catch (error) { 499 // Fallback to gradient colored text if toilet fails 500 if (isSubtree) { 501 return [gradient('lime', 'green')(text.toUpperCase())]; 502 } else { 503 return [gradient('cyan', 'magenta')(text.toUpperCase())]; 504 } 505 } 506} 507 508// Add a beautiful border around the entire output 509function addBorder() { 510 const terminalWidth = process.stdout.columns || 80; 511 const borderChar = '█'; 512 const border = borderChar.repeat(terminalWidth); 513 console.log(addFullWidthBackground(border, '40;40;40')); 514} 515 516// Add full-width background to any line with optional side borders 517function addFullWidthBackground(content, bgColor = '20;20;20', withBorders = false) { 518 const terminalWidth = process.stdout.columns || 80; 519 // More comprehensive ANSI code removal (including 24-bit RGB codes) 520 const cleanContent = content.replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\[48;2;[0-9;]*m/g, '').replace(/\x1b\[38;2;[0-9;]*m/g, ''); 521 522 if (withBorders) { 523 // Calculate available content width (minus 4 for double borders on each side) 524 const availableWidth = terminalWidth - 4; 525 const padding = Math.max(0, availableWidth - cleanContent.length); 526 const spaces = ' '.repeat(padding); 527 528 const borderChar = '██'; // Double block for thicker border 529 return `\x1b[48;2;40;40;40m${borderChar}\x1b[0m\x1b[48;2;${bgColor}m${content}${spaces}\x1b[0m\x1b[48;2;40;40;40m${borderChar}\x1b[0m`; 530 } else { 531 // Original behavior for content lines 532 const padding = Math.max(0, terminalWidth - cleanContent.length); 533 const spaces = ' '.repeat(padding); 534 return `\x1b[48;2;${bgColor}m${content}${spaces}\x1b[0m`; 535 } 536} 537 538// Wrap long lines to fit within the bordered display 539function wrapLine(text, maxWidth) { 540 const terminalWidth = process.stdout.columns || 80; 541 const availableWidth = maxWidth || (terminalWidth - 8); // Account for borders and spacing 542 543 // Remove ANSI codes to measure actual text length 544 const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\[48;2;[0-9;]*m/g, '').replace(/\x1b\[38;2;[0-9;]*m/g, ''); 545 546 if (stripAnsi(text).length <= availableWidth) { 547 return [text]; 548 } 549 550 const words = text.split(' '); 551 const lines = []; 552 let currentLine = ''; 553 554 for (const word of words) { 555 const testLine = currentLine ? `${currentLine} ${word}` : word; 556 if (stripAnsi(testLine).length <= availableWidth) { 557 currentLine = testLine; 558 } else { 559 if (currentLine) { 560 lines.push(currentLine); 561 currentLine = word; 562 } else { 563 // Word is too long, force break 564 lines.push(word); 565 } 566 } 567 } 568 569 if (currentLine) { 570 lines.push(currentLine); 571 } 572 573 return lines; 574} 575 576// Generate unique color for each character based on its value 577function getCharacterColor(char, index, pieceName) { 578 // Create hash from character and its position 579 const charCode = char.charCodeAt(0); 580 const positionHash = (index + 1) * 37; // Prime number for better distribution 581 const nameHash = pieceName.length * 23; 582 const combinedHash = (charCode + positionHash + nameHash) * 17; 583 584 // Generate RGB values 585 const r = 120 + (combinedHash * 7) % 135; // 120-255 range for good visibility 586 const g = 120 + (combinedHash * 11) % 135; 587 const b = 120 + (combinedHash * 13) % 135; 588 589 // Generate background RGB (darker) 590 const bgR = 20 + (combinedHash * 3) % 40; // 20-60 range for dark background 591 const bgG = 20 + (combinedHash * 5) % 40; 592 const bgB = 20 + (combinedHash * 7) % 40; 593 594 return { 595 fg: `\x1b[38;2;${r};${g};${b}m`, 596 bg: `\x1b[48;2;${bgR};${bgG};${bgB}m`, 597 reset: '\x1b[0m' 598 }; 599} 600 601// Get color for tree depth (creates a nice gradient effect) 602function getDepthColor(depth) { 603 const depthColors = [ 604 colors.cyanCode, // Level 0: cyan 605 colors.blueCode, // Level 1: blue 606 colors.magentaCode, // Level 2: magenta 607 colors.yellowCode, // Level 3: yellow 608 colors.greenCode, // Level 4: green 609 colors.redCode // Level 5+: red 610 ]; 611 return depthColors[Math.min(depth, depthColors.length - 1)]; 612} 613 614async function printTreeNode(pieceName, depth = 0, prefix = "", showSource = false) { 615 const indent = " ".repeat(depth); // 2-character indentation 616 617 try { 618 const response = await fetchSource(pieceName); 619 const source = response.source; 620 const cleanSource = source.replace(/\\n/g, '\n').replace(/\\"/g, '"'); 621 const embeddedPieces = extractEmbeddedPieces(source); 622 623 // Display metadata (handle, timestamp, hits) at the top for root level pieces 624 if (depth === 0 && (response.handle || response.when || response.hits)) { 625 const metadataLine = []; 626 if (response.handle) { 627 metadataLine.push(colors.cyan(`Author: ${response.handle}`)); 628 } 629 if (response.when) { 630 const timestamp = new Date(response.when); 631 const localTime = timestamp.toLocaleString('en-US', { 632 year: 'numeric', 633 month: '2-digit', 634 day: '2-digit', 635 hour: '2-digit', 636 minute: '2-digit', 637 second: '2-digit', 638 hour12: false 639 }); 640 metadataLine.push(colors.dim(`Created: ${localTime}`)); 641 } 642 if (response.hits) { 643 metadataLine.push(colors.gray(`Hits: ${response.hits}`)); 644 } 645 646 if (metadataLine.length > 0) { 647 console.log(addFullWidthBackground(` ${metadataLine.join(' • ')}`, '20;20;20', true)); 648 console.log(addFullWidthBackground('', '20;20;20', true)); 649 } 650 } 651 652 // Print current piece with ASCII art for subtree $codes (depth > 0) 653 if (depth > 0) { 654 // Add extra spacing before subtree items 655 console.log(addFullWidthBackground('', '20;20;20', true)); 656 657 // Generate ASCII art for subtree piece names using smbraille font with green coloring 658 const asciiLines = generateAsciiArt('$' + pieceName, true); 659 asciiLines.forEach(line => { 660 console.log(addFullWidthBackground(` ${line}`, '20;20;20', true)); // 4-space indent with borders 661 }); 662 } 663 664 if (showSource) { 665 // Print full source code with dark backdrop and barbershop-style striped line numbers 666 const sourceLines = cleanSource.split('\n'); 667 sourceLines.forEach((line, index) => { 668 if (line.trim()) { 669 const lineNumber = (index + 1).toString().padStart(3, ' '); // 3-digit line numbers for consistent alignment 670 const highlightedLine = syntaxHighlight(line); 671 672 // Wrap long lines to fit within borders 673 const wrappedLines = wrapLine(highlightedLine); 674 675 wrappedLines.forEach((wrappedLine, wrapIndex) => { 676 // Barbershop alternating backgrounds only on line numbers 677 const lineNumBgColor = (index + 1) % 2 === 0 ? '35;35;35' : '10;10;10'; // Striped line numbers 678 679 if (wrapIndex === 0) { 680 // First line gets the line number 681 const lineNumFormatted = `\x1b[48;2;${lineNumBgColor}m${lineNumber}\x1b[0m\x1b[48;2;20;20;20m`; // Line number with stripe, then immediately switch to dark backdrop 682 console.log(addFullWidthBackground(`${lineNumFormatted} ${wrappedLine}`, '20;20;20', true)); 683 } else { 684 // Continuation lines get indented without line numbers 685 const continuationSpaces = `\x1b[48;2;20;20;20m `; // 3 spaces to align with line numbers 686 console.log(addFullWidthBackground(`${continuationSpaces} ${wrappedLine}`, '20;20;20', true)); 687 } 688 }); 689 } 690 }); 691 // Remove extra spacing before children - only add if we're at root level AND have embedded pieces 692 if (embeddedPieces.length > 0 && depth === 0) { 693 console.log(addFullWidthBackground('', '20;20;20', true)); // Space only after root level 694 } 695 } else { 696 // Show just first line as preview with striped line number and dark backdrop 697 const firstLine = cleanSource.split('\n')[0]; 698 if (firstLine && firstLine.trim()) { 699 const highlightedLine = syntaxHighlight(firstLine); 700 701 // Wrap the preview line too 702 const wrappedLines = wrapLine(highlightedLine); 703 704 wrappedLines.forEach((wrappedLine, wrapIndex) => { 705 if (wrapIndex === 0) { 706 // First line gets line number 707 const lineNumFormatted = `\x1b[48;2;10;10;10m 1\x1b[0m\x1b[48;2;20;20;20m`; // First line gets dark stripe, then dark backdrop 708 console.log(addFullWidthBackground(`${lineNumFormatted} ${wrappedLine}`, '20;20;20', true)); 709 } else { 710 // Continuation lines 711 const continuationSpaces = `\x1b[48;2;20;20;20m `; 712 console.log(addFullWidthBackground(`${continuationSpaces} ${wrappedLine}`, '20;20;20', true)); 713 } 714 }); 715 } 716 } 717 718 // Process embedded pieces recursively (no pipe graphics, just indentation) 719 if (depth < 5) { 720 for (let i = 0; i < embeddedPieces.length; i++) { 721 const embeddedPiece = embeddedPieces[i]; 722 await printTreeNode(embeddedPiece, depth + 1, "", showSource); 723 } 724 } else if (embeddedPieces.length > 0) { 725 console.log(addFullWidthBackground(`${indent} ... (max depth reached)`, '20;20;20', true)); 726 } 727 } catch (error) { 728 const formattedName = formatPieceName(pieceName); 729 console.log(`${indent}${prefix}${formattedName} ${colors.gray(`(${error.message})`)}`); 730 } 731} 732 733function analyzePerformanceFeatures(source) { 734 process.stdout.write(`\n${colors.gray('Performance: ')}`); 735 736 // Check for expensive operations 737 const expensiveOps = []; 738 if (source.includes('blur')) expensiveOps.push('blur'); 739 if (source.includes('zoom')) expensiveOps.push('zoom'); 740 if (source.includes('contrast')) expensiveOps.push('contrast'); 741 if (source.includes('spin')) expensiveOps.push('spin'); 742 if (source.includes('flood')) expensiveOps.push('flood'); 743 744 if (expensiveOps.length > 0) { 745 console.log(`${expensiveOps.join(', ')}`); 746 } else { 747 console.log('optimized'); 748 } 749 750 // Check for timing expressions and randomness on same line 751 const features = []; 752 if (source.includes('s(')) features.push('animated'); 753 if (source.includes('?')) features.push('random'); 754 755 // Count embedded layers 756 const embeddedCount = extractEmbeddedPieces(source).length; 757 if (embeddedCount > 0) features.push(`${embeddedCount} layers`); 758 759 if (features.length > 0) { 760 console.log(`${colors.gray('Features: ')}${features.join(', ')}`); 761 } 762} 763 764function testCSSColorsMapping() { 765 console.log('\n🎨 CSS Colors → Terminal Mapping Test:'); 766 767 // Test common CSS color names that should be recognizable 768 const cssColors = [ 769 'white', 'black', 'gray', 'grey', 770 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 771 'orange', 'purple', 'pink', 'brown', 772 'lime', 'navy', 'teal', 'violet', 'indigo', 773 'salmon', 'tan', 'gold', 'silver', 774 'palegreen', 'lightblue', 'darkred', 'lightgray' 775 ]; 776 777 console.log('\nColor Name → RGB → Terminal Result:'); 778 console.log('═══════════════════════════════════════'); 779 780 for (const colorName of cssColors) { 781 // Get RGB from graph.findColor 782 let rgba = null; 783 if (graph && graph.findColor) { 784 try { 785 rgba = graph.findColor(colorName); 786 } catch (e) { 787 rgba = null; 788 } 789 } 790 791 if (rgba && rgba.length >= 3) { 792 const [r, g, b] = rgba; 793 const ansiColor = rgbToAnsi(r, g, b); 794 795 // Create test text with both foreground and background 796 const fgText = `${ansiColor}${colorName}${COLORS.reset}`; 797 const bgText = `\x1b[48;2;${r};${g};${b}m ${colorName} \x1b[0m`; 798 799 console.log(`${colorName.padEnd(12)} → RGB(${r.toString().padStart(3)},${g.toString().padStart(3)},${b.toString().padStart(3)}) → FG: ${fgText} BG: ${bgText}`); 800 } else { 801 console.log(`${colorName.padEnd(12)} → NOT FOUND`); 802 } 803 } 804 805 console.log('\n💡 Color visibility recommendations:'); 806 console.log('• Light colors (white, yellow, cyan) work better with dark backgrounds'); 807 console.log('• Dark colors (black, navy, darkred) work better with light backgrounds'); 808 console.log('• Consider using background colors for very light/dark text'); 809} 810 811// Display all codes by a user handle with syntax highlighting 812async function displayUserCodes(handle, limit = 1000) { 813 const cleanHandle = handle.startsWith('@') ? handle.substring(1) : handle; 814 815 // Create beautiful ASCII art title 816 const asciiLines = generateAsciiArt(`@${cleanHandle}`); 817 818 // Add top border 819 addBorder(); 820 console.log(addFullWidthBackground('', '20;20;20', true)); 821 822 asciiLines.forEach(line => { 823 console.log(addFullWidthBackground(` ${line}`, '20;20;20', true)); 824 }); 825 console.log(addFullWidthBackground('', '20;20;20', true)); 826 827 try { 828 console.log(addFullWidthBackground(colors.cyan(` Fetching codes by @${cleanHandle}...`), '20;20;20', true)); 829 console.log(addFullWidthBackground('', '20;20;20', true)); 830 831 const userCodes = await fetchCodesByHandle(cleanHandle, limit); 832 833 if (userCodes.length === 0) { 834 console.log(addFullWidthBackground(colors.yellow(` No codes found for @${cleanHandle} in recent ${limit} codes`), '20;20;20', true)); 835 console.log(addFullWidthBackground(colors.dim(` Note: Only recent codes are searchable (max 100)`), '20;20;20', true)); 836 console.log(addFullWidthBackground(colors.dim(` Try a user with recent activity like @fifi`), '20;20;20', true)); 837 console.log(addFullWidthBackground('', '20;20;20', true)); 838 addBorder(); 839 return; 840 } 841 842 const header = ` Found ${userCodes.length} code${userCodes.length === 1 ? '' : 's'} by @${cleanHandle}`; 843 console.log(addFullWidthBackground(colors.green(header), '20;20;20', true)); 844 console.log(addFullWidthBackground('', '20;20;20', true)); 845 846 // Display each code with syntax highlighting 847 for (let i = 0; i < userCodes.length; i++) { 848 const codeData = userCodes[i]; 849 const codeNum = `${i + 1}`.padStart(3, ' '); 850 851 // Format timestamp to local time 852 const timestamp = new Date(codeData.when); 853 const localTime = timestamp.toLocaleString('en-US', { 854 year: 'numeric', 855 month: '2-digit', 856 day: '2-digit', 857 hour: '2-digit', 858 minute: '2-digit', 859 second: '2-digit', 860 hour12: false 861 }); 862 863 // Code header with piece name 864 const codeName = formatPieceName(codeData.code, true); 865 const codeHeader = ` ${colors.gray(`${codeNum}.`)} ${codeName} ${colors.dim(`${localTime}`)} ${colors.gray(`${codeData.hits || 0} hits`)}`; 866 console.log(addFullWidthBackground(codeHeader, '20;20;20', true)); 867 868 // URL 869 const url = ` ${colors.dim(`https://aesthetic.computer/$${codeData.code}`)}`; 870 console.log(addFullWidthBackground(url, '20;20;20', true)); 871 872 // Syntax highlighted source 873 const sourceLines = codeData.source.split('\n'); 874 875 for (const line of sourceLines) { 876 if (line.trim() === '') { 877 console.log(addFullWidthBackground(' ', '20;20;20', true)); 878 continue; 879 } 880 881 // Apply syntax highlighting 882 const highlighted = syntaxHighlight(line); 883 const indentedLine = ` ${highlighted}`; 884 console.log(addFullWidthBackground(indentedLine, '20;20;20', true)); 885 } 886 887 // Separator between codes 888 if (i < userCodes.length - 1) { 889 console.log(addFullWidthBackground('', '20;20;20', true)); 890 const separator = ` ${colors.gray('─'.repeat(60))}`; 891 console.log(addFullWidthBackground(separator, '20;20;20', true)); 892 console.log(addFullWidthBackground('', '20;20;20', true)); 893 } 894 } 895 896 console.log(addFullWidthBackground('', '20;20;20', true)); 897 898 } catch (error) { 899 console.log(addFullWidthBackground(colors.red(` Error: ${error.message}`), '20;20;20', true)); 900 console.log(addFullWidthBackground('', '20;20;20', true)); 901 } finally { 902 addBorder(); 903 } 904} 905 906function testColorConversions() { 907 console.log('\n🔍 Color Conversion Debug:'); 908 909 const testColors = ['white', 'black', 'gray', 'salmon', 'brown', 'palegreen', 'rainbow', 'red', 'orange']; 910 911 for (const colorName of testColors) { 912 // Test graph.findColor 913 let rgba = null; 914 if (graph && graph.findColor) { 915 try { 916 rgba = graph.findColor(colorName); 917 } catch (e) { 918 rgba = null; 919 } 920 } 921 922 console.log(`\n${colorName}:`); 923 console.log(` graph.findColor: ${rgba ? `RGB(${rgba.slice(0,3).join(', ')})` : 'not found'}`); 924 925 // Test rgbToAnsi directly 926 if (rgba && rgba.length >= 3) { 927 const [r, g, b] = rgba; 928 const ansiColor = rgbToAnsi(r, g, b); 929 console.log(` terminal result: ${ansiColor}${colorName}${COLORS.reset}`); 930 } 931 932 // Test full KidLisp format conversion 933 const testText = `\\${colorName}\\${colorName}`; 934 const converted = convertKidlispColorsToAnsi(testText); 935 console.log(` kidlisp format result: ${converted}`); 936 } 937} 938 939// Color test function 940function testTerminalColors() { 941 console.log('\n🎨 Terminal Color Support Test\n'); 942 943 // Test 16 basic colors 944 console.log('Basic 16 Colors:'); 945 for (let i = 0; i < 16; i++) { 946 process.stdout.write(`\x1b[48;5;${i}m ${i.toString().padStart(2)} \x1b[0m`); 947 if ((i + 1) % 8 === 0) console.log(''); 948 } 949 950 // Test 216 color cube (6x6x6) 951 console.log('\n216 Color Cube (6x6x6):'); 952 for (let r = 0; r < 6; r++) { 953 for (let g = 0; g < 6; g++) { 954 for (let b = 0; b < 6; b++) { 955 const color = 16 + (r * 36) + (g * 6) + b; 956 process.stdout.write(`\x1b[48;5;${color}m \x1b[0m`); 957 } 958 process.stdout.write(' '); 959 } 960 console.log(''); 961 } 962 963 // Test 24 grayscale colors 964 console.log('\nGrayscale (24 colors):'); 965 for (let i = 232; i < 256; i++) { 966 process.stdout.write(`\x1b[48;5;${i}m \x1b[0m`); 967 } 968 console.log(''); 969 970 // Test RGB colors (if supported) 971 console.log('\nTrue Color (24-bit RGB) Test:'); 972 const rgbColors = [ 973 [255, 0, 0], // Red 974 [255, 165, 0], // Orange 975 [255, 255, 0], // Yellow 976 [0, 255, 0], // Green 977 [0, 255, 255], // Cyan 978 [0, 0, 255], // Blue 979 [128, 0, 128], // Purple 980 [255, 192, 203] // Pink 981 ]; 982 983 rgbColors.forEach(([r, g, b], i) => { 984 process.stdout.write(`\x1b[48;2;${r};${g};${b}m RGB(${r},${g},${b}) \x1b[0m `); 985 if ((i + 1) % 4 === 0) console.log(''); 986 }); 987 988 // Test KidLisp named colors 989 console.log('\n\nKidLisp Named Colors Test:'); 990 const kidlispColors = [ 991 'red', 'orange', 'yellow', 'lime', 'green', 'cyan', 'blue', 'purple', 'magenta', 'pink', 992 'white', 'gray', 'black', 'salmon', 'brown', 'tan', 'violet', 'indigo', 'navy', 'teal' 993 ]; 994 995 kidlispColors.forEach((colorName, i) => { 996 const ansiCode = simpleColorToAnsi(colorName); 997 process.stdout.write(`${ansiCode}${colorName}\x1b[0m `); 998 if ((i + 1) % 5 === 0) console.log(''); 999 }); 1000 1001 console.log('\n\n💡 Tips:'); 1002 console.log('- If you see solid color blocks, your terminal supports 256 colors'); 1003 console.log('- If RGB colors look smooth, your terminal supports 24-bit true color'); 1004 console.log('- Modern terminals usually support 24-bit color (16.7M colors)'); 1005 console.log('- Check $COLORTERM environment variable for truecolor support'); 1006 console.log(`- Your COLORTERM: ${process.env.COLORTERM || 'not set'}`); 1007 console.log(`- Your TERM: ${process.env.TERM || 'not set'}`); 1008} 1009 1010// Main script 1011async function main() { 1012 const args = process.argv.slice(2); 1013 1014 if (args.length === 0) { 1015 printUsage(); 1016 process.exit(1); 1017 } 1018 1019 // Check for special commands 1020 if (args[0] === '--test-colors' || args[0] === 'test-colors') { 1021 testTerminalColors(); 1022 process.exit(0); 1023 } 1024 1025 if (args[0] === '--debug-colors' || args[0] === 'debug-colors') { 1026 testColorConversions(); 1027 process.exit(0); 1028 } 1029 1030 if (args[0] === '--test-css-colors' || args[0] === 'test-css-colors') { 1031 testCSSColorsMapping(); 1032 process.exit(0); 1033 } 1034 1035 const pieceName = args[0]; 1036 1037 // Check if it's a user handle request (starts with @) 1038 if (pieceName.startsWith('@')) { 1039 const handle = pieceName; 1040 const limit = args[1] ? parseInt(args[1]) : 1000; // Default to 1000 for handle searches 1041 1042 try { 1043 await displayUserCodes(handle, limit); 1044 } catch (error) { 1045 console.error(colors.red(`Error: ${error.message}`)); 1046 } finally { 1047 process.exit(0); 1048 } 1049 return; 1050 } 1051 1052 const showSource = true; // Always show source by default 1053 1054 // Remove $ prefix if present for display 1055 const cleanName = pieceName.startsWith('$') ? pieceName.substring(1) : pieceName; 1056 1057 // Create beautiful ASCII art title with gradients 1058 const asciiLines = generateAsciiArt('$' + cleanName); 1059 1060 // Add top border 1061 addBorder(); 1062 console.log(addFullWidthBackground('', '20;20;20', true)); // Spacing with borders 1063 1064 asciiLines.forEach(line => { 1065 console.log(addFullWidthBackground(` ${line}`, '20;20;20', true)); // 2-space indent with borders 1066 }); 1067 console.log(addFullWidthBackground('', '20;20;20', true)); // Single spacing line with borders 1068 1069 try { 1070 // Build the tree 1071 await printTreeNode(pieceName, 0, "", showSource); 1072 1073 } catch (error) { 1074 console.error(colors.red(`Error: ${error.message}`)); 1075 } finally { 1076 // Add bottom border 1077 console.log(addFullWidthBackground('', '20;20;20', true)); // Spacing with borders 1078 addBorder(); 1079 1080 // Explicitly exit to prevent hanging 1081 process.exit(0); 1082 } 1083} 1084 1085main().catch((error) => { 1086 console.error(colors.red(`❌ Unhandled error: ${error.message}`)); 1087 process.exit(1); 1088});