@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
291 lines • 9.66 kB
JavaScript
/**
* Five CLI Configuration Manager
*
* Handles configuration file loading, saving, and management with XDG directory support.
* Implements singleton pattern for global config access throughout the CLI.
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { DEFAULT_CONFIG, CONFIG_VALIDATORS } from './types.js';
/**
* Configuration management errors
*/
export class ConfigError extends Error {
cause;
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ConfigError';
}
}
/**
* Five CLI Configuration Manager
*
* Manages configuration file operations with XDG Base Directory support.
* Provides a singleton interface for configuration access across the CLI.
*/
export class ConfigManager {
static instance;
config;
configPath;
initialized = false;
/**
* Private constructor for singleton pattern
*/
constructor() {
this.config = { ...DEFAULT_CONFIG };
this.configPath = this.getConfigPath();
}
/**
* Get the singleton ConfigManager instance
*/
static getInstance() {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
/**
* Get the XDG-compliant config file path
* Follows XDG Base Directory specification:
* - Uses $XDG_CONFIG_HOME if set
* - Falls back to ~/.config/five/config.json
*/
getConfigPath() {
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
const configDir = xdgConfigHome
? join(xdgConfigHome, 'five')
: join(homedir(), '.config', 'five');
return join(configDir, 'config.json');
}
/**
* Initialize the configuration system
* Creates config directory and default config file if they don't exist
*/
async init() {
try {
// Ensure config directory exists
const configDir = this.configPath.replace('/config.json', '');
await fs.mkdir(configDir, { recursive: true });
// Try to load existing config, create default if none exists
try {
await this.load();
}
catch (error) {
if (error instanceof ConfigError && error.message.includes('not found')) {
// Create default config file
await this.save();
}
else {
throw error;
}
}
this.initialized = true;
}
catch (error) {
throw new ConfigError(`Failed to initialize configuration: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Load configuration from file
* @throws ConfigError if file doesn't exist or is invalid
*/
async load() {
try {
const configData = await fs.readFile(this.configPath, 'utf8');
let parsedConfig;
try {
parsedConfig = JSON.parse(configData);
}
catch (parseError) {
throw new ConfigError(`Invalid JSON in config file: ${this.configPath}`, parseError instanceof Error ? parseError : undefined);
}
// Validate the parsed configuration
if (!CONFIG_VALIDATORS.isValidConfig(parsedConfig)) {
throw new ConfigError(`Invalid configuration structure in: ${this.configPath}`);
}
// Merge with defaults to ensure all required fields are present
this.config = {
...DEFAULT_CONFIG,
...parsedConfig,
networks: {
...DEFAULT_CONFIG.networks,
...parsedConfig.networks,
},
};
return this.config;
}
catch (error) {
if (error instanceof ConfigError) {
throw error;
}
if (error?.code === 'ENOENT') {
throw new ConfigError(`Config file not found: ${this.configPath}`);
}
throw new ConfigError(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Save current configuration to file
* @throws ConfigError if save operation fails
*/
async save() {
try {
// Validate configuration before saving
if (!CONFIG_VALIDATORS.isValidConfig(this.config)) {
throw new ConfigError('Cannot save invalid configuration');
}
// Ensure config directory exists
const configDir = this.configPath.replace('/config.json', '');
await fs.mkdir(configDir, { recursive: true });
// Write configuration with pretty formatting
const configData = JSON.stringify(this.config, null, 2);
await fs.writeFile(this.configPath, configData, 'utf8');
}
catch (error) {
throw new ConfigError(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Get the current configuration
* Ensures the config manager is initialized
*/
async get() {
if (!this.initialized) {
await this.init();
}
return { ...this.config };
}
/**
* Update configuration with partial values
* @param updates Partial configuration updates
*/
async set(updates) {
if (!this.initialized) {
await this.init();
}
// Create updated configuration
const updatedConfig = {
...this.config,
...updates,
};
// If networks are being updated, merge with existing networks
if (updates.networks) {
updatedConfig.networks = {
...this.config.networks,
...updates.networks,
};
}
// Validate the updated configuration
if (!CONFIG_VALIDATORS.isValidConfig(updatedConfig)) {
throw new ConfigError('Invalid configuration update');
}
this.config = updatedConfig;
await this.save();
}
/**
* Set the target network
* @param target The target network to set
*/
async setTarget(target) {
if (!CONFIG_VALIDATORS.isValidTarget(target)) {
throw new ConfigError(`Invalid target: ${target}`);
}
await this.set({ target });
}
/**
* Set the keypair file path
* @param keypairPath Path to the keypair file
*/
async setKeypair(keypairPath) {
await this.set({ keypair: keypairPath });
}
/**
* Toggle config display in command output
* @param show Whether to show config details
*/
async setShowConfig(show) {
await this.set({ showConfig: show });
}
/**
* Get the current target network
*/
async getTarget() {
const config = await this.get();
return config.target;
}
/**
* Get the current network endpoint configuration
*/
async getCurrentNetworkEndpoint() {
const config = await this.get();
return config.networks[config.target];
}
/**
* Reset configuration to defaults
*/
async reset() {
this.config = { ...DEFAULT_CONFIG };
await this.save();
}
/**
* Check if the configuration is properly initialized
*/
isInitialized() {
return this.initialized;
}
/**
* Apply configuration overrides and return merged config
* @param overrides CLI option overrides
* @returns Merged configuration with overrides applied
*/
async applyOverrides(overrides) {
const baseConfig = await this.get();
// Create merged configuration
const mergedConfig = {
...baseConfig,
target: overrides.target || baseConfig.target,
keypairPath: overrides.keypair || baseConfig.keypair || this.getDefaultKeypairPath()
};
// Handle network override - if provided, create custom network config
if (overrides.network) {
mergedConfig.networks = {
...baseConfig.networks,
[mergedConfig.target]: {
...baseConfig.networks[mergedConfig.target],
rpcUrl: overrides.network
}
};
}
return mergedConfig;
}
/**
* Get target context prefix for display
* @param target The target network
* @returns Formatted target prefix like '[devnet]'
*/
static getTargetPrefix(target) {
const colors = {
wasm: '\x1b[36m', // cyan
local: '\x1b[90m', // gray
devnet: '\x1b[33m', // yellow
testnet: '\x1b[35m', // magenta
mainnet: '\x1b[31m' // red
};
const reset = '\x1b[0m';
const color = colors[target] || colors.devnet;
return `${color}[${target}]${reset}`;
}
/**
* Get default keypair path following Solana CLI conventions
*/
getDefaultKeypairPath() {
return join(homedir(), '.config', 'solana', 'id.json');
}
}
/**
* Singleton instance export for convenient access
*/
export const configManager = ConfigManager.getInstance();
//# sourceMappingURL=ConfigManager.js.map