UNPKG

@five-vm/cli

Version:

High-performance CLI for Five VM development with WebAssembly integration

707 lines • 28.1 kB
/** * Five CLI Local Command * * Complete local command namespace for WASM execution that bypasses config * and always runs locally. Provides fast development experience without * network dependencies. */ import { readFile, readdir, stat } from 'fs/promises'; import { join, extname, basename } from 'path'; import chalk from 'chalk'; import ora from 'ora'; import { FiveSDK, compileAndExecuteLocally } from '../sdk/index.js'; /** * Main local command - parent for all local subcommands */ export const localCommand = { name: 'local', description: 'Local WASM execution', aliases: ['l'], options: [ { flags: '-f, --function <call>', description: 'Function call: add(5, 3) or function name/index' }, { flags: '-p, --params <values...>', description: 'Function parameters (space-separated or JSON)' }, { flags: '--debug', description: 'Enable debug output' }, { flags: '--trace', description: 'Enable execution trace' }, { flags: '--max-cu <units>', description: 'Max compute units (default: 1000000)' }, { flags: '--max-compute-units <units>', description: 'Alias for --max-cu' }, { flags: '--format <format>', description: 'Output format: text, json (default: text)' } ], arguments: [ { name: 'subcommand', description: 'Local subcommand (execute, test, compile)', required: true }, { name: 'args', description: 'Arguments for the subcommand', required: false, variadic: true } ], examples: [ { command: 'five local execute script.bin', description: 'Execute script locally using WASM VM' }, { command: 'five local execute script.five add 5 3', description: 'Execute add function with parameters 5 and 3' }, { command: 'five local execute script.five test', description: 'Execute test function with no parameters' }, { command: 'five local execute script.five 2', description: 'Execute function at index 2' }, { command: 'five local test', description: 'Run local test suite with WASM VM' } ], handler: async (args, options, context) => { const { logger } = context; // Fix: Handle nested array structure from Commander.js const flatArgs = Array.isArray(args[0]) ? args[0] : args; if (flatArgs.length === 0) { showLocalHelp(logger); return; } const subcommand = flatArgs[0]; const subArgs = flatArgs.slice(1); // Local WASM execution mode if (context.options.verbose) { logger.info(chalk.blue('šŸ”§ Local WASM VM')); } try { switch (subcommand) { case 'execute': case 'exec': case 'run': await executeLocalSubcommand(subArgs, options, context); break; case 'test': case 't': await testLocalSubcommand(subArgs, options, context); break; case 'compile': case 'c': await compileLocalSubcommand(subArgs, options, context); break; default: logger.error(`Unknown local subcommand: ${subcommand}`); showLocalHelp(logger); process.exit(1); } } catch (error) { logger.error(`Local ${subcommand} failed:`, error); throw error; } } }; /** * Local execute subcommand - execute bytecode in local WASM VM */ async function executeLocalSubcommand(args, options, context) { const { logger } = context; console.log(`[LocalCommand] executeLocalSubcommand called with:`, { args, optionsDebug: options.debug }); if (args.length === 0) { logger.error('No file specified for execution'); logger.info('Usage: five local execute <file> [function] [params...]'); logger.info(' <file> .five/.bin/.v file to execute'); logger.info(' [function] Function name or index'); logger.info(' [params...] Function parameters (space-separated)'); logger.info('Options:'); logger.info(' --debug Enable debug output'); logger.info(' --trace Enable execution trace'); logger.info(' --max-cu <n> Max compute units (default: 1000000)'); logger.info(' --format <f> Output format: text, json (default: text)'); logger.info('Examples:'); logger.info(' five local execute script.five add 5 3'); logger.info(' five local execute script.five test'); logger.info(' five local execute script.five 2'); return; } // Fix: Handle nested array structure from Commander.js const flatArgs = Array.isArray(args[0]) ? args[0] : args; const inputFile = flatArgs[0]; const rawFunctionName = flatArgs.length > 1 ? flatArgs[1] : undefined; // Convert numeric strings to numbers for function index let functionName = rawFunctionName; if (rawFunctionName && /^\d+$/.test(rawFunctionName)) { functionName = parseInt(rawFunctionName, 10); } const functionParams = flatArgs.length > 2 ? flatArgs.slice(2) : []; console.log(`[LocalCommand] Parsed arguments:`, { inputFile, functionName, functionParams }); // Initialize for local execution if (context.options.verbose) { const spinner = ora('Preparing local execution...').start(); spinner.text = 'Loading Five SDK...'; spinner.succeed('Five SDK ready for local execution'); } try { // Use space-separated syntax: file function param1 param2 ... let functionRef = functionName || options.function || options.functionIndex || 0; const parameters = functionParams.length > 0 ? (Array.isArray(functionParams) ? functionParams.map(parseValue) : [parseValue(functionParams)]) : parseParameters(options.params); const debug = Boolean(options.debug || options.trace); const trace = Boolean(options.trace); const maxCU = options.maxCu || options.maxComputeUnits || 1000000; const format = options.format || 'text'; // Keep function reference as-is - let the compiler handle function name resolution // The Five compiler can resolve function names to indices properly // Debug parameter parsing if (debug) { console.log(`[LocalCommand] Function: ${functionRef}, Parameters: ${JSON.stringify(parameters)}`); console.log(`[LocalCommand] Parameter types:`, parameters.map((p) => `${typeof p}: ${p}`)); } let result; if (extname(inputFile) === '.five') { // Handle .five files with embedded ABI logger.info(`Executing Five file: ${inputFile}`); const fileContent = await readFile(inputFile, 'utf8'); const { bytecode, abi } = await FiveSDK.loadFiveFile(fileContent); // FUNCTION RESOLUTION FIX: Systematic approach for .five files let resolvedFunctionRef = functionRef; if (functionRef === undefined) { // SYSTEMATIC APPROACH: Public functions always have the lowest indices (0, 1, 2...) // So we always default to function index 0, which is guaranteed to be public if any functions exist resolvedFunctionRef = 0; if (debug && abi && abi.functions) { let functionName = 'function_0'; if (Array.isArray(abi.functions)) { // FIVEABI format: find function with index 0 const func0 = abi.functions.find((f) => f.index === 0); if (func0 && func0.name) functionName = func0.name; } else { // SimpleABI format: find function with index 0 const func0Entry = Object.entries(abi.functions).find(([_, f]) => f.index === 0); if (func0Entry) functionName = func0Entry[0]; } console.log(`[LocalCommand] Auto-detected public function: ${functionName} (index 0 - first public function)`); } } result = await FiveSDK.executeLocally(bytecode, resolvedFunctionRef, parameters, { debug, trace, computeUnitLimit: maxCU, abi // Pass ABI for function name resolution }); } else if (extname(inputFile) === '.v') { // Compile and execute Five source file logger.info(`Compiling and executing Five source: ${inputFile}`); const sourceCode = await readFile(inputFile, 'utf8'); result = await compileAndExecuteLocally(sourceCode, functionRef, parameters, { debug, trace, optimize: true, computeUnitLimit: maxCU }); // Show compilation info if ('compilation' in result && result.compilation) { console.log(chalk.gray(`Compilation: ${result.compilation.success ? 'Success' : 'Failed'}`)); if ('bytecodeSize' in result && result.bytecodeSize) { console.log(chalk.gray(`Bytecode size: ${result.bytecodeSize} bytes`)); } } } else { // Execute existing bytecode file logger.info(`Executing bytecode: ${inputFile}`); const bytecode = await readFile(inputFile); result = await FiveSDK.executeLocally(new Uint8Array(bytecode), functionRef, parameters, { debug, trace, computeUnitLimit: maxCU }); } // Validate execution result structure if (typeof result !== 'object' || result === null) { logger.error('Invalid execution result from SDK'); process.exit(1); } if (typeof result.success !== 'boolean') { logger.error('Execution result missing success field'); process.exit(1); } // Display results displayLocalExecutionResult(result, { format, trace, debug }, logger); // CRITICAL FIX: Check execution success and exit with proper code if (!result.success) { if (context.options.verbose) { logger.error(`Execution failed: ${result.error || 'Unknown error'}`); } process.exit(1); // EXIT WITH FAILURE CODE } } catch (error) { if (context.options.verbose) { ora().fail('Local execution failed'); } process.exit(1); // Also ensure exceptions exit with failure } } /** * Local test subcommand - run test suite locally */ async function testLocalSubcommand(args, options, context) { const { logger } = context; // Fix: Handle nested array structure from Commander.js const testPath = (Array.isArray(args[0]) ? args[0][0] : args[0]) || './tests'; const pattern = options.pattern || '**/*.test.json'; const filter = options.filter; const verbose = options.verbose || options.debug; const format = options.format || 'text'; logger.info('Running local test suite with WASM VM'); logger.info(`Test path: ${testPath}`); logger.info(`Pattern: ${pattern}`); const spinner = ora('Discovering test files...').start(); try { // Discover test files const testFiles = await discoverTestFiles(testPath, pattern); if (testFiles.length === 0) { spinner.warn('No test files found'); logger.warn(`No test files matching pattern "${pattern}" found in ${testPath}`); logger.info('Expected test file format: JSON files with .test.json extension'); return; } spinner.succeed(`Found ${testFiles.length} test file(s)`); // Run tests const results = await runLocalTests(testFiles, { filter, verbose, maxCU: options.maxCu || 1000000, timeout: options.timeout || 30000 }, logger); // Display results displayTestResults(results, { format, verbose }, logger); // Exit with error if any tests failed const failed = results.some(r => !r.passed); if (failed) { process.exit(1); } } catch (error) { spinner.fail('Test discovery failed'); throw error; } } /** * Local compile subcommand - alias for main compile command */ async function compileLocalSubcommand(args, options, context) { const { logger } = context; if (args.length === 0) { logger.error('No file specified for compilation'); logger.info('Usage: five local compile <file> [options]'); logger.info('This is an alias for: five compile <file>'); return; } logger.info('Compiling locally (same as five compile)'); // Import and delegate to compile command const { compileCommand } = await import('./compile.js'); await compileCommand.handler(args, options, context); } /** * Show local command help */ function showLocalHelp(logger) { logger.info('Local Five VM Commands (WASM execution, no config needed):'); logger.info(''); logger.info('Subcommands:'); logger.info(' execute <file> Execute bytecode or source file locally'); logger.info(' test [pattern] Run local test suite'); logger.info(' compile <file> Compile source file (alias for main compile)'); logger.info(''); logger.info('Examples:'); logger.info(' five local execute script.bin'); logger.info(' five local execute script.v --function 1 --parameters "[10, 5]"'); logger.info(' five local test --pattern "*.test.json" --verbose'); logger.info(' five local compile script.v --optimize'); logger.info(''); logger.info('Features:'); logger.info(' • Always uses WASM VM (no network required)'); logger.info(' • No configuration dependencies'); logger.info(' • Fast development iteration'); logger.info(' • Compute unit tracking'); logger.info(' • Debug and trace support'); } /** * Parse function call syntax: add(5, 3) -> { name: 'add', params: [5, 3] } */ function parseFunctionCall(functionOption) { if (typeof functionOption === 'number') { return { functionRef: functionOption, parameters: [] }; } const functionStr = functionOption.toString().trim(); // Check if it's a function call syntax: name(params...) const callMatch = functionStr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*(.*?)\s*\)$/); if (callMatch) { const [, functionName, paramsStr] = callMatch; let parameters = []; if (paramsStr.trim()) { // Parse comma-separated parameters try { // Split by comma and parse each parameter const paramStrs = paramsStr.split(',').map(p => p.trim()); parameters = paramStrs.map(parseValue); } catch (error) { throw new Error(`Failed to parse function parameters in "${functionStr}": ${error}`); } } return { functionRef: functionName, parameters }; } // Check if it's a plain number (function index) const numericValue = parseInt(functionStr, 10); if (!isNaN(numericValue) && numericValue.toString() === functionStr) { return { functionRef: numericValue, parameters: [] }; } // Plain function name return { functionRef: functionStr, parameters: [] }; } /** * Parse parameters from various formats: space-separated, JSON string, or array */ function parseParameters(paramsOption) { if (!paramsOption) { return []; } try { // Handle arrays directly (from --params a b c) if (Array.isArray(paramsOption)) { return paramsOption.map(parseValue); } // Handle string input if (typeof paramsOption === 'string') { // JSON array format if (paramsOption.startsWith('[') || paramsOption.startsWith('{')) { return JSON.parse(paramsOption); } // Single parameter return [parseValue(paramsOption)]; } // Single value return [parseValue(paramsOption)]; } catch (error) { throw new Error(`Failed to parse parameters: ${error}. Use space-separated values like: -p 10 5, or JSON format: --parameters "[10, 5]"`); } } /** * Parse a single parameter value, converting strings to appropriate types */ function parseValue(value) { if (typeof value !== 'string') { return value; } // Try to parse as number const numValue = Number(value); if (!isNaN(numValue) && isFinite(numValue)) { return numValue; } // Try to parse as boolean if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; // Return as string return value; } /** * Display local execution results with clear formatting */ function displayLocalExecutionResult(result, options, logger) { // Only show header in verbose mode if (options.verbose || options.debug) { console.log('\n' + chalk.bold('šŸ”§ Local WASM VM Execution Results:')); } if (result.success) { console.log(chalk.green('āœ“ Execution successful')); if (result.result !== undefined) { console.log(` Result: ${chalk.cyan(JSON.stringify(result.result))}`); } if (result.executionTime) { console.log(` Execution time: ${chalk.gray(result.executionTime + 'ms')}`); } if (result.computeUnitsUsed !== undefined) { console.log(` Compute units used: ${chalk.gray(result.computeUnitsUsed.toLocaleString())}`); } if (result.logs && result.logs.length > 0) { console.log('\n' + chalk.bold('Execution logs:')); result.logs.forEach((log) => { console.log(chalk.gray(` ${log}`)); }); } if (result.trace && options.trace) { console.log('\n' + chalk.bold('Execution trace:')); if (Array.isArray(result.trace)) { result.trace.slice(0, 10).forEach((step, i) => { console.log(chalk.gray(` ${i}: ${JSON.stringify(step)}`)); }); if (result.trace.length > 10) { console.log(chalk.gray(` ... and ${result.trace.length - 10} more steps`)); } } } } else { console.log(chalk.red('āœ— Execution failed')); if (result.error) { console.log(chalk.red(` Error: ${result.error}`)); } // Enhanced VM debugging information for runtime errors if (result.vmState || result.debug) { const vmInfo = result.vmState || result.debug || result; console.log(chalk.yellow('\n šŸ” VM Debug Information:')); if (vmInfo.instructionPointer !== undefined) { console.log(chalk.yellow(` Instruction Pointer: 0x${vmInfo.instructionPointer.toString(16).toUpperCase().padStart(4, '0')}`)); } if (vmInfo.stoppedAtOpcode !== undefined) { console.log(chalk.yellow(` Stopped at Opcode: 0x${vmInfo.stoppedAtOpcode.toString(16).toUpperCase().padStart(2, '0')} (${vmInfo.stoppedAtOpcode})`)); } if (vmInfo.stoppedAtOpcodeName) { console.log(chalk.yellow(` Opcode Name: ${vmInfo.stoppedAtOpcodeName}`)); } if (vmInfo.errorMessage) { console.log(chalk.yellow(` VM Error: ${vmInfo.errorMessage}`)); } if (vmInfo.finalStack && Array.isArray(vmInfo.finalStack)) { console.log(chalk.yellow(` Final Stack (${vmInfo.finalStack.length} items):`)); vmInfo.finalStack.slice(-5).forEach((item, i) => { const index = vmInfo.finalStack.length - 5 + i; console.log(chalk.yellow(` [${index}]: ${JSON.stringify(item)}`)); }); if (vmInfo.finalStack.length > 5) { console.log(chalk.yellow(` ... (${vmInfo.finalStack.length - 5} more items)`)); } } if (vmInfo.executionContext) { console.log(chalk.yellow(` Execution Context: ${vmInfo.executionContext}`)); } } if (result.compilationErrors && result.compilationErrors.length > 0) { console.log(chalk.red('\n Compilation errors:')); result.compilationErrors.forEach((error) => { console.log(chalk.red(` • ${error.message || error}`)); }); } } // JSON format output if (options.format === 'json') { console.log('\n' + chalk.bold('JSON Output:')); console.log(JSON.stringify(result, null, 2)); } } /** * Discover test files based on pattern */ async function discoverTestFiles(testPath, pattern) { const testFiles = []; try { const stats = await stat(testPath); if (stats.isFile()) { // Single test file if (testPath.endsWith('.test.json')) { testFiles.push(testPath); } } else if (stats.isDirectory()) { // Directory - recursively find test files const files = await readdir(testPath, { recursive: true }); for (const file of files) { if (typeof file === 'string' && file.endsWith('.test.json')) { testFiles.push(join(testPath, file)); } } } } catch (error) { // Directory/file doesn't exist - that's ok } return testFiles; } /** * Run tests locally using WASM VM */ async function runLocalTests(testFiles, options, logger) { const results = []; for (const testFile of testFiles) { logger.info(`Running tests from: ${testFile}`); try { const content = await readFile(testFile, 'utf8'); const testSuite = JSON.parse(content); const suiteName = testSuite.name || basename(testFile, '.test.json'); const testCases = testSuite.tests || testSuite.testCases || []; // Apply filter if specified let filteredTests = testCases; if (options.filter) { filteredTests = testCases.filter((test) => test.name?.includes(options.filter) || suiteName.includes(options.filter)); } for (const testCase of filteredTests) { const result = await runSingleLocalTest(testCase, options, logger); results.push(result); if (options.verbose) { displaySingleTestResult(result, logger); } } } catch (error) { results.push({ name: `Load ${basename(testFile)}`, passed: false, duration: 0, error: `Failed to load test file: ${error}` }); } } return results; } /** * Run a single test case locally */ async function runSingleLocalTest(testCase, options, logger) { const startTime = Date.now(); try { // Load bytecode const bytecode = await readFile(testCase.bytecode); // Parse input parameters let parameters = []; if (testCase.input) { try { const inputData = await readFile(testCase.input, 'utf8'); parameters = JSON.parse(inputData); } catch { parameters = []; } } // Execute with timeout const executionPromise = FiveSDK.executeLocally(new Uint8Array(bytecode), testCase.functionIndex || 0, parameters, { debug: options.verbose, trace: options.verbose, computeUnitLimit: options.maxCU }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), options.timeout)); const result = await Promise.race([executionPromise, timeoutPromise]); const duration = Date.now() - startTime; // Validate result const passed = validateTestResult(result, testCase.expected || {}); return { name: testCase.name, passed, duration, details: options.verbose ? result : undefined }; } catch (error) { const duration = Date.now() - startTime; // Check if error was expected const passed = testCase.expected?.success === false && testCase.expected?.error !== undefined && error instanceof Error && error.message.includes(testCase.expected.error); return { name: testCase.name, passed, duration, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Validate test result against expected outcome */ function validateTestResult(result, expected) { // Check success/failure if (result.success !== expected.success) { return false; } // If expecting success, check result value if (expected.success && expected.result !== undefined) { if (JSON.stringify(result.result) !== JSON.stringify(expected.result)) { return false; } } // Check compute units limit if (expected.maxComputeUnits && result.computeUnitsUsed > expected.maxComputeUnits) { return false; } return true; } /** * Display single test result */ function displaySingleTestResult(result, logger) { const status = result.passed ? chalk.green('āœ“') : chalk.red('āœ—'); const duration = chalk.gray(`(${result.duration}ms)`); console.log(` ${status} ${result.name} ${duration}`); if (!result.passed && result.error) { console.log(chalk.red(` Error: ${result.error}`)); } } /** * Display comprehensive test results */ function displayTestResults(results, options, logger) { if (options.format === 'json') { console.log(JSON.stringify(results, null, 2)); return; } console.log('\n' + chalk.bold('šŸ”§ Local Test Results:')); const passed = results.filter(r => r.passed).length; const total = results.length; const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); // Summary console.log(`\nSummary:`); console.log(` Total: ${total} tests`); console.log(` ${chalk.green(`Passed: ${passed}`)}`); const failed = total - passed; if (failed > 0) { console.log(` ${chalk.red(`Failed: ${failed}`)}`); // Show failed tests if (!options.verbose) { console.log('\nFailed tests:'); results.filter(r => !r.passed).forEach(result => { console.log(` ${chalk.red('āœ—')} ${result.name}: ${result.error || 'Test failed'}`); }); } } console.log(` Duration: ${totalDuration}ms`); if (failed === 0) { console.log(chalk.green('\nšŸŽ‰ All tests passed!')); } else { console.log(chalk.red(`\nšŸ’„ ${failed} test(s) failed`)); } } //# sourceMappingURL=local.js.map