@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
1,381 lines (1,380 loc) β’ 71.1 kB
JavaScript
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';
}