UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

309 lines 11.1 kB
import { $ } from '@xec-sh/core'; export class VariableInterpolator { static { this.VARIABLE_REGEX = /(?<!\\)\$\{([^}]+)\}/g; } static { this.MAX_DEPTH = 10; } constructor(secretManager) { this.secretsCache = new Map(); this.secretManager = secretManager; } interpolate(value, context) { if (typeof value !== 'string') { return value; } const resolving = new Set(); return this.interpolateWithDepth(value, context, resolving, 0); } async interpolateAsync(value, context) { if (typeof value !== 'string') { return value; } const resolving = new Set(); return this.interpolateWithDepthAsync(value, context, resolving, 0); } async resolveConfig(config, context) { if (config.vars) { config.vars = await this.resolveObject(config.vars, { ...context, vars: config.vars }); context.vars = config.vars; } const resolved = await this.resolveObject(config, context); return resolved; } hasVariables(value) { if (typeof value !== 'string') { return false; } const regex = new RegExp(VariableInterpolator.VARIABLE_REGEX.source, VariableInterpolator.VARIABLE_REGEX.flags); return regex.test(value); } parseVariables(value) { const references = []; const regex = new RegExp(VariableInterpolator.VARIABLE_REGEX); let match; while ((match = regex.exec(value)) !== null) { if (match[1]) { const ref = this.parseReference(match[1]); if (ref) { references.push({ ...ref, raw: match[0] }); } } } return references; } interpolateWithDepth(value, context, resolving, depth) { if (depth > VariableInterpolator.MAX_DEPTH) { throw new Error(`Maximum variable interpolation depth (${VariableInterpolator.MAX_DEPTH}) exceeded`); } return value.replace(VariableInterpolator.VARIABLE_REGEX, (match, inner) => { if (resolving.has(match)) { throw new Error(`Circular variable reference detected: ${match}`); } resolving.add(match); try { const resolved = this.resolveVariable(inner, context); if (resolved === undefined) { return match; } if (typeof resolved === 'string' && this.hasVariables(resolved)) { return this.interpolateWithDepth(resolved, context, resolving, depth + 1); } return String(resolved); } finally { resolving.delete(match); } }); } async interpolateWithDepthAsync(value, context, resolving, depth) { if (depth > VariableInterpolator.MAX_DEPTH) { throw new Error(`Maximum variable interpolation depth (${VariableInterpolator.MAX_DEPTH}) exceeded`); } const regex = new RegExp(VariableInterpolator.VARIABLE_REGEX.source, 'g'); const matches = Array.from(value.matchAll(regex)); let result = value; for (const match of matches) { const fullMatch = match[0]; const inner = match[1]; if (!inner) { continue; } if (resolving.has(fullMatch)) { throw new Error(`Circular variable reference detected: ${fullMatch}`); } resolving.add(fullMatch); try { const resolved = await this.resolveVariableAsync(inner, context); if (resolved === undefined) { continue; } let finalValue = String(resolved); if (typeof resolved === 'string' && this.hasVariables(resolved)) { finalValue = await this.interpolateWithDepthAsync(resolved, context, resolving, depth + 1); } result = result.replace(fullMatch, finalValue); } finally { resolving.delete(fullMatch); } } return result; } parseReference(reference) { const colonIndex = reference.indexOf(':'); let path = reference; let defaultValue; if (colonIndex !== -1) { const prefix = reference.substring(0, colonIndex); if (prefix === 'cmd' || prefix === 'secret') { return { type: prefix, path: reference.substring(colonIndex + 1), defaultValue: undefined }; } else { path = prefix; defaultValue = reference.substring(colonIndex + 1); } } const parts = path.split('.'); const firstPart = parts[0]; let type; let actualPath; switch (firstPart) { case 'vars': case 'env': case 'params': type = firstPart; actualPath = parts.slice(1).join('.'); break; case 'cmd': case 'secret': return null; default: type = 'vars'; actualPath = path; } return { type, path: actualPath, defaultValue }; } resolveVariable(reference, context) { const parsed = this.parseReference(reference); if (!parsed) { return undefined; } let value; switch (parsed.type) { case 'vars': value = this.getByPath(context.vars || {}, parsed.path); break; case 'env': value = context.env ? context.env[parsed.path] : process.env[parsed.path]; break; case 'cmd': console.warn(`Command substitution '${parsed.path}' not supported in synchronous context. Use interpolateAsync() instead.`); value = `[cmd:${parsed.path}]`; break; case 'secret': value = this.getSecretSync(parsed.path, context); break; case 'params': value = this.getByPath(context.params || {}, parsed.path); break; } if (value === undefined && parsed.defaultValue !== undefined) { value = parsed.defaultValue; } return value; } async resolveVariableAsync(reference, context) { const parsed = this.parseReference(reference); if (!parsed) { return undefined; } let value; switch (parsed.type) { case 'vars': value = this.getByPath(context.vars || {}, parsed.path); break; case 'env': value = context.env ? context.env[parsed.path] : process.env[parsed.path]; break; case 'cmd': value = await this.executeCommandAsync(parsed.path); break; case 'secret': value = await this.getSecretAsync(parsed.path, context); break; case 'params': value = this.getByPath(context.params || {}, parsed.path); break; } if (value === undefined && parsed.defaultValue !== undefined) { value = parsed.defaultValue; } return value; } async resolveObject(obj, context) { if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'string') { return this.interpolateAsync(obj, context); } if (Array.isArray(obj)) { return Promise.all(obj.map(item => this.resolveObject(item, context))); } if (typeof obj === 'object') { const resolved = {}; for (const [key, value] of Object.entries(obj)) { if (value === '$unset') { continue; } const resolvedKey = await this.interpolateAsync(key, context); resolved[resolvedKey] = await this.resolveObject(value, context); } return resolved; } return obj; } async executeCommandAsync(command) { try { const trimmedCommand = command.trim(); const result = await $.raw `${trimmedCommand}`.shell(true).nothrow(); if (!result.ok) { console.warn(`Command substitution failed for '${command}': ${result.stderr || `Exit code ${result.exitCode}`}`); return ''; } return result.stdout.trim(); } catch (error) { console.warn(`Command substitution failed for '${command}': ${error.message}`); return ''; } } getSecretSync(key, context) { if (this.secretsCache.has(key)) { return this.secretsCache.get(key); } const envKey = `SECRET_${key.toUpperCase().replace(/[.-]/g, '_')}`; const value = process.env[envKey]; if (value) { this.secretsCache.set(key, value); return value; } console.warn(`Secret '${key}' not available in synchronous context. Use interpolateAsync() instead.`); return `[secret:${key}]`; } async getSecretAsync(key, context) { if (this.secretsCache.has(key)) { return this.secretsCache.get(key); } if (this.secretManager) { try { const value = await this.secretManager.get(key); if (value !== null) { this.secretsCache.set(key, value); return value; } } catch (error) { console.warn(`Failed to retrieve secret '${key}' from secret manager: ${error instanceof Error ? error.message : 'Unknown error'}`); } } const envKey = `SECRET_${key.toUpperCase().replace(/[.-]/g, '_')}`; const value = process.env[envKey]; if (value) { this.secretsCache.set(key, value); return value; } console.warn(`Secret '${key}' not found`); return ''; } getByPath(obj, path) { if (!path) { return obj; } const parts = path.split('.'); let current = obj; for (const part of parts) { if (current == null || typeof current !== 'object') { return undefined; } current = current[part]; } return current; } clearSecretsCache() { this.secretsCache.clear(); } } //# sourceMappingURL=variable-interpolator.js.map