#!/usr/bin/env node /** * Baba Yaga REPL - Interactive Language Playground & Harness Integration Demo * * This REPL serves two primary purposes: * 1. **Language Playground**: Interactive exploration of the Baba Yaga functional language * 2. **Harness Demo**: Demonstration of scripting harness integration patterns * * ## Architecture Overview * * The REPL integrates with a TEA-inspired functional harness: * * ```javascript * // Model: Current state (currentState) * // Update: Pure function (harness.update) → { model, commands, version } * // Commands: Side effects processed by adapters * * // Example flow: * const result = await harness.update(currentState); * // result = { model: newState, commands: [...], version: 1 } * * for (const command of result.commands) { * await adapter.process(command); * } * ``` * * ## Key Integration Patterns * * ### 1. Harness Integration * The FunctionalHarness manages script execution and state versioning: * - Scripts are executed in a controlled environment * - State changes are tracked with version history * - Commands are extracted for adapter processing * * ### 2. Adapter Pattern * Adapters handle side effects (I/O, network, etc.): * - Console Adapter: Output and logging * - File Adapter: File read/write operations * - Network Adapter: HTTP requests * * ### 3. State Management * - Automatic version tracking * - History with rollback capabilities * - Basic branching support * * ## Usage Examples * * ### Basic Script Execution * ```javascript * // User types: "result : add 5 3;" * // REPL executes: harness.update(currentState) with script content * // Result: { model: { result: 8 }, commands: [], version: 1 } * ``` * * ### Adapter Command Processing * ```javascript * // User types: "..emit { action: 'http_request', url: 'https://api.example.com' };" * // REPL extracts command and routes to Network Adapter * // Network Adapter makes actual HTTP request * ``` * * ### State Versioning * ```javascript * // Each script execution creates a new version * // Users can rollback: ":rollback 2" * // Users can create branches: ":branch 3 experimental" * ``` * * ## Integration Guide for Developers * * To integrate Baba Yaga and the harness into your own application: * * 1. **Import the harness**: `import { FunctionalHarness } from './scripting-harness/core/harness.js'` * 2. **Create adapters**: Define your own adapter objects with `process()` methods * 3. **Initialize harness**: `await harness.initialize()` * 4. **Execute scripts**: `const result = await harness.update(currentState)` * 5. **Process commands**: Route `result.commands` to appropriate adapters * * See the constructor and adapter definitions below for working examples. */ import { FunctionalHarness } from '../scripting-harness/core/harness.js'; import { createInterface } from 'readline'; import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Baba Yaga REPL Class * * This class demonstrates integration of the Baba Yaga language * with the functional harness architecture. It serves as both a language * playground and a reference for harness integration patterns. * * ## Architecture Principles Demonstrated * * 1. **Separation of Concerns**: Script execution vs. side effects * 2. **Adapter Pattern**: Pluggable side-effect handlers * 3. **State Management**: Versioned state with history and rollback * 4. **Command Processing**: Structured communication between pure and impure code * * ## Key Methods for Integration Reference * * - `init()`: Harness initialization and setup * - `executeScript()`: Core script execution with harness integration * - `processAdapterCommand()`: Adapter routing and command processing * - `handleInput()`: Input parsing and command routing * * ## State Flow * * ``` * User Input → handleInput() → executeScript() → harness.update() * ↓ * { model, commands, version } * ↓ * processAdapterCommand() * ↓ * adapter.process(command) * ``` */ class REPL { /** * Initialize the REPL with harness integration * * This constructor sets up the core components needed for both * language playground functionality and harness integration demonstration. * * ## Key Components * * ### 1. Readline Interface * Handles user input with multi-line support and history management. * * ### 2. Harness Instance * The FunctionalHarness that manages script execution and state. * * ### 3. Adapter Registry * Side-effect handlers that demonstrate the adapter pattern. * * ## Integration Pattern * * This constructor demonstrates how to set up harness integration: * * ```javascript * // 1. Create harness instance * this.harness = new FunctionalHarness(scriptPath, config); * * // 2. Define adapters for side effects * this.adapters = { * console: { process: async (command) => { // handle console output } }, * file: { process: async (command) => { // handle file operations } }, * network: { process: async (command) => { // handle HTTP requests } } * }; * * // 3. Initialize state tracking * this.currentState = {}; * this.currentVersion = 0; * ``` * * ## Adapter Pattern Explanation * * Adapters are the bridge between script execution and side effects. * Each adapter handles a specific type of side effect: * * - **Console Adapter**: Handles output and logging * - **File Adapter**: Handles file system operations * - **Network Adapter**: Handles HTTP requests * * This pattern allows the harness to focus on script execution while * enabling real-world functionality through structured command processing. */ constructor() { // Readline interface for user interaction this.rl = null; // Command history management this.history = []; this.historyFile = join(__dirname, '.repl_history'); // Multi-line input support this.isMultiLine = false; this.multiLineBuffer = ''; // Harness integration - Core of the architecture this.harness = null; this.currentState = {}; this.currentVersion = 0; /** * Adapter Registry - Side Effect Handlers * * This registry demonstrates the adapter pattern, where each adapter * handles a specific type of side effect. This allows the harness * to remain pure while enabling real-world functionality. * * ## Adapter Structure * * Each adapter has: * - `name`: Human-readable identifier * - `description`: Purpose and capabilities * - `process(command)`: Async function that handles commands * * ## Command Format * * Commands are structured objects with: * - `type`: Usually 'emit' for side effects * - `value`: Action-specific data (e.g., { action: 'http_request', url: '...' }) * * ## Integration Example * * ```javascript * // Script generates command * ..emit { action: 'save_file', filename: 'data.json', data: { x: 1 } }; * * // Harness extracts command * const result = await harness.update({ script: userCode }); * // result.commands = [{ type: 'emit', value: { action: 'save_file', ... } }] * * // REPL routes to appropriate adapter * await this.processAdapterCommand(result.commands[0]); * // Routes to file adapter's process() method * ``` */ this.adapters = { // Console Adapter - Output and Logging // Handles console output commands from scripts. This adapter // demonstrates how to process simple output commands. // // Usage in Scripts: // ..emit "Hello, World!"; // ..emit { message: "Debug info", level: "info" }; console: { name: 'Console Adapter', description: 'Handles console output and logging', process: async (command) => { if (command.type === 'emit') { console.log('\x1b[36m[Console Adapter]\x1b[0m', command.value); } } }, // File Adapter - File System Operations // Handles file read and write operations. This adapter demonstrates // how to process structured file commands with error handling. // // Supported Actions: // - save_file: ..emit { action: 'save_file', filename: 'data.json', data: {...} }; // - read_file: ..emit { action: 'read_file', filename: 'config.json' }; file: { name: 'File Adapter', description: 'Handles file operations (read and write)', process: async (command) => { if (command.type === 'emit' && command.value.action === 'save_file') { try { await fs.writeFile(command.value.filename, JSON.stringify(command.value.data, null, 2)); console.log(`\x1b[32m[File Adapter]\x1b[0m ✅ Saved to ${command.value.filename}`); } catch (error) { console.log(`\x1b[31m[File Adapter]\x1b[0m ❌ Error: ${error.message}`); } } else if (command.type === 'emit' && command.value.action === 'read_file') { try { const content = await fs.readFile(command.value.filename, 'utf8'); console.log(`\x1b[32m[File Adapter]\x1b[0m ✅ Read from ${command.value.filename}`); console.log(`\x1b[36m[File Adapter]\x1b[0m Content length: ${content.length} characters`); // Store the content for script processing command.value.content = content; console.log(`\x1b[33m[File Adapter]\x1b[0m 💡 File content available for script processing`); } catch (error) { console.log(`\x1b[31m[File Adapter]\x1b[0m ❌ Error reading ${command.value.filename}: ${error.message}`); } } } }, // Network Adapter - HTTP Requests // Handles HTTP requests with real network calls. This adapter // demonstrates how to process network commands with proper // request configuration and error handling. // // Supported Actions: // - http_request: ..emit { action: 'http_request', method: 'GET', url: 'https://api.example.com' }; network: { name: 'Network Adapter', description: 'Handles HTTP requests with real network calls', process: async (command) => { if (command.type === 'emit' && command.value.action === 'http_request') { const { method = 'GET', url, headers = {}, body, timeout = 5000 } = command.value; console.log(`\x1b[33m[Network Adapter]\x1b[0m Making ${method} request to ${url}`); try { // Prepare request options const options = { method: method.toUpperCase(), headers: { 'User-Agent': 'Baba-Yaga-REPL/1.0', 'Accept': 'application/json', ...headers }, timeout: timeout }; // Add body for POST/PUT requests if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { options.body = typeof body === 'string' ? body : JSON.stringify(body); if (typeof body === 'object') { options.headers['Content-Type'] = 'application/json'; } } // Make the actual HTTP request const response = await fetch(url, options); // Process response const responseText = await response.text(); let responseData; try { responseData = JSON.parse(responseText); } catch { responseData = responseText; } // Display results console.log(`\x1b[32m[Network Adapter]\x1b[0m ✅ ${method} ${url} - Status: ${response.status}`); console.log(`\x1b[36m[Network Adapter]\x1b[0m Response Headers:`, Object.fromEntries(response.headers.entries())); if (typeof responseData === 'object') { console.log(`\x1b[36m[Network Adapter]\x1b[0m Response Data:`, JSON.stringify(responseData, null, 2)); } else { console.log(`\x1b[36m[Network Adapter]\x1b[0m Response Data:`, responseData); } // Emit response data for further processing console.log(`\x1b[33m[Network Adapter]\x1b[0m 💡 Response data available for script processing`); } catch (error) { console.log(`\x1b[31m[Network Adapter]\x1b[0m ❌ Error making ${method} request to ${url}:`); console.log(`\x1b[31m[Network Adapter]\x1b[0m ${error.message}`); if (error.name === 'TypeError' && error.message.includes('fetch')) { console.log(`\x1b[33m[Network Adapter]\x1b[0m 💡 Note: Node.js fetch requires Node 18+ or a polyfill`); } } } } } }; // Built-in harness examples this.examples = { 'basic': { title: 'Basic State Management', description: 'Simple state processing with basic operations', code: `/* Basic state management example */ /* Create and process state */ x : 5; y : 10; sum : x + y; result : { x, y, sum }; /* Return processed state */ result` }, 'counter': { title: 'Counter with State', description: 'Counter that maintains state across executions', code: `/* Counter example with state persistence */ /* Simple counter logic */ count : 0; new_count : count + 1; result : { count: new_count, name: "Counter" }; /* Return updated state */ result` }, 'data-pipeline': { title: 'Data Processing Pipeline', description: 'Simple data transformation', code: `/* Data processing pipeline */ /* Process simple data */ numbers : {1: 10, 2: 3, 3: 8}; doubled : map @(multiply 2) numbers; result : { original: numbers, processed: doubled }; /* Return processed result */ result` }, 'user-management': { title: 'User Management System', description: 'User state management with validation', code: `/* User management system */ /* Simple user validation */ name : "Alice"; age : 25; status : when age >= 18 then "valid" _ then "underage"; user : { name, age, status }; /* Return validated state */ user` }, 'error-handling': { title: 'Error Handling', description: 'Demonstrates error handling in harness', code: `/* Error handling example */ /* Safe operations through harness */ data : 10; safe_operation : when data > 0 then data / 2 _ then 0; result : { operation: "safe_division", result: safe_operation }; /* Return safe result */ result` }, 'recursive': { title: 'Recursive Functions', description: 'Factorial and Fibonacci using recursion', code: `/* Recursive functions example */ /* Factorial function */ factorial : n -> when n is 0 then 1 _ then n * (factorial (n - 1)); /* Fibonacci function */ fibonacci : n -> when n is 0 then 0 1 then 1 _ then (fibonacci (n - 1)) + (fibonacci (n - 2)); /* Test factorial */ fact_5 : factorial 5; fact_0 : factorial 0; /* Test fibonacci */ fib_6 : fibonacci 6; fib_10 : fibonacci 10; /* Return results */ { factorial, fibonacci, fact_5, fact_0, fib_6, fib_10 }` }, 'network': { title: 'Network API Integration', description: 'Fetch Pokémon data using PokéAPI', code: `/* Network API integration example */ /* Using PokéAPI to fetch Pokémon data */ /* Get current state to see if we have a Pokémon name */ state : ..listen; /* Determine which Pokémon to fetch */ pokemon_name : when state is { pokemon: name } then name _ then "ditto"; /* Default to ditto */ /* Emit network request to PokéAPI */ ..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon/" + pokemon_name }; /* Also fetch a list of Pokémon */ ..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon?limit=5" }; /* Return the request configuration */ { pokemon_name, requests: [ { method: "GET", url: "https://pokeapi.co/api/v2/pokemon/" + pokemon_name }, { method: "GET", url: "https://pokeapi.co/api/v2/pokemon?limit=5" } ] }` }, 'http-get': { title: 'HTTP GET Request', description: 'Simple GET request to a public API', code: `/* HTTP GET request example */ /* Simple GET request to JSONPlaceholder API */ /* Make a GET request to fetch a post */ ..emit { action: "http_request", method: "GET", url: "https://jsonplaceholder.typicode.com/posts/1", headers: { "Accept": "application/json" } }; /* Return request info */ { request_type: "GET", url: "https://jsonplaceholder.typicode.com/posts/1", description: "Fetching a sample post from JSONPlaceholder" }` }, 'http-post': { title: 'HTTP POST Request', description: 'POST request with JSON body', code: `/* HTTP POST request example */ /* Creating a new post via JSONPlaceholder API */ /* Prepare post data */ post_data : { title: "Baba Yaga REPL Test", body: "This is a test post from the Baba Yaga REPL", userId: 1 }; /* Make POST request */ ..emit { action: "http_request", method: "POST", url: "https://jsonplaceholder.typicode.com/posts", headers: { "Content-Type": "application/json" }, body: post_data }; /* Return request info */ { request_type: "POST", url: "https://jsonplaceholder.typicode.com/posts", data: post_data, description: "Creating a new post" }` }, 'http-weather': { title: 'Weather API Request', description: 'Fetch weather data from OpenWeatherMap', code: `/* Weather API request example */ /* Using OpenWeatherMap API (free tier) */ /* Get current state for city */ state : ..listen; /* Determine city to fetch weather for */ city : when state is { city: name } then name _ then "London"; /* Default city */ /* Make weather request */ ..emit { action: "http_request", method: "GET", url: "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=YOUR_API_KEY&units=metric", headers: { "Accept": "application/json" } }; /* Return request info */ { city: city, request_type: "GET", url: "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=YOUR_API_KEY&units=metric", note: "Replace YOUR_API_KEY with actual OpenWeatherMap API key" }` }, 'file-operations': { title: 'File Operations with Adapters', description: 'Demonstrates file adapter usage for read/write operations', code: `/* File operations example */ /* Demonstrates file adapter integration */ /* Get current state */ state : ..listen; /* Read a file using file adapter */ ..emit { action: "read_file", filename: "tests/09_tables.txt" }; /* Save current state to file */ ..emit { action: "save_file", filename: "current_state.json", data: state }; /* Return operation info */ { operations: [ { action: "read_file", filename: "tests/09_tables.txt" }, { action: "save_file", filename: "current_state.json", data: state } ], note: "File operations processed through file adapter" }` }, 'state-driven-adapters': { title: 'State-Driven Adapter Usage', description: 'Demonstrates conditional adapter usage based on state', code: `/* State-driven adapter usage */ /* Shows how state determines which adapters to use */ /* Get current state */ state : ..listen; /* Process state and emit appropriate commands */ when state.action is "save_data" then ..emit { action: "save_file", filename: state.filename, data: state.data } "fetch_data" then ..emit { action: "http_request", method: "GET", url: state.url } "log_info" then ..emit { action: "console_log", message: state.message } _ then ..emit { action: "console_log", message: "Unknown action: " + state.action }; /* Return processed state */ { action: state.action, processed: true, timestamp: Date.now() }` }, 'harness-features': { title: 'Harness Features Demo', description: 'Demonstrates versioning, branching, and state management', code: `/* Harness features demonstration */ /* Shows versioning, state management, and adapter integration */ /* Get current state */ state : ..listen; /* Process state with version tracking */ processed_state : when state is { action: "initialize" } then { version: 1, status: "initialized", timestamp: Date.now(), data: {} } { action: "update", data: newData } then { version: state.version + 1, status: "updated", timestamp: Date.now(), data: newData } { action: "save" } then { version: state.version, status: "saved", timestamp: Date.now(), data: state.data } _ then { version: state.version || 1, status: "unknown", timestamp: Date.now(), data: state.data || {} }; /* Save state to file for persistence */ ..emit { action: "save_file", filename: "harness_state_v" + processed_state.version + ".json", data: processed_state }; /* Log the operation */ ..emit { action: "console_log", message: "State processed: " + processed_state.status + " (v" + processed_state.version + ")" }; /* Return processed state */ processed_state` }, 'branching-demo': { title: 'Branching and State Management', description: 'Demonstrates branching, state diffing, and version control', code: `/* Branching and state management demonstration */ /* Shows advanced harness features */ /* Get current state */ state : ..listen; /* Create a branching scenario */ branch_scenario : when state is { action: "create_branch", name: branchName, fromVersion: version } then { action: "branch_created", branch_name: branchName, base_version: version, timestamp: Date.now(), status: "ready" } { action: "merge_branch", source: sourceBranch, target: targetBranch } then { action: "branch_merged", source_branch: sourceBranch, target_branch: targetBranch, timestamp: Date.now(), status: "merged" } { action: "compare_versions", from: fromVersion, to: toVersion } then { action: "version_compared", from_version: fromVersion, to_version: toVersion, timestamp: Date.now(), status: "compared" } _ then { action: "unknown", timestamp: Date.now(), status: "unknown" }; /* Log the branching operation */ ..emit { action: "console_log", message: "Branching operation: " + branch_scenario.action + " - " + branch_scenario.status }; /* Save branch state */ ..emit { action: "save_file", filename: "branch_" + branch_scenario.action + "_" + Date.now() + ".json", data: branch_scenario }; /* Return branch scenario */ branch_scenario` }, 'error-recovery-demo': { title: 'Error Recovery and Resilience', description: 'Demonstrates error recovery, retry mechanisms, and resilience', code: `/* Error recovery and resilience demonstration */ /* Shows how the harness handles errors gracefully */ /* Get current state */ state : ..listen; /* Simulate different error scenarios */ error_scenario : when state is { action: "simulate_timeout" } then { action: "timeout_simulation", retry_count: 0, max_retries: 3, status: "retrying" } { action: "simulate_network_error" } then { action: "network_error_simulation", retry_count: 0, max_retries: 5, backoff_delay: 2000, status: "retrying" } { action: "simulate_script_error" } then { action: "script_error_simulation", recovery_action: "rollback", rollback_version: state.version - 1, status: "recovering" } { action: "test_resilience", data: testData } then { action: "resilience_test", test_data: testData, attempts: 0, max_attempts: 3, status: "testing" } _ then { action: "no_error", status: "normal", timestamp: Date.now() }; /* Log the error recovery operation */ ..emit { action: "console_log", message: "Error recovery: " + error_scenario.action + " - " + error_scenario.status }; /* Save error recovery state */ ..emit { action: "save_file", filename: "error_recovery_" + error_scenario.action + ".json", data: error_scenario }; /* Return error scenario */ error_scenario` }, 'state-diffing-demo': { title: 'State Diffing and Analysis', description: 'Demonstrates state comparison, diffing, and analysis', code: `/* State diffing and analysis demonstration */ /* Shows how to compare and analyze state changes */ /* Get current state */ state : ..listen; /* Analyze state changes */ state_analysis : when state is { action: "analyze_changes", fromVersion: fromVersion, toVersion: toVersion } then { action: "state_analysis", from_version: fromVersion, to_version: toVersion, analysis_type: "diff", timestamp: Date.now() } { action: "track_properties", properties: propList } then { action: "property_tracking", tracked_properties: propList, change_count: 0, timestamp: Date.now() } { action: "detect_drift", baseline: baselineState } then { action: "drift_detection", baseline_state: baselineState, current_state: state, drift_detected: false, timestamp: Date.now() } { action: "optimize_state", optimization: optType } then { action: "state_optimization", optimization_type: optType, original_size: 0, optimized_size: 0, timestamp: Date.now() } _ then { action: "state_snapshot", snapshot_data: state, timestamp: Date.now() }; /* Log the state analysis */ ..emit { action: "console_log", message: "State analysis: " + state_analysis.action + " completed" }; /* Save analysis results */ ..emit { action: "save_file", filename: "state_analysis_" + state_analysis.action + ".json", data: state_analysis }; /* Return analysis results */ state_analysis` } }; } /** * Initialize the REPL and harness integration * * This method sets up the complete REPL environment, including: * - Display welcome message and feature overview * - Load command history from file * - Set up readline interface for user interaction * - Initialize the harness (deferred until first script execution) * * ## Integration Pattern * * This method demonstrates the initialization sequence for a harness-integrated application: * * ```javascript * // 1. Display application information * console.log('Welcome to Baba Yaga REPL'); * * // 2. Load persistent state (history, configuration) * await this.loadHistory(); * * // 3. Set up user interface * this.setupReadline(); * * // 4. Harness initialization is deferred until first use * // This improves startup performance and allows for lazy loading * ``` * * ## Key Design Decisions * * ### Lazy Harness Initialization * The harness is not initialized here but deferred until the first script execution. * This improves startup performance and allows the REPL to start even if there are * issues with the harness setup. * * ### History Management * Command history is loaded from a persistent file, demonstrating how to maintain * user state across sessions. * * ### User Interface Setup * The readline interface is configured with custom prompts and event handlers, * showing how to create an interactive command-line interface. * * ## Usage in Integration * * When integrating the harness into your own application, follow this pattern: * * ```javascript * class MyApp { * async init() { * // 1. Set up your application UI/interface * this.setupInterface(); * * // 2. Load any persistent state * await this.loadState(); * * // 3. Set up harness (or defer until needed) * this.harness = new FunctionalHarness(scriptPath, config); * * // 4. Start your application * this.start(); * } * } * ``` */ async init() { console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m'); console.log('\x1b[36m║ Baba Yaga ║\x1b[0m'); console.log('\x1b[36m║ REPL ║\x1b[0m'); console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m'); console.log(''); console.log('\x1b[33m🎯 Features:\x1b[0m'); console.log(' • Multi-line input (end with semicolon)'); console.log(' • Always shows execution results'); console.log(' • Function calls: result : func args;'); console.log(' • Branching history, and versioning with rollbacks'); console.log(''); console.log('\x1b[33mQuick Commands:\x1b[0m'); console.log(' :help - Show full help'); console.log(' :examples - List examples'); console.log(' :run - Run a script from a file (supports any path)'); console.log(' :branch - Create branches'); console.log(' :menu - Interactive history'); console.log(' :state - Show current state'); console.log(' :quit - Exit REPL'); console.log(' :exit - Exit REPL'); console.log(' :bye - Exit REPL'); console.log(''); await this.loadHistory(); this.setupReadline(); } /** * Set up readline interface */ setupReadline() { this.rl = createInterface({ input: process.stdin, output: process.stdout, prompt: this.getPrompt(), historySize: 1000 }); this.rl.on('line', (input) => this.handleInput(input)); this.rl.on('close', () => this.cleanup()); this.rl.prompt(); } /** * Get current prompt */ getPrompt() { if (this.isMultiLine) { return '\x1b[32m... \x1b[0m'; } return `\x1b[32m[${this.currentVersion}] .. \x1b[0m`; } /** * Handle user input */ async handleInput(input) { const trimmed = input.trim(); // Handle empty input if (!trimmed) { if (this.isMultiLine) { // Continue multi-line input this.rl.prompt(); return; } this.rl.prompt(); return; } // Handle REPL commands if (trimmed.startsWith(':')) { await this.processCommand(trimmed); this.rl.prompt(); return; } // Handle multi-line input (continue if no semicolon) if (!trimmed.endsWith(';')) { this.isMultiLine = true; this.multiLineBuffer += (this.multiLineBuffer ? '\n' : '') + trimmed; this.rl.setPrompt(this.getPrompt()); this.rl.prompt(); return; } // Handle single line or end of multi-line (has semicolon) if (this.isMultiLine) { this.multiLineBuffer += '\n' + trimmed; await this.executeMultiLine(); } else { await this.executeScript(trimmed); } this.rl.prompt(); } /** * Execute multi-line script */ async executeMultiLine() { const script = this.multiLineBuffer; this.multiLineBuffer = ''; this.isMultiLine = false; this.rl.setPrompt(this.getPrompt()); // Auto-format the script for better readability const formattedScript = this.autoFormatScript(script); await this.executeScript(formattedScript); } /** * Auto-format multi-line script for better readability */ autoFormatScript(script) { // Remove trailing semicolon from the last line const lines = script.split('\n'); if (lines[lines.length - 1].trim().endsWith(';')) { lines[lines.length - 1] = lines[lines.length - 1].trim().slice(0, -1); } // Join lines and clean up return lines.join('\n').trim(); } /** * Execute Baba Yaga script using the functional harness * * This method demonstrates harness integration by: * - Initializing the harness on first use (lazy initialization) * - Executing scripts with the current state * - Processing side effects through adapters * - Managing state versioning * - Handling errors gracefully * * ## Core Integration Pattern * * This method implements the harness integration flow: * * ```javascript * // 1. Lazy harness initialization * if (!this.harness) { * this.harness = new FunctionalHarness(script, config); * await this.harness.initialize(); * } * * // 2. Execute script with current state * const result = await this.harness.update(this.currentState); * // result = { model: newState, commands: [...], version: 1 } * * // 3. Update application state * this.currentState = result.model; * this.currentVersion = result.version; * * // 4. Process side effects through adapters * for (const command of result.commands) { * await this.processAdapterCommand(command); * } * * // 5. Display results to user * this.displayResult(result); * ``` * * ## Key Design Principles * * ### 1. Script Execution * Scripts are executed in a controlled environment managed by the harness. * Side effects are extracted as commands and processed separately. * * ### 2. State Management * State is managed with automatic versioning. Each script execution * creates a new version, enabling history tracking and rollback capabilities. * * ### 3. Side Effect Isolation * Side effects (I/O, network, etc.) are isolated from script execution * through the command/adapter pattern. * * ### 4. Error Handling * Errors are caught and displayed gracefully, with the harness maintaining * its state even when scripts fail. * * ## Integration Example * * When integrating the harness into your own application: * * ```javascript * class MyApp { * async executeUserScript(script) { * try { * // 1. Initialize harness if needed * if (!this.harness) { * this.harness = new FunctionalHarness(script, config); * await this.harness.initialize(); * } * * // 2. Execute script with current state * const result = await this.harness.update(this.currentState); * * // 3. Update application state * this.currentState = result.model; * this.currentVersion = result.version; * * // 4. Process side effects * for (const command of result.commands) { * await this.processCommand(command); * } * * // 5. Handle results * this.handleResult(result); * * } catch (error) { * this.handleError(error); * } * } * } * ``` * * ## State Flow * * ``` * User Input → executeScript() → harness.update(currentState) * ↓ * { model, commands, version } * ↓ * Update currentState & version * ↓ * Process commands through adapters * ↓ * Display results to user * ``` * * @param {string} script - The Baba Yaga script to execute * @returns {Promise} - Resolves when script execution is complete */ async executeScript(script) { try { // Add to history this.addToHistory(script); // Create or update harness if (!this.harness) { this.harness = new FunctionalHarness(script, { logStateChanges: false, logCommands: false, debug: false }); // Initialize the harness await this.harness.initialize(); } else { // Update script content for this execution this.harness.scriptContent = script; } // Process state through harness (get commands without processing them) const result = await this.harness.update(this.currentState); // Update current state and version this.currentState = result.model; this.currentVersion = result.version; // Update the prompt to reflect the new version this.rl.setPrompt(this.getPrompt()); // Process commands through adapters (silently) if (result.commands && result.commands.length > 0) { for (const command of result.commands) { await this.processAdapterCommand(command); } } // Always display the result clearly this.displayResult(result); } catch (error) { this.displayError(error); } } /** * Process commands through the adapter registry * * This method demonstrates the adapter pattern by routing commands * from script execution to side-effect handlers (adapters). * * ## Adapter Pattern Implementation * * The adapter pattern allows the harness to focus on script execution while * enabling side effects through structured command processing: * * ```javascript * // Script generates command * ..emit { action: 'save_file', filename: 'data.json', data: { x: 1 } }; * * // Harness extracts command * const result = await harness.update(currentState); * // result.commands = [{ type: 'emit', value: { action: 'save_file', ... } }] * * // REPL routes to appropriate adapter * await this.processAdapterCommand(result.commands[0]); * // Routes to file adapter's process() method * ``` * * ## Command Routing Strategy * * This implementation uses a "broadcast" strategy where each command is * sent to all adapters. Each adapter decides whether to handle the command * based on its content: * * ```javascript * // Each adapter checks if it should handle the command * if (command.type === 'emit' && command.value.action === 'save_file') { * // File adapter handles this command * await fs.writeFile(command.value.filename, JSON.stringify(command.value.data)); * } * * if (command.type === 'emit' && command.value.action === 'http_request') { * // Network adapter handles this command * const response = await fetch(command.value.url, options); * } * ``` * * ## Command Structure * * Commands are structured objects with: * - `type`: Usually 'emit' for side effects * - `value`: Action-specific data (e.g., { action: 'http_request', url: '...' }) * * ## Error Handling * * Each adapter processes commands independently. If one adapter fails, * others continue processing. This provides error isolation. * * ## Integration Example * * When implementing adapters in your own application: * * ```javascript * class MyApp { * constructor() { * this.adapters = { * database: { * name: 'Database Adapter', * process: async (command) => { * if (command.type === 'emit' && command.value.action === 'save_record') { * await this.db.save(command.value.table, command.value.data); * } * } * }, * email: { * name: 'Email Adapter', * process: async (command) => { * if (command.type === 'emit' && command.value.action === 'send_email') { * await this.emailService.send(command.value.to, command.value.subject); * } * } * } * }; * } * * async processCommand(command) { * for (const [name, adapter] of Object.entries(this.adapters)) { * try { * await adapter.process(command); * } catch (error) { * console.error(`[${adapter.name}] Error:`, error.message); * } * } * } * } * ``` * * ## Alternative Routing Strategies * * Instead of broadcasting to all adapters, you could implement: * * ### 1. Action-Based Routing * ```javascript * const action = command.value?.action; * const adapter = this.adapters[action]; * if (adapter) { * await adapter.process(command); * } * ``` * * ### 2. Type-Based Routing * ```javascript * const type = command.type; * const adapter = this.adapters[type]; * if (adapter) { * await adapter.process(command); * } * ``` * * ### 3. Priority-Based Routing * ```javascript * const adapters = Object.values(this.adapters).sort((a, b) => b.priority - a.priority); * for (const adapter of adapters) { * if (await adapter.canHandle(command)) { * await adapter.process(command); * break; // Stop after first handler * } * } * ``` * * @param {Object} command - The command object from harness execution * @param {string} command.type - Command type (usually 'emit') * @param {Object} command.value - Action-specific data * @returns {Promise} - Resolves when all adapters have processed the command */ async processAdapterCommand(command) { // Process through all adapters silently for (const [name, adapter] of Object.entries(this.adapters)) { try { await adapter.process(command); } catch (error) { console.log(`\x1b[31m[${adapter.name}] Error: ${error.message}\x1b[0m`); } } } /** * Display execution result */ displayResult(result) { // Find the last result from the script execution const lastResult = this.findLastResult(result.model); if (lastResult !== undefined) { console.log(`\x1b[32m→\x1b[0m ${this.formatValue(lastResult)}`); } else { console.log(`\x1b[90m→\x1b[0m (no result)`); } } /** * Find the last result from the model (usually the last defined variable) */ findLastResult(model) { if (!model || typeof model !== 'object') return undefined; const keys = Object.keys(model); if (keys.length === 0) return undefined; // Look for common result variable names const resultKeys = ['result', 'output', 'value', 'data']; for (const key of resultKeys) { if (model[key] !== undefined) { return model[key]; } } // Return the last defined variable return model[keys[keys.length - 1]]; } /** * Display error */ displayError(error) { console.log('\x1b[31m✗ Error:\x1b[0m', error.message); if (error.message.includes('Unexpected token')) { console.log('\x1b[33m💡 Tip:\x1b[0m Check your syntax. Use :help for examples.'); } else if (error.message.includes('not defined')) { console.log('\x1b[33m💡 Tip:\x1b[0m Use :examples to see available patterns.'); } } /** * Format value for display */ formatValue(value, depth = 0) { if (depth > 2) return '...'; if (value === null) return '\x1b[90mnull\x1b[0m'; if (value === undefined) return '\x1b[90mundefined\x1b[0m'; const type = typeof value; switch (type) { case 'string': return `\x1b[32m"${value}"\x1b[0m`; case 'number': return `\x1b[33m${value}\x1b[0m`; case 'boolean': return `\x1b[35m${value}\x1b[0m`; case 'function': return `\x1b[36m[Function]\x1b[0m`; case 'object': if (Array.isArray(value)) { return `\x1b[34m[${value.map(v => this.formatValue(v, depth + 1)).join(', ')}]\x1b[0m`; } const entries = Object.entries(value).slice(0, 5).map(([k, v]) => `${k}: ${this.formatValue(v, depth + 1)}` ); const suffix = Object.keys(value).length > 5 ? '...' : ''; return `\x1b[34m{${entries.join(', ')}${suffix}}\x1b[0m`; default: return String(value); } } /** * Process REPL commands */ async processCommand(command) { const args = command.trim().split(/\s+/); const cmd = args[0].toLowerCase(); switch (cmd) { case ':help': this.showHelp(); break; case ':examples': this.showExamples(); break; case ':state': this.showState(); break; case ':history': this.showHistory(); break; case ':adapters': this.showAdapters(); break; case ':clear': this.clearState(); break; case ':save': await this.saveState(); break; case ':load': await this.loadState(); break; case ':menu': await this.showInteractiveMenu(); break; case ':branch': if (args.length >= 3) { await this.createBranch(parseInt(args[1]), args[2]); } else { console.log('\x1b[31mUsage: :branch \x1b[0m'); } break; case ':diff': if (args.length >= 2) { const fromVersion = parseInt(args[1]); const toVersion = args.length >= 3 ? parseInt(args[2]) : this.currentVersion; this.showStateDiff(fromVersion, toVersion); } else { console.log('\x1b[31mUsage: :diff [toVersion]\x1b[0m'); } break; case ':replay': if (args.length >= 2) { const fromVersion = parseInt(args[1]); const newState = args.length >= 3 ? JSON.parse(args[2]) : {}; await this.replayFromVersion(fromVersion, newState); } else { console.log('\x1b[31mUsage: :replay [newState]\x1b[0m'); } break; case ':recover': if (args.length >= 2) { const errorType = args[1]; await this.simulateErrorRecovery(errorType); } else { console.log('\x1b[31mUsage: :recover \x1b[0m'); console.log('\x1b[33mError types: timeout, network, script, filesystem\x1b[0m'); } break; case ':quit': case ':exit': case ':bye': await this.cleanup(); process.exit(0); break; default: if (cmd === ':run' && args.length >= 2) { const filename = args[1]; await this.runScriptFile(filename); } else if (cmd === ':example' && args.length >= 2) { const exampleName = args[1]; await this.loadExample(exampleName); } else { console.log(`\x1b[31mUnknown command: ${cmd}\x1b[0m`); console.log('\x1b[33mType :help for available commands\x1b[0m'); } } } /** * Show help information */ showHelp() { console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m'); console.log('\x1b[36m║ Baba Yaga ║\x1b[0m'); console.log('\x1b[36m║ REPL ║\x1b[0m'); console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m'); console.log(''); console.log('\x1b[33m🎯 Features:\x1b[0m'); console.log(' • Multi-line input (end with semicolon)'); console.log(' • Always shows execution results'); console.log(' • Function calls: result : func args;'); console.log(' • Branching history, and versioning with rollbacks'); console.log(''); console.log('\x1b[32mQuick Commands:\x1b[0m'); console.log(' :help - Show full help'); console.log(' :examples - List examples'); console.log(' :run - Run a script from a file (supports any path)'); console.log(' :branch - Create branches'); console.log(' :menu - Interactive history'); console.log(' :state - Show current state'); console.log(' :quit - Exit REPL'); console.log(' :exit - Exit REPL'); console.log(' :bye - Exit REPL'); console.log(''); console.log('\x1b[34mLanguage Examples:\x1b[0m'); console.log(' result : add 5 3; // Basic arithmetic'); console.log(' result : multiply 4 7; // Multiplication'); console.log(' result : subtract 10 3; // Subtraction'); console.log(' result : divide 15 3; // Division'); console.log(' result : modulo 17 5; // Modulo'); console.log(' result : negate 5; // Unary minus'); console.log(' result : subtract 5 -3; // Binary minus with unary'); console.log(''); console.log(' result : equals 5 5; // Comparison'); console.log(' result : greater 10 5; // Greater than'); console.log(' result : less 3 7; // Less than'); console.log(' result : greaterEqual 5 5; // Greater or equal'); console.log(' result : lessEqual 3 7; // Less or equal'); console.log(' result : notEqual 5 3; // Not equal'); console.log(''); console.log(' result : and true false; // Logical AND'); console.log(' result : or true false; // Logical OR'); console.log(' result : not true; // Logical NOT'); console.log(''); console.log(' result : print "Hello"; // Output'); console.log(' result : input; // Input'); console.log(''); console.log(' result : when 5 is 5 then "yes" else "no"; // Conditional'); console.log(' result : when x is 10 then "ten" else "other"; // Pattern matching'); console.log(''); console.log(' result : {1, 2, 3}; // Table literal'); console.log(' result : t.get {1, 2, 3} 1; // Table access'); console.log(' result : t.set {1, 2, 3} 1 10; // Table update'); console.log(' result : t.length {1, 2, 3}; // Table length'); console.log(''); console.log(' result : compose add1 multiply2; // Function composition'); console.log(' result : pipe 5 add1 multiply2; // Pipeline'); console.log(' result : each add1 {1, 2, 3}; // Map over table'); console.log(' result : filter greater5 {1, 6, 3, 8}; // Filter table'); console.log(' result : reduce add 0 {1, 2, 3}; // Reduce table'); console.log(''); console.log('\x1b[35m💡 Tips:\x1b[0m'); console.log(' • Use semicolon (;) to end multi-line expressions'); console.log(' • Negative numbers work without parentheses: -5'); console.log(' • Use spaces around binary operators: 5 - 3'); console.log(' • Tables are the primary data structure'); console.log(' • All operations are function calls'); console.log(' • Use :menu for interactive history navigation'); console.log(''); console.log('\x1b[36m📁 :run Command Examples:\x1b[0m'); console.log(' :run tests/09_tables.txt // Relative to project'); console.log(' :run ./my_script.txt // Relative to current dir'); console.log(' :run ~/Documents/scripts/test.txt // Relative to home'); console.log(' :run /absolute/path/to/script.txt // Absolute path'); console.log(' :run ../other-project/script.txt // Parent directory'); console.log(''); console.log('\x1b[36m🌐 HTTP Adapter Examples:\x1b[0m'); console.log(' ..emit { action: "http_request", method: "GET", url: "..." }'); console.log(' ..emit { action: "http_request", method: "POST", url: "...", body: {...} }'); console.log(' ..emit { action: "http_request", method: "PUT", url: "...", headers: {...} }'); console.log(' :example http-get // Simple GET request'); console.log(' :example http-post // POST with JSON body'); console.log(' :example http-weather // Weather API integration'); console.log(''); console.log('\x1b[36m📁 File Adapter Examples:\x1b[0m'); console.log(' ..emit { action: "read_file", filename: "..." }'); console.log(' ..emit { action: "save_file", filename: "...", data: {...} }'); console.log(' :example file-operations // File read/write operations'); console.log(' :example state-driven-adapters // Conditional adapter usage'); console.log(' :example harness-features // Versioning and state management'); console.log(' :example branching-demo // Branching and state diffing'); console.log(' :example error-recovery-demo // Error recovery and resilience'); console.log(' :example state-diffing-demo // State diffing and analysis'); console.log(''); console.log('\x1b[36m🔄 Advanced Harness Commands:\x1b[0m'); console.log(' :branch - Create branch from version'); console.log(' :diff [to] - Show state diff between versions'); console.log(' :replay [state] - Replay from version with new state'); console.log(' :recover - Simulate error recovery'); console.log(''); console.log('\x1b[36m📁 File Adapter Examples:\x1b[0m'); console.log(' ..emit { action: "read_file", filename: "..." }'); console.log(' ..emit { action: "save_file", filename: "...", data: {...} }'); console.log(' :example file-operations // File read/write operations'); console.log(' :example state-driven-adapters // Conditional adapter usage'); console.log(' :example harness-features // Versioning and state management'); console.log(' :example branching-demo // Branching and state diffing'); console.log(' :example error-recovery-demo // Error recovery and resilience'); console.log(' :example state-diffing-demo // State diffing and analysis'); console.log(''); console.log('\x1b[36m🧪 Advanced Features Examples:\x1b[0m'); console.log(' :example branching-demo // Branching and version control'); console.log(' :example error-recovery-demo // Error recovery patterns'); console.log(' :example state-diffing-demo // State analysis and diffing'); console.log(''); } /** * Show available examples */ showExamples() { console.log('\x1b[33mAvailable Examples:\x1b[0m'); Object.entries(this.examples).forEach(([name, example]) => { console.log(` ${name.padEnd(15)} - ${example.title}`); console.log(` ${' '.repeat(17)} ${example.description}`); }); console.log(''); console.log('Use :example to load an example'); } /** * Load an example */ async loadExample(name) { const example = this.examples[name]; if (!example) { console.log(`\x1b[31mExample '${name}' not found\x1b[0m`); this.showExamples(); return; } console.log(`\x1b[33mLoading example: ${example.title}\x1b[0m`); console.log(`\x1b[90m${example.description}\x1b[0m`); console.log('\x1b[90m' + example.code + '\x1b[0m'); console.log(''); // Execute the example await this.executeScript(example.code); } /** * Show current state */ showState() { if (Object.keys(this.currentState).length === 0) { console.log('\x1b[90mNo state defined\x1b[0m'); return; } console.log('\x1b[33mCurrent State (Version ' + this.currentVersion + '):\x1b[0m'); console.log(this.formatValue(this.currentState)); } /** * Show version history */ showHistory() { if (!this.harness || !this.harness.stateHistory) { console.log('\x1b[90mNo history available - no scripts executed yet\x1b[0m'); return; } try { const history = this.harness.getVersionHistory(); if (!history || history.length === 0) { console.log('\x1b[90mNo version history available\x1b[0m'); return; } console.log('\x1b[33mVersion History:\x1b[0m'); history.slice(-10).forEach((entry, i) => { console.log(` ${entry.version}: ${new Date(entry.timestamp).toLocaleTimeString()}`); }); } catch (error) { console.log(`\x1b[31mError loading history: ${error.message}\x1b[0m`); } } /** * Rollback to version */ async rollbackToVersion(version) { if (!this.harness || !this.harness.stateHistory) { throw new Error('No harness or history available'); } try { await this.harness.rollbackToVersion(version); this.currentState = this.harness.getCurrentState(); this.currentVersion = version; // Update the prompt to reflect the new version this.rl.setPrompt(this.getPrompt()); console.log(`\x1b[32mRolled back to version ${version}\x1b[0m`); this.showState(); } catch (error) { throw new Error(`Rollback failed: ${error.message}`); } } /** * Show available adapters */ showAdapters() { console.log('\x1b[33mAvailable Adapters:\x1b[0m'); Object.entries(this.adapters).forEach(([name, adapter]) => { console.log(` ${name.padEnd(10)} - ${adapter.name}`); console.log(` ${' '.repeat(12)} ${adapter.description}`); }); } /** * Clear current state */ clearState() { this.currentState = {}; this.currentVersion = 0; this.harness = null; console.log('\x1b[33mState cleared\x1b[0m'); } /** * Save state to file */ async saveState(filename = 'harness_state.json') { try { const stateData = { state: this.currentState, version: this.currentVersion, timestamp: Date.now() }; await fs.writeFile(filename, JSON.stringify(stateData, null, 2)); console.log(`\x1b[32mState saved to ${filename}\x1b[0m`); } catch (error) { console.log(`\x1b[31mFailed to save state: ${error.message}\x1b[0m`); } } /** * Load state from file */ async loadState(filename = 'harness_state.json') { try { const content = await fs.readFile(filename, 'utf8'); const stateData = JSON.parse(content); this.currentState = stateData.state; this.currentVersion = stateData.version; console.log(`\x1b[32mState loaded from ${filename}\x1b[0m`); this.showState(); } catch (error) { console.log(`\x1b[31mFailed to load state: ${error.message}\x1b[0m`); } } /** * Run script from file */ async runScriptFile(filename) { try { // Import path module for robust path handling const path = await import('path'); const { fileURLToPath } = await import('url'); let resolvedPath; // Check if the path is absolute (starts with / on Unix or C:\ on Windows) if (path.isAbsolute(filename)) { // Use absolute path as-is resolvedPath = filename; } else { // For relative paths, try multiple resolution strategies const __filename = fileURLToPath(import.meta.url); const replDir = path.dirname(__filename); const projectRoot = path.dirname(replDir); // Go up one level from repl/ to project root // Strategy 1: Try relative to project root (current behavior) const projectPath = path.resolve(projectRoot, filename); // Strategy 2: Try relative to current working directory const cwdPath = path.resolve(process.cwd(), filename); // Strategy 3: Try relative to user's home directory const homePath = path.resolve(process.env.HOME || process.env.USERPROFILE || '', filename); // Check which path exists const fs = await import('fs'); if (fs.existsSync(projectPath)) { resolvedPath = projectPath; } else if (fs.existsSync(cwdPath)) { resolvedPath = cwdPath; } else if (fs.existsSync(homePath)) { resolvedPath = homePath; } else { // If none exist, use project root as fallback (will show clear error) resolvedPath = projectPath; } } console.log(`\x1b[33mRunning script from ${resolvedPath}:\x1b[0m`); // Create a script that uses the file adapter to read the file const fileAdapterScript = `/* File adapter demonstration */ /* This script uses the file adapter to read and execute the target file */ /* Emit command to read the file using file adapter */ ..emit { action: "read_file", filename: "${resolvedPath.replace(/\\/g, '\\\\')}" }; /* Return info about the operation */ { operation: "read_file", filename: "${resolvedPath.replace(/\\/g, '\\\\')}", note: "File content will be available through file adapter" }`; // Execute the file adapter script await this.executeScript(fileAdapterScript); // Also read and display the file content directly for immediate feedback const content = await fs.readFile(resolvedPath, 'utf8'); console.log('\x1b[90m' + content + '\x1b[0m'); console.log(''); // Execute the actual file content await this.executeScript(content); } catch (error) { console.log(`\x1b[31mFailed to run script: ${error.message}\x1b[0m`); console.log(`\x1b[33m💡 Path resolution strategies:\x1b[0m`); console.log(` • Absolute paths: /path/to/script.txt`); console.log(` • Relative to project: tests/script.txt`); console.log(` • Relative to current directory: ./script.txt`); console.log(` • Relative to home: ~/scripts/script.txt`); console.log(` • Full paths: /Users/username/Documents/script.txt`); } } /** * Show state diff between versions */ showStateDiff(fromVersion, toVersion) { if (!this.harness) { console.log('\x1b[31mNo harness available. Execute a script first.\x1b[0m'); return; } const diff = this.harness.getStateDiff(fromVersion, toVersion); if (!diff) { console.log(`\x1b[31mCould not generate diff between versions ${fromVersion} and ${toVersion}\x1b[0m`); return; } console.log(`\x1b[36m📊 State Diff: v${fromVersion} → v${toVersion}\x1b[0m`); console.log(''); if (Object.keys(diff.added).length > 0) { console.log('\x1b[32m➕ Added Properties:\x1b[0m'); for (const [key, value] of Object.entries(diff.added)) { console.log(` ${key}: ${JSON.stringify(value)}`); } console.log(''); } if (Object.keys(diff.removed).length > 0) { console.log('\x1b[31m➖ Removed Properties:\x1b[0m'); for (const [key, value] of Object.entries(diff.removed)) { console.log(` ${key}: ${JSON.stringify(value)}`); } console.log(''); } if (Object.keys(diff.changed).length > 0) { console.log('\x1b[33m🔄 Changed Properties:\x1b[0m'); for (const [key, change] of Object.entries(diff.changed)) { console.log(` ${key}:`); console.log(` From: ${JSON.stringify(change.from)}`); console.log(` To: ${JSON.stringify(change.to)}`); } console.log(''); } if (Object.keys(diff.added).length === 0 && Object.keys(diff.removed).length === 0 && Object.keys(diff.changed).length === 0) { console.log('\x1b[90mNo changes detected between versions\x1b[0m'); } } /** * Replay from a specific version with new state */ async replayFromVersion(fromVersion, newState) { if (!this.harness) { console.log('\x1b[31mNo harness available. Execute a script first.\x1b[0m'); return; } console.log(`\x1b[36m🔄 Replaying from version ${fromVersion} with new state...\x1b[0m`); try { const result = await this.harness.replayFromVersion(fromVersion, newState); console.log(`\x1b[32m✅ Replay completed successfully\x1b[0m`); console.log(`\x1b[36m📊 Result: ${JSON.stringify(result, null, 2)}\x1b[0m`); } catch (error) { console.log(`\x1b[31m❌ Replay failed: ${error.message}\x1b[0m`); } } /** * Simulate error recovery scenarios */ async simulateErrorRecovery(errorType) { if (!this.harness) { console.log('\x1b[31mNo harness available. Execute a script first.\x1b[0m'); return; } console.log(`\x1b[36m🧪 Simulating ${errorType} error recovery...\x1b[0m`); // Create a mock error based on the type let mockError; switch (errorType) { case 'timeout': mockError = new Error('Script execution timeout'); break; case 'network': mockError = new Error('Network error: ECONNREFUSED'); break; case 'script': mockError = new Error('Unexpected token in parsePrimary: INVALID'); break; case 'filesystem': mockError = new Error('File system error: ENOENT'); break; default: mockError = new Error(`Unknown error type: ${errorType}`); } try { const recoveryResult = await this.harness.recoverFromError(mockError, { lastState: this.currentState }); console.log(`\x1b[32m✅ Error recovery completed\x1b[0m`); console.log(`\x1b[36m📊 Recovery result: ${JSON.stringify(recoveryResult, null, 2)}\x1b[0m`); } catch (error) { console.log(`\x1b[31m❌ Error recovery failed: ${error.message}\x1b[0m`); } } /** * Enhanced branch creation with better feedback */ async createBranch(fromVersion, branchName) { if (!this.harness) { console.log('\x1b[31mNo harness available. Execute a script first.\x1b[0m'); return; } console.log(`\x1b[36m🌿 Creating branch '${branchName}' from version ${fromVersion}...\x1b[0m`); try { const branchHarness = await this.harness.createBranch(fromVersion, branchName); const branchInfo = branchHarness.getBranchInfo(); console.log(`\x1b[32m✅ Branch '${branchName}' created successfully\x1b[0m`); console.log(`\x1b[36m📊 Branch info: ${JSON.stringify(branchInfo, null, 2)}\x1b[0m`); // Store the branch harness for potential use this.branches = this.branches || {}; this.branches[branchName] = branchHarness; } catch (error) { console.log(`\x1b[31m❌ Branch creation failed: ${error.message}\x1b[0m`); } } /** * Show interactive menu for navigating history and branches */ async showInteractiveMenu() { const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const question = (prompt) => new Promise((resolve) => { rl.question(prompt, (answer) => { resolve(answer); }); }); // Handle Ctrl+C gracefully const originalSigint = process.listeners('SIGINT').length > 0 ? process.listeners('SIGINT')[0] : null; const handleSigint = () => { console.log('\n\x1b[33mReturning to REPL...\x1b[0m'); rl.close(); process.exit(0); }; process.on('SIGINT', handleSigint); try { while (true) { console.clear(); console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m'); console.log('\x1b[36m║ Interactive Menu ║\x1b[0m'); console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m'); // Show current state console.log(`\x1b[33m📍 Current Version: ${this.currentVersion}\x1b[0m`); console.log(`\x1b[33m📊 State Keys: ${Object.keys(this.currentState || {}).length}\x1b[0m`); console.log(''); // Show history - handle null harness gracefully console.log('\x1b[32m📜 Version History:\x1b[0m'); if (!this.harness || !this.harness.stateHistory) { console.log(' \x1b[90mNo history available - no scripts executed yet\x1b[0m'); } else { try { const history = this.harness.stateHistory.getAllVersions(); if (history && history.length > 0) { history.forEach((entry, index) => { const isCurrent = entry.version === this.currentVersion; const marker = isCurrent ? '\x1b[33m▶\x1b[0m' : ' '; const time = new Date(entry.timestamp).toLocaleTimeString(); console.log(` ${marker} ${entry.version}: ${time}`); }); } else { console.log(' \x1b[90mNo version history available\x1b[0m'); } } catch (error) { console.log(` \x1b[31mError loading history: ${error.message}\x1b[0m`); } } console.log(''); // Show branches (if any) - handle null harness gracefully if (this.harness) { try { // Check if getBranches method exists if (typeof this.harness.getBranches === 'function') { const branches = this.harness.getBranches(); if (branches && branches.length > 0) { console.log('\x1b[35m🌿 Branches:\x1b[0m'); branches.forEach(branch => { console.log(` 🌿 ${branch.name} (from v${branch.fromVersion})`); }); console.log(''); } } else { // Branches feature not implemented yet console.log('\x1b[90m🌿 Branches: Feature not implemented yet\x1b[0m'); console.log(''); } } catch (error) { console.log(`\x1b[31mError loading branches: ${error.message}\x1b[0m`); } } // Menu options - disable options that require harness const hasHarness = this.harness && this.harness.stateHistory; console.log('\x1b[34m🎯 Options:\x1b[0m'); console.log(` 1. View version details${!hasHarness ? ' (disabled)' : ''}`); console.log(` 2. Rollback to version${!hasHarness ? ' (disabled)' : ''}`); console.log(` 3. Create branch${!hasHarness ? ' (disabled)' : ''}`); console.log(` 4. Compare versions${!hasHarness ? ' (disabled)' : ''}`); console.log(' 5. Show current state'); console.log(' 6. Return to REPL'); console.log(' 0. Cancel / Exit menu'); console.log(''); console.log('\x1b[90m💡 Tip: Press Ctrl+C to exit at any time\x1b[0m'); console.log(''); if (!hasHarness) { console.log('\x1b[33m💡 Tip: Execute a script first to enable history features\x1b[0m'); console.log(''); } const choice = await question('\x1b[33mEnter choice (0-6): \x1b[0m'); switch (choice.trim()) { case '0': console.log('\x1b[33mReturning to REPL...\x1b[0m'); rl.close(); return; case '1': if (hasHarness) { await this.menuViewVersionDetails(question); } else { console.log('\x1b[31mNo history available. Execute a script first.\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); } break; case '2': if (hasHarness) { await this.menuRollbackToVersion(question); } else { console.log('\x1b[31mNo history available. Execute a script first.\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); } break; case '3': if (hasHarness) { await this.menuCreateBranch(question); } else { console.log('\x1b[31mNo history available. Execute a script first.\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); } break; case '4': if (hasHarness) { await this.menuCompareVersions(question); } else { console.log('\x1b[31mNo history available. Execute a script first.\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); } break; case '5': await this.menuShowCurrentState(question); break; case '6': console.log('\x1b[33mReturning to REPL...\x1b[0m'); rl.close(); return; default: console.log('\x1b[31mInvalid choice. Press Enter to continue...\x1b[0m'); await question(''); } } } catch (error) { console.log(`\x1b[31mMenu error: ${error.message}\x1b[0m`); console.log('\x1b[33mPress Enter to return to REPL...\x1b[0m'); await question(''); } finally { // Restore original SIGINT handler process.removeListener('SIGINT', handleSigint); if (originalSigint) { process.on('SIGINT', originalSigint); } rl.close(); } } /** * Menu option: View version details */ async menuViewVersionDetails(question) { if (!this.harness || !this.harness.stateHistory) { console.log('\x1b[31mNo history available\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } const version = await question('\x1b[33mEnter version number: \x1b[0m'); const versionNum = parseInt(version.trim()); if (isNaN(versionNum)) { console.log('\x1b[31mInvalid version number\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } try { const state = this.harness.stateHistory.getVersion(versionNum); if (!state) { console.log('\x1b[31mVersion not found\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } console.log(`\x1b[32m📋 Version ${versionNum} Details:\x1b[0m`); const versionData = this.harness.stateHistory.versions.get(versionNum); if (versionData) { console.log(`Time: ${new Date(versionData.timestamp).toLocaleString()}`); console.log(`State Keys: ${Object.keys(state).length}`); console.log('\x1b[33mState Contents:\x1b[0m'); console.log(this.formatValue(state)); } else { console.log('Time: Unknown'); console.log(`State Keys: ${Object.keys(state).length}`); console.log('\x1b[33mState Contents:\x1b[0m'); console.log(this.formatValue(state)); } } catch (error) { console.log(`\x1b[31mError loading version details: ${error.message}\x1b[0m`); } await question('\x1b[33mPress Enter to continue...\x1b[0m'); } /** * Menu option: Rollback to version */ async menuRollbackToVersion(question) { if (!this.harness || !this.harness.stateHistory) { console.log('\x1b[31mNo history available\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } const version = await question('\x1b[33mEnter version to rollback to: \x1b[0m'); const versionNum = parseInt(version.trim()); if (isNaN(versionNum)) { console.log('\x1b[31mInvalid version number\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } const confirm = await question('\x1b[31m⚠️ This will reset current state. Continue? (y/N): \x1b[0m'); if (confirm.toLowerCase() !== 'y') { console.log('\x1b[33mRollback cancelled\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } try { await this.rollbackToVersion(versionNum); console.log(`\x1b[32m✅ Rolled back to version ${versionNum}\x1b[0m`); } catch (error) { console.log(`\x1b[31mRollback failed: ${error.message}\x1b[0m`); } await question('\x1b[33mPress Enter to continue...\x1b[0m'); } /** * Menu option: Create branch */ async menuCreateBranch(question) { if (!this.harness || !this.harness.stateHistory) { console.log('\x1b[31mNo history available\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } const fromVersion = await question('\x1b[33mEnter source version: \x1b[0m'); const branchName = await question('\x1b[33mEnter branch name: \x1b[0m'); const versionNum = parseInt(fromVersion.trim()); if (isNaN(versionNum)) { console.log('\x1b[31mInvalid version number\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } if (!branchName.trim()) { console.log('\x1b[31mBranch name required\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } try { await this.createBranch(versionNum, branchName.trim()); } catch (error) { console.log(`\x1b[31mBranch creation failed: ${error.message}\x1b[0m`); } await question('\x1b[33mPress Enter to continue...\x1b[0m'); } /** * Menu option: Compare versions */ async menuCompareVersions(question) { if (!this.harness || !this.harness.stateHistory) { console.log('\x1b[31mNo history available\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } const version1 = await question('\x1b[33mEnter first version: \x1b[0m'); const version2 = await question('\x1b[33mEnter second version: \x1b[0m'); const v1 = parseInt(version1.trim()); const v2 = parseInt(version2.trim()); if (isNaN(v1) || isNaN(v2)) { console.log('\x1b[31mInvalid version number\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } try { const state1 = this.harness.stateHistory.getVersion(v1); const state2 = this.harness.stateHistory.getVersion(v2); if (!state1 || !state2) { console.log('\x1b[31mOne or both versions not found\x1b[0m'); await question('\x1b[33mPress Enter to continue...\x1b[0m'); return; } console.log(`\x1b[32m📊 Comparing Version ${v1} vs ${v2}:\x1b[0m`); const keys1 = Object.keys(state1); const keys2 = Object.keys(state2); console.log(`\x1b[33mKeys in v${v1}: ${keys1.length}\x1b[0m`); console.log(`\x1b[33mKeys in v${v2}: ${keys2.length}\x1b[0m`); const onlyInV1 = keys1.filter(k => !keys2.includes(k)); const onlyInV2 = keys2.filter(k => !keys1.includes(k)); const common = keys1.filter(k => keys2.includes(k)); if (onlyInV1.length > 0) { console.log(`\x1b[31mOnly in v${v1}: ${onlyInV1.join(', ')}\x1b[0m`); } if (onlyInV2.length > 0) { console.log(`\x1b[32mOnly in v${v2}: ${onlyInV2.join(', ')}\x1b[0m`); } if (common.length > 0) { console.log(`\x1b[33mCommon keys: ${common.join(', ')}\x1b[0m`); } } catch (error) { console.log(`\x1b[31mError comparing versions: ${error.message}\x1b[0m`); } await question('\x1b[33mPress Enter to continue...\x1b[0m'); } /** * Menu option: Show current state */ async menuShowCurrentState(question) { console.log('\x1b[32m📋 Current State:\x1b[0m'); this.showState(); await question('\x1b[33mPress Enter to continue...\x1b[0m'); } /** * Add command to history */ addToHistory(command) { this.history.push(command); if (this.history.length > 100) { this.history.shift(); } } /** * Load history from file */ async loadHistory() { try { const content = await fs.readFile(this.historyFile, 'utf8'); this.history = content.split('\n').filter(line => line.trim()); } catch (error) { this.history = []; } } /** * Save history to file */ async saveHistory() { try { await fs.writeFile(this.historyFile, this.history.join('\n')); } catch (error) { // Ignore history save errors } } /** * Cleanup on exit */ async cleanup() { await this.saveHistory(); console.log('\n\x1b[33mGoodbye! 👋\x1b[0m'); } } // Main execution async function main() { const repl = new REPL(); await repl.init(); } // Handle process termination process.on('SIGINT', () => { console.log('\n'); process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); // Start the REPL if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } export { REPL };