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