initial
This commit is contained in:
314
ts/cli/config-handler.ts
Normal file
314
ts/cli/config-handler.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user