mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 05:02:04 +03:00
fix(onboarding): auto-install shell completion in QuickStart
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram: render markdown spoilers with `<tg-spoiler>` HTML tags. (#11543) Thanks @ezhikkk.
|
- Telegram: render markdown spoilers with `<tg-spoiler>` HTML tags. (#11543) Thanks @ezhikkk.
|
||||||
- Telegram: truncate command registration to 100 entries to avoid `BOT_COMMANDS_TOO_MUCH` failures on startup. (#12356) Thanks @arosstale.
|
- Telegram: truncate command registration to 100 entries to avoid `BOT_COMMANDS_TOO_MUCH` failures on startup. (#12356) Thanks @arosstale.
|
||||||
- Telegram: match DM `allowFrom` against sender user id (fallback to chat id) and clarify pairing logs. (#12779) Thanks @liuxiaopai-ai.
|
- Telegram: match DM `allowFrom` against sender user id (fallback to chat id) and clarify pairing logs. (#12779) Thanks @liuxiaopai-ai.
|
||||||
|
- Onboarding: QuickStart now auto-installs shell completion (prompt only in Manual).
|
||||||
- Auth: strip embedded line breaks from pasted API keys and tokens before storing/resolving credentials.
|
- Auth: strip embedded line breaks from pasted API keys and tokens before storing/resolving credentials.
|
||||||
- Web UI: make chat refresh smoothly scroll to the latest messages and suppress new-messages badge flash during manual refresh.
|
- Web UI: make chat refresh smoothly scroll to the latest messages and suppress new-messages badge flash during manual refresh.
|
||||||
- Tools/web_search: include provider-specific settings in the web search cache key, and pass `inlineCitations` for Grok. (#12419) Thanks @tmchow.
|
- Tools/web_search: include provider-specific settings in the web search cache key, and pass `inlineCitations` for Grok. (#12419) Thanks @tmchow.
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { setupOnboardingShellCompletion } from "./onboarding.completion.js";
|
||||||
|
|
||||||
|
describe("setupOnboardingShellCompletion", () => {
|
||||||
|
it("QuickStart: installs without prompting", async () => {
|
||||||
|
const prompter = {
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
note: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
resolveCliName: () => "openclaw",
|
||||||
|
checkShellCompletionStatus: vi.fn(async () => ({
|
||||||
|
shell: "zsh",
|
||||||
|
profileInstalled: false,
|
||||||
|
cacheExists: false,
|
||||||
|
cachePath: "/tmp/openclaw.zsh",
|
||||||
|
usesSlowPattern: false,
|
||||||
|
})),
|
||||||
|
ensureCompletionCacheExists: vi.fn(async () => true),
|
||||||
|
installCompletion: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await setupOnboardingShellCompletion({ flow: "quickstart", prompter, deps });
|
||||||
|
|
||||||
|
expect(prompter.confirm).not.toHaveBeenCalled();
|
||||||
|
expect(deps.ensureCompletionCacheExists).toHaveBeenCalledWith("openclaw");
|
||||||
|
expect(deps.installCompletion).toHaveBeenCalledWith("zsh", true, "openclaw");
|
||||||
|
expect(prompter.note).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Advanced: prompts; skip means no install", async () => {
|
||||||
|
const prompter = {
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
note: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
resolveCliName: () => "openclaw",
|
||||||
|
checkShellCompletionStatus: vi.fn(async () => ({
|
||||||
|
shell: "zsh",
|
||||||
|
profileInstalled: false,
|
||||||
|
cacheExists: false,
|
||||||
|
cachePath: "/tmp/openclaw.zsh",
|
||||||
|
usesSlowPattern: false,
|
||||||
|
})),
|
||||||
|
ensureCompletionCacheExists: vi.fn(async () => true),
|
||||||
|
installCompletion: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await setupOnboardingShellCompletion({ flow: "advanced", prompter, deps });
|
||||||
|
|
||||||
|
expect(prompter.confirm).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.ensureCompletionCacheExists).not.toHaveBeenCalled();
|
||||||
|
expect(deps.installCompletion).not.toHaveBeenCalled();
|
||||||
|
expect(prompter.note).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import type { ShellCompletionStatus } from "../commands/doctor-completion.js";
|
||||||
|
import type { WizardFlow } from "./onboarding.types.js";
|
||||||
|
import type { WizardPrompter } from "./prompts.js";
|
||||||
|
import { resolveCliName } from "../cli/cli-name.js";
|
||||||
|
import { installCompletion } from "../cli/completion-cli.js";
|
||||||
|
import {
|
||||||
|
checkShellCompletionStatus,
|
||||||
|
ensureCompletionCacheExists,
|
||||||
|
} from "../commands/doctor-completion.js";
|
||||||
|
|
||||||
|
type CompletionDeps = {
|
||||||
|
resolveCliName: () => string;
|
||||||
|
checkShellCompletionStatus: (binName: string) => Promise<ShellCompletionStatus>;
|
||||||
|
ensureCompletionCacheExists: (binName: string) => Promise<boolean>;
|
||||||
|
installCompletion: (shell: string, yes: boolean, binName?: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function pathExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveProfileHint(shell: ShellCompletionStatus["shell"]): Promise<string> {
|
||||||
|
const home = process.env.HOME || os.homedir();
|
||||||
|
if (shell === "zsh") {
|
||||||
|
return "~/.zshrc";
|
||||||
|
}
|
||||||
|
if (shell === "bash") {
|
||||||
|
const bashrc = path.join(home, ".bashrc");
|
||||||
|
return (await pathExists(bashrc)) ? "~/.bashrc" : "~/.bash_profile";
|
||||||
|
}
|
||||||
|
if (shell === "fish") {
|
||||||
|
return "~/.config/fish/config.fish";
|
||||||
|
}
|
||||||
|
// Best-effort. PowerShell profile path varies; restart hint is still correct.
|
||||||
|
return "$PROFILE";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReloadHint(shell: ShellCompletionStatus["shell"], profileHint: string): string {
|
||||||
|
if (shell === "powershell") {
|
||||||
|
return "Restart your shell (or reload your PowerShell profile).";
|
||||||
|
}
|
||||||
|
return `Restart your shell or run: source ${profileHint}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupOnboardingShellCompletion(params: {
|
||||||
|
flow: WizardFlow;
|
||||||
|
prompter: Pick<WizardPrompter, "confirm" | "note">;
|
||||||
|
deps?: Partial<CompletionDeps>;
|
||||||
|
}): Promise<void> {
|
||||||
|
const deps: CompletionDeps = {
|
||||||
|
resolveCliName,
|
||||||
|
checkShellCompletionStatus,
|
||||||
|
ensureCompletionCacheExists,
|
||||||
|
installCompletion,
|
||||||
|
...params.deps,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cliName = deps.resolveCliName();
|
||||||
|
const completionStatus = await deps.checkShellCompletionStatus(cliName);
|
||||||
|
|
||||||
|
if (completionStatus.usesSlowPattern) {
|
||||||
|
// Case 1: Profile uses slow dynamic pattern - silently upgrade to cached version
|
||||||
|
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
|
||||||
|
if (cacheGenerated) {
|
||||||
|
await deps.installCompletion(completionStatus.shell, true, cliName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completionStatus.profileInstalled && !completionStatus.cacheExists) {
|
||||||
|
// Case 2: Profile has completion but no cache - auto-fix silently
|
||||||
|
await deps.ensureCompletionCacheExists(cliName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!completionStatus.profileInstalled) {
|
||||||
|
// Case 3: No completion at all
|
||||||
|
const shouldInstall =
|
||||||
|
params.flow === "quickstart"
|
||||||
|
? true
|
||||||
|
: await params.prompter.confirm({
|
||||||
|
message: `Enable ${completionStatus.shell} shell completion for ${cliName}?`,
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldInstall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cache first (required for fast shell startup)
|
||||||
|
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
|
||||||
|
if (!cacheGenerated) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`Failed to generate completion cache. Run \`${cliName} completion --install\` later.`,
|
||||||
|
"Shell completion",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install to shell profile
|
||||||
|
await deps.installCompletion(completionStatus.shell, true, cliName);
|
||||||
|
|
||||||
|
const profileHint = await resolveProfileHint(completionStatus.shell);
|
||||||
|
await params.prompter.note(
|
||||||
|
`Shell completion installed. ${formatReloadHint(completionStatus.shell, profileHint)}`,
|
||||||
|
"Shell completion",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Case 4: Both profile and cache exist (using cached version) - all good, nothing to do
|
||||||
|
}
|
||||||
@@ -6,9 +6,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||||||
import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js";
|
import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js";
|
||||||
import type { WizardPrompter } from "./prompts.js";
|
import type { WizardPrompter } from "./prompts.js";
|
||||||
import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js";
|
import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js";
|
||||||
import { resolveCliName } from "../cli/cli-name.js";
|
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
import { installCompletion } from "../cli/completion-cli.js";
|
|
||||||
import {
|
import {
|
||||||
buildGatewayInstallPlan,
|
buildGatewayInstallPlan,
|
||||||
gatewayInstallErrorHint,
|
gatewayInstallErrorHint,
|
||||||
@@ -17,10 +15,6 @@ import {
|
|||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
} from "../commands/daemon-runtime.js";
|
} from "../commands/daemon-runtime.js";
|
||||||
import {
|
|
||||||
checkShellCompletionStatus,
|
|
||||||
ensureCompletionCacheExists,
|
|
||||||
} from "../commands/doctor-completion.js";
|
|
||||||
import { formatHealthCheckFailure } from "../commands/health-format.js";
|
import { formatHealthCheckFailure } from "../commands/health-format.js";
|
||||||
import { healthCommand } from "../commands/health.js";
|
import { healthCommand } from "../commands/health.js";
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +31,7 @@ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
|||||||
import { restoreTerminalState } from "../terminal/restore.js";
|
import { restoreTerminalState } from "../terminal/restore.js";
|
||||||
import { runTui } from "../tui/tui.js";
|
import { runTui } from "../tui/tui.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
import { setupOnboardingShellCompletion } from "./onboarding.completion.js";
|
||||||
|
|
||||||
type FinalizeOnboardingOptions = {
|
type FinalizeOnboardingOptions = {
|
||||||
flow: WizardFlow;
|
flow: WizardFlow;
|
||||||
@@ -397,50 +392,7 @@ export async function finalizeOnboardingWizard(
|
|||||||
"Security",
|
"Security",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Shell completion setup
|
await setupOnboardingShellCompletion({ flow, prompter });
|
||||||
const cliName = resolveCliName();
|
|
||||||
const completionStatus = await checkShellCompletionStatus(cliName);
|
|
||||||
|
|
||||||
if (completionStatus.usesSlowPattern) {
|
|
||||||
// Case 1: Profile uses slow dynamic pattern - silently upgrade to cached version
|
|
||||||
const cacheGenerated = await ensureCompletionCacheExists(cliName);
|
|
||||||
if (cacheGenerated) {
|
|
||||||
await installCompletion(completionStatus.shell, true, cliName);
|
|
||||||
}
|
|
||||||
} else if (completionStatus.profileInstalled && !completionStatus.cacheExists) {
|
|
||||||
// Case 2: Profile has completion but no cache - auto-fix silently
|
|
||||||
await ensureCompletionCacheExists(cliName);
|
|
||||||
} else if (!completionStatus.profileInstalled) {
|
|
||||||
// Case 3: No completion at all - prompt to install
|
|
||||||
const installShellCompletion = await prompter.confirm({
|
|
||||||
message: `Enable ${completionStatus.shell} shell completion for ${cliName}?`,
|
|
||||||
initialValue: true,
|
|
||||||
});
|
|
||||||
if (installShellCompletion) {
|
|
||||||
// Generate cache first (required for fast shell startup)
|
|
||||||
const cacheGenerated = await ensureCompletionCacheExists(cliName);
|
|
||||||
if (cacheGenerated) {
|
|
||||||
// Install to shell profile
|
|
||||||
await installCompletion(completionStatus.shell, true, cliName);
|
|
||||||
const profileHint =
|
|
||||||
completionStatus.shell === "zsh"
|
|
||||||
? "~/.zshrc"
|
|
||||||
: completionStatus.shell === "bash"
|
|
||||||
? "~/.bashrc"
|
|
||||||
: "~/.config/fish/config.fish";
|
|
||||||
await prompter.note(
|
|
||||||
`Shell completion installed. Restart your shell or run: source ${profileHint}`,
|
|
||||||
"Shell completion",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await prompter.note(
|
|
||||||
`Failed to generate completion cache. Run \`${cliName} completion --install\` later.`,
|
|
||||||
"Shell completion",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Case 4: Both profile and cache exist (using cached version) - all good, nothing to do
|
|
||||||
|
|
||||||
const shouldOpenControlUi =
|
const shouldOpenControlUi =
|
||||||
!opts.skipUi &&
|
!opts.skipUi &&
|
||||||
|
|||||||
Reference in New Issue
Block a user