@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
423 lines ⢠16.6 kB
JavaScript
/**
* 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