Add shared parseBooleanValue()/isTruthyEnvValue() and apply across CLI, gateway, memory, and live-test flags for consistent env handling. Introduce route-first fast paths, lazy subcommand registration, and deferred plugin loading to reduce CLI startup overhead. Centralize config validation via ensureConfigReady() and add config caching/deferred shell env fallback for fewer IO passes. Harden logger initialization/imports and add focused tests for argv, boolean parsing, frontmatter, and CLI subcommands.
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
|
|
import { isTruthyEnvValue } from "./env.js";
|
|
|
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
let lastAppliedKeys: string[] = [];
|
|
|
|
function resolveShell(env: NodeJS.ProcessEnv): string {
|
|
const shell = env.SHELL?.trim();
|
|
return shell && shell.length > 0 ? shell : "/bin/sh";
|
|
}
|
|
|
|
export type ShellEnvFallbackResult =
|
|
| { ok: true; applied: string[]; skippedReason?: never }
|
|
| { ok: true; applied: []; skippedReason: "already-has-keys" | "disabled" }
|
|
| { ok: false; error: string; applied: [] };
|
|
|
|
export type ShellEnvFallbackOptions = {
|
|
enabled: boolean;
|
|
env: NodeJS.ProcessEnv;
|
|
expectedKeys: string[];
|
|
logger?: Pick<typeof console, "warn">;
|
|
timeoutMs?: number;
|
|
exec?: typeof execFileSync;
|
|
};
|
|
|
|
export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFallbackResult {
|
|
const logger = opts.logger ?? console;
|
|
const exec = opts.exec ?? execFileSync;
|
|
|
|
if (!opts.enabled) {
|
|
lastAppliedKeys = [];
|
|
return { ok: true, applied: [], skippedReason: "disabled" };
|
|
}
|
|
|
|
const hasAnyKey = opts.expectedKeys.some((key) => Boolean(opts.env[key]?.trim()));
|
|
if (hasAnyKey) {
|
|
lastAppliedKeys = [];
|
|
return { ok: true, applied: [], skippedReason: "already-has-keys" };
|
|
}
|
|
|
|
const timeoutMs =
|
|
typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
|
? Math.max(0, opts.timeoutMs)
|
|
: DEFAULT_TIMEOUT_MS;
|
|
|
|
const shell = resolveShell(opts.env);
|
|
|
|
let stdout: Buffer;
|
|
try {
|
|
stdout = exec(shell, ["-l", "-c", "env -0"], {
|
|
encoding: "buffer",
|
|
timeout: timeoutMs,
|
|
maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
|
|
env: opts.env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
logger.warn(`[clawdbot] shell env fallback failed: ${msg}`);
|
|
lastAppliedKeys = [];
|
|
return { ok: false, error: msg, applied: [] };
|
|
}
|
|
|
|
const shellEnv = new Map<string, string>();
|
|
const parts = stdout.toString("utf8").split("\0");
|
|
for (const part of parts) {
|
|
if (!part) continue;
|
|
const eq = part.indexOf("=");
|
|
if (eq <= 0) continue;
|
|
const key = part.slice(0, eq);
|
|
const value = part.slice(eq + 1);
|
|
if (!key) continue;
|
|
shellEnv.set(key, value);
|
|
}
|
|
|
|
const applied: string[] = [];
|
|
for (const key of opts.expectedKeys) {
|
|
if (opts.env[key]?.trim()) continue;
|
|
const value = shellEnv.get(key);
|
|
if (!value?.trim()) continue;
|
|
opts.env[key] = value;
|
|
applied.push(key);
|
|
}
|
|
|
|
lastAppliedKeys = applied;
|
|
return { ok: true, applied };
|
|
}
|
|
|
|
export function shouldEnableShellEnvFallback(env: NodeJS.ProcessEnv): boolean {
|
|
return isTruthyEnvValue(env.CLAWDBOT_LOAD_SHELL_ENV);
|
|
}
|
|
|
|
export function shouldDeferShellEnvFallback(env: NodeJS.ProcessEnv): boolean {
|
|
return isTruthyEnvValue(env.CLAWDBOT_DEFER_SHELL_ENV_FALLBACK);
|
|
}
|
|
|
|
export function resolveShellEnvFallbackTimeoutMs(env: NodeJS.ProcessEnv): number {
|
|
const raw = env.CLAWDBOT_SHELL_ENV_TIMEOUT_MS?.trim();
|
|
if (!raw) return DEFAULT_TIMEOUT_MS;
|
|
const parsed = Number.parseInt(raw, 10);
|
|
if (!Number.isFinite(parsed)) return DEFAULT_TIMEOUT_MS;
|
|
return Math.max(0, parsed);
|
|
}
|
|
|
|
export function getShellEnvAppliedKeys(): string[] {
|
|
return [...lastAppliedKeys];
|
|
}
|