UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

146 lines 5.07 kB
/** * Animated welcome screen for the experimental artifact workflow setup. * Shows side-by-side layout with animated ASCII art on left and welcome text on right. */ import chalk from 'chalk'; import { WELCOME_ANIMATION } from './ascii-patterns.js'; // Minimum terminal width for side-by-side layout const MIN_WIDTH = 60; // Width of the ASCII art column (with padding) const ART_COLUMN_WIDTH = 24; /** * Welcome text content (right column) */ function getWelcomeText() { return [ chalk.white.bold('Welcome to OpenSpec'), chalk.dim('A lightweight spec-driven framework'), '', chalk.white('This setup will configure:'), chalk.dim(' • Agent Skills for AI tools'), chalk.dim(' • /opsx:* slash commands'), '', chalk.white('Quick start after setup:'), ` ${chalk.yellow('/opsx:new')} ${chalk.dim('Create a change')}`, ` ${chalk.yellow('/opsx:continue')} ${chalk.dim('Next artifact')}`, ` ${chalk.yellow('/opsx:apply')} ${chalk.dim('Implement tasks')}`, '', chalk.cyan('Press Enter to select tools...'), ]; } /** * Renders a single frame with side-by-side layout */ function renderFrame(artLines, textLines) { const maxLines = Math.max(artLines.length, textLines.length); const lines = []; for (let i = 0; i < maxLines; i++) { const artLine = artLines[i] || ''; const textLine = textLines[i] || ''; // Pad the art column to fixed width const paddedArt = artLine.padEnd(ART_COLUMN_WIDTH); // Color the ASCII art with cyan for visual appeal const coloredArt = chalk.cyan(paddedArt); // Clear line before writing to prevent residual characters lines.push(`\x1b[2K${coloredArt}${textLine}`); } return lines.join('\n'); } /** * Checks if the terminal supports animation */ function canAnimate() { // Must be TTY if (!process.stdout.isTTY) return false; // Respect NO_COLOR if (process.env.NO_COLOR) return false; // Check terminal width const columns = process.stdout.columns || 80; if (columns < MIN_WIDTH) return false; return true; } /** * Wait for Enter key press */ function waitForEnter() { return new Promise((resolve) => { const { stdin } = process; // Handle non-TTY gracefully if (!stdin.isTTY) { resolve(); return; } const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); const onData = (data) => { const char = data.toString(); // Enter key or Ctrl+C if (char === '\r' || char === '\n' || char === '\u0003') { stdin.removeListener('data', onData); stdin.setRawMode(wasRaw); stdin.pause(); // Handle Ctrl+C if (char === '\u0003') { process.stdout.write('\n'); process.exit(0); } resolve(); } }; stdin.on('data', onData); }); } /** * Shows the animated welcome screen. * Returns when user presses Enter. */ export async function showWelcomeScreen() { const textLines = getWelcomeText(); if (!canAnimate()) { // Fallback: show static welcome const frame = WELCOME_ANIMATION.frames[3]; // Peak frame process.stdout.write('\n' + renderFrame(frame, textLines) + '\n\n'); return; } let frameIndex = 0; let running = true; let isFirstRender = true; // Content height for cursor movement between frames const numContentLines = Math.max(WELCOME_ANIMATION.frames[0].length, textLines.length); const frameHeight = numContentLines + 1; // internal newlines (11) + trailing newlines (2) = 13 // Total height including initial newline (for cleanup) const totalHeight = frameHeight + 1; // 14 // Initial render process.stdout.write('\n'); // Animation loop const interval = setInterval(() => { if (!running) return; const frame = WELCOME_ANIMATION.frames[frameIndex]; // Move cursor up to overwrite previous frame (always after first render) if (!isFirstRender) { process.stdout.write(`\x1b[${frameHeight}A`); } isFirstRender = false; // Render current frame process.stdout.write(renderFrame(frame, textLines) + '\n\n'); // Advance to next frame frameIndex = (frameIndex + 1) % WELCOME_ANIMATION.frames.length; }, WELCOME_ANIMATION.interval); // Wait for Enter await waitForEnter(); // Stop animation running = false; clearInterval(interval); // Clear the welcome screen and move on process.stdout.write(`\x1b[${totalHeight}A`); for (let i = 0; i < totalHeight; i++) { process.stdout.write('\x1b[2K\n'); // Clear line } process.stdout.write(`\x1b[${totalHeight}A`); // Move back up } //# sourceMappingURL=welcome-screen.js.map