mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-20 00:59:21 +00:00
Fix memory plugin CLI help dispatch (#83841)
* fix cli help for active memory plugin * docs add changelog for memory cli help * test fix root help mock type
This commit is contained in:
parent
0b4fc26d4a
commit
eb6dd2c65d
14 changed files with 434 additions and 24 deletions
|
|
@ -137,6 +137,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Mac app: render channel quick config as aligned Settings rows and hide schema-only variants that cannot be edited safely from the quick pane.
|
||||
- Gateway/webchat: hide internal runtime-context and other `display: false` transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.
|
||||
- CLI/help: keep `gateway`, `doctor`, `status`, and `health` help registration out of action/runtime imports so subcommand `--help` stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.
|
||||
- CLI/help: show plugin-owned command help based on the active memory slot so LanceDB memory users see `ltm` instead of unavailable `memory` commands. Fixes #83745. (#83841) Thanks @joshavant.
|
||||
- Cron/Discord: keep explicit announce runs in message-tool-only source-reply mode so scheduled agent turns post once instead of also echoing through automatic visible replies. Fixes #83261. Thanks @Theralley.
|
||||
- Telegram: preserve forum-topic origin targets in inbound, audio-preflight, and skipped-message hook contexts so follow-up delivery stays bound to the originating topic. Fixes #83302. Thanks @M00zyx.
|
||||
- Telegram: retry HTTP 421 Misdirected Request send failures on a fresh fallback transport so transient edge-node routing errors no longer drop outbound replies. Fixes #48892. (#48908) Thanks @MarsDoge.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ title: "Memory"
|
|||
# `openclaw memory`
|
||||
|
||||
Manage semantic memory indexing and search.
|
||||
Provided by the active memory plugin (default: `memory-core`; set `plugins.slots.memory = "none"` to disable).
|
||||
Provided by the bundled `memory-core` plugin. The command is available when
|
||||
`plugins.slots.memory` selects `memory-core` (the default); other memory plugins
|
||||
expose their own CLI namespaces.
|
||||
|
||||
Related:
|
||||
|
||||
|
|
|
|||
|
|
@ -238,12 +238,12 @@ openclaw ltm search "project preferences"
|
|||
openclaw ltm stats
|
||||
```
|
||||
|
||||
The plugin also extends `openclaw memory` with a non-vector `query` subcommand
|
||||
that runs against the LanceDB table directly:
|
||||
The `query` subcommand runs a non-vector query against the LanceDB table
|
||||
directly:
|
||||
|
||||
```bash
|
||||
openclaw memory query --cols id,text,createdAt --limit 20
|
||||
openclaw memory query --filter "category = 'preference'" --order-by createdAt:desc
|
||||
openclaw ltm query --cols id,text,createdAt --limit 20
|
||||
openclaw ltm query --filter "category = 'preference'" --order-by createdAt:desc
|
||||
```
|
||||
|
||||
- `--cols <columns>`: comma-separated column allowlist (defaults to `id`, `text`, `importance`, `category`, `createdAt`).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ export default definePluginEntry({
|
|||
name: "Memory LanceDB",
|
||||
description: "LanceDB-backed memory provider",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, { commands: ["ltm"] });
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "ltm",
|
||||
description: "Inspect and query LanceDB-backed memory",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
71
openclaw.mjs
71
openclaw.mjs
|
|
@ -334,6 +334,72 @@ const isBrowserHelpInvocation = (argv) =>
|
|||
const isHelpFastPathDisabled = () =>
|
||||
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
|
||||
|
||||
const normalizeLauncherHomeValue = (value) => {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const resolveLauncherOsHomeDir = () =>
|
||||
normalizeLauncherHomeValue(process.env.HOME) ??
|
||||
normalizeLauncherHomeValue(process.env.USERPROFILE) ??
|
||||
os.homedir();
|
||||
|
||||
const resolveLauncherHomeDir = () => {
|
||||
const explicit = normalizeLauncherHomeValue(process.env.OPENCLAW_HOME);
|
||||
const rawHome =
|
||||
explicit && (explicit === "~" || explicit.startsWith("~/") || explicit.startsWith("~\\"))
|
||||
? explicit.replace(/^~(?=$|[\\/])/, resolveLauncherOsHomeDir())
|
||||
: (explicit ?? resolveLauncherOsHomeDir());
|
||||
return path.resolve(rawHome);
|
||||
};
|
||||
|
||||
const resolveLauncherUserPath = (input) => {
|
||||
if (input === "~") {
|
||||
return resolveLauncherHomeDir();
|
||||
}
|
||||
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
||||
return path.join(resolveLauncherHomeDir(), input.slice(2));
|
||||
}
|
||||
return path.resolve(input);
|
||||
};
|
||||
|
||||
const resolveLauncherConfigPaths = () => {
|
||||
const explicit = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
||||
if (explicit) {
|
||||
return [resolveLauncherUserPath(explicit)];
|
||||
}
|
||||
const stateOverride = process.env.OPENCLAW_STATE_DIR?.trim();
|
||||
if (stateOverride) {
|
||||
const stateDir = resolveLauncherUserPath(stateOverride);
|
||||
return [path.join(stateDir, "openclaw.json"), path.join(stateDir, "clawdbot.json")];
|
||||
}
|
||||
const homeDir = resolveLauncherHomeDir();
|
||||
return [
|
||||
path.join(homeDir, ".openclaw", "openclaw.json"),
|
||||
path.join(homeDir, ".openclaw", "clawdbot.json"),
|
||||
path.join(homeDir, ".clawdbot", "openclaw.json"),
|
||||
path.join(homeDir, ".clawdbot", "clawdbot.json"),
|
||||
];
|
||||
};
|
||||
|
||||
const shouldDeferRootHelpToRuntimeEntry = () => {
|
||||
if (
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() ||
|
||||
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (const configPath of resolveLauncherConfigPaths()) {
|
||||
try {
|
||||
const raw = readFileSync(configPath, "utf8");
|
||||
return /\bplugins\b|\$include\b/.test(raw);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadPrecomputedHelpText = (key) => {
|
||||
try {
|
||||
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
|
||||
|
|
@ -349,6 +415,9 @@ const tryOutputBareRootHelp = async () => {
|
|||
if (!isBareRootHelpInvocation(process.argv)) {
|
||||
return false;
|
||||
}
|
||||
if (shouldDeferRootHelpToRuntimeEntry()) {
|
||||
return false;
|
||||
}
|
||||
const precomputed = loadPrecomputedHelpText("rootHelpText");
|
||||
if (precomputed) {
|
||||
process.stdout.write(precomputed);
|
||||
|
|
@ -358,7 +427,7 @@ const tryOutputBareRootHelp = async () => {
|
|||
try {
|
||||
const mod = await import(specifier);
|
||||
if (typeof mod.outputRootHelp === "function") {
|
||||
mod.outputRootHelp();
|
||||
await mod.outputRootHelp();
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe("command-registration-policy", () => {
|
|||
primary: "voicecall",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "help", "--help"],
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ export function shouldSkipPluginCommandRegistration(params: {
|
|||
return invocation.hasHelpOrVersion && invocation.commandPath.length <= 1;
|
||||
}
|
||||
if (invocation.hasHelpOrVersion) {
|
||||
return true;
|
||||
return (
|
||||
!params.primary || params.hasBuiltinPrimary || isReservedNonPluginCommandRoot(params.primary)
|
||||
);
|
||||
}
|
||||
if (params.hasBuiltinPrimary) {
|
||||
return true;
|
||||
|
|
|
|||
51
src/cli/root-help-live-config.test.ts
Normal file
51
src/cli/root-help-live-config.test.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadRootHelpRenderOptionsForConfigSensitivePlugins } from "./root-help-live-config.js";
|
||||
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
}));
|
||||
|
||||
describe("root help live config", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses precomputed help when plugin-sensitive config is invalid", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
valid: false,
|
||||
sourceConfig: {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeConfig: {},
|
||||
});
|
||||
|
||||
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins({})).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("uses snapshot runtime config when plugin config affects help", async () => {
|
||||
const runtimeConfig = {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
},
|
||||
};
|
||||
const env = {};
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
valid: true,
|
||||
sourceConfig: runtimeConfig,
|
||||
runtimeConfig,
|
||||
});
|
||||
|
||||
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins(env)).resolves.toEqual({
|
||||
config: runtimeConfig,
|
||||
env,
|
||||
});
|
||||
});
|
||||
});
|
||||
53
src/cli/root-help-live-config.ts
Normal file
53
src/cli/root-help-live-config.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RootHelpRenderOptions } from "./program/root-help.js";
|
||||
|
||||
function hasEntries(value: object | undefined): boolean {
|
||||
return !!value && Object.keys(value).length > 0;
|
||||
}
|
||||
|
||||
function hasListEntries(value: string[] | undefined): boolean {
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
}
|
||||
|
||||
export function hasPluginHelpAffectingConfig(config: OpenClawConfig | null | undefined): boolean {
|
||||
const plugins = config?.plugins;
|
||||
if (!plugins) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
plugins.enabled === false ||
|
||||
hasListEntries(plugins.allow) ||
|
||||
hasListEntries(plugins.deny) ||
|
||||
plugins.bundledDiscovery !== undefined ||
|
||||
hasListEntries(plugins.load?.paths) ||
|
||||
hasEntries(plugins.slots) ||
|
||||
hasEntries(plugins.entries) ||
|
||||
hasEntries(plugins.installs)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasPluginHelpAffectingEnv(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(
|
||||
env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() || env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadRootHelpRenderOptionsForConfigSensitivePlugins(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<RootHelpRenderOptions | null> {
|
||||
const configModule = await import("../config/config.js");
|
||||
const snapshot = await configModule.readConfigFileSnapshot({
|
||||
observe: false,
|
||||
skipPluginValidation: true,
|
||||
});
|
||||
if (!snapshot.valid) {
|
||||
return null;
|
||||
}
|
||||
if (!hasPluginHelpAffectingEnv(env) && !hasPluginHelpAffectingConfig(snapshot.sourceConfig)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
config: snapshot.runtimeConfig,
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import process from "node:process";
|
|||
import { CommanderError } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import type { RootHelpRenderOptions } from "./program/root-help.js";
|
||||
import { runCli, shouldStartProxyForCli } from "./run-main.js";
|
||||
|
||||
const tryRouteCliMock = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -18,6 +19,9 @@ const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
|
|||
const outputRootHelpMock = vi.hoisted(() => vi.fn());
|
||||
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const loadRootHelpRenderOptionsForConfigSensitivePluginsMock = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<RootHelpRenderOptions | null>>(async () => null),
|
||||
);
|
||||
const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
|
||||
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -168,6 +172,11 @@ vi.mock("./root-help-metadata.js", () => ({
|
|||
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
|
||||
}));
|
||||
|
||||
vi.mock("./root-help-live-config.js", () => ({
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins:
|
||||
loadRootHelpRenderOptionsForConfigSensitivePluginsMock,
|
||||
}));
|
||||
|
||||
vi.mock("./program.js", () => ({
|
||||
buildProgram: buildProgramMock,
|
||||
}));
|
||||
|
|
@ -242,6 +251,7 @@ describe("runCli exit behavior", () => {
|
|||
listAgentHarnessIdsMock.mockReturnValue([]);
|
||||
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
|
||||
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValue(null);
|
||||
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
|
||||
loadConfigMock.mockReturnValue({});
|
||||
startProxyMock.mockResolvedValue(null);
|
||||
|
|
@ -401,6 +411,7 @@ describe("runCli exit behavior", () => {
|
|||
|
||||
await runCli(["node", "openclaw", "--help"]);
|
||||
|
||||
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled();
|
||||
expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled();
|
||||
|
|
@ -416,6 +427,7 @@ describe("runCli exit behavior", () => {
|
|||
|
||||
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "--help"]);
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
|
|
@ -424,6 +436,28 @@ describe("runCli exit behavior", () => {
|
|||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("renders config-sensitive root help live instead of precomputed metadata", async () => {
|
||||
const liveOptions: RootHelpRenderOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
},
|
||||
},
|
||||
env: process.env,
|
||||
};
|
||||
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValueOnce(liveOptions);
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
|
||||
|
||||
await runCli(["node", "openclaw", "--help"]);
|
||||
|
||||
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputPrecomputedRootHelpTextMock).not.toHaveBeenCalled();
|
||||
expect(outputRootHelpMock).toHaveBeenCalledWith(liveOptions);
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not start the managed proxy for local gateway client commands", async () => {
|
||||
tryRouteCliMock.mockResolvedValueOnce(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -534,11 +534,19 @@ export async function runCli(argv: string[] = process.argv) {
|
|||
|
||||
try {
|
||||
if (shouldUseRootHelpFastPath(normalizedArgv)) {
|
||||
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
|
||||
if (!outputPrecomputedRootHelpText()) {
|
||||
const { outputRootHelp } = await import("./program/root-help.js");
|
||||
await outputRootHelp();
|
||||
const { loadRootHelpRenderOptionsForConfigSensitivePlugins } =
|
||||
await import("./root-help-live-config.js");
|
||||
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(
|
||||
process.env,
|
||||
);
|
||||
if (!liveRootHelpOptions) {
|
||||
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
|
||||
if (outputPrecomputedRootHelpText()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { outputRootHelp } = await import("./program/root-help.js");
|
||||
await outputRootHelp(liveRootHelpOptions ?? undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ describe("entry root help fast path", () => {
|
|||
outputPrecomputedRootHelpTextCalls += 1;
|
||||
return true;
|
||||
},
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
|
|
@ -24,6 +25,7 @@ describe("entry root help fast path", () => {
|
|||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
||||
env: {},
|
||||
});
|
||||
|
||||
|
|
@ -31,6 +33,37 @@ describe("entry root help fast path", () => {
|
|||
expect(outputRootHelpCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("renders live root help when plugin config changes command descriptors", async () => {
|
||||
let outputPrecomputedRootHelpTextCalls = 0;
|
||||
const outputRootHelpOptions: unknown[] = [];
|
||||
const liveOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
};
|
||||
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
env: {},
|
||||
outputPrecomputedRootHelpText: () => {
|
||||
outputPrecomputedRootHelpTextCalls += 1;
|
||||
return true;
|
||||
},
|
||||
outputRootHelp: (options) => {
|
||||
outputRootHelpOptions.push(options);
|
||||
},
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => liveOptions,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedRootHelpTextCalls).toBe(0);
|
||||
expect(outputRootHelpOptions).toEqual([liveOptions]);
|
||||
});
|
||||
|
||||
it("ignores non-root help invocations", async () => {
|
||||
let outputRootHelpCalls = 0;
|
||||
|
||||
|
|
@ -38,6 +71,7 @@ describe("entry root help fast path", () => {
|
|||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
||||
env: {},
|
||||
});
|
||||
|
||||
|
|
@ -54,6 +88,7 @@ describe("entry root help fast path", () => {
|
|||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
||||
env: {},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
31
src/entry.ts
31
src/entry.ts
|
|
@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|||
import { isRootHelpInvocation } from "./cli/argv.js";
|
||||
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
|
||||
import type { RootHelpRenderOptions } from "./cli/program/root-help.js";
|
||||
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
|
||||
import {
|
||||
enableOpenClawCompileCache,
|
||||
|
|
@ -157,7 +158,10 @@ export async function tryHandleRootHelpFastPath(
|
|||
argv: string[],
|
||||
deps: {
|
||||
outputPrecomputedRootHelpText?: () => boolean;
|
||||
outputRootHelp?: () => void | Promise<void>;
|
||||
outputRootHelp?: (options?: RootHelpRenderOptions) => void | Promise<void>;
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins?: (
|
||||
env?: NodeJS.ProcessEnv,
|
||||
) => Promise<RootHelpRenderOptions | null>;
|
||||
onError?: (error: unknown) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
} = {},
|
||||
|
|
@ -178,17 +182,22 @@ export async function tryHandleRootHelpFastPath(
|
|||
process.exitCode = 1;
|
||||
});
|
||||
try {
|
||||
if (deps.outputRootHelp) {
|
||||
await deps.outputRootHelp();
|
||||
return true;
|
||||
}
|
||||
const outputPrecomputedRootHelpText =
|
||||
deps.outputPrecomputedRootHelpText ??
|
||||
(await import("./cli/root-help-metadata.js")).outputPrecomputedRootHelpText;
|
||||
if (!outputPrecomputedRootHelpText()) {
|
||||
const { outputRootHelp } = await import("./cli/program/root-help.js");
|
||||
await outputRootHelp();
|
||||
const loadRootHelpRenderOptionsForConfigSensitivePlugins =
|
||||
deps.loadRootHelpRenderOptionsForConfigSensitivePlugins ??
|
||||
(await import("./cli/root-help-live-config.js"))
|
||||
.loadRootHelpRenderOptionsForConfigSensitivePlugins;
|
||||
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(deps.env);
|
||||
if (!liveRootHelpOptions) {
|
||||
const outputPrecomputedRootHelpText =
|
||||
deps.outputPrecomputedRootHelpText ??
|
||||
(await import("./cli/root-help-metadata.js")).outputPrecomputedRootHelpText;
|
||||
if (outputPrecomputedRootHelpText()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const outputRootHelp =
|
||||
deps.outputRootHelp ?? (await import("./cli/program/root-help.js")).outputRootHelp;
|
||||
await outputRootHelp(liveRootHelpOptions ?? undefined);
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
|
|
|
|||
|
|
@ -203,6 +203,144 @@ describe("openclaw launcher", () => {
|
|||
expect(result.stderr).toContain("missing dist/entry.(m)js");
|
||||
});
|
||||
|
||||
it("uses precomputed root help when plugin config does not invalidate it", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ rootHelpText: "PRECOMPUTED help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("PRECOMPUTED help\n");
|
||||
});
|
||||
|
||||
it("defers root help to the runtime entry when plugin config can change help", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const configPath = path.join(fixtureRoot, "openclaw.json");
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv({ OPENCLAW_CONFIG_PATH: configPath }),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it("checks the OPENCLAW_HOME default config path before using precomputed root help", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const openclawHome = path.join(fixtureRoot, "home");
|
||||
const configDir = path.join(openclawHome, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv({ OPENCLAW_HOME: openclawHome }),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it("checks legacy config candidates before using precomputed root help", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const home = path.join(fixtureRoot, "home");
|
||||
const legacyConfigDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(legacyConfigDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(legacyConfigDir, "clawdbot.json"),
|
||||
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv({ HOME: home, OPENCLAW_HOME: undefined }),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it("defers root help when the active config has includes", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const configPath = path.join(fixtureRoot, "openclaw.json");
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(configPath, JSON.stringify({ $include: "memory.json" }), "utf8");
|
||||
|
||||
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv({ OPENCLAW_CONFIG_PATH: configPath }),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it("explains how to recover from an unbuilt source install", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await addSourceTreeMarker(fixtureRoot);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue