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:
committed by
Peter Steinberger
parent
97531f174f
commit
acb523de86
@@ -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
88
src/infra/cli-timing.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user