UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

1,381 lines (1,380 loc) β€’ 71.1 kB
import chalk from 'chalk'; import * as yaml from 'js-yaml'; import { Command } from 'commander'; import { join, dirname } from 'path'; import * as clack from '@clack/prompts'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { BaseCommand } from '../utils/command-base.js'; import { ConfigurationManager } from '../config/configuration-manager.js'; import { getDefaultConfig, sortConfigKeys, mergeWithDefaults } from '../config/defaults.js'; export class ConfigCommand extends BaseCommand { constructor() { super({ name: 'config', description: 'Manage xec configuration', aliases: ['conf', 'cfg'] }); this.MANAGED_KEYS = ['version', 'targets', 'vars', 'tasks', 'defaults', 'commands', 'name', 'description']; } create() { const command = new Command(this.config.name) .description(this.config.description); if (this.config.aliases) { this.config.aliases.forEach(alias => command.alias(alias)); } command.action(async () => { await this.execute([]); }); this.setupSubcommands(command); return command; } async execute(args) { await this.ensureInitialized(); if (process.stdin.isTTY) { await this.interactiveMode(); } } async ensureInitialized() { if (!this.configManager) { this.configManager = new ConfigurationManager({ projectRoot: process.cwd(), profile: undefined }); } } setupSubcommands(command) { command .command('get <key>') .description('Get configuration value by key (use dot notation for nested values)') .action(async (key) => { await this.ensureInitialized(); await this.getConfigValue(key); }); command .command('set <key> <value>') .description('Set configuration value (use dot notation for nested values)') .option('--json', 'Parse value as JSON') .action(async (key, value, options) => { await this.ensureInitialized(); await this.setConfigValue(key, value, options); }); command .command('unset <key>') .description('Remove configuration value') .action(async (key) => { await this.ensureInitialized(); await this.unsetConfigValue(key); }); command .command('list') .description('List all configuration values') .option('--json', 'Output as JSON') .option('--path <path>', 'List values under specific path') .action(async (options) => { await this.ensureInitialized(); await this.listConfig(options); }); command .command('view') .description('View current configuration (alias for list)') .option('--defaults', 'Show default values in dimmer color') .action(async (options) => { await this.ensureInitialized(); await this.viewConfig(options); }); command .command('doctor') .description('Check and fix configuration issues') .option('--defaults', 'Show all possible configuration options with default values') .action(async (options) => { await this.ensureInitialized(); await this.runDoctor(options); }); command .command('validate') .description('Validate configuration') .action(async () => { await this.ensureInitialized(); await this.validateConfig(); }); this.setupTargetCommands(command); this.setupVarCommands(command); this.setupTaskCommands(command); this.setupDefaultsCommands(command); } setupTargetCommands(parent) { const targets = parent .command('targets') .description('Manage targets'); targets .command('list') .description('List all targets') .action(async () => { await this.ensureInitialized(); await this.listTargets(); }); targets .command('add') .description('Add new target') .action(async () => { await this.ensureInitialized(); await this.addTarget(); }); targets .command('edit <name>') .description('Edit target') .action(async (name) => { await this.ensureInitialized(); await this.editTargetWithName(name); }); targets .command('delete <name>') .description('Delete target') .action(async (name) => { await this.ensureInitialized(); await this.deleteTargetWithName(name); }); targets .command('test <name>') .description('Test target connection') .action(async (name) => { await this.ensureInitialized(); await this.testTargetWithName(name); }); } setupVarCommands(parent) { const vars = parent .command('vars') .description('Manage variables'); vars .command('list') .description('List all variables') .action(async () => { await this.ensureInitialized(); await this.listVars(); }); vars .command('set <key> [value]') .description('Set variable') .action(async (key, value) => { await this.ensureInitialized(); await this.setVarWithKeyValue(key, value); }); vars .command('delete <key>') .description('Delete variable') .action(async (key) => { await this.ensureInitialized(); await this.deleteVarWithKey(key); }); vars .command('import <file>') .description('Import variables from file') .action(async (file) => { await this.ensureInitialized(); await this.importVarsFromFile(file); }); vars .command('export <file>') .description('Export variables to file') .action(async (file) => { await this.ensureInitialized(); await this.exportVarsToFile(file); }); } setupTaskCommands(parent) { const tasks = parent .command('tasks') .description('Manage tasks'); tasks .command('list') .description('List all tasks') .action(async () => { await this.ensureInitialized(); await this.listTasks(); }); tasks .command('view <name>') .description('View task details') .action(async (name) => { await this.ensureInitialized(); await this.viewTaskWithName(name); }); tasks .command('create') .description('Create new task') .action(async () => { await this.ensureInitialized(); await this.createTask(); }); tasks .command('delete <name>') .description('Delete task') .action(async (name) => { await this.ensureInitialized(); await this.deleteTaskWithName(name); }); tasks .command('validate') .description('Validate all tasks') .action(async () => { await this.ensureInitialized(); await this.validateTasks(); }); } setupDefaultsCommands(parent) { const defaults = parent .command('defaults') .description('Manage default configurations'); defaults .command('view') .description('View current defaults') .action(async () => { await this.ensureInitialized(); await this.viewDefaults(); }); defaults .command('ssh') .description('Set SSH defaults') .action(async () => { await this.ensureInitialized(); await this.setSSHDefaults(); }); defaults .command('docker') .description('Set Docker defaults') .action(async () => { await this.ensureInitialized(); await this.setDockerDefaults(); }); defaults .command('k8s') .description('Set Kubernetes defaults') .action(async () => { await this.ensureInitialized(); await this.setK8sDefaults(); }); defaults .command('commands') .description('Set command defaults') .action(async () => { await this.ensureInitialized(); await this.setCommandDefaults(); }); defaults .command('reset') .description('Reset to system defaults') .action(async () => { await this.ensureInitialized(); await this.resetDefaults(); }); } async interactiveMode() { clack.intro('πŸ”§ Xec Configuration Manager'); while (true) { const action = await clack.select({ message: 'What would you like to do?', options: [ { value: 'view', label: 'πŸ“– View configuration' }, { value: 'targets', label: '🎯 Manage targets' }, { value: 'vars', label: 'πŸ“ Manage variables' }, { value: 'tasks', label: '⚑ Manage tasks' }, { value: 'defaults', label: 'βš™οΈ Manage defaults' }, { value: 'custom', label: 'πŸ”§ Manage custom parameters' }, { value: 'doctor', label: 'πŸ₯ Run doctor (add all defaults)' }, { value: 'validate', label: 'βœ… Validate configuration' }, { value: 'exit', label: '❌ Exit' }, ], }); if (clack.isCancel(action)) { clack.cancel('Configuration management cancelled'); break; } if (action === 'exit') { clack.outro('✨ Configuration management completed'); break; } switch (action) { case 'view': await this.viewConfig({ defaults: true }); break; case 'targets': await this.manageTargets(); break; case 'vars': await this.manageVars(); break; case 'tasks': await this.manageTasks(); break; case 'defaults': await this.manageDefaults(); break; case 'custom': await this.manageCustomParameters(); break; case 'doctor': const showDefaults = await clack.confirm({ message: 'Show all possible configuration options with default values?', initialValue: false }); if (!clack.isCancel(showDefaults)) { await this.runDoctor({ defaults: showDefaults }); } break; case 'validate': await this.validateConfig(); break; } } } async viewConfig(options) { let config = await this.configManager.load(); if (options?.defaults) { const defaults = getDefaultConfig(); const merged = mergeWithDefaults(config, defaults); const sorted = sortConfigKeys(merged); const formattedYaml = this.formatYamlWithDefaults(sorted, config, ''); console.log(formattedYaml); } else { const sorted = sortConfigKeys(config); console.log(yaml.dump(sorted, { indent: 2 })); } } formatYamlWithDefaults(obj, userConfig, path, indent = 0) { const defaults = getDefaultConfig(); let result = ''; const indentStr = ' '.repeat(indent); for (const key in obj) { const fullPath = path ? `${path}.${key}` : key; const value = obj[key]; const userValue = userConfig?.[key]; const isUserDefined = userValue !== undefined; if (value === null || value === undefined) { continue; } if (typeof value === 'object' && !Array.isArray(value)) { result += `${indentStr}${key}:\n`; result += this.formatYamlWithDefaults(value, userValue || {}, fullPath, indent + 1); } else { const valueStr = yaml.dump({ [key]: value }, { indent: 2 }) .replace(/^\{\s*/, '') .replace(/\s*\}$/, '') .trim(); if (isUserDefined) { result += `${indentStr}${valueStr}\n`; } else { result += chalk.dim(`${indentStr}${valueStr}`) + '\n'; } } } return result; } async getConfigValue(key) { const config = await this.configManager.load(); const keys = key.split('.'); let value = config; for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { clack.log.error(`Configuration key '${key}' not found`); return; } } if (typeof value === 'object') { console.log(yaml.dump(value, { indent: 2 })); } else { console.log(value); } } async setConfigValue(key, value, options) { const config = await this.configManager.load(); let parsedValue = value; if (options.json) { try { parsedValue = JSON.parse(value); } catch (error) { clack.log.error(`Invalid JSON value: ${error}`); return; } } else { if (value === 'true') parsedValue = true; else if (value === 'false') parsedValue = false; else if (!isNaN(Number(value)) && value !== '') parsedValue = Number(value); } const keys = key.split('.'); const lastKey = keys.pop(); let target = config; for (const k of keys) { if (!target[k] || typeof target[k] !== 'object') { target[k] = {}; } target = target[k]; } target[lastKey] = parsedValue; await this.saveConfig(config); clack.log.success(`Configuration value '${key}' set successfully`); } async unsetConfigValue(key) { const config = await this.configManager.load(); const keys = key.split('.'); const lastKey = keys.pop(); let target = config; for (const k of keys) { if (target && typeof target === 'object' && k in target) { target = target[k]; } else { clack.log.error(`Configuration key '${key}' not found`); return; } } if (target && typeof target === 'object' && lastKey in target) { delete target[lastKey]; await this.saveConfig(config); clack.log.success(`Configuration value '${key}' removed successfully`); } else { clack.log.error(`Configuration key '${key}' not found`); } } async listConfig(options) { const config = await this.configManager.load(); let displayConfig = config; if (options.path) { const keys = options.path.split('.'); for (const k of keys) { if (displayConfig && typeof displayConfig === 'object' && k in displayConfig) { displayConfig = displayConfig[k]; } else { clack.log.error(`Configuration path '${options.path}' not found`); return; } } } if (options.json) { console.log(JSON.stringify(displayConfig, null, 2)); } else { console.log(yaml.dump(displayConfig, { indent: 2, sortKeys: false })); } } async manageTargets() { const action = await clack.select({ message: 'Target management', options: [ { value: 'list', label: 'List all targets' }, { value: 'add', label: 'Add new target' }, { value: 'edit', label: 'Edit existing target' }, { value: 'delete', label: 'Delete target' }, { value: 'test', label: 'Test target connection' }, ], }); if (clack.isCancel(action)) return; switch (action) { case 'list': await this.listTargets(); break; case 'add': await this.addTarget(); break; case 'edit': await this.editTarget(); break; case 'delete': await this.deleteTarget(); break; case 'test': await this.testTarget(); break; } } async listTargets() { const config = await this.configManager.load(); const targets = config.targets || {}; console.log('\n🎯 Configured Targets:\n'); if (targets.local) { console.log(' πŸ“ local (type: local)'); } if (targets.hosts) { console.log('\n SSH Hosts:'); for (const [name, host] of Object.entries(targets.hosts)) { console.log(` πŸ–₯️ ${name} (${host.host}:${host.port || 22})`); } } if (targets.containers) { console.log('\n Docker Containers:'); for (const [name, container] of Object.entries(targets.containers)) { console.log(` 🐳 ${name} (${container.container || container.image})`); } } if (targets.pods) { console.log('\n Kubernetes Pods:'); for (const [name, pod] of Object.entries(targets.pods)) { console.log(` ☸️ ${name} (${pod.namespace || 'default'}/${pod.pod})`); } } } async addTarget() { const targetType = await clack.select({ message: 'Select target type', options: [ { value: 'ssh', label: 'πŸ–₯️ SSH Host' }, { value: 'docker', label: '🐳 Docker Container' }, { value: 'k8s', label: '☸️ Kubernetes Pod' }, ], }); if (clack.isCancel(targetType)) return; const name = await clack.text({ message: 'Target name', placeholder: 'my-target', validate: (value) => { if (!value) return 'Name is required'; if (!/^[a-z0-9-]+$/.test(value)) return 'Name must contain only lowercase letters, numbers, and hyphens'; return; }, }); if (clack.isCancel(name)) return; let targetConfig = { type: targetType }; switch (targetType) { case 'ssh': targetConfig = await this.promptSSHConfig(); break; case 'docker': targetConfig = await this.promptDockerConfig(); break; case 'k8s': targetConfig = await this.promptK8sConfig(); break; } if (!targetConfig) return; const config = await this.configManager.load(); if (!config.targets) config.targets = {}; switch (targetType) { case 'ssh': if (!config.targets.hosts) config.targets.hosts = {}; config.targets.hosts[name] = targetConfig; break; case 'docker': if (!config.targets.containers) config.targets.containers = {}; config.targets.containers[name] = targetConfig; break; case 'k8s': if (!config.targets.pods) config.targets.pods = {}; config.targets.pods[name] = targetConfig; break; } await this.saveConfig(config); clack.log.success(`Target '${name}' added successfully`); } async promptSSHConfig() { const host = await clack.text({ message: 'SSH host', placeholder: 'example.com', validate: (value) => value ? undefined : 'Host is required', }); if (clack.isCancel(host)) return null; const port = await clack.text({ message: 'SSH port', placeholder: '22', defaultValue: '22', }); if (clack.isCancel(port)) return null; const username = await clack.text({ message: 'SSH username', placeholder: 'user', validate: (value) => value ? undefined : 'Username is required', }); if (clack.isCancel(username)) return null; const authMethod = await clack.select({ message: 'Authentication method', options: [ { value: 'key', label: 'πŸ”‘ SSH Key' }, { value: 'password', label: 'πŸ”’ Password (not recommended)' }, ], }); if (clack.isCancel(authMethod)) return null; const config = { type: 'ssh', host, port: parseInt(port), username, }; if (authMethod === 'key') { const privateKey = await clack.text({ message: 'Path to SSH private key', placeholder: '~/.ssh/id_rsa', defaultValue: '~/.ssh/id_rsa', }); if (clack.isCancel(privateKey)) return null; config.privateKey = privateKey; const passphrase = await clack.password({ message: 'SSH key passphrase (optional)', }); if (passphrase && !clack.isCancel(passphrase)) { clack.log.warn('⚠️ Passphrase will be stored in plain text. Consider using the secrets command instead.'); config.passphrase = passphrase; } } else { clack.log.error('❌ Password authentication is not supported in config. Use the secrets command to manage passwords securely.'); return null; } return config; } async promptDockerConfig() { const useContainer = await clack.confirm({ message: 'Use existing container?', }); const config = { type: 'docker' }; if (useContainer) { const container = await clack.text({ message: 'Container name or ID', placeholder: 'my-container', validate: (value) => value ? undefined : 'Container is required', }); if (clack.isCancel(container)) return null; config.container = container; } else { const image = await clack.text({ message: 'Docker image', placeholder: 'ubuntu:latest', validate: (value) => value ? undefined : 'Image is required', }); if (clack.isCancel(image)) return null; config.image = image; const workdir = await clack.text({ message: 'Working directory (optional)', placeholder: '/app', }); if (workdir && !clack.isCancel(workdir)) { config.workdir = workdir; } } return config; } async promptK8sConfig() { const pod = await clack.text({ message: 'Pod name', placeholder: 'my-pod', validate: (value) => value ? undefined : 'Pod name is required', }); if (clack.isCancel(pod)) return null; const namespace = await clack.text({ message: 'Namespace', placeholder: 'default', defaultValue: 'default', }); if (clack.isCancel(namespace)) return null; const container = await clack.text({ message: 'Container name (for multi-container pods)', placeholder: 'main', }); const config = { type: 'k8s', pod, namespace, }; if (container && !clack.isCancel(container)) { config.container = container; } const context = await clack.text({ message: 'Kubernetes context (optional)', placeholder: 'default', }); if (context && !clack.isCancel(context)) { config.context = context; } return config; } async editTarget() { const config = await this.configManager.load(); const allTargets = []; if (config.targets?.hosts) { for (const name of Object.keys(config.targets.hosts)) { allTargets.push(`hosts.${name}`); } } if (config.targets?.containers) { for (const name of Object.keys(config.targets.containers)) { allTargets.push(`containers.${name}`); } } if (config.targets?.pods) { for (const name of Object.keys(config.targets.pods)) { allTargets.push(`pods.${name}`); } } if (allTargets.length === 0) { clack.log.warn('No targets configured'); return; } const target = await clack.select({ message: 'Select target to edit', options: allTargets.map(t => ({ value: t, label: t })), }); if (clack.isCancel(target)) return; const [type, name] = target.split('.'); if (!name) { clack.log.error('Invalid target format'); return; } let currentConfig; switch (type) { case 'hosts': currentConfig = config.targets.hosts[name]; break; case 'containers': currentConfig = config.targets.containers[name]; break; case 'pods': currentConfig = config.targets.pods[name]; break; } clack.log.info('Current configuration:'); console.log(yaml.dump(currentConfig, { indent: 2 })); const edit = await clack.confirm({ message: 'Edit this target?', }); if (!edit || clack.isCancel(edit)) return; let newConfig; switch (type) { case 'hosts': newConfig = await this.promptSSHConfig(); break; case 'containers': newConfig = await this.promptDockerConfig(); break; case 'pods': newConfig = await this.promptK8sConfig(); break; } if (!newConfig) return; if (name) { switch (type) { case 'hosts': config.targets.hosts[name] = newConfig; break; case 'containers': config.targets.containers[name] = newConfig; break; case 'pods': config.targets.pods[name] = newConfig; break; } } await this.saveConfig(config); clack.log.success(`Target '${target}' updated successfully`); } async deleteTarget() { const config = await this.configManager.load(); const allTargets = []; if (config.targets?.hosts) { for (const name of Object.keys(config.targets.hosts)) { allTargets.push(`hosts.${name}`); } } if (config.targets?.containers) { for (const name of Object.keys(config.targets.containers)) { allTargets.push(`containers.${name}`); } } if (config.targets?.pods) { for (const name of Object.keys(config.targets.pods)) { allTargets.push(`pods.${name}`); } } if (allTargets.length === 0) { clack.log.warn('No targets configured'); return; } const target = await clack.select({ message: 'Select target to delete', options: allTargets.map(t => ({ value: t, label: t })), }); if (clack.isCancel(target)) return; const confirm = await clack.confirm({ message: `Are you sure you want to delete '${target}'?`, }); if (!confirm || clack.isCancel(confirm)) return; const [type, name] = target.split('.'); if (!name) { clack.log.error('Invalid target format'); return; } switch (type) { case 'hosts': delete config.targets.hosts[name]; break; case 'containers': delete config.targets.containers[name]; break; case 'pods': delete config.targets.pods[name]; break; } await this.saveConfig(config); clack.log.success(`Target '${target}' deleted successfully`); } async testTarget() { clack.log.info('Target testing will be implemented with the test command'); } async manageVars() { const action = await clack.select({ message: 'Variable management', options: [ { value: 'list', label: 'List all variables' }, { value: 'set', label: 'Set variable' }, { value: 'delete', label: 'Delete variable' }, { value: 'import', label: 'Import from .env file' }, { value: 'export', label: 'Export to .env file' }, ], }); if (clack.isCancel(action)) return; switch (action) { case 'list': await this.listVars(); break; case 'set': await this.setVar(); break; case 'delete': await this.deleteVar(); break; case 'import': await this.importVars(); break; case 'export': await this.exportVars(); break; } } async listVars() { const config = await this.configManager.load(); const vars = config.vars || {}; if (Object.keys(vars).length === 0) { clack.log.info('No variables configured'); return; } console.log('\nπŸ“ Variables:\n'); for (const [key, value] of Object.entries(vars)) { if (typeof value === 'string' && value.startsWith('$secret:')) { console.log(` ${key}: πŸ”’ [secret]`); } else { console.log(` ${key}: ${value}`); } } } async setVar() { const name = await clack.text({ message: 'Variable name', placeholder: 'MY_VAR', validate: (value) => { if (!value) return 'Name is required'; if (!/^[A-Z][A-Z0-9_]*$/.test(value)) return 'Variable names should be UPPER_SNAKE_CASE'; return; }, }); if (clack.isCancel(name)) return; const isSecret = await clack.confirm({ message: 'Is this a secret value?', }); if (clack.isCancel(isSecret)) return; if (isSecret) { clack.log.error('❌ Secrets cannot be managed through the config command. Use the secrets command instead.'); clack.log.info('Run: xec secrets set ' + name); return; } const value = await clack.text({ message: 'Variable value', placeholder: 'value', validate: (value) => value ? undefined : 'Value is required', }); if (clack.isCancel(value)) return; const config = await this.configManager.load(); if (!config.vars) config.vars = {}; config.vars[name] = value; await this.saveConfig(config); clack.log.success(`Variable '${name}' set successfully`); } async deleteVar() { const config = await this.configManager.load(); const vars = config.vars || {}; if (Object.keys(vars).length === 0) { clack.log.info('No variables configured'); return; } const name = await clack.select({ message: 'Select variable to delete', options: Object.keys(vars).map(v => ({ value: v, label: v })), }); if (clack.isCancel(name)) return; const confirm = await clack.confirm({ message: `Delete variable '${name}'?`, }); if (!confirm || clack.isCancel(confirm)) return; delete config.vars[name]; await this.saveConfig(config); clack.log.success(`Variable '${name}' deleted successfully`); } async importVars() { const envFile = await clack.text({ message: 'Path to .env file', placeholder: '.env', defaultValue: '.env', }); if (clack.isCancel(envFile)) return; if (!existsSync(envFile)) { clack.log.error(`File not found: ${envFile}`); return; } const content = readFileSync(envFile, 'utf-8'); const lines = content.split('\n'); const vars = {}; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const [key, ...valueParts] = trimmed.split('='); if (key) { const value = valueParts.join('=').replace(/^["']|["']$/g, ''); vars[key] = value; } } if (Object.keys(vars).length === 0) { clack.log.warn('No variables found in .env file'); return; } const config = await this.configManager.load(); if (!config.vars) config.vars = {}; for (const [key, value] of Object.entries(vars)) { config.vars[key] = value; } await this.saveConfig(config); clack.log.success(`Imported ${Object.keys(vars).length} variables from ${envFile}`); } async exportVars() { const config = await this.configManager.load(); const vars = config.vars || {}; if (Object.keys(vars).length === 0) { clack.log.info('No variables to export'); return; } const envFile = await clack.text({ message: 'Output .env file', placeholder: '.env', defaultValue: '.env', }); if (clack.isCancel(envFile)) return; const lines = ['# Exported from Xec configuration']; for (const [key, value] of Object.entries(vars)) { if (typeof value === 'string' && value.startsWith('$secret:')) { lines.push(`# ${key}=[secret - use 'xec secrets get ${key}']`); } else { lines.push(`${key}="${value}"`); } } writeFileSync(envFile, lines.join('\n')); clack.log.success(`Exported ${Object.keys(vars).length} variables to ${envFile}`); } async manageTasks() { const action = await clack.select({ message: 'Task management', options: [ { value: 'list', label: 'List all tasks' }, { value: 'view', label: 'View task details' }, { value: 'create', label: 'Create new task' }, { value: 'edit', label: 'Edit task' }, { value: 'delete', label: 'Delete task' }, { value: 'validate', label: 'Validate tasks' }, ], }); if (clack.isCancel(action)) return; switch (action) { case 'list': await this.listTasks(); break; case 'view': await this.viewTask(); break; case 'create': await this.createTask(); break; case 'edit': await this.editTask(); break; case 'delete': await this.deleteTask(); break; case 'validate': await this.validateTasks(); break; } } async listTasks() { const config = await this.configManager.load(); const tasks = config.tasks || {}; if (Object.keys(tasks).length === 0) { clack.log.info('No tasks configured'); return; } console.log('\n⚑ Tasks:\n'); for (const [name, task] of Object.entries(tasks)) { console.log(` ${name}: ${task.description || 'No description'}`); } } async viewTask() { const config = await this.configManager.load(); const tasks = config.tasks || {}; if (Object.keys(tasks).length === 0) { clack.log.info('No tasks configured'); return; } const name = await clack.select({ message: 'Select task to view', options: Object.keys(tasks).map(t => ({ value: t, label: t })), }); if (clack.isCancel(name)) return; console.log('\nTask configuration:'); console.log(yaml.dump(tasks[name], { indent: 2 })); } async createTask() { const name = await clack.text({ message: 'Task name', placeholder: 'my-task', validate: (value) => { if (!value) return 'Name is required'; if (!/^[a-z][a-z0-9-]*$/.test(value)) return 'Task names should be lowercase with hyphens'; return; }, }); if (clack.isCancel(name)) return; const description = await clack.text({ message: 'Task description', placeholder: 'Describe what this task does', }); if (clack.isCancel(description)) return; const taskType = await clack.select({ message: 'Task type', options: [ { value: 'command', label: 'Shell command' }, { value: 'script', label: 'Script file' }, { value: 'composite', label: 'Multiple steps' }, ], }); if (clack.isCancel(taskType)) return; const config = await this.configManager.load(); if (!config.tasks) config.tasks = {}; const task = { description, }; switch (taskType) { case 'command': { const command = await clack.text({ message: 'Command to run', placeholder: 'echo "Hello, World!"', validate: (value) => value ? undefined : 'Command is required', }); if (clack.isCancel(command)) return; task.steps = [{ command }]; break; } case 'script': { const script = await clack.text({ message: 'Script file path', placeholder: './scripts/my-script.sh', validate: (value) => value ? undefined : 'Script path is required', }); if (clack.isCancel(script)) return; task.steps = [{ script }]; break; } case 'composite': clack.log.info('Multi-step tasks can be edited manually in the config file'); task.steps = [ { name: 'Step 1', command: 'echo "Step 1"' }, { name: 'Step 2', command: 'echo "Step 2"' }, ]; break; } config.tasks[name] = task; await this.saveConfig(config); clack.log.success(`Task '${name}' created successfully`); } async editTask() { clack.log.info('Task editing can be done manually in the config file'); const config = await this.configManager.load(); const configPath = this.getConfigPath(); clack.log.info(`Edit tasks in: ${configPath}`); } async deleteTask() { const config = await this.configManager.load(); const tasks = config.tasks || {}; if (Object.keys(tasks).length === 0) { clack.log.info('No tasks configured'); return; } const name = await clack.select({ message: 'Select task to delete', options: Object.keys(tasks).map(t => ({ value: t, label: t })), }); if (clack.isCancel(name)) return; const confirm = await clack.confirm({ message: `Delete task '${name}'?`, }); if (!confirm || clack.isCancel(confirm)) return; delete config.tasks[name]; await this.saveConfig(config); clack.log.success(`Task '${name}' deleted successfully`); } async validateTasks() { const config = await this.configManager.load(); const tasks = config.tasks || {}; if (Object.keys(tasks).length === 0) { clack.log.info('No tasks to validate'); return; } clack.log.info('Validating tasks...'); let hasErrors = false; for (const [name, task] of Object.entries(tasks)) { const taskConfig = task; if (!taskConfig.steps || !Array.isArray(taskConfig.steps)) { clack.log.error(`Task '${name}': Missing or invalid 'steps' field`); hasErrors = true; continue; } for (let i = 0; i < taskConfig.steps.length; i++) { const step = taskConfig.steps[i]; if (!step.command && !step.script && !step.task) { clack.log.error(`Task '${name}', step ${i + 1}: Must have either 'command', 'script', or 'task'`); hasErrors = true; } } } if (!hasErrors) { clack.log.success('All tasks are valid'); } } async manageDefaults() { const action = await clack.select({ message: 'Defaults management', options: [ { value: 'view', label: 'View current defaults' }, { value: 'ssh', label: 'Set SSH defaults' }, { value: 'docker', label: 'Set Docker defaults' }, { value: 'k8s', label: 'Set Kubernetes defaults' }, { value: 'commands', label: 'Set command defaults' }, { value: 'reset', label: 'Reset to system defaults' }, ], }); if (clack.isCancel(action)) return; switch (action) { case 'view': await this.viewDefaults(); break; case 'ssh': await this.setSSHDefaults(); break; case 'docker': await this.setDockerDefaults(); break; case 'k8s': await this.setK8sDefaults(); break; case 'commands': await this.setCommandDefaults(); break; case 'reset': await this.resetDefaults(); break; } } async manageCustomParameters() { const action = await clack.select({ message: 'Custom parameter management', options: [ { value: 'list', label: 'πŸ“‹ List custom parameters' }, { value: 'set', label: 'βž• Set custom parameter' }, { value: 'get', label: 'πŸ” Get custom parameter' }, { value: 'delete', label: '❌ Delete custom parameter' }, { value: 'export', label: 'πŸ“€ Export custom parameters' }, { value: 'back', label: '⬅️ Back' }, ], }); if (clack.isCancel(action) || action === 'back') return; switch (action) { case 'list': await this.listCustomParameters(); break; case 'set': await this.setCustomParameter(); break; case 'get': await this.getCustomParameter(); break; case 'delete': await this.deleteCustomParameter(); break; case 'export': await this.exportCustomParameters(); break; } } isCustomParameter(key) { const topLevelKey = key.split('.')[0]; return topLevelKey ? !this.MANAGED_KEYS.includes(topLevelKey) : false; } async listCustomParameters() { const config = await this.configManager.load(); const customParams = {}; for (const [key, value] of Object.entries(config)) { if (this.isCustomParameter(key)) { customParams[key] = value; } } if (Object.keys(customParams).length === 0) { clack.log.info('No custom parameters configured'); return; } console.log('\nπŸ”§ Custom Parameters:\n'); console.log(yaml.dump(customParams, { indent: 2, sortKeys: true })); } async setCustomParameter() { const key = await clack.text({ message: 'Parameter key (use dot notation for nested values)', placeholder: 'myapp.config.port', validate: (value) => { if (!value) return 'Key is required'; const topLevelKey = value.split('.')[0]; if (topLevelKey && this.MANAGED_KEYS.includes(topLevelKey)) { return `Cannot set '${topLevelKey}' - this is a managed parameter. Use the appropriate manager instead.`; } return; }, }); if (clack.isCancel(key)) return; const valueType = await clack.select({ message: 'Value type', options: [ { value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Boolean' }, { value: 'json', label: 'JSON (for objects/arrays)' }, ], }); if (clack.isCancel(valueType)) return; let parsedValue; switch (valueType) { case 'string': const stringValue = await clack.text({ message: 'Value', placeholder: 'my-value', }); if (clack.isCancel(stringValue)) return; parsedValue = stringValue; break; case 'number': const numberValue = await clack.text({ message: 'Value', placeholder: '8080', validate: (value) => { if (!value || isNaN(Number(value))) return 'Must be a valid number'; return; }, }); if (clack.isCancel(numberValue)) return; parsedValue = Number(numberValue); break; case 'boolean': const boolValue = await clack.confirm({ message: 'Value', }); if (clack.isCancel(boolValue)) return; parsedValue = boolValue; break; case 'json': const jsonValue = await clack.text({ message: 'JSON value', placeholder: '{"key": "value"}', validate: (value) => { try { JSON.parse(value); return; } catch (error) { return 'Invalid JSON'; }