@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
309 lines • 11.1 kB
JavaScript
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