@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
383 lines ⢠13.3 kB
JavaScript
/**
* Five CLI Config Command
*
* Manage Five CLI configuration settings for networks, keypairs, and deployment options.
*/
import chalk from 'chalk';
import ora from 'ora';
import { readFile } from 'fs/promises';
import { resolve } from 'path';
import { homedir } from 'os';
import * as readline from 'readline';
import { ConfigManager } from '../config/ConfigManager.js';
export const configCommand = {
name: 'config',
description: 'Manage Five CLI configuration',
aliases: ['cfg'],
options: [],
arguments: [
{
name: 'action',
description: 'Configuration action (init, get, set, reset)',
required: false
},
{
name: 'key',
description: 'Configuration key for get operations',
required: false
}
],
examples: [
{
command: 'five config init',
description: 'Initialize configuration with interactive setup'
},
{
command: 'five config get',
description: 'Show all configuration values'
},
{
command: 'five config get target',
description: 'Show current target network'
},
{
command: 'five config set --target devnet',
description: 'Set target network to devnet'
},
{
command: 'five config set --keypair ~/.solana/deployer.json',
description: 'Set keypair file path'
},
{
command: 'five config set --rpc-url https://api.custom.solana.com',
description: 'Set custom RPC URL for current target'
},
{
command: 'five config reset',
description: 'Reset configuration to defaults'
}
],
handler: async (args, options, context) => {
const { logger } = context;
const configManager = ConfigManager.getInstance();
const action = args[0] || 'get';
try {
switch (action) {
case 'init':
await handleInit(configManager, options, logger);
break;
case 'get':
await handleGet(configManager, args[1], options, logger);
break;
case 'set':
await handleSet(configManager, options, logger);
break;
case 'reset':
await handleReset(configManager, options, logger);
break;
default:
logger.error(`Unknown config action: ${action}`);
logger.info('Available actions: init, get, set, reset');
process.exit(1);
}
}
catch (error) {
logger.error('Configuration error:', error);
throw error;
}
}
};
/**
* Handle config init command with interactive setup
*/
async function handleInit(configManager, options, logger) {
try {
console.log(chalk.blue('\nđ§ Five CLI Configuration Setup\n'));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query) => {
return new Promise((resolve) => rl.question(query, resolve));
};
// Interactive setup
console.log('Let\'s configure your Five CLI environment:\n');
// Target network selection
const targetAnswer = await question(`Select target network (local/devnet/testnet/mainnet) [devnet]: `);
const target = targetAnswer.trim() || 'devnet';
if (!['local', 'devnet', 'testnet', 'mainnet'].includes(target)) {
throw new Error(`Invalid target: ${target}`);
}
// Keypair file path
const keypairAnswer = await question(`Keypair file path (optional) [~/.config/solana/id.json]: `);
const keypairPath = keypairAnswer.trim() || '~/.config/solana/id.json';
// Show config preference
const showConfigAnswer = await question(`Show config details in command output? (y/n) [n]: `);
const showConfig = showConfigAnswer.toLowerCase().startsWith('y');
rl.close();
// Initialize with default configuration first
await configManager.init();
// Apply user settings
await configManager.setTarget(target);
if (keypairPath && keypairPath !== '~/.config/solana/id.json') {
await configManager.setKeypair(keypairPath);
}
await configManager.setShowConfig(showConfig);
const config = await configManager.get();
console.log('\n' + formatConfig(config));
console.log(chalk.green('\nâ Configuration initialized successfully'));
}
catch (error) {
logger.error('Failed to initialize configuration:', error);
throw error;
}
}
/**
* Handle config get command
*/
async function handleGet(configManager, key, options, logger) {
try {
const config = await configManager.get();
if (options.format === 'json') {
if (key) {
const value = getConfigValue(config, key);
if (value === undefined) {
console.log(chalk.yellow(`Configuration key '${key}' not found`));
return;
}
console.log(JSON.stringify({ [key]: value }, null, 2));
}
else {
console.log(JSON.stringify(config, null, 2));
}
return;
}
if (key) {
const value = getConfigValue(config, key);
if (value === undefined) {
console.log(chalk.yellow(`Configuration key '${key}' not found`));
console.log(chalk.dim('Available keys: target, networks, keypair, showConfig'));
return;
}
console.log(`${chalk.cyan(key)}: ${formatValue(value)}`);
}
else {
// Display all configuration
console.log(formatConfig(config));
}
}
catch (error) {
logger.error('Failed to get configuration:', error);
throw error;
}
}
/**
* Handle config set command
*/
async function handleSet(configManager, options, logger) {
let hasChanges = false;
const changes = [];
try {
// Parse command line options and apply changes
if (options.target) {
if (!['local', 'devnet', 'testnet', 'mainnet'].includes(options.target)) {
throw new Error(`Invalid target: ${options.target}. Must be one of: devnet, testnet, mainnet, local`);
}
await configManager.setTarget(options.target);
changes.push(`${chalk.cyan('Target:')} ${options.target}`);
hasChanges = true;
}
if (options.keypair) {
// Validate keypair file exists and is readable
try {
const expandedPath = expandPath(options.keypair);
await readFile(expandedPath);
await configManager.setKeypair(options.keypair);
changes.push(`${chalk.cyan('Keypair:')} ${options.keypair}`);
hasChanges = true;
}
catch (error) {
throw new Error(`Keypair file not found or unreadable: ${options.keypair}`);
}
}
if (options.rpcUrl) {
const config = await configManager.get();
const currentTarget = config.target;
await configManager.set({
networks: {
...config.networks,
[currentTarget]: {
...config.networks[currentTarget],
rpcUrl: options.rpcUrl
}
}
});
changes.push(`${chalk.cyan('RPC URL')} (${currentTarget}): ${options.rpcUrl}`);
hasChanges = true;
}
if (options.showConfig !== undefined) {
await configManager.setShowConfig(Boolean(options.showConfig));
changes.push(`${chalk.cyan('Show Config:')} ${Boolean(options.showConfig)}`);
hasChanges = true;
}
if (!hasChanges) {
logger.error('No configuration changes specified');
logger.info('Available options: --target, --keypair, --rpc-url, --show-config');
process.exit(1);
}
const spinner = ora('Updating configuration...').start();
spinner.succeed('Configuration updated successfully');
// Show what was changed
console.log('\n' + chalk.bold('Updated configuration:'));
changes.forEach(change => console.log(` ${change}`));
// Optionally show full config
if (options.showConfig || options.verbose) {
const config = await configManager.get();
console.log('\n' + formatConfig(config));
}
}
catch (error) {
logger.error('Failed to update configuration:', error);
throw error;
}
}
/**
* Handle config reset command
*/
async function handleReset(configManager, options, logger) {
try {
console.log(chalk.yellow('\nâ ď¸ This will reset all configuration to defaults.'));
if (!options.yes) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query) => {
return new Promise((resolve) => rl.question(query, resolve));
};
const confirmation = await question('Are you sure? (y/N): ');
rl.close();
if (!confirmation.toLowerCase().startsWith('y')) {
console.log(chalk.dim('Reset cancelled'));
return;
}
}
const spinner = ora('Resetting configuration...').start();
await configManager.reset();
spinner.succeed('Configuration reset to defaults');
const config = await configManager.get();
console.log('\n' + formatConfig(config));
}
catch (error) {
logger.error('Failed to reset configuration:', error);
throw error;
}
}
/**
* Format configuration for display
*/
function formatConfig(config) {
const lines = [];
lines.push(chalk.bold('Five CLI Configuration:'));
lines.push('');
// Current target
lines.push(`${chalk.cyan('Target:')} ${config.target}`);
// Current network endpoint
const currentNetwork = config.networks[config.target];
lines.push(`${chalk.cyan('RPC URL:')} ${currentNetwork.rpcUrl}`);
if (currentNetwork.wsUrl) {
lines.push(`${chalk.cyan('WebSocket URL:')} ${currentNetwork.wsUrl}`);
}
// Keypair
if (config.keypair) {
lines.push(`${chalk.cyan('Keypair:')} ${config.keypair}`);
}
else {
lines.push(`${chalk.cyan('Keypair:')} ${chalk.dim('(not set)')}`);
}
// Show config preference
lines.push(`${chalk.cyan('Show Config:')} ${config.showConfig}`);
// All networks
lines.push('');
lines.push(chalk.bold('Available Networks:'));
for (const [target, network] of Object.entries(config.networks)) {
const isActive = target === config.target;
const prefix = isActive ? 'â' : 'â';
const color = isActive ? chalk.green : chalk.dim;
lines.push(` ${color(prefix)} ${target}: ${network.rpcUrl}`);
}
return lines.join('\n');
}
/**
* Get a nested configuration value by key
*/
function getConfigValue(config, key) {
const keys = key.split('.');
let value = config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
}
else {
return undefined;
}
}
return value;
}
/**
* Expand path with home directory substitution
*/
function expandPath(filePath) {
if (filePath.startsWith('~/')) {
return resolve(homedir(), filePath.slice(2));
}
return resolve(filePath);
}
/**
* Format configuration value for display
*/
function formatValue(value) {
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value, null, 2);
}
return String(value);
}
// Add config command options for the CLI parser
configCommand.options = [
{
flags: '--target <target>',
description: 'Set target network (local, devnet, testnet, mainnet)',
required: false
},
{
flags: '--keypair <file>',
description: 'Set keypair file path',
required: false
},
{
flags: '--rpc-url <url>',
description: 'Set custom RPC URL for current target network',
required: false
},
{
flags: '--show-config [value]',
description: 'Toggle config display in command output (true/false)',
required: false
},
{
flags: '--format <format>',
description: 'Output format (json, text)',
defaultValue: 'text'
},
{
flags: '--yes',
description: 'Skip confirmation prompts',
defaultValue: false
},
{
flags: '--verbose',
description: 'Show detailed output',
defaultValue: false
}
];
//# sourceMappingURL=config.js.map