218 lines
6.2 KiB
TypeScript
218 lines
6.2 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
|
|
import { isMainModule } from "../infra/is-main.js";
|
|
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
|
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
|
import { VERSION } from "../version.js";
|
|
import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
|
import { emitCliBanner } from "./banner.js";
|
|
import { resolveCliName } from "./cli-name.js";
|
|
import { normalizeWindowsArgv } from "./windows-argv.js";
|
|
|
|
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
|
const index = argv.indexOf("--update");
|
|
if (index === -1) {
|
|
return argv;
|
|
}
|
|
|
|
const next = [...argv];
|
|
next.splice(index, 1, "update");
|
|
return next;
|
|
}
|
|
|
|
export function shouldRegisterPrimarySubcommand(argv: string[]): boolean {
|
|
return !hasHelpOrVersion(argv);
|
|
}
|
|
|
|
export function shouldEnsureCliPath(argv: string[]): boolean {
|
|
if (hasHelpOrVersion(argv)) {
|
|
return false;
|
|
}
|
|
const [primary, secondary] = getCommandPath(argv, 2);
|
|
if (!primary) {
|
|
return true;
|
|
}
|
|
if (primary === "status" || primary === "health" || primary === "sessions") {
|
|
return false;
|
|
}
|
|
if (primary === "config" && (secondary === "get" || secondary === "unset")) {
|
|
return false;
|
|
}
|
|
if (primary === "models" && (secondary === "list" || secondary === "status")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export type BootstrapRolloutStage = "legacy" | "internal" | "beta" | "default";
|
|
|
|
function normalizeBootstrapRolloutStage(
|
|
value: string | undefined,
|
|
): BootstrapRolloutStage | undefined {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (
|
|
normalized === "legacy" ||
|
|
normalized === "internal" ||
|
|
normalized === "beta" ||
|
|
normalized === "default"
|
|
) {
|
|
return normalized;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveBootstrapRolloutStage(
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): BootstrapRolloutStage {
|
|
const raw = env.DENCHCLAW_BOOTSTRAP_ROLLOUT ?? env.OPENCLAW_BOOTSTRAP_ROLLOUT;
|
|
return normalizeBootstrapRolloutStage(raw) ?? "default";
|
|
}
|
|
|
|
export function shouldEnableBootstrapCutover(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
if (
|
|
isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK) ||
|
|
isTruthyEnvValue(env.OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK)
|
|
) {
|
|
return false;
|
|
}
|
|
const stage = resolveBootstrapRolloutStage(env);
|
|
if (stage === "legacy") {
|
|
return false;
|
|
}
|
|
if (stage === "beta") {
|
|
return (
|
|
isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_BETA_OPT_IN) ||
|
|
isTruthyEnvValue(env.OPENCLAW_BOOTSTRAP_BETA_OPT_IN)
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function rewriteBareArgvToBootstrap(
|
|
argv: string[],
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): string[] {
|
|
if (hasHelpOrVersion(argv)) {
|
|
return argv;
|
|
}
|
|
if (getPrimaryCommand(argv)) {
|
|
return argv;
|
|
}
|
|
if (resolveCliName(argv) !== "denchclaw") {
|
|
return argv;
|
|
}
|
|
if (!shouldEnableBootstrapCutover(env)) {
|
|
return argv;
|
|
}
|
|
return [...argv.slice(0, 2), "bootstrap", ...argv.slice(2)];
|
|
}
|
|
|
|
function isDelegationDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
return (
|
|
isTruthyEnvValue(env.DENCHCLAW_DISABLE_OPENCLAW_DELEGATION) ||
|
|
isTruthyEnvValue(env.OPENCLAW_DISABLE_OPENCLAW_DELEGATION)
|
|
);
|
|
}
|
|
|
|
export function shouldDelegateToGlobalOpenClaw(
|
|
argv: string[],
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): boolean {
|
|
if (isDelegationDisabled(env)) {
|
|
return false;
|
|
}
|
|
const primary = getPrimaryCommand(argv);
|
|
if (!primary) {
|
|
return false;
|
|
}
|
|
return primary !== "bootstrap";
|
|
}
|
|
|
|
async function delegateToGlobalOpenClaw(argv: string[]): Promise<number> {
|
|
if (
|
|
isTruthyEnvValue(process.env.DENCHCLAW_DELEGATED) ||
|
|
isTruthyEnvValue(process.env.OPENCLAW_DELEGATED)
|
|
) {
|
|
throw new Error(
|
|
"OpenClaw delegation loop detected. Check PATH so `openclaw` resolves to the global OpenClaw CLI.",
|
|
);
|
|
}
|
|
const delegatedArgv = argv.slice(2);
|
|
return await new Promise<number>((resolve, reject) => {
|
|
const child = spawn("openclaw", delegatedArgv, {
|
|
stdio: "inherit",
|
|
env: {
|
|
...process.env,
|
|
DENCHCLAW_DELEGATED: "1",
|
|
OPENCLAW_DELEGATED: "1",
|
|
},
|
|
});
|
|
|
|
child.once("error", (error) => {
|
|
const err = error as NodeJS.ErrnoException;
|
|
if (err?.code === "ENOENT") {
|
|
reject(
|
|
new Error(
|
|
[
|
|
"Global `openclaw` CLI was not found on PATH.",
|
|
"Install it once with: npm install -g openclaw",
|
|
].join("\n"),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
reject(error);
|
|
});
|
|
|
|
child.once("exit", (code, signal) => {
|
|
if (signal) {
|
|
resolve(1);
|
|
return;
|
|
}
|
|
resolve(code ?? 1);
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function runCli(argv: string[] = process.argv) {
|
|
const normalizedArgv = normalizeWindowsArgv(argv);
|
|
normalizeEnv();
|
|
if (shouldEnsureCliPath(normalizedArgv)) {
|
|
ensureOpenClawCliOnPath();
|
|
}
|
|
|
|
// Enforce the minimum supported runtime before doing any work.
|
|
assertSupportedRuntime();
|
|
|
|
// Show the animated DenchClaw banner early so it appears for ALL invocations
|
|
// (bare `denchclaw`, subcommands, help, etc.). The bannerEmitted flag inside
|
|
// emitCliBanner prevents double-emission from the route / preAction hooks.
|
|
const commandPath = getCommandPath(normalizedArgv, 2);
|
|
const hideBanner =
|
|
isTruthyEnvValue(process.env.DENCHCLAW_HIDE_BANNER) ||
|
|
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
|
|
commandPath[0] === "update" ||
|
|
commandPath[0] === "completion" ||
|
|
(commandPath[0] === "plugins" && commandPath[1] === "update");
|
|
if (!hideBanner) {
|
|
await emitCliBanner(VERSION, { argv: normalizedArgv });
|
|
}
|
|
|
|
const parseArgv = rewriteBareArgvToBootstrap(rewriteUpdateFlagArgv(normalizedArgv));
|
|
if (shouldDelegateToGlobalOpenClaw(parseArgv)) {
|
|
const exitCode = await delegateToGlobalOpenClaw(parseArgv);
|
|
process.exitCode = exitCode;
|
|
return;
|
|
}
|
|
|
|
const { buildProgram } = await import("./program.js");
|
|
const program = buildProgram();
|
|
await program.parseAsync(parseArgv);
|
|
}
|
|
|
|
export function isCliMainModule(): boolean {
|
|
return isMainModule({ currentFile: fileURLToPath(import.meta.url) });
|
|
}
|