1import { execSync } from "node:child_process";
2
3/**
4 * Resolve a configuration value (API key, header value, etc.) to a concrete string.
5 *
6 * Resolution order, following pi-coding-agent's convention:
7 * - `"!command"` — executes the rest as a shell command, uses trimmed stdout
8 * - `"$VAR"` or `"${VAR}"` — treats as an env var reference (with sigil)
9 * - Otherwise checks `process.env[value]` — bare name is tried as env var
10 * - If no env var matches, the string is used as a literal value
11 *
12 * Returns `undefined` only when a shell command fails or produces empty output.
13 */
14export function resolveConfigValue(value: string): string | undefined {
15 if (value.startsWith("!")) {
16 return executeShellCommand(value.slice(1));
17 }
18
19 // Explicit $VAR or ${VAR} reference
20 const envRef = value.match(/^\$\{(.+)\}$|^\$([A-Za-z_][A-Za-z0-9_]*)$/);
21 if (envRef) {
22 const name = envRef[1] ?? envRef[2]!;
23 return process.env[name] ?? undefined;
24 }
25
26 // Bare name — check as env var first, then use as literal
27 const envValue = process.env[value];
28 return envValue || value;
29}
30
31/**
32 * Expand `$VAR` and `${VAR}` references embedded within a larger string.
33 * Unlike `resolveConfigValue`, this handles mixed literal + env-var strings
34 * like `"Bearer $API_KEY"`.
35 */
36export function expandEnvVars(value: string): string {
37 return value.replace(
38 /\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
39 (_, braced, bare) => {
40 const name = braced ?? bare;
41 return process.env[name] ?? "";
42 },
43 );
44}
45
46/**
47 * Resolve all values in a headers record using `resolveConfigValue`.
48 * Drops entries whose values resolve to `undefined`.
49 */
50export function resolveHeaders(
51 headers: Record<string, string> | undefined,
52): Record<string, string> | undefined {
53 if (!headers) return undefined;
54 const resolved: Record<string, string> = {};
55 for (const [key, value] of Object.entries(headers)) {
56 const resolvedValue = resolveConfigValue(value);
57 if (resolvedValue) {
58 resolved[key] = resolvedValue;
59 }
60 }
61 return Object.keys(resolved).length > 0 ? resolved : undefined;
62}
63
64function executeShellCommand(command: string): string | undefined {
65 try {
66 const output = execSync(command, {
67 encoding: "utf-8",
68 timeout: 10_000,
69 stdio: ["ignore", "pipe", "ignore"],
70 });
71 return output.trim() || undefined;
72 } catch {
73 return undefined;
74 }
75}