doctor: add shell completion check module

- Add `checkShellCompletionStatus` to get profile/cache/slow-pattern status
- Add `ensureCompletionCacheExists` for silent cache regeneration
- Add `doctorShellCompletion` to check and fix completion issues:
  - Auto-upgrade old slow dynamic patterns to cached version
  - Auto-regenerate cache if profile exists but cache is missing
  - Prompt to install if no completion is configured
This commit is contained in:
Shakker
2026-02-04 15:37:40 +00:00
committed by Shakker
parent d5f8208c38
commit 3fae903863

View File

@@ -0,0 +1,179 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import type { RuntimeEnv } from "../runtime.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
import { resolveCliName } from "../cli/cli-name.js";
import {
completionCacheExists,
installCompletion,
isCompletionInstalled,
resolveCompletionCachePath,
resolveShellFromEnv,
usesSlowDynamicCompletion,
} from "../cli/completion-cli.js";
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
import { note } from "../terminal/note.js";
type CompletionShell = "zsh" | "bash" | "fish" | "powershell";
/** Generate the completion cache by spawning the CLI. */
async function generateCompletionCache(): Promise<boolean> {
const root = await resolveOpenClawPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (!root) {
return false;
}
const binPath = path.join(root, "openclaw.mjs");
const result = spawnSync(process.execPath, [binPath, "completion", "--write-state"], {
cwd: root,
env: process.env,
encoding: "utf-8",
});
return result.status === 0;
}
export type ShellCompletionStatus = {
shell: CompletionShell;
profileInstalled: boolean;
cacheExists: boolean;
cachePath: string;
/** True if profile uses slow dynamic pattern like `source <(openclaw completion ...)` */
usesSlowPattern: boolean;
};
/** Check the status of shell completion for the current shell. */
export async function checkShellCompletionStatus(
binName = "openclaw",
): Promise<ShellCompletionStatus> {
const shell = resolveShellFromEnv() as CompletionShell;
const profileInstalled = await isCompletionInstalled(shell, binName);
const cacheExists = await completionCacheExists(shell, binName);
const cachePath = resolveCompletionCachePath(shell, binName);
const usesSlowPattern = await usesSlowDynamicCompletion(shell, binName);
return {
shell,
profileInstalled,
cacheExists,
cachePath,
usesSlowPattern,
};
}
export type DoctorCompletionOptions = {
nonInteractive?: boolean;
};
/**
* Doctor check for shell completion.
* - If profile uses slow dynamic pattern: upgrade to cached version
* - If profile has completion but no cache: auto-generate cache and upgrade profile
* - If no completion at all: prompt to install (with user confirmation)
*/
export async function doctorShellCompletion(
runtime: RuntimeEnv,
prompter: DoctorPrompter,
options: DoctorCompletionOptions = {},
): Promise<void> {
const cliName = resolveCliName();
const status = await checkShellCompletionStatus(cliName);
// Profile uses slow dynamic pattern - upgrade to cached version
if (status.usesSlowPattern) {
note(
`Your ${status.shell} profile uses slow dynamic completion (source <(...)).\nUpgrading to cached completion for faster shell startup...`,
"Shell completion",
);
// Ensure cache exists first
if (!status.cacheExists) {
const generated = await generateCompletionCache();
if (!generated) {
note(
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
"Shell completion",
);
return;
}
}
// Upgrade profile to use cached file
await installCompletion(status.shell, true, cliName);
note(
`Shell completion upgraded. Restart your shell or run: source ~/.${status.shell === "zsh" ? "zshrc" : status.shell === "bash" ? "bashrc" : "config/fish/config.fish"}`,
"Shell completion",
);
return;
}
// Profile has completion but no cache - auto-fix
if (status.profileInstalled && !status.cacheExists) {
note(
`Shell completion is configured in your ${status.shell} profile but the cache is missing.\nRegenerating cache...`,
"Shell completion",
);
const generated = await generateCompletionCache();
if (generated) {
note(`Completion cache regenerated at ${status.cachePath}`, "Shell completion");
} else {
note(
`Failed to regenerate completion cache. Run \`${cliName} completion --write-state\` manually.`,
"Shell completion",
);
}
return;
}
// No completion at all - prompt to install
if (!status.profileInstalled) {
if (options.nonInteractive) {
// In non-interactive mode, just note that completion is not installed
return;
}
const shouldInstall = await prompter.confirm({
message: `Enable ${status.shell} shell completion for ${cliName}?`,
initialValue: true,
});
if (shouldInstall) {
// First generate the cache
const generated = await generateCompletionCache();
if (!generated) {
note(
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
"Shell completion",
);
return;
}
// Then install to profile
await installCompletion(status.shell, true, cliName);
note(
`Shell completion installed. Restart your shell or run: source ~/.${status.shell === "zsh" ? "zshrc" : status.shell === "bash" ? "bashrc" : "config/fish/config.fish"}`,
"Shell completion",
);
}
}
}
/**
* Ensure completion cache exists. Used during onboarding/update to fix
* cases where profile has completion but no cache.
* This is a silent fix - no prompts.
*/
export async function ensureCompletionCacheExists(binName = "openclaw"): Promise<boolean> {
const shell = resolveShellFromEnv() as CompletionShell;
const cacheExists = await completionCacheExists(shell, binName);
if (cacheExists) {
return true;
}
return generateCompletionCache();
}