Files
oh-my-pi/packages/coding-agent/src/config/resolve-config-value.ts
T
can1357 9b78a6fa85 refactor(natives): migrated native bindings to NAPI-RS with auto-generated types
- Migrated native bindings from TypeScript wrappers to NAPI-RS generated modules with auto-generated type definitions and runtime enums.
- Replaced chunk tree API with stateful ChunkState class supporting render, edit, and resolve operations with improved error handling.
- Converted callback signatures to error-first pattern (error, result) for shell, PTY, glob, and grep operations.
- Introduced type-safe enums for MacOSAppearance, GrepOutputMode, KeyEventType, ImageFormat, and AstMatchStrictness replacing string literals.
- Refactored chunk tree implementation with dedicated modules for edit, indent, resolve, and state management with comprehensive validation.
- Moved clipboard utilities from native bindings to coding-agent package with improved OSC 52 and Termux compatibility.
2026-04-07 02:00:59 +02:00

95 lines
2.7 KiB
TypeScript

/**
* Resolve configuration values that may be shell commands, environment variables, or literals.
*
* Note: command execution is async to avoid blocking the TUI.
*/
import { executeShell } from "@oh-my-pi/pi-natives";
/** Cache for successful shell command results (persists for process lifetime). */
const commandResultCache = new Map<string, string>();
/** De-duplicates concurrent executions for the same command. */
const commandInFlight = new Map<string, Promise<string | undefined>>();
/**
* Resolve a config value (API key, header value, etc.) to an actual value.
* - If starts with "!", executes the rest as a shell command and uses stdout (cached)
* - Otherwise checks environment variable first, then treats as literal (not cached)
*/
export async function resolveConfigValue(config: string): Promise<string | undefined> {
if (config.startsWith("!")) {
return await executeCommand(config);
}
const envValue = process.env[config];
return envValue || config;
}
async function executeCommand(commandConfig: string): Promise<string | undefined> {
const cached = commandResultCache.get(commandConfig);
if (cached !== undefined) {
return cached;
}
const existing = commandInFlight.get(commandConfig);
if (existing) {
return await existing;
}
const command = commandConfig.slice(1);
const promise = runShellCommand(command, 10_000)
.then(result => {
if (result !== undefined) {
commandResultCache.set(commandConfig, result);
}
return result;
})
.finally(() => {
commandInFlight.delete(commandConfig);
});
commandInFlight.set(commandConfig, promise);
return await promise;
}
async function runShellCommand(command: string, timeoutMs: number): Promise<string | undefined> {
try {
let output = "";
const result = await executeShell({ command, timeoutMs }, (err, chunk) => {
if (!err) {
output += chunk;
}
});
if (result.timedOut || result.exitCode !== 0) {
return undefined;
}
const trimmed = output.trim();
return trimmed.length > 0 ? trimmed : undefined;
} catch {
return undefined;
}
}
/**
* Resolve all header values using the same resolution logic as API keys.
*/
export async function resolveHeaders(
headers: Record<string, string> | undefined,
): Promise<Record<string, string> | undefined> {
if (!headers) return undefined;
const resolved: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const resolvedValue = await resolveConfigValue(value);
if (resolvedValue) {
resolved[key] = resolvedValue;
}
}
return Object.keys(resolved).length > 0 ? resolved : undefined;
}
/** Clear the config value command cache. Exported for testing. */
export function clearConfigValueCache(): void {
commandResultCache.clear();
commandInFlight.clear();
}