automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
280 lines (279 loc) • 9.99 kB
JavaScript
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import { config } from 'dotenv';
export class SettingsManager {
settingsDir;
settingsPath;
SETTINGS_VERSION = '1.0.0';
constructor() {
this.settingsDir = path.join(os.homedir(), '.automagik-cli');
this.settingsPath = path.join(this.settingsDir, 'settings.json');
}
/**
* Ensure the settings directory exists
*/
async ensureDirectoryExists() {
try {
await fs.mkdir(this.settingsDir, { recursive: true });
}
catch (error) {
if (error.code !== 'EEXIST') {
throw new Error(`Failed to create settings directory: ${error}`);
}
}
}
/**
* Get default configuration with sensible defaults
*/
getDefaultConfig() {
const now = new Date().toISOString();
return {
// API Configuration - Default to localhost:8886 as specified
apiBaseUrl: 'http://localhost:8886',
apiKey: undefined, // Optional
apiTimeout: 30000,
apiRetryAttempts: 3,
// CLI Configuration
cliDebug: false,
// Session Configuration
sessionDir: path.join(this.settingsDir, 'sessions'),
sessionMaxHistory: 100,
sessionAutoSave: true,
// Display Configuration
enableColors: true,
enableSpinner: true,
maxDisplayWidth: 200,
// Development Configuration
nodeEnv: 'production',
logLevel: 'error',
// Meta fields
version: this.SETTINGS_VERSION,
createdAt: now,
updatedAt: now,
};
}
/**
* Validate settings configuration
*/
validate(config) {
const errors = [];
// Required fields
if (!config.apiBaseUrl) {
errors.push('apiBaseUrl is required');
}
else if (!this.isValidUrl(config.apiBaseUrl)) {
errors.push('apiBaseUrl must be a valid URL');
}
// Type validations
if (config.apiTimeout !== undefined && (typeof config.apiTimeout !== 'number' || config.apiTimeout <= 0)) {
errors.push('apiTimeout must be a positive number');
}
if (config.apiRetryAttempts !== undefined && (typeof config.apiRetryAttempts !== 'number' || config.apiRetryAttempts < 0)) {
errors.push('apiRetryAttempts must be a non-negative number');
}
if (config.sessionMaxHistory !== undefined && (typeof config.sessionMaxHistory !== 'number' || config.sessionMaxHistory <= 0)) {
errors.push('sessionMaxHistory must be a positive number');
}
if (config.maxDisplayWidth !== undefined && (typeof config.maxDisplayWidth !== 'number' || config.maxDisplayWidth <= 0)) {
errors.push('maxDisplayWidth must be a positive number');
}
return { valid: errors.length === 0, errors };
}
/**
* Load settings from JSON file
*/
async load() {
try {
await this.ensureDirectoryExists();
// Check if settings file exists
const exists = await this.fileExists(this.settingsPath);
if (!exists) {
// No settings file - return defaults
return this.getDefaultConfig();
}
// Read and parse settings file
const data = await fs.readFile(this.settingsPath, 'utf8');
const parsed = JSON.parse(data);
// Validate loaded settings
const validation = this.validate(parsed);
if (!validation.valid) {
console.warn('Settings validation failed:', validation.errors);
// Return defaults with warnings but don't throw
return this.getDefaultConfig();
}
// Merge with defaults to ensure all fields are present
const defaults = this.getDefaultConfig();
const merged = { ...defaults, ...parsed };
merged.updatedAt = new Date().toISOString();
return merged;
}
catch (error) {
if (error instanceof SyntaxError) {
// JSON parse error - backup corrupted file and return defaults
await this.backupCorruptedFile();
console.warn('Settings file corrupted, using defaults');
return this.getDefaultConfig();
}
throw new Error(`Failed to load settings: ${error}`);
}
}
/**
* Save settings to JSON file with atomic operations
*/
async save(config) {
try {
await this.ensureDirectoryExists();
// Validate before saving
const validation = this.validate(config);
if (!validation.valid) {
throw new Error(`Invalid settings: ${validation.errors.join(', ')}`);
}
// Update metadata
config.version = this.SETTINGS_VERSION;
config.updatedAt = new Date().toISOString();
// Atomic write: write to temp file then rename
const tempPath = `${this.settingsPath}.tmp`;
const jsonContent = JSON.stringify(config, null, 2);
await fs.writeFile(tempPath, jsonContent, 'utf8');
await fs.rename(tempPath, this.settingsPath);
}
catch (error) {
throw new Error(`Failed to save settings: ${error}`);
}
}
/**
* Check if settings file exists
*/
async settingsExist() {
return this.fileExists(this.settingsPath);
}
/**
* Detect and migrate from .env file
*/
async migrate() {
try {
// Check if settings already exist
if (await this.settingsExist()) {
return { migrated: false, source: 'defaults' };
}
// Look for .env file in current directory
const envPath = path.join(process.cwd(), '.env');
const envExists = await this.fileExists(envPath);
if (!envExists) {
// No .env file found, create with defaults
const defaults = this.getDefaultConfig();
await this.save(defaults);
return { migrated: true, source: 'defaults' };
}
// Load .env file
config({ path: envPath });
// Create settings from environment variables
const envConfig = this.createFromEnv();
// Validate migrated config
const validation = this.validate(envConfig);
if (!validation.valid) {
return {
migrated: false,
source: 'env',
errors: validation.errors
};
}
// Save migrated settings
await this.save(envConfig);
return {
migrated: true,
source: 'env'
};
}
catch (error) {
return {
migrated: false,
source: 'env',
errors: [`Migration failed: ${error}`]
};
}
}
/**
* Create settings config from environment variables (for migration)
*/
createFromEnv() {
const now = new Date().toISOString();
return {
// API Configuration
apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8886',
apiKey: process.env.API_KEY || undefined,
apiTimeout: parseInt(process.env.API_TIMEOUT || '30000', 10),
apiRetryAttempts: parseInt(process.env.API_RETRY_ATTEMPTS || '3', 10),
// CLI Configuration
cliDebug: process.env.CLI_DEBUG?.toLowerCase() === 'true',
// Session Configuration
sessionDir: process.env.SESSION_DIR || path.join(this.settingsDir, 'sessions'),
sessionMaxHistory: parseInt(process.env.SESSION_MAX_HISTORY || '100', 10),
sessionAutoSave: process.env.SESSION_AUTO_SAVE?.toLowerCase() !== 'false',
// Display Configuration
enableColors: process.env.ENABLE_COLORS?.toLowerCase() !== 'false',
enableSpinner: process.env.ENABLE_SPINNER?.toLowerCase() !== 'false',
maxDisplayWidth: parseInt(process.env.MAX_DISPLAY_WIDTH || '200', 10),
// Development Configuration
nodeEnv: process.env.NODE_ENV || 'production',
logLevel: process.env.LOG_LEVEL || 'error',
// Meta fields
version: this.SETTINGS_VERSION,
createdAt: now,
updatedAt: now,
};
}
/**
* Backup corrupted settings file
*/
async backupCorruptedFile() {
try {
const backupPath = `${this.settingsPath}.corrupted.${Date.now()}`;
await fs.copyFile(this.settingsPath, backupPath);
await fs.unlink(this.settingsPath);
}
catch (error) {
// Ignore backup failures - we'll create new settings anyway
}
}
/**
* Check if file exists
*/
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* Validate URL format
*/
isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
/**
* Get settings file path (for external access)
*/
getSettingsPath() {
return this.settingsPath;
}
/**
* Reset settings to defaults
*/
async reset() {
const defaults = this.getDefaultConfig();
await this.save(defaults);
}
}
// Export singleton instance
export const settingsManager = new SettingsManager();