CLI: streamline startup paths and env parsing

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.
This commit is contained in:
Gustavo Madeira Santana
2026-01-18 15:56:24 -05:00
committed by Peter Steinberger
parent 97531f174f
commit acb523de86
58 changed files with 1274 additions and 500 deletions

View File

@@ -4,6 +4,7 @@ import { logDebug, logWarn } from "../logger.js";
import { getLogger } from "../logging.js";
import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js";
import { formatBonjourError } from "./bonjour-errors.js";
import { isTruthyEnvValue } from "./env.js";
import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js";
export type GatewayBonjourAdvertiser = {
@@ -23,7 +24,7 @@ export type GatewayBonjourAdvertiseOpts = {
};
function isDisabledByEnv() {
if (process.env.CLAWDBOT_DISABLE_BONJOUR === "1") return true;
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_BONJOUR)) return true;
if (process.env.NODE_ENV === "test") return true;
if (process.env.VITEST) return true;
return false;

88
src/infra/cli-timing.ts Normal file
View File

@@ -0,0 +1,88 @@
import { isTruthyEnvValue } from "./env.js";
type CliTimingEntry = {
label: string;
ms: number;
};
type CliTimingPayload = {
type: "clawdbot.cli.timing";
pid: number;
entries: CliTimingEntry[];
extra?: Record<string, unknown> | null;
};
const enabled = isTruthyEnvValue(process.env.CLAWDBOT_CLI_TIMING);
let emitted = false;
let disabled = false;
const startNs = (() => {
if (!enabled) return 0n;
const envStart = process.env.CLAWDBOT_CLI_START_NS;
if (envStart) {
try {
return BigInt(envStart);
} catch {
// ignore
}
}
const now = process.hrtime.bigint();
process.env.CLAWDBOT_CLI_START_NS = String(now);
return now;
})();
const marks: Array<{ label: string; ns: bigint }> = [];
const toMs = (ns: bigint) => Number(ns) / 1_000_000;
const buildEntries = (endNs: bigint): CliTimingEntry[] => {
const entries: CliTimingEntry[] = [{ label: "start", ms: 0 }];
for (const mark of marks) {
entries.push({ label: mark.label, ms: toMs(mark.ns - startNs) });
}
entries.push({ label: "end", ms: toMs(endNs - startNs) });
return entries;
};
const emitTiming = (extra?: Record<string, unknown> | null) => {
if (!enabled || emitted || disabled) return;
emitted = true;
const endNs = process.hrtime.bigint();
const payload: CliTimingPayload = {
type: "clawdbot.cli.timing",
pid: process.pid,
entries: buildEntries(endNs),
extra: extra ?? null,
};
try {
process.stderr.write(`${JSON.stringify(payload)}\n`);
} catch {
// ignore timing failures
}
};
if (enabled) {
process.once("exit", () => {
emitTiming({ exitCode: process.exitCode ?? 0 });
});
}
export function getCliTiming(): {
mark: (label: string) => void;
emit: (extra?: Record<string, unknown> | null) => void;
} | null {
if (!enabled || disabled) return null;
return {
mark: (label: string) => {
if (!enabled || disabled) return;
marks.push({ label, ns: process.hrtime.bigint() });
},
emit: (extra?: Record<string, unknown> | null) => {
emitTiming(extra);
},
};
}
export function disableCliTiming(): void {
disabled = true;
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { normalizeZaiEnv } from "./env.js";
import { isTruthyEnvValue, normalizeZaiEnv } from "./env.js";
describe("normalizeZaiEnv", () => {
it("copies Z_AI_API_KEY to ZAI_API_KEY when missing", () => {
@@ -35,3 +35,19 @@ describe("normalizeZaiEnv", () => {
else process.env.Z_AI_API_KEY = prevZAi;
});
});
describe("isTruthyEnvValue", () => {
it("accepts common truthy values", () => {
expect(isTruthyEnvValue("1")).toBe(true);
expect(isTruthyEnvValue("true")).toBe(true);
expect(isTruthyEnvValue(" yes ")).toBe(true);
expect(isTruthyEnvValue("ON")).toBe(true);
});
it("rejects other values", () => {
expect(isTruthyEnvValue("0")).toBe(false);
expect(isTruthyEnvValue("false")).toBe(false);
expect(isTruthyEnvValue("")).toBe(false);
expect(isTruthyEnvValue(undefined)).toBe(false);
});
});

View File

@@ -1,9 +1,15 @@
import { parseBooleanValue } from "../utils/boolean.js";
export function normalizeZaiEnv(): void {
if (!process.env.ZAI_API_KEY?.trim() && process.env.Z_AI_API_KEY?.trim()) {
process.env.ZAI_API_KEY = process.env.Z_AI_API_KEY;
}
}
export function isTruthyEnvValue(value?: string): boolean {
return parseBooleanValue(value) === true;
}
export function normalizeEnv(): void {
normalizeZaiEnv();
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isTruthyEnvValue } from "./env.js";
import { resolveBrewPathDirs } from "./brew.js";
@@ -94,7 +95,7 @@ function candidateBinDirs(opts: EnsureClawdbotPathOpts): string[] {
* under launchd/minimal environments (and inside the macOS app bundle).
*/
export function ensureClawdbotCliOnPath(opts: EnsureClawdbotPathOpts = {}) {
if (process.env.CLAWDBOT_PATH_BOOTSTRAPPED === "1") return;
if (isTruthyEnvValue(process.env.CLAWDBOT_PATH_BOOTSTRAPPED)) return;
process.env.CLAWDBOT_PATH_BOOTSTRAPPED = "1";
const existing = opts.pathEnv ?? process.env.PATH ?? "";

View File

@@ -1,15 +1,11 @@
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 isTruthy(raw: string | undefined): boolean {
if (!raw) return false;
const value = raw.trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
}
function resolveShell(env: NodeJS.ProcessEnv): string {
const shell = env.SHELL?.trim();
return shell && shell.length > 0 ? shell : "/bin/sh";
@@ -93,7 +89,11 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal
}
export function shouldEnableShellEnvFallback(env: NodeJS.ProcessEnv): boolean {
return isTruthy(env.CLAWDBOT_LOAD_SHELL_ENV);
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 {