/** * Config Handler * * CLI commands for configuration management. */ import { logger } from '../logger.ts'; import { theme } from '../colors.ts'; import { PATHS } from '../constants.ts'; import type { IModelGridConfig } from '../interfaces/config.ts'; import type { ITableColumn } from '../logger.ts'; import * as fs from 'node:fs/promises'; /** * Handler for configuration-related CLI commands */ export class ConfigHandler { /** * Show current configuration */ public async show(): Promise { logger.log(''); try { const configPath = PATHS.CONFIG_FILE; const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent) as IModelGridConfig; // Overview logger.logBox( 'ModelGrid Configuration', [ `Version: ${theme.highlight(config.version)}`, `Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`, '', theme.dim('Configuration File:'), ` ${theme.path(configPath)}`, ], 60, 'info', ); // API Configuration logger.log(''); logger.logBox( 'API Server', [ `Host: ${theme.info(config.api.host)}`, `Port: ${theme.highlight(String(config.api.port))}`, `API Keys: ${config.api.apiKeys.length} configured`, ...(config.api.rateLimit ? [`Rate Limit: ${config.api.rateLimit} req/min`] : []), '', theme.dim('Endpoint:'), ` http://${config.api.host}:${config.api.port}/v1/chat/completions`, ], 60, 'info', ); // Docker Configuration logger.log(''); logger.logBox( 'Docker', [ `Runtime: ${theme.info(config.docker.runtime)}`, `Network: ${config.docker.networkName}`, ], 60, 'default', ); // GPU Configuration logger.log(''); logger.logBox( 'GPU', [ `Auto Detect: ${config.gpus.autoDetect ? theme.success('Yes') : theme.dim('No')}`, `Assignments: ${Object.keys(config.gpus.assignments).length} GPU(s)`, ], 60, 'default', ); // Model Configuration logger.log(''); logger.logBox( 'Models', [ `Auto Pull: ${config.models.autoPull ? theme.success('Enabled') : theme.dim('Disabled')}`, `Default Container: ${config.models.defaultContainer}`, `Auto Load: ${config.models.autoLoad.length} model(s)`, '', theme.dim('Greenlist URL:'), ` ${config.models.greenlistUrl}`, ], 70, 'default', ); // Containers if (config.containers.length > 0) { logger.log(''); logger.info(`Containers (${config.containers.length}):`); logger.log(''); const rows = config.containers.map((c) => ({ id: c.id, name: c.name, type: c.type, image: c.image.length > 40 ? c.image.substring(0, 37) + '...' : c.image, port: c.port, gpus: c.gpuIds.length > 0 ? c.gpuIds.join(',') : theme.dim('None'), })); const columns: ITableColumn[] = [ { header: 'ID', key: 'id', align: 'left' }, { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'Type', key: 'type', align: 'left' }, { header: 'Image', key: 'image', align: 'left', color: theme.dim }, { header: 'Port', key: 'port', align: 'right' }, { header: 'GPUs', key: 'gpus', align: 'left' }, ]; logger.logTable(columns, rows); } logger.log(''); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { logger.logBox( 'No Configuration', [ 'No configuration file found.', '', theme.dim('Create configuration with:'), ` ${theme.command('modelgrid service enable')}`, '', theme.dim('Or manually create:'), ` ${PATHS.CONFIG_FILE}`, ], 60, 'warning', ); } else { logger.error(`Failed to read configuration: ${(error as Error).message}`); } } } /** * Initialize default configuration */ public async init(): Promise { const configPath = PATHS.CONFIG_FILE; // Check if config already exists try { await fs.access(configPath); logger.warn('Configuration file already exists'); logger.dim(` ${configPath}`); return; } catch { // File doesn't exist, continue } // Create config directory const configDir = PATHS.CONFIG_DIR; await fs.mkdir(configDir, { recursive: true }); // Create default config const defaultConfig: IModelGridConfig = { version: '1.0.0', api: { port: 8080, host: '0.0.0.0', apiKeys: [], cors: true, corsOrigins: ['*'], }, docker: { networkName: 'modelgrid', runtime: 'docker', }, gpus: { autoDetect: true, assignments: {}, }, containers: [], models: { greenlistUrl: 'https://code.foss.global/modelgrid.com/model_lists/raw/branch/main/greenlit.json', autoPull: true, defaultContainer: 'ollama', autoLoad: [], }, checkInterval: 30000, }; await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); logger.success('Configuration initialized'); logger.dim(` ${configPath}`); } /** * Add an API key */ public async addApiKey(key?: string): Promise { const configPath = PATHS.CONFIG_FILE; try { const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent) as IModelGridConfig; // Generate key if not provided const apiKey = key || this.generateApiKey(); if (config.api.apiKeys.includes(apiKey)) { logger.warn('API key already exists'); return; } config.api.apiKeys.push(apiKey); await fs.writeFile(configPath, JSON.stringify(config, null, 2)); logger.success('API key added:'); logger.log(` ${theme.highlight(apiKey)}`); logger.log(''); logger.dim('Use with Authorization header:'); logger.dim(` curl -H "Authorization: Bearer ${apiKey}" ...`); } catch (error) { logger.error(`Failed to add API key: ${(error as Error).message}`); } } /** * Remove an API key */ public async removeApiKey(key: string): Promise { if (!key) { logger.error('API key is required'); return; } const configPath = PATHS.CONFIG_FILE; try { const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent) as IModelGridConfig; const index = config.api.apiKeys.indexOf(key); if (index === -1) { logger.warn('API key not found'); return; } config.api.apiKeys.splice(index, 1); await fs.writeFile(configPath, JSON.stringify(config, null, 2)); logger.success('API key removed'); } catch (error) { logger.error(`Failed to remove API key: ${(error as Error).message}`); } } /** * List API keys */ public async listApiKeys(): Promise { const configPath = PATHS.CONFIG_FILE; try { const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent) as IModelGridConfig; if (config.api.apiKeys.length === 0) { logger.warn('No API keys configured'); logger.dim('Add a key with: modelgrid config apikey add'); return; } logger.info(`API Keys (${config.api.apiKeys.length}):`); logger.log(''); for (const key of config.api.apiKeys) { // Show partial key for security const masked = key.substring(0, 8) + '...' + key.substring(key.length - 4); logger.log(` ${masked}`); } logger.log(''); } catch (error) { logger.error(`Failed to list API keys: ${(error as Error).message}`); } } /** * Generate a random API key */ private generateApiKey(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const length = 48; let key = 'sk-'; for (let i = 0; i < length; i++) { key += chars.charAt(Math.floor(Math.random() * chars.length)); } return key; } }