UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

423 lines • 16.6 kB
/** * Configuration commands */ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import { globalOptions } from '../cli.js'; import { getConfig } from '../types/config.js'; import { ACEServerClient } from '../services/server-client.js'; import chalk from 'chalk'; import * as readline from 'readline/promises'; import { stdin, stdout } from 'process'; /** * Discover ACE server by pinging common URLs */ async function discoverServer() { const commonUrls = [ 'http://localhost:9000', 'https://ace-api.code-engine.app' ]; console.log(chalk.dim('šŸ” Searching for ACE server...')); for (const url of commonUrls) { try { const response = await fetch(`${url}/health`, { method: 'GET', signal: AbortSignal.timeout(3000) }); if (response.ok) { console.log(chalk.green(` āœ“ Found server at ${url}`)); return url; } } catch { // Continue to next URL } } console.log(chalk.yellow(' āœ— No server auto-detected')); return null; } /** * Validate token and fetch organization info using /api/v1/config/verify */ async function validateToken(serverUrl, apiToken) { try { const tempConfig = { serverUrl, apiToken, projectId: 'temp', cacheTtlMinutes: 120 }; const client = new ACEServerClient(tempConfig); // Use the proper /api/v1/config/verify endpoint const result = await client.verifyToken(); return { valid: true, orgId: result.org_id, orgName: result.org_name, projects: result.projects }; } catch (error) { return { valid: false }; } } /** * Non-interactive configuration (using flags) */ async function configNonInteractive(options) { try { // Validate token first const validation = await validateToken(options.serverUrl, options.apiToken); if (!validation.valid) { if (globalOptions.json) { console.log(JSON.stringify({ success: false, error: 'Invalid token' })); } else { console.log(chalk.red('āŒ Invalid token')); } process.exit(1); } // Create configuration const config = { serverUrl: options.serverUrl, apiToken: options.apiToken, projectId: options.projectId, cacheTtlMinutes: 120 }; const client = new ACEServerClient(config); // Save configuration await client.saveConfig(options.serverUrl, options.apiToken, options.projectId); // Output success if (globalOptions.json) { console.log(JSON.stringify({ success: true, serverUrl: options.serverUrl, projectId: options.projectId, orgId: validation.orgId, orgName: validation.orgName })); } else { console.log(chalk.green('\nāœ… Configuration saved successfully!')); console.log(chalk.dim(` Server: ${options.serverUrl}`)); console.log(chalk.dim(` Organization: ${validation.orgName} (${validation.orgId})`)); console.log(chalk.dim(` Project: ${options.projectId}`)); console.log(chalk.dim(` Config: ~/.config/ace/config.json\n`)); } } catch (error) { if (globalOptions.json) { console.error(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })); } else { console.log(chalk.red(`āŒ Error: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } /** * Configuration wizard - Interactive or non-interactive */ export async function configCommand(options) { // Non-interactive mode: all required flags provided if (options?.serverUrl && options?.apiToken && options?.projectId) { return await configNonInteractive({ serverUrl: options.serverUrl, apiToken: options.apiToken, orgId: options.orgId, projectId: options.projectId }); } // Interactive mode if (globalOptions.json) { console.error(JSON.stringify({ error: 'Interactive mode not supported with --json flag. Use --server-url, --api-token, and --project-id for non-interactive configuration.' })); process.exit(1); } console.log(chalk.bold('\nšŸ”§ ACE Configuration Wizard\n')); console.log(chalk.dim('Press Enter to keep existing values\n')); const rl = readline.createInterface({ input: stdin, output: stdout }); try { // Get existing config if it exists let existingConfig = {}; try { const config = getConfig(); existingConfig = config; } catch (error) { // No existing config } // Step 1: Server URL (with discovery) console.log(chalk.bold('Step 1: Server URL')); let discoveredUrl = null; if (!existingConfig.serverUrl) { discoveredUrl = await discoverServer(); } const defaultUrl = existingConfig.serverUrl || discoveredUrl || 'https://ace-api.code-engine.app'; const serverUrl = await rl.question(`Server URL ${chalk.dim(`[${defaultUrl}]`)}: `); const finalServerUrl = serverUrl.trim() || defaultUrl; console.log(''); // Step 2: API Token (with validation) console.log(chalk.bold('Step 2: API Token')); let finalApiToken = ''; let tokenValid = false; while (!tokenValid) { const apiToken = await rl.question(`API Token ${existingConfig.apiToken ? chalk.dim('[hidden]') : ''}: `); finalApiToken = apiToken.trim() || existingConfig.apiToken || ''; if (!finalApiToken) { console.log(chalk.red(' āœ— API token is required')); continue; } // Validate token process.stdout.write(chalk.dim(' Validating token...')); const validation = await validateToken(finalServerUrl, finalApiToken); if (validation.valid) { console.log(chalk.green(' āœ“ Valid')); tokenValid = true; // Show organization info if (validation.orgName && validation.orgId) { console.log(chalk.dim(` Organization: ${validation.orgName} (${validation.orgId})`)); } // Step 3: Project ID (with list if available) console.log(''); console.log(chalk.bold('Step 3: Project ID')); if (validation.projects && validation.projects.length > 0) { console.log(chalk.dim('Available projects:')); validation.projects.forEach((proj, idx) => { console.log(chalk.dim(` ${idx + 1}. ${proj.project_name} (${proj.project_id})`)); }); console.log(''); } const projectId = await rl.question(`Project ID ${existingConfig.projectId ? chalk.dim(`[${existingConfig.projectId}]`) : ''}: `); const finalProjectId = projectId.trim() || existingConfig.projectId; if (!finalProjectId) { console.log(chalk.red('\nāŒ Error: Project ID is required')); rl.close(); process.exit(1); } rl.close(); // Step 4: Test connection console.log(''); console.log(chalk.bold('Step 4: Testing connection...')); const testConfig = { serverUrl: finalServerUrl, apiToken: finalApiToken, projectId: finalProjectId, cacheTtlMinutes: 120 }; const testClient = new ACEServerClient(testConfig); try { await testClient.getAnalytics(); console.log(chalk.green(' āœ“ Connection successful')); } catch (error) { console.log(chalk.yellow(' ⚠ Warning: Could not connect to project')); } // Step 5: Save configuration console.log(''); console.log(chalk.bold('Step 5: Saving configuration...')); await testClient.saveConfig(finalServerUrl, finalApiToken, finalProjectId); console.log(chalk.green('\nāœ… Configuration saved successfully!')); console.log(chalk.dim(` Server: ${finalServerUrl}`)); console.log(chalk.dim(` Project: ${finalProjectId}`)); console.log(chalk.dim(` Config: ~/.config/ace/config.json\n`)); return; } else { console.log(chalk.red(' āœ— Invalid token')); console.log(chalk.yellow(' Please check your API token and try again\n')); } } } catch (error) { rl.close(); console.log(chalk.red(`\nāŒ Error: ${error instanceof Error ? error.message : String(error)}`)); process.exit(1); } } /** * Display current configuration */ export async function configShowCommand() { try { const config = getConfig(); if (globalOptions.json) { console.log(JSON.stringify({ serverUrl: config.serverUrl, projectId: config.projectId, apiToken: config.apiToken ? '***' : undefined, cacheTtlMinutes: config.cacheTtlMinutes, orgs: config.orgs ? Object.keys(config.orgs) : undefined }, null, 2)); } else { console.log(chalk.bold('\nšŸ“‹ Current Configuration\n')); console.log(` ${chalk.cyan('Server URL:')} ${config.serverUrl}`); console.log(` ${chalk.cyan('Project ID:')} ${config.projectId}`); console.log(` ${chalk.cyan('API Token:')} ${config.apiToken ? chalk.dim('***hidden***') : chalk.red('not set')}`); console.log(` ${chalk.cyan('Cache TTL:')} ${config.cacheTtlMinutes} minutes`); if (config.orgs && Object.keys(config.orgs).length > 0) { console.log(` ${chalk.cyan('Organizations:')} ${Object.keys(config.orgs).join(', ')}`); } console.log(''); } } catch (error) { if (globalOptions.json) { console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } else { console.log(chalk.red(`āŒ Error: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } /** * Set a configuration value */ export async function configSetCommand(key, value) { try { const configPath = join(homedir(), '.ace', 'config.json'); if (!existsSync(configPath)) { throw new Error('Configuration file not found. Run "ce-ace config" first.'); } const configData = JSON.parse(readFileSync(configPath, 'utf8')); // Validate key const validKeys = ['serverUrl', 'apiToken', 'projectId', 'cacheTtlMinutes']; if (!validKeys.includes(key)) { throw new Error(`Invalid key "${key}". Valid keys: ${validKeys.join(', ')}`); } // Set value (with type conversion for numbers) if (key === 'cacheTtlMinutes') { configData[key] = parseInt(value, 10); } else { configData[key] = value; } // Save config writeFileSync(configPath, JSON.stringify(configData, null, 2)); if (globalOptions.json) { console.log(JSON.stringify({ success: true, key, value: configData[key] })); } else { console.log(chalk.green(`āœ… Set ${chalk.cyan(key)} = ${value}`)); } } catch (error) { if (globalOptions.json) { console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } else { console.log(chalk.red(`āŒ Error: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } /** * Reset configuration to defaults */ export async function configResetCommand(options) { try { const configPath = join(homedir(), '.ace', 'config.json'); if (!existsSync(configPath)) { if (globalOptions.json) { console.log(JSON.stringify({ message: 'No configuration file exists' })); } else { console.log(chalk.yellow('āš ļø No configuration file exists')); } return; } // Confirm if --yes not provided if (!options.yes && !globalOptions.json) { const rl = readline.createInterface({ input: stdin, output: stdout }); const answer = await rl.question(chalk.yellow('āš ļø Are you sure you want to reset configuration? (y/N): ')); rl.close(); if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log(chalk.dim('Cancelled')); return; } } // Delete config file const fs = await import('fs/promises'); await fs.unlink(configPath); if (globalOptions.json) { console.log(JSON.stringify({ success: true, message: 'Configuration reset' })); } else { console.log(chalk.green('āœ… Configuration reset successfully')); } } catch (error) { if (globalOptions.json) { console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } else { console.log(chalk.red(`āŒ Error: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } /** * Validate token and return organization/project info (non-destructive) */ export async function configValidateCommand(options) { try { // Get server URL const serverUrl = options.serverUrl || process.env.ACE_SERVER_URL; if (!serverUrl) { throw new Error('Server URL required. Use --server-url or set ACE_SERVER_URL'); } // Get API token const apiToken = options.apiToken || process.env.ACE_API_TOKEN; if (!apiToken) { throw new Error('API token required. Use --api-token or set ACE_API_TOKEN'); } // Validate token using /api/v1/config/verify const validation = await validateToken(serverUrl, apiToken); if (!validation.valid) { if (globalOptions.json) { console.log(JSON.stringify({ valid: false, error: 'Invalid token' })); } else { console.log(chalk.red('āŒ Invalid token')); } process.exit(1); } // Return org/project info const result = { valid: true, org_id: validation.orgId, org_name: validation.orgName, projects: validation.projects }; if (globalOptions.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log(chalk.green('\nāœ… Token is valid\n')); console.log(chalk.bold(`Organization: ${validation.orgName} (${validation.orgId})\n`)); console.log(chalk.bold('Available Projects:')); validation.projects?.forEach((proj, idx) => { console.log(` ${idx + 1}. ${proj.project_name} ${chalk.dim(`(${proj.project_id})`)}`); }); console.log(''); } } catch (error) { if (globalOptions.json) { console.error(JSON.stringify({ valid: false, error: error instanceof Error ? error.message : String(error) })); } else { console.log(chalk.red(`āŒ Error: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } //# sourceMappingURL=config.js.map