env.ts

 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}