diff options
Diffstat (limited to 'js/scripting-lang/repl')
-rw-r--r-- | js/scripting-lang/repl/.repl_history | 411 | ||||
-rw-r--r-- | js/scripting-lang/repl/README.md | 14 | ||||
-rw-r--r-- | js/scripting-lang/repl/repl.js | 1648 |
3 files changed, 1732 insertions, 341 deletions
diff --git a/js/scripting-lang/repl/.repl_history b/js/scripting-lang/repl/.repl_history index 32607e5..6f69f53 100644 --- a/js/scripting-lang/repl/.repl_history +++ b/js/scripting-lang/repl/.repl_history @@ -1,205 +1,216 @@ -/* Create and process state */ -x : 5; -y : 10; -sum : x + y; -result : { x, y, sum }; -/* Return processed state */ -result -double : x -> - x * 2; -result : double 10; -age : 25; -status : when age is - age >= 18 then "adult" - _ then "minor"; -result : { age, status }; -numbers : {1: 1, 2: 2, 3: 3, 4: 4, 5: 5}; -double : x -> x * 2; -filter_even : x -> when x % 2 is 0 then true _ then false; -doubled : map @double numbers; -even_numbers : filter @filter_even doubled; -result : { - original: numbers, - doubled: doubled, - even: even_numbers +first_mixed : mixed[1]; +name_mixed : mixed.name; +second_mixed : mixed[2]; +..assert first_mixed = 1; +..assert name_mixed = "Bob"; +..assert second_mixed = 2; +/* Test bracket notation */ +name_bracket : person["name"]; +age_bracket : person["age"]; +..assert name_bracket = "Alice"; +..assert age_bracket = 30; +..out "Tables test completed"; +/* 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" + } }; -x : 3; -y : 3; -y + y; -add y y; -apply add y x; -/* Basic state management example */ -/* Create and process state */ -x : 5; -y : 10; -sum : x + y; -result : { x, y, sum }; -/* Return processed state */ -result -/* Basic state management example */ -/* Create and process state */ -x : 5; -y : 10; -sum : x + y; -result : { x, y, sum }; -/* Return processed state */ -result -test : a b c -> (a + b) / c; -test 12 12 100 -sum : x + y; -x : 42; -result : when x is -42 then "the result" -_ then "nope" -acc : x y -> x + y; -acc 3 3; -add_func : x y -> x + y; -add_func 3 3; -add_func : x y -> x + y; -result : add_func 3 3; -acc : x y -> x + y; -result : acc 3 3; -x : 5; y : 10; sum : x + y; -double : x -> x * 2; result : double sum; -square : x -> x * x; squared : square result; -/* 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 }10 } -/* FizzBuzz recursive example */ -/* FizzBuzz function using recursion with tables */ -fizzbuzz : n -> - when n is - 0 then {} - _ then - { (fizzbuzz (n - 1)), n: - when n is - n % 15 == 0 then "FizzBuzz" - n % 3 == 0 then "Fizz" - n % 5 == 0 then "Buzz" - _ then n - }; -/* Test FizzBuzz for first 15 numbers */ -result : fizzbuzz 15; -/* Also test smaller ranges */ -fizzbuzz_5 : fizzbuzz 5; -fizzbuzz_10 : fizzbuzz 10; -/* Return results */ -{ fizzbuzz, result, fizzbuzz_5, fizzbuzz_10 } -fizzbuzz_20 : fizzbuzz 20; fizzbuzz_30 : fizzbuzz 30; -fizzbuzz_0 : fizzbuzz 0; fizzbuzz_1 : fizzbuzz 1; -/* FizzBuzz recursive example */ -/* FizzBuzz function using recursion with tables */ -fizzbuzz : n -> - when n is - 0 then {} - _ then - { (fizzbuzz (n - 1)), n: - when n is - n % 15 = 0 then "FizzBuzz" - n % 3 = 0 then "Fizz" - n % 5 = 0 then "Buzz" - _ then n - }; -/* Test FizzBuzz for first 15 numbers */ -result : fizzbuzz 15; -/* Also test smaller ranges */ -fizzbuzz_5 : fizzbuzz 5; -fizzbuzz_10 : fizzbuzz 10; -/* Return results */ -{ fizzbuzz, result, fizzbuzz_5, fizzbuzz_10 } -fizzbuzz_20 : fizzbuzz 20; fizzbuzz_30 : fizzbuzz 30; -fizzbuzz_0 : fizzbuzz 0; fizzbuzz_1 : fizzbuzz 1; -/* FizzBuzz recursive example */ -/* FizzBuzz function using recursion with tables */ -fizzbuzz : n -> - when n is - 0 then {} - _ then - { (fizzbuzz (n - 1)), n: - when n is - n % 15 = 0 then "FizzBuzz" - n % 3 = 0 then "Fizz" - n % 5 = 0 then "Buzz" - _ then n - }; -/* Test FizzBuzz for first 15 numbers */ -result : fizzbuzz 15; -/* Also test smaller ranges */ -fizzbuzz_5 : fizzbuzz 5; -fizzbuzz_10 : fizzbuzz 10; -/* Return results */ -{ fizzbuzz, result, fizzbuzz_5, fizzbuzz_10 } -result : fizzbuzz 5; -/* FizzBuzz recursive example */ -/* FizzBuzz function using recursion with tables */ -fizzbuzz : n -> - when n is - 0 then {} - _ then - { (fizzbuzz (n - 1)), n: - when n is - n % 15 = 0 then "FizzBuzz" - n % 3 = 0 then "Fizz" - n % 5 = 0 then "Buzz" - _ then n - }; -/* Test FizzBuzz for first 15 numbers */ -result : fizzbuzz 15; -/* Also test smaller ranges */ -fizzbuzz_5 : fizzbuzz 5; -fizzbuzz_10 : fizzbuzz 10; -/* Return results */ -{ fizzbuzz, result, fizzbuzz_5, fizzbuzz_10 } -/* Network API integration example */ -/* Using PokéAPI to fetch Pokémon data */ -/* Get current state to see if we have a Pokémon name */ +/* Return request info */ +{ + request_type: "GET", + url: "https://jsonplaceholder.typicode.com/posts/1", + description: "Fetching a sample post from JSONPlaceholder" +} +/* 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" +} +/* File operations example */ +/* Demonstrates file adapter integration */ +/* Get current state */ 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 +/* 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 }; -/* Also fetch a list of Pokémon */ -..emit { - action: "http_request", - method: "GET", - url: "https://pokeapi.co/api/v2/pokemon?limit=5" +/* 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" +} +/* 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: "/Users/eli/Code/tour/js/scripting-lang/tests/09_tables.txt" +}; +/* Return info about the operation */ +{ + operation: "read_file", + filename: "/Users/eli/Code/tour/js/scripting-lang/tests/09_tables.txt", + note: "File content will be available through file adapter" +} +state : ..listen; result : { message: 'Hello from harness', state: state }; +state : ..listen; result : { message: "Hello from harness", state: state }; +/* 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 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" } - ] +/* Return request info */ +{ + request_type: "GET", + url: "https://jsonplaceholder.typicode.com/posts/1", + description: "Fetching a sample post from JSONPlaceholder" } -..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon/ditto" }; -..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon/ditto" }; -test : 42; -..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon/ditto" }; -..emit { action: "http_request", method: "GET", url: "https://pokeapi.co/api/v2/pokemon/ditto" }; -test : 42; -test : 42;kemon/ditto" }; -test : 42; \ No newline at end of file +..emit { action: "test" }; +..emit { action: "http_request", method: "GET", url: "https://jsonplaceholder.typicode.com/posts/1" }; +..emit { action: "http_request", method: "GET", url: "https://jsonplaceholder.typicode.com/posts/1" }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +state : ..listen; result : { message: "Test state", version: 1 }; +/* 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 +state : ..listen; result : { message: "Initial state", version: 1 }; +state : ..listen; result : { message: "Updated state", version: 2, newField: "value" }; +state : ..listen; result : { message: "Test state", version: 1 }; +/* 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 +result : add 5 3; +a : 1; +b : 2; +c : x y -> x + y; +apply c a b; +d : c a b; \ No newline at end of file diff --git a/js/scripting-lang/repl/README.md b/js/scripting-lang/repl/README.md index b4e3714..fa7b846 100644 --- a/js/scripting-lang/repl/README.md +++ b/js/scripting-lang/repl/README.md @@ -18,8 +18,8 @@ This REPL serves as a **comprehensive demo** of how to leverage the scripting-ha ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Model │ │ Update │ │ Commands │ -│ (Pure State) │───▶│ (Pure Script) │───▶│ (..emit) │ +│ Model │ │ Update │ │ Commands │ +│ (Pure State) │───▶│ (Pure Script) │───▶│ (..emit) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ │ ▼ @@ -30,8 +30,8 @@ This REPL serves as a **comprehensive demo** of how to leverage the scripting-ha │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ -│ ..listen │ │ New State │ -│ (State Access) │ │ (Returned) │ +│ ..listen │ │ New State │ +│ (State Access) │ │ (Returned) │ └─────────────────┘ └─────────────────┘ ``` @@ -251,7 +251,7 @@ Pure function transforms state. ### 3. Commands (Side Effects) ```bash -📤 Processing 1 command(s)... +Processing 1 command(s)... → emit: {"action":"counter_updated","count":2} [Console Adapter] { action: "counter_updated", count: 2 } ``` @@ -265,7 +265,7 @@ Commands: 1 ``` New state is returned and displayed. -## 🎯 Learning Objectives +## Learning Objectives This REPL demonstrates: @@ -356,4 +356,4 @@ This creates a powerful demonstration of how the scripting-harness architecture **Primary Purpose**: Educational demonstration of functional state management patterns and adapter architecture. -**Production Readiness**: The core architecture is sound, but requires resolution of the harness initialization issue for full functionality. \ No newline at end of file +**Production Readiness**: The core architecture is sound, but requires resolution of the harness initialization issue for full functionality. diff --git a/js/scripting-lang/repl/repl.js b/js/scripting-lang/repl/repl.js index b6b582d..c3f01d4 100644 --- a/js/scripting-lang/repl/repl.js +++ b/js/scripting-lang/repl/repl.js @@ -1,15 +1,83 @@ #!/usr/bin/env node /** - * Harness-Integrated REPL - Demonstrates Scripting Harness Architecture + * Baba Yaga REPL - Interactive Language Playground & Harness Integration Demo * - * This REPL showcases the scripting-harness integration, demonstrating: - * - TEA (The Elm Architecture) principles - * - State management with versioning and history - * - Command processing with ..emit and ..listen - * - Adapter integration for side effects - * - Pure function script execution - * - State rollbacks and branching + * 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'; @@ -21,21 +89,149 @@ 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 instance + // Harness integration - Core of the architecture this.harness = null; this.currentState = {}; this.currentVersion = 0; - // Adapter registry for command processing + /** + * 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', @@ -45,26 +241,111 @@ class REPL { } } }, + + // 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', + 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}`); + 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}`); + 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 network requests', + description: 'Handles HTTP requests with real network calls', process: async (command) => { if (command.type === 'emit' && command.value.action === 'http_request') { - console.log(`\x1b[33m[Network Adapter]\x1b[0m Would make ${command.value.method} request to ${command.value.url}`); + 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`); + } + } } } } @@ -193,35 +474,477 @@ pokemon_name : when state is { 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 + * 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║ Scripting REPL ║\x1b[0m'); - console.log('\x1b[36m║ TEA Architecture Demo ║\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(' • Auto-formatting for readability'); console.log(' • Always shows execution results'); console.log(' • Function calls: result : func args;'); - console.log(' • TEA Architecture with state management'); - console.log(' • Versioning and rollbacks'); + 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'); + console.log(' :run - Run a script from a file (supports any path)'); console.log(' :branch - Create branches'); - console.log(' :menu - Interactive menu'); + 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(); @@ -274,7 +997,7 @@ pokemon_name : when state is // Handle REPL commands if (trimmed.startsWith(':')) { - await this.handleCommand(trimmed); + await this.processCommand(trimmed); this.rl.prompt(); return; } @@ -328,7 +1051,113 @@ pokemon_name : when state is } /** - * Execute script using harness + * 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 { @@ -356,10 +1185,13 @@ pokemon_name : when state is 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.processCommand(command); + await this.processAdapterCommand(command); } } @@ -372,9 +1204,137 @@ pokemon_name : when state is } /** - * Process command through adapters + * 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 processCommand(command) { + async processAdapterCommand(command) { // Process through all adapters silently for (const [name, adapter] of Object.entries(this.adapters)) { try { @@ -468,59 +1428,91 @@ pokemon_name : when state is } /** - * Handle REPL commands + * Process REPL commands */ - async handleCommand(command) { - const [cmd, ...args] = command.slice(1).split(' '); + async processCommand(command) { + const args = command.trim().split(/\s+/); + const cmd = args[0].toLowerCase(); switch (cmd) { - case 'help': + case ':help': this.showHelp(); break; - case 'examples': + case ':examples': this.showExamples(); break; - case 'example': - await this.loadExample(args[0]); - break; - case 'state': + case ':state': this.showState(); break; - case 'history': + case ':history': this.showHistory(); break; - case 'rollback': - await this.rollbackToVersion(parseInt(args[0])); - break; - case 'adapters': + case ':adapters': this.showAdapters(); break; - case 'clear': + case ':clear': this.clearState(); break; - case 'save': - await this.saveState(args[0]); + case ':save': + await this.saveState(); break; - case 'load': - await this.loadState(args[0]); + case ':load': + await this.loadState(); break; - case 'run': - await this.runScriptFile(args[0]); + case ':menu': + await this.showInteractiveMenu(); break; - case 'branch': - await this.createBranch(args[0], args[1]); + 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 'menu': - await this.showInteractiveMenu(); + 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': - this.cleanup(); + case ':quit': + case ':exit': + case ':bye': + await this.cleanup(); process.exit(0); break; default: - console.log(`\x1b[31mUnknown command: ${cmd}\x1b[0m`); - console.log('Use :help for available commands'); + 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'); + } } } @@ -528,33 +1520,119 @@ pokemon_name : when state is * Show help information */ showHelp() { - console.log('\x1b[33mREPL Commands:\x1b[0m'); - console.log(' :help - Show this help message'); - console.log(' :examples - List available examples'); - console.log(' :example <name> - Load an example'); - console.log(' :state - Show current state'); - console.log(' :history - Show version history'); - console.log(' :rollback <version> - Rollback to version'); - console.log(' :adapters - Show available adapters'); - console.log(' :clear - Clear current state'); - console.log(' :save [filename] - Save state to file'); - console.log(' :load [filename] - Load state from file'); - console.log(' :run [filename] - Run script from file'); - console.log(' :branch <ver> <name> - Create branch from version'); - console.log(' :menu - Interactive history menu'); - console.log(' :quit, :exit - Exit REPL'); + 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('\x1b[33mInput Features:\x1b[0m'); - console.log(' • Multi-line input - End with semicolon (;) to execute'); - console.log(' • Auto-formatting - Multi-line scripts are auto-formatted'); - console.log(' • Always shows result - Last expression result is displayed'); - console.log(' • Function calls - Use assignment: result : func arg1 arg2;'); + 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(''); - console.log('\x1b[33mHarness Features:\x1b[0m'); - console.log(' • Pure functions - Scripts transform state'); - console.log(' • Versioning - Automatic state history'); - console.log(' • State management - Harness maintains state'); - console.log(' • TEA principles - Model → Update → View flow'); } /** @@ -607,14 +1685,15 @@ pokemon_name : when state is * Show version history */ showHistory() { - if (!this.harness) { - console.log('\x1b[90mNo history available\x1b[0m'); + 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.length === 0) { - console.log('\x1b[90mNo history\x1b[0m'); + if (!history || history.length === 0) { + console.log('\x1b[90mNo version history available\x1b[0m'); return; } @@ -622,25 +1701,31 @@ pokemon_name : when state is 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) { - console.log('\x1b[31mNo harness available\x1b[0m'); - return; + 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) { - console.log(`\x1b[31mRollback failed: ${error.message}\x1b[0m`); + throw new Error(`Rollback failed: ${error.message}`); } } @@ -703,51 +1788,224 @@ pokemon_name : when state is */ async runScriptFile(filename) { try { - const content = await fs.readFile(filename, 'utf8'); - console.log(`\x1b[33mRunning script from ${filename}:\x1b[0m`); + // 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`); } } /** - * Create a new branch from a specific version + * Show state diff between versions */ - async createBranch(fromVersion, branchName) { - try { - if (!fromVersion || !branchName) { - console.log('\x1b[31mUsage: :branch <version> <branch-name>\x1b[0m'); - console.log('\x1b[33mExample: :branch 1 experimental\x1b[0m'); - return; + 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)}`); } - - const version = parseInt(fromVersion); - if (isNaN(version)) { - console.log('\x1b[31mInvalid version number\x1b[0m'); - return; + 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'); + } + } - // Create new harness instance for the branch - const branchHarness = await this.harness.createBranch(version, branchName); + /** + * 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 + }); - // Create new REPL instance for the branch - const branchRepl = new REPL(); - branchRepl.harness = branchHarness; - branchRepl.currentState = this.harness.stateHistory.getVersion(version); - branchRepl.currentVersion = version; + 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✅ Created branch '${branchName}' from version ${version}\x1b[0m`); - console.log(`\x1b[33mBranch state:\x1b[0m`, this.formatValue(branchRepl.currentState)); + console.log(`\x1b[32m✅ Branch '${branchName}' created successfully\x1b[0m`); + console.log(`\x1b[36m📊 Branch info: ${JSON.stringify(branchInfo, null, 2)}\x1b[0m`); - // For now, just show the branch info - // In a full implementation, you might switch to the branch REPL - console.log(`\x1b[90m💡 To switch to this branch, you would use a branch manager\x1b[0m`); + // Store the branch harness for potential use + this.branches = this.branches || {}; + this.branches[branchName] = branchHarness; } catch (error) { - console.log(`\x1b[31mFailed to create branch: ${error.message}\x1b[0m`); + console.log(`\x1b[31m❌ Branch creation failed: ${error.message}\x1b[0m`); } } @@ -761,7 +2019,23 @@ pokemon_name : when state is output: process.stdout }); - const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve)); + 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) { @@ -775,18 +2049,34 @@ pokemon_name : when state is console.log(`\x1b[33m📊 State Keys: ${Object.keys(this.currentState || {}).length}\x1b[0m`); console.log(''); - // Show history + // 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(); - history.forEach((version, index) => { - const isCurrent = version === this.currentVersion; + if (history && history.length > 0) { + history.forEach((entry, index) => { + const isCurrent = entry.version === this.currentVersion; const marker = isCurrent ? '\x1b[33m▶\x1b[0m' : ' '; - const time = this.harness.stateHistory.getVersionTime(version); - console.log(` ${marker} ${version}: ${time || 'Unknown'}`); - }); + 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) + // 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'); @@ -794,37 +2084,80 @@ pokemon_name : when state is 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 + // 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'); - console.log(' 2. Rollback to version'); - console.log(' 3. Create branch'); - console.log(' 4. Compare versions'); + 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 (1-6): \x1b[0m'); + 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(); + await this.menuShowCurrentState(question); break; case '6': + console.log('\x1b[33mReturning to REPL...\x1b[0m'); rl.close(); return; default: @@ -834,7 +2167,14 @@ pokemon_name : when state is } } 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(); } } @@ -843,6 +2183,12 @@ pokemon_name : when state is * 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()); @@ -852,6 +2198,7 @@ pokemon_name : when state is return; } + try { const state = this.harness.stateHistory.getVersion(versionNum); if (!state) { console.log('\x1b[31mVersion not found\x1b[0m'); @@ -860,10 +2207,21 @@ pokemon_name : when state is } console.log(`\x1b[32m📋 Version ${versionNum} Details:\x1b[0m`); - console.log(`Time: ${this.harness.stateHistory.getVersionTime(versionNum) || 'Unknown'}`); + 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'); } @@ -872,6 +2230,12 @@ pokemon_name : when state is * 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()); @@ -902,6 +2266,12 @@ pokemon_name : when state is * 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'); @@ -931,6 +2301,12 @@ pokemon_name : when state is * 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'); @@ -943,6 +2319,7 @@ pokemon_name : when state is return; } + try { const state1 = this.harness.stateHistory.getVersion(v1); const state2 = this.harness.stateHistory.getVersion(v2); @@ -972,6 +2349,9 @@ pokemon_name : when state is } 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'); |