@xec-sh/core
Version:
Universal shell execution engine
369 lines • 12.9 kB
JavaScript
import { z } from 'zod';
import * as os from 'os';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as fs from 'fs/promises';
const HostConfigSchema = z.object({
host: z.string(),
username: z.string().optional(),
password: z.string().optional(),
privateKey: z.string().optional(),
privateKeyPath: z.string().optional(),
port: z.number().optional(),
readyTimeout: z.number().optional(),
keepaliveInterval: z.number().optional(),
env: z.record(z.string(), z.string()).optional()
});
const ContainerConfigSchema = z.object({
name: z.string().optional(),
image: z.string().optional(),
container: z.string().optional(),
env: z.record(z.string(), z.string()).optional()
});
const PodConfigSchema = z.object({
name: z.string(),
namespace: z.string().optional(),
container: z.string().optional(),
context: z.string().optional(),
kubeconfig: z.string().optional()
});
const DefaultsSchema = z.object({
timeout: z.union([z.number(), z.string()]).optional(),
shell: z.union([z.string(), z.boolean()]).optional(),
cwd: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
encoding: z.string().optional(),
throwOnNonZeroExit: z.boolean().optional()
});
const ProfileSchema = z.object({
extends: z.string().optional(),
defaults: DefaultsSchema.optional(),
hosts: z.record(z.string(), HostConfigSchema).optional(),
containers: z.record(z.string(), ContainerConfigSchema).optional(),
pods: z.record(z.string(), PodConfigSchema).optional()
});
const UnifiedConfigSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
version: z.string().optional(),
defaults: DefaultsSchema.optional(),
hosts: z.record(z.string(), HostConfigSchema).optional(),
containers: z.record(z.string(), ContainerConfigSchema).optional(),
pods: z.record(z.string(), PodConfigSchema).optional(),
aliases: z.record(z.string(), z.string()).optional(),
profiles: z.record(z.string(), ProfileSchema).optional(),
plugins: z.array(z.string()).optional()
});
export class UnifiedConfigLoader {
constructor() {
this.config = {};
this.loadedPaths = [];
this.searchPaths = [
() => process.env['XEC_CONFIG'],
() => path.join(process.cwd(), '.xec', 'config.yaml'),
() => path.join(process.cwd(), '.xec.yaml'),
() => path.join(process.cwd(), 'xec.yaml'),
() => path.join(os.homedir(), '.xec', 'config.yaml'),
() => path.join(os.homedir(), '.xec.yaml')
];
}
static getInstance() {
if (!UnifiedConfigLoader.instance) {
UnifiedConfigLoader.instance = new UnifiedConfigLoader();
}
return UnifiedConfigLoader.instance;
}
async load(additionalPaths) {
this.config = {};
this.loadedPaths = [];
const pathsToCheck = [];
for (const pathFn of this.searchPaths) {
const path = pathFn();
if (path)
pathsToCheck.push(path);
}
if (additionalPaths) {
pathsToCheck.push(...additionalPaths);
}
for (const configPath of pathsToCheck) {
try {
const stats = await fs.stat(configPath);
if (stats.isFile()) {
const content = await fs.readFile(configPath, 'utf-8');
const parsed = yaml.load(content);
const validated = UnifiedConfigSchema.parse(parsed);
this.config = this.mergeConfigs(this.config, validated);
this.loadedPaths.push(configPath);
}
}
catch (error) {
if (error?.code === 'ENOENT') {
continue;
}
console.warn(`Failed to load config from ${configPath}:`, error);
}
}
this.applyEnvironmentOverrides();
const profileName = process.env['XEC_PROFILE'] || this.activeProfile;
if (profileName) {
this.applyProfile(profileName);
}
return this.config;
}
async save(config, filePath) {
const targetPath = filePath || path.join(process.cwd(), '.xec', 'config.yaml');
const dir = path.dirname(targetPath);
await fs.mkdir(dir, { recursive: true });
const content = yaml.dump(config, {
indent: 2,
sortKeys: false,
noRefs: true
});
await fs.writeFile(targetPath, content, 'utf-8');
}
get() {
return this.config;
}
getValue(path) {
const parts = path.split('.');
let current = this.config;
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = current[part];
}
else {
return undefined;
}
}
return current;
}
setValue(path, value) {
const parts = path.split('.');
let current = this.config;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part] || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}
const lastPart = parts[parts.length - 1];
current[lastPart] = value;
}
applyProfile(profileName) {
const profile = this.config.profiles?.[profileName];
if (!profile) {
throw new Error(`Profile '${profileName}' not found`);
}
if (profile.extends) {
this.applyProfile(profile.extends);
}
if (profile.defaults) {
this.config.defaults = this.mergeDefaults(this.config.defaults || {}, profile.defaults);
}
if (profile.hosts) {
this.config.hosts = { ...this.config.hosts, ...profile.hosts };
}
if (profile.containers) {
this.config.containers = { ...this.config.containers, ...profile.containers };
}
if (profile.pods) {
this.config.pods = { ...this.config.pods, ...profile.pods };
}
this.activeProfile = profileName;
}
getActiveProfile() {
return this.activeProfile;
}
getHost(name) {
return this.config.hosts?.[name];
}
getContainer(name) {
return this.config.containers?.[name];
}
getPod(name) {
return this.config.pods?.[name];
}
resolveAlias(alias) {
return this.config.aliases?.[alias];
}
listHosts() {
return Object.keys(this.config.hosts || {});
}
listContainers() {
return Object.keys(this.config.containers || {});
}
listPods() {
return Object.keys(this.config.pods || {});
}
listProfiles() {
return Object.keys(this.config.profiles || {});
}
toEngineConfig() {
const config = {};
if (this.config.defaults) {
if (this.config.defaults.timeout) {
config.defaultTimeout = this.parseTimeout(this.config.defaults.timeout);
}
if (this.config.defaults.cwd) {
config.defaultCwd = this.config.defaults.cwd;
}
if (this.config.defaults.env) {
config.defaultEnv = this.config.defaults.env;
}
if (this.config.defaults.shell !== undefined) {
config.defaultShell = this.config.defaults.shell;
}
if (this.config.defaults.encoding) {
config.encoding = this.config.defaults.encoding;
}
if (this.config.defaults.throwOnNonZeroExit !== undefined) {
config.throwOnNonZeroExit = this.config.defaults.throwOnNonZeroExit;
}
}
return config;
}
async hostToSSHOptions(name) {
const host = this.getHost(name);
if (!host) {
throw new Error(`Host '${name}' not found in configuration`);
}
return {
host: host.host,
username: host.username,
password: host.password,
privateKey: host.privateKey || (host.privateKeyPath ? await this.readPrivateKey(host.privateKeyPath) : undefined),
port: host.port
};
}
containerToDockerOptions(name) {
const container = this.getContainer(name);
if (!container) {
throw new Error(`Container '${name}' not found in configuration`);
}
return {
container: container.container || container.name
};
}
podToK8sOptions(name) {
const pod = this.getPod(name);
if (!pod) {
throw new Error(`Pod '${name}' not found in configuration`);
}
return {
pod: pod.name,
namespace: pod.namespace,
container: pod.container
};
}
getLoadedPaths() {
return [...this.loadedPaths];
}
async exists() {
for (const pathFn of this.searchPaths) {
const configPath = pathFn();
if (!configPath)
continue;
try {
const stats = await fs.stat(configPath);
if (stats.isFile())
return true;
}
catch {
continue;
}
}
return false;
}
async readPrivateKey(path) {
try {
return await fs.readFile(path, 'utf-8');
}
catch {
return undefined;
}
}
applyEnvironmentOverrides() {
if (process.env['XEC_TIMEOUT']) {
this.config.defaults = this.config.defaults || {};
this.config.defaults.timeout = process.env['XEC_TIMEOUT'];
}
if (process.env['XEC_SHELL']) {
this.config.defaults = this.config.defaults || {};
this.config.defaults.shell = process.env['XEC_SHELL'];
}
if (process.env['XEC_CWD']) {
this.config.defaults = this.config.defaults || {};
this.config.defaults.cwd = process.env['XEC_CWD'];
}
}
mergeConfigs(base, override) {
const merged = { ...base };
if (override.name)
merged.name = override.name;
if (override.description)
merged.description = override.description;
if (override.version)
merged.version = override.version;
if (override.defaults) {
merged.defaults = this.mergeDefaults(merged.defaults || {}, override.defaults);
}
if (override.hosts) {
merged.hosts = { ...merged.hosts, ...override.hosts };
}
if (override.containers) {
merged.containers = { ...merged.containers, ...override.containers };
}
if (override.pods) {
merged.pods = { ...merged.pods, ...override.pods };
}
if (override.aliases) {
merged.aliases = { ...merged.aliases, ...override.aliases };
}
if (override.profiles) {
merged.profiles = { ...merged.profiles, ...override.profiles };
}
if (override.plugins) {
const existingPlugins = new Set(merged.plugins || []);
override.plugins.forEach(p => existingPlugins.add(p));
merged.plugins = Array.from(existingPlugins);
}
return merged;
}
mergeDefaults(base, override) {
const merged = { ...base };
if (override.timeout !== undefined)
merged.timeout = override.timeout;
if (override.shell !== undefined)
merged.shell = override.shell;
if (override.cwd !== undefined)
merged.cwd = override.cwd;
if (override.encoding !== undefined)
merged.encoding = override.encoding;
if (override.throwOnNonZeroExit !== undefined)
merged.throwOnNonZeroExit = override.throwOnNonZeroExit;
if (override.env) {
merged.env = { ...merged.env, ...override.env };
}
return merged;
}
parseTimeout(value) {
if (typeof value === 'number')
return value;
const match = value.match(/^(\d+)(ms|s|m|h)?$/);
if (!match) {
throw new Error(`Invalid timeout format: ${value}`);
}
const num = parseInt(match[1], 10);
const unit = match[2] || 'ms';
switch (unit) {
case 'ms': return num;
case 's': return num * 1000;
case 'm': return num * 60 * 1000;
case 'h': return num * 60 * 60 * 1000;
default: return num;
}
}
}
export const unifiedConfig = UnifiedConfigLoader.getInstance();
//# sourceMappingURL=unified-config.js.map