refactor: split manifest command alias helpers

This commit is contained in:
Peter Steinberger
2026-04-10 17:34:29 +01:00
parent 5f3356a746
commit 777c6f7580
13 changed files with 312 additions and 119 deletions

View File

@@ -0,0 +1,26 @@
import type { OpenClawConfig } from "../config/config.js";
import {
resolveManifestCommandAliasOwnerInRegistry,
type PluginManifestCommandAliasRecord,
} from "./manifest-command-aliases.js";
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
export function resolveManifestCommandAliasOwner(params: {
command: string | undefined;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
registry?: PluginManifestRegistry;
}): PluginManifestCommandAliasRecord | undefined {
const registry =
params.registry ??
loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
return resolveManifestCommandAliasOwnerInRegistry({
command: params.command,
registry,
});
}

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import {
normalizeManifestCommandAliases,
resolveManifestCommandAliasOwnerInRegistry,
} from "./manifest-command-aliases.js";
describe("manifest command aliases", () => {
it("normalizes string and object entries", () => {
expect(
normalizeManifestCommandAliases([
"memory",
{ name: "reindex", kind: "runtime-slash", cliCommand: "memory" },
{ name: "" },
{ name: "bad-kind", kind: "unknown" },
]),
).toEqual([
{ name: "memory" },
{ name: "reindex", kind: "runtime-slash", cliCommand: "memory" },
{ name: "bad-kind" },
]);
});
it("resolves aliases without treating plugin ids as command aliases", () => {
const registry = {
plugins: [
{
id: "memory-core",
commandAliases: [{ name: "memory", kind: "runtime-slash" as const }],
},
{
id: "memory",
commandAliases: [{ name: "legacy-memory" }],
},
],
};
expect(resolveManifestCommandAliasOwnerInRegistry({ command: "memory", registry })).toBe(
undefined,
);
expect(
resolveManifestCommandAliasOwnerInRegistry({ command: "legacy-memory", registry }),
).toMatchObject({
pluginId: "memory",
name: "legacy-memory",
});
});
});

View File

@@ -0,0 +1,88 @@
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { isRecord } from "../utils.js";
export type PluginManifestCommandAliasKind = "runtime-slash";
export type PluginManifestCommandAlias = {
/** Command-like name users may put in plugin config by mistake. */
name: string;
/** Command family, used for targeted diagnostics. */
kind?: PluginManifestCommandAliasKind;
/** Optional root CLI command that handles related CLI operations. */
cliCommand?: string;
};
export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
pluginId: string;
};
export type PluginManifestCommandAliasRegistry = {
plugins: readonly {
id: string;
commandAliases?: readonly PluginManifestCommandAlias[];
}[];
};
export function normalizeManifestCommandAliases(
value: unknown,
): PluginManifestCommandAlias[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: PluginManifestCommandAlias[] = [];
for (const entry of value) {
if (typeof entry === "string") {
const name = normalizeOptionalString(entry) ?? "";
if (name) {
normalized.push({ name });
}
continue;
}
if (!isRecord(entry)) {
continue;
}
const name = normalizeOptionalString(entry.name) ?? "";
if (!name) {
continue;
}
const kind = entry.kind === "runtime-slash" ? entry.kind : undefined;
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
normalized.push({
name,
...(kind ? { kind } : {}),
...(cliCommand ? { cliCommand } : {}),
});
}
return normalized.length > 0 ? normalized : undefined;
}
export function resolveManifestCommandAliasOwnerInRegistry(params: {
command: string | undefined;
registry: PluginManifestCommandAliasRegistry;
}): PluginManifestCommandAliasRecord | undefined {
const normalizedCommand = normalizeOptionalLowercaseString(params.command);
if (!normalizedCommand) {
return undefined;
}
const commandIsPluginId = params.registry.plugins.some(
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
);
if (commandIsPluginId) {
return undefined;
}
for (const plugin of params.registry.plugins) {
const alias = plugin.commandAliases?.find(
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
);
if (alias) {
return { ...alias, pluginId: plugin.id };
}
}
return undefined;
}

View File

@@ -14,10 +14,10 @@ import {
type NormalizedPluginsConfig,
} from "./config-policy.js";
import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
import type { PluginManifestCommandAlias } from "./manifest-command-aliases.js";
import {
loadPluginManifest,
type OpenClawPackageManifest,
type PluginManifestCommandAlias,
type PluginManifestConfigContracts,
type PluginManifest,
type PluginManifestChannelConfig,
@@ -206,47 +206,6 @@ export function resolveManifestContractOwnerPluginId(params: {
)?.id;
}
export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
pluginId: string;
};
export function resolveManifestCommandAliasOwner(params: {
command: string | undefined;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
registry?: PluginManifestRegistry;
}): PluginManifestCommandAliasRecord | undefined {
const normalizedCommand = normalizeOptionalLowercaseString(params.command);
if (!normalizedCommand) {
return undefined;
}
const registry =
params.registry ??
loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
const commandIsPluginId = registry.plugins.some(
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
);
if (commandIsPluginId) {
return undefined;
}
for (const plugin of registry.plugins) {
const alias = plugin.commandAliases?.find(
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
);
if (alias) {
return { ...alias, pluginId: plugin.id };
}
}
return undefined;
}
function resolveManifestCacheMs(env: NodeJS.ProcessEnv): number {
const raw = env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS?.trim();
if (raw === "" || raw === "0") {

View File

@@ -7,6 +7,10 @@ import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/bou
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeTrimmedStringList } from "../shared/string-normalization.js";
import { isRecord } from "../utils.js";
import {
normalizeManifestCommandAliases,
type PluginManifestCommandAlias,
} from "./manifest-command-aliases.js";
import type { PluginConfigUiHint, PluginKind } from "./types.js";
export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
@@ -34,17 +38,6 @@ export type PluginManifestModelSupport = {
modelPatterns?: string[];
};
export type PluginManifestCommandAliasKind = "runtime-slash";
export type PluginManifestCommandAlias = {
/** Command-like name users may put in plugin config by mistake. */
name: string;
/** Command family, used for targeted diagnostics. */
kind?: PluginManifestCommandAliasKind;
/** Optional root CLI command that handles related CLI operations. */
cliCommand?: string;
};
export type PluginManifestConfigLiteral = string | number | boolean | null;
export type PluginManifestDangerousConfigFlag = {
@@ -373,38 +366,6 @@ function normalizeManifestModelSupport(value: unknown): PluginManifestModelSuppo
return Object.keys(modelSupport).length > 0 ? modelSupport : undefined;
}
function normalizeManifestCommandAliases(value: unknown): PluginManifestCommandAlias[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: PluginManifestCommandAlias[] = [];
for (const entry of value) {
if (typeof entry === "string") {
const name = normalizeOptionalString(entry) ?? "";
if (name) {
normalized.push({ name });
}
continue;
}
if (!isRecord(entry)) {
continue;
}
const name = normalizeOptionalString(entry.name) ?? "";
if (!name) {
continue;
}
const kind = entry.kind === "runtime-slash" ? entry.kind : undefined;
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
normalized.push({
name,
...(kind ? { kind } : {}),
...(cliCommand ? { cliCommand } : {}),
});
}
return normalized.length > 0 ? normalized : undefined;
}
function normalizeProviderAuthChoices(
value: unknown,
): PluginManifestProviderAuthChoice[] | undefined {

View File

@@ -0,0 +1,46 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const loadPluginManifestRegistryMock = vi.hoisted(() => vi.fn());
vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistry: loadPluginManifestRegistryMock,
}));
afterEach(() => {
loadPluginManifestRegistryMock.mockReset();
});
describe("setup-registry runtime fallback", () => {
it("uses bundled manifest cliBackends when the runtime registry has no match", async () => {
loadPluginManifestRegistryMock.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "openai",
origin: "bundled",
cliBackends: ["Codex-CLI"],
},
{
id: "local",
origin: "workspace",
cliBackends: ["local-cli"],
},
],
});
const { __testing, resolvePluginSetupCliBackendRuntime } =
await import("./setup-registry.runtime.js");
__testing.resetRuntimeState();
__testing.setRuntimeModuleForTest({
resolvePluginSetupCliBackend: () => undefined,
});
expect(resolvePluginSetupCliBackendRuntime({ backend: "codex-cli" })).toEqual({
pluginId: "openai",
backend: { id: "Codex-CLI" },
});
expect(resolvePluginSetupCliBackendRuntime({ backend: "local-cli" })).toBeUndefined();
expect(loadPluginManifestRegistryMock).toHaveBeenCalledTimes(1);
expect(loadPluginManifestRegistryMock).toHaveBeenCalledWith({ cache: true });
});
});

View File

@@ -20,6 +20,16 @@ const SETUP_REGISTRY_RUNTIME_CANDIDATES = ["./setup-registry.js", "./setup-regis
let setupRegistryRuntimeModule: SetupRegistryRuntimeModule | undefined;
let bundledSetupCliBackendsCache: SetupCliBackendRuntimeEntry[] | undefined;
export const __testing = {
resetRuntimeState(): void {
setupRegistryRuntimeModule = undefined;
bundledSetupCliBackendsCache = undefined;
},
setRuntimeModuleForTest(module: SetupRegistryRuntimeModule | undefined): void {
setupRegistryRuntimeModule = module;
},
};
function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
if (bundledSetupCliBackendsCache) {
return bundledSetupCliBackendsCache;