UNPKG

@xec-sh/core

Version:

Universal shell execution engine

369 lines 12.9 kB
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