refactor(acp): extract install hint resolver

This commit is contained in:
Peter Steinberger
2026-03-03 02:51:24 +00:00
parent ac318be405
commit fa4ff5f3d2
4 changed files with 81 additions and 24 deletions

View File

@@ -331,7 +331,7 @@ Then verify backend health:
### acpx command and version configuration
By default, `@openclaw/acpx` uses the plugin-local pinned binary:
By default, the acpx plugin (published as `@openclaw/acpx`) uses the plugin-local pinned binary:
1. Command defaults to `extensions/acpx/node_modules/.bin/acpx`.
2. Expected version defaults to the extension pin.

View File

@@ -0,0 +1,56 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import { resolveAcpInstallCommandHint, resolveConfiguredAcpBackendId } from "./install-hints.js";
const originalCwd = process.cwd();
const tempDirs: string[] = [];
function withAcpConfig(acp: OpenClawConfig["acp"]): OpenClawConfig {
return { acp } as OpenClawConfig;
}
afterEach(() => {
process.chdir(originalCwd);
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("ACP install hints", () => {
it("prefers explicit runtime install command", () => {
const cfg = withAcpConfig({
runtime: { installCommand: "pnpm openclaw plugins install acpx" },
});
expect(resolveAcpInstallCommandHint(cfg)).toBe("pnpm openclaw plugins install acpx");
});
it("uses local acpx extension path when present", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acp-install-hint-"));
tempDirs.push(tempRoot);
fs.mkdirSync(path.join(tempRoot, "extensions", "acpx"), { recursive: true });
process.chdir(tempRoot);
const cfg = withAcpConfig({ backend: "acpx" });
const hint = resolveAcpInstallCommandHint(cfg);
expect(hint).toContain("openclaw plugins install ");
expect(hint).toContain(path.join("extensions", "acpx"));
});
it("falls back to npm install hint for acpx when local extension is absent", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acp-install-hint-"));
tempDirs.push(tempRoot);
process.chdir(tempRoot);
const cfg = withAcpConfig({ backend: "acpx" });
expect(resolveAcpInstallCommandHint(cfg)).toBe("openclaw plugins install acpx");
});
it("returns generic plugin hint for non-acpx backend", () => {
const cfg = withAcpConfig({ backend: "custom-backend" });
expect(resolveConfiguredAcpBackendId(cfg)).toBe("custom-backend");
expect(resolveAcpInstallCommandHint(cfg)).toContain('ACP backend "custom-backend"');
});
});

View File

@@ -0,0 +1,23 @@
import { existsSync } from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../../../config/config.js";
export function resolveConfiguredAcpBackendId(cfg: OpenClawConfig): string {
return cfg.acp?.backend?.trim() || "acpx";
}
export function resolveAcpInstallCommandHint(cfg: OpenClawConfig): string {
const configured = cfg.acp?.runtime?.installCommand?.trim();
if (configured) {
return configured;
}
const backendId = resolveConfiguredAcpBackendId(cfg).toLowerCase();
if (backendId === "acpx") {
const localPath = path.resolve(process.cwd(), "extensions/acpx");
if (existsSync(localPath)) {
return `openclaw plugins install ${localPath}`;
}
return "openclaw plugins install acpx";
}
return `Install and enable the plugin that provides ACP backend "${backendId}".`;
}

View File

@@ -1,15 +1,13 @@
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import path from "node:path";
import { toAcpRuntimeErrorText } from "../../../acp/runtime/error-text.js";
import type { AcpRuntimeError } from "../../../acp/runtime/errors.js";
import type { AcpRuntimeSessionMode } from "../../../acp/runtime/types.js";
import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js";
import type { OpenClawConfig } from "../../../config/config.js";
import type { AcpSessionRuntimeOptions } from "../../../config/sessions/types.js";
import { normalizeAgentId } from "../../../routing/session-key.js";
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
import { resolveAcpCommandChannel, resolveAcpCommandThreadId } from "./context.js";
export { resolveAcpInstallCommandHint, resolveConfiguredAcpBackendId } from "./install-hints.js";
export const COMMAND = "/acp";
export const ACP_SPAWN_USAGE =
@@ -404,26 +402,6 @@ export function resolveAcpHelpText(): string {
].join("\n");
}
export function resolveConfiguredAcpBackendId(cfg: OpenClawConfig): string {
return cfg.acp?.backend?.trim() || "acpx";
}
export function resolveAcpInstallCommandHint(cfg: OpenClawConfig): string {
const configured = cfg.acp?.runtime?.installCommand?.trim();
if (configured) {
return configured;
}
const backendId = resolveConfiguredAcpBackendId(cfg).toLowerCase();
if (backendId === "acpx") {
const localPath = path.resolve(process.cwd(), "extensions/acpx");
if (existsSync(localPath)) {
return `openclaw plugins install ${localPath}`;
}
return "openclaw plugins install acpx";
}
return `Install and enable the plugin that provides ACP backend "${backendId}".`;
}
export function formatRuntimeOptionsText(options: AcpSessionRuntimeOptions): string {
const extras = options.backendExtras
? Object.entries(options.backendExtras)