cui-server
Version:
Web UI Agent Platform based on Claude Code
170 lines • 6.36 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
import { DEFAULT_CONFIG } from '../types/config.js';
import { generateMachineId } from '../utils/machine-id.js';
import { createLogger } from './logger.js';
/**
* ConfigService manages CUI configuration
* Loads from ~/.cui/config.json
* Creates default config on first run
*/
export class ConfigService {
static instance;
config = null;
logger;
configPath;
configDir;
constructor() {
this.logger = createLogger('ConfigService');
this.configDir = path.join(os.homedir(), '.cui');
this.configPath = path.join(this.configDir, 'config.json');
}
/**
* Get singleton instance
*/
static getInstance() {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
/**
* Initialize configuration
* Creates config file if it doesn't exist
* Throws error if initialization fails
*/
async initialize() {
this.logger.info('Initializing configuration', { configPath: this.configPath });
try {
// Check if config exists
if (!fs.existsSync(this.configPath)) {
await this.createDefaultConfig();
}
// Load and validate config
await this.loadConfig();
}
catch (error) {
this.logger.error('Failed to initialize configuration', error);
throw new Error(`Configuration initialization failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get current configuration
* Throws if not initialized
*/
getConfig() {
if (!this.config) {
throw new Error('Configuration not initialized. Call initialize() first.');
}
return this.config;
}
/**
* Create default configuration
*/
async createDefaultConfig() {
this.logger.info('Creating default configuration');
try {
// Ensure config directory exists
if (!fs.existsSync(this.configDir)) {
fs.mkdirSync(this.configDir, { recursive: true });
this.logger.debug('Created config directory', { dir: this.configDir });
}
// Generate machine ID
const machineId = await generateMachineId();
this.logger.debug('Generated machine ID', { machineId });
// Generate crypto-secure auth token
const authToken = crypto.randomBytes(16).toString('hex'); // 32 character hex string
this.logger.debug('Generated auth token', { tokenLength: authToken.length });
// Create default config
const config = {
machine_id: machineId,
authToken,
...DEFAULT_CONFIG
};
// Write config file
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
this.logger.info('Default configuration created', {
path: this.configPath,
machineId: config.machine_id
});
}
catch (error) {
throw new Error(`Failed to create default config: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load configuration from file
*/
async loadConfig() {
try {
const configData = fs.readFileSync(this.configPath, 'utf-8');
const fileConfig = JSON.parse(configData);
// Merge with defaults for missing sections
let updated = false;
const merged = {
machine_id: fileConfig.machine_id,
authToken: fileConfig.authToken,
server: { ...DEFAULT_CONFIG.server, ...(fileConfig.server || {}) },
gemini: fileConfig.gemini,
interface: { ...DEFAULT_CONFIG.interface, ...(fileConfig.interface || {}) }
};
if (!fileConfig.server)
updated = true;
if (!fileConfig.interface)
updated = true;
// Check if any interface fields missing
if (JSON.stringify(merged.interface) !== JSON.stringify(fileConfig.interface)) {
updated = true;
}
// Validate required fields
if (!merged.machine_id) {
throw new Error('Invalid config: missing machine_id');
}
if (!merged.server || typeof merged.server.port !== 'number') {
throw new Error('Invalid config: missing or invalid server configuration');
}
if (!merged.authToken) {
throw new Error('Invalid config: missing authToken');
}
this.config = merged;
if (updated) {
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
this.logger.info('Configuration updated with defaults');
}
this.logger.debug('Configuration loaded successfully');
}
catch (error) {
throw new Error(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Update configuration
*/
async updateConfig(updates) {
if (!this.config) {
throw new Error('Configuration not initialized');
}
this.logger.info('Updating configuration', { updates });
// Update in-memory config
this.config = { ...this.config, ...updates };
// Write to file
try {
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
this.logger.info('Configuration updated successfully');
}
catch (error) {
this.logger.error('Failed to update configuration', error);
throw new Error(`Failed to update config: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Reset singleton instance (for testing)
*/
static resetInstance() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ConfigService.instance = null;
}
}
//# sourceMappingURL=config-service.js.map