diff options
Diffstat (limited to 'js/scripting-lang/repl/repl.js')
-rw-r--r-- | js/scripting-lang/repl/repl.js | 2432 |
1 files changed, 2432 insertions, 0 deletions
diff --git a/js/scripting-lang/repl/repl.js b/js/scripting-lang/repl/repl.js new file mode 100644 index 0000000..c3f01d4 --- /dev/null +++ b/js/scripting-lang/repl/repl.js @@ -0,0 +1,2432 @@ +#!/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<void>} - 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<void>} - 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 <version> <name>\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 <fromVersion> [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 <fromVersion> [newState]\x1b[0m'); + } + break; + case ':recover': + if (args.length >= 2) { + const errorType = args[1]; + await this.simulateErrorRecovery(errorType); + } else { + console.log('\x1b[31mUsage: :recover <errorType>\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 <version> <name> - Create branch from version'); + console.log(' :diff <from> [to] - Show state diff between versions'); + console.log(' :replay <version> [state] - Replay from version with new state'); + console.log(' :recover <type> - 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 <name> 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 }; \ No newline at end of file |