@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
707 lines ⢠28.1 kB
JavaScript
/**
* 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