Monorepo for Aesthetic.Computer
aesthetic.computer
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});