fix(cli): keep channel add plugin install noninteractive

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Peter Steinberger 2026-04-26 12:58:33 +01:00
parent 7e13f3f514
commit 9089e6b595
No known key found for this signature in database
8 changed files with 46 additions and 9 deletions

View file

@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/CLI: let flag-driven `openclaw channels add` install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation. Thanks @codex.
- Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd.
- Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd.
- Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd.

View file

@ -59,6 +59,8 @@ Common non-interactive add surfaces include:
- Tlon fields: `--ship`, `--url`, `--code`, `--group-channels`, `--dm-allowlist`, `--auto-discover-channels`
- `--use-env` for default-account env-backed auth where supported
If a channel plugin needs to be installed during a flag-driven add command, OpenClaw uses the channel's default install source without opening the interactive plugin install prompt.
When you run `openclaw channels add` without flags, the interactive wizard can prompt:
- account ids per selected channel

View file

@ -1,3 +1,6 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { captureEnv } from "../test-utils/env.js";
@ -139,17 +142,19 @@ function parseFirstJsonRuntimeLine<T>() {
describe("daemon-cli coverage", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
let tmpDir: string;
beforeEach(() => {
daemonProgram = createDaemonProgram();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-daemon-cli-"));
envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_PORT",
"OPENCLAW_PROFILE",
]);
process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-cli-state";
process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-cli-state/openclaw.json";
process.env.OPENCLAW_STATE_DIR = tmpDir;
process.env.OPENCLAW_CONFIG_PATH = path.join(tmpDir, "openclaw.json");
delete process.env.OPENCLAW_GATEWAY_PORT;
delete process.env.OPENCLAW_PROFILE;
serviceReadCommand.mockResolvedValue(null);
@ -160,6 +165,7 @@ describe("daemon-cli coverage", () => {
afterEach(() => {
envSnapshot.restore();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("probes gateway status by default", async () => {

View file

@ -430,6 +430,27 @@ describe("ensureChannelSetupPluginInstalled", () => {
);
});
it("uses the bundled default install source without prompting in non-interactive mode", async () => {
const runtime = makeRuntime();
const { prompter, select } = makeSkipInstallPrompter();
const cfg: OpenClawConfig = { update: { channel: "beta" } };
mockBundledChatSource();
const result = await ensureChannelSetupPluginInstalled({
cfg,
entry: baseEntry,
prompter,
runtime,
promptInstall: false,
});
expect(select).not.toHaveBeenCalled();
expect(result.installed).toBe(true);
expect(result.cfg.plugins?.load?.paths).toContain(
bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
);
});
it("does not default to bundled local path when an external catalog overrides the npm spec", async () => {
const runtime = makeRuntime();
const { prompter, select } = makeSkipInstallPrompter();

View file

@ -41,6 +41,7 @@ export async function ensureChannelSetupPluginInstalled(params: {
prompter: WizardPrompter;
runtime: RuntimeEnv;
workspaceDir?: string;
promptInstall?: boolean;
}): Promise<InstallResult> {
const result = await ensureOnboardingPluginInstalled({
cfg: params.cfg,
@ -48,6 +49,7 @@ export async function ensureChannelSetupPluginInstalled(params: {
prompter: params.prompter,
runtime: params.runtime,
workspaceDir: params.workspaceDir,
...(params.promptInstall !== undefined ? { promptInstall: params.promptInstall } : {}),
});
return {
cfg: result.cfg,

View file

@ -501,7 +501,7 @@ describe("channelsAddCommand", () => {
);
expect(ensureChannelSetupPluginInstalled).toHaveBeenCalledWith(
expect.objectContaining({ entry: catalogEntry }),
expect.objectContaining({ entry: catalogEntry, promptInstall: false }),
);
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1);
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(

View file

@ -311,6 +311,7 @@ export async function channelsAddCommand(
prompter,
runtime,
workspaceDir,
promptInstall: false,
});
nextConfig = result.cfg;
if (!result.installed) {

View file

@ -422,6 +422,7 @@ export async function ensureOnboardingPluginInstalled(params: {
prompter: WizardPrompter;
runtime: RuntimeEnv;
workspaceDir?: string;
promptInstall?: boolean;
}): Promise<OnboardingPluginInstallResult> {
const { entry, prompter, runtime, workspaceDir } = params;
let next = params.cfg;
@ -442,12 +443,15 @@ export async function ensureOnboardingPluginInstalled(params: {
bundledLocalPath,
hasNpmSpec: Boolean(npmSpec),
});
const choice = await promptInstallChoice({
entry,
localPath,
defaultChoice,
prompter,
});
const choice =
params.promptInstall === false
? defaultChoice
: await promptInstallChoice({
entry,
localPath,
defaultChoice,
prompter,
});
if (choice === "skip") {
return {