@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
313 lines β’ 11 kB
JavaScript
import chalk from 'chalk';
import * as clack from '@clack/prompts';
import { ConfigurationManager } from '../config/configuration-manager.js';
export class InteractiveHelpers {
static { this.cancelled = false; }
static { this.configManager = null; }
static async getConfigManager() {
if (!this.configManager) {
this.configManager = new ConfigurationManager();
await this.configManager.load();
}
return this.configManager;
}
static setupCancelHandlers() {
process.on('SIGINT', () => {
this.cancelled = true;
clack.outro(chalk.gray('Cancelled'));
process.exit(0);
});
}
static isCancelled(value) {
return clack.isCancel(value) || this.cancelled;
}
static async selectTarget(options) {
const configManager = await this.getConfigManager();
const config = configManager.getConfig();
const targets = [];
if (config.targets) {
for (const [name, targetConfig] of Object.entries(config.targets)) {
if (!targetConfig || !targetConfig.type)
continue;
const targetType = targetConfig.type;
if (options.type && options.type !== 'all') {
const typeMap = {
'ssh': 'ssh',
'docker': 'docker',
'k8s': 'k8s',
'local': 'local'
};
if (targetType !== typeMap[options.type]) {
continue;
}
}
targets.push({
id: `targets.${name}`,
type: targetType,
name,
config: targetConfig,
source: 'configured',
});
}
}
if ((options.type === 'all' || options.type === 'local') &&
!targets.some(t => t.type === 'local')) {
targets.push({
id: 'local',
type: 'local',
name: 'local',
config: { type: 'local' },
source: 'configured',
});
}
const targetOptions = targets.map(target => ({
value: target,
label: `${this.getTargetIcon(target.type)} ${target.id} ${chalk.gray(`(${target.type})`)}`,
}));
if (options.allowCustom) {
targetOptions.push({
value: { custom: true },
label: chalk.cyan('β Enter custom target...'),
});
}
if (targetOptions.length === 0) {
clack.log.warning('No targets configured. Use "xec new profile" to create a configuration.');
return null;
}
if (options.allowMultiple) {
const selected = await clack.multiselect({
message: options.message,
options: targetOptions,
required: true,
});
if (this.isCancelled(selected))
return null;
if (Array.isArray(selected) && selected.some((t) => typeof t === 'object' && t && 'custom' in t && t.custom)) {
const customTarget = await this.enterCustomTarget();
if (!customTarget)
return null;
return [customTarget];
}
return selected;
}
else {
const selected = await clack.select({
message: options.message,
options: targetOptions,
});
if (this.isCancelled(selected))
return null;
if (typeof selected === 'object' && selected && 'custom' in selected && selected.custom) {
return await this.enterCustomTarget();
}
return selected;
}
}
static async enterCustomTarget() {
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 (this.isCancelled(targetType))
return null;
if (this.isCancelled(targetType))
return null;
switch (targetType) {
case 'ssh': {
const hostInput = await clack.text({
message: 'Enter SSH host:',
placeholder: 'user@hostname or hostname',
validate: (value) => {
if (!value || value.trim().length === 0) {
return 'Host cannot be empty';
}
return undefined;
},
});
if (this.isCancelled(hostInput))
return null;
const hostStr = String(hostInput);
const [user, host] = hostStr.includes('@')
? hostStr.split('@')
: [process.env['USER'] || 'root', hostStr];
return {
id: `ssh:${hostStr}`,
type: 'ssh',
name: hostStr,
config: {
type: 'ssh',
host,
username: user,
},
source: 'configured',
};
}
case 'docker': {
const container = await clack.text({
message: 'Enter container name or ID:',
placeholder: 'myapp',
validate: (value) => {
if (!value || value.trim().length === 0) {
return 'Container name cannot be empty';
}
return undefined;
},
});
if (this.isCancelled(container))
return null;
const containerStr = String(container);
return {
id: `docker:${containerStr}`,
type: 'docker',
name: containerStr,
config: {
type: 'docker',
name: containerStr,
},
source: 'configured',
};
}
case 'k8s': {
const namespace = await clack.text({
message: 'Enter namespace:',
placeholder: 'default',
initialValue: 'default',
});
if (this.isCancelled(namespace))
return null;
const pod = await clack.text({
message: 'Enter pod name:',
placeholder: 'myapp-pod',
validate: (value) => {
if (!value || value.trim().length === 0) {
return 'Pod name cannot be empty';
}
return undefined;
},
});
if (this.isCancelled(pod))
return null;
const namespaceStr = String(namespace);
const podStr = String(pod);
return {
id: `k8s:${namespaceStr}/${podStr}`,
type: 'k8s',
name: podStr,
config: {
type: 'k8s',
name: podStr,
namespace: namespaceStr,
},
source: 'configured',
};
}
}
}
static getTargetIcon(type) {
switch (type) {
case 'ssh':
return 'π₯οΈ ';
case 'docker':
return 'π³';
case 'k8s':
return 'βΈοΈ ';
case 'local':
return 'π»';
default:
return 'π¦';
}
}
static async confirmAction(message, defaultValue = false) {
const result = await clack.confirm({
message,
initialValue: defaultValue,
});
return !this.isCancelled(result) && result;
}
static async selectFromList(message, items, getLabelFn, allowCustom = false) {
if (items.length === 0) {
clack.log.warning('No items available');
return null;
}
const options = items.map(item => ({
value: item,
label: getLabelFn(item),
}));
if (allowCustom) {
options.push({
value: { custom: true },
label: chalk.cyan('β Enter custom value...'),
});
}
const selected = await clack.select({
message,
options: options,
});
if (this.isCancelled(selected))
return null;
return selected;
}
static async inputText(message, options = {}) {
const result = await clack.text({
message,
placeholder: options.placeholder,
initialValue: options.initialValue,
validate: options.validate,
});
if (this.isCancelled(result))
return null;
return result;
}
static async selectMultiple(message, items, getLabelFn, required = true) {
if (items.length === 0) {
clack.log.warning('No items available');
return null;
}
const options = items.map(item => ({
value: item,
label: getLabelFn(item),
}));
const selected = await clack.multiselect({
message,
options: options,
required,
});
if (this.isCancelled(selected))
return null;
return selected;
}
static startInteractiveMode(title) {
this.setupCancelHandlers();
clack.intro(chalk.bgBlue(` ${title} `));
}
static endInteractiveMode(message) {
if (message) {
clack.outro(chalk.green(message));
}
else {
clack.outro(chalk.green('β Done!'));
}
}
static showError(message) {
clack.log.error(chalk.red(message));
}
static showSuccess(message) {
clack.log.success(chalk.green(message));
}
static showInfo(message) {
clack.log.info(chalk.blue(message));
}
static showWarning(message) {
clack.log.warning(chalk.yellow(message));
}
static createSpinner(message) {
const spinner = clack.spinner();
spinner.start(message);
return spinner;
}
}
//# sourceMappingURL=interactive-helpers.js.map