mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 18:10:24 +00:00
Merge branch 'openclaw:main' into qianfan
This commit is contained in:
@@ -43,13 +43,22 @@ function resolveCompletionCacheDir(env: NodeJS.ProcessEnv = process.env): string
|
||||
return path.join(stateDir, "completions");
|
||||
}
|
||||
|
||||
function resolveCompletionCachePath(shell: CompletionShell, binName: string): string {
|
||||
export function resolveCompletionCachePath(shell: CompletionShell, binName: string): string {
|
||||
const basename = sanitizeCompletionBasename(binName);
|
||||
const extension =
|
||||
shell === "powershell" ? "ps1" : shell === "fish" ? "fish" : shell === "bash" ? "bash" : "zsh";
|
||||
return path.join(resolveCompletionCacheDir(), `${basename}.${extension}`);
|
||||
}
|
||||
|
||||
/** Check if the completion cache file exists for the given shell. */
|
||||
export async function completionCacheExists(
|
||||
shell: CompletionShell,
|
||||
binName = "openclaw",
|
||||
): Promise<boolean> {
|
||||
const cachePath = resolveCompletionCachePath(shell, binName);
|
||||
return pathExists(cachePath);
|
||||
}
|
||||
|
||||
function getCompletionScript(shell: CompletionShell, program: Command): string {
|
||||
if (shell === "zsh") {
|
||||
return generateZshCompletion(program);
|
||||
@@ -89,15 +98,12 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
||||
function formatCompletionSourceLine(
|
||||
shell: CompletionShell,
|
||||
binName: string,
|
||||
cachePath: string | null,
|
||||
cachePath: string,
|
||||
): string {
|
||||
if (cachePath) {
|
||||
if (shell === "fish") {
|
||||
return `source "${cachePath}"`;
|
||||
}
|
||||
if (shell === "fish") {
|
||||
return `${binName} completion --shell fish | source`;
|
||||
}
|
||||
return `source <(${binName} completion --shell ${shell})`;
|
||||
return `source "${cachePath}"`;
|
||||
}
|
||||
|
||||
function isCompletionProfileHeader(line: string): boolean {
|
||||
@@ -114,6 +120,15 @@ function isCompletionProfileLine(line: string, binName: string, cachePath: strin
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Check if a line uses the slow dynamic completion pattern (source <(...)) */
|
||||
function isSlowDynamicCompletionLine(line: string, binName: string): boolean {
|
||||
// Matches patterns like: source <(openclaw completion --shell zsh)
|
||||
return (
|
||||
line.includes(`<(${binName} completion`) ||
|
||||
(line.includes(`${binName} completion`) && line.includes("| source"))
|
||||
);
|
||||
}
|
||||
|
||||
function updateCompletionProfile(
|
||||
content: string,
|
||||
binName: string,
|
||||
@@ -144,24 +159,34 @@ function updateCompletionProfile(
|
||||
return { next, changed: next !== content, hadExisting };
|
||||
}
|
||||
|
||||
function getShellProfilePath(shell: CompletionShell): string {
|
||||
const home = process.env.HOME || os.homedir();
|
||||
if (shell === "zsh") {
|
||||
return path.join(home, ".zshrc");
|
||||
}
|
||||
if (shell === "bash") {
|
||||
return path.join(home, ".bashrc");
|
||||
}
|
||||
if (shell === "fish") {
|
||||
return path.join(home, ".config", "fish", "config.fish");
|
||||
}
|
||||
// PowerShell
|
||||
if (process.platform === "win32") {
|
||||
return path.join(
|
||||
process.env.USERPROFILE || home,
|
||||
"Documents",
|
||||
"PowerShell",
|
||||
"Microsoft.PowerShell_profile.ps1",
|
||||
);
|
||||
}
|
||||
return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
||||
}
|
||||
|
||||
export async function isCompletionInstalled(
|
||||
shell: CompletionShell,
|
||||
binName = "openclaw",
|
||||
): Promise<boolean> {
|
||||
const home = process.env.HOME || os.homedir();
|
||||
let profilePath = "";
|
||||
if (shell === "zsh") {
|
||||
profilePath = path.join(home, ".zshrc");
|
||||
} else if (shell === "bash") {
|
||||
profilePath = path.join(home, ".bashrc");
|
||||
if (!(await pathExists(profilePath))) {
|
||||
profilePath = path.join(home, ".bash_profile");
|
||||
}
|
||||
} else if (shell === "fish") {
|
||||
profilePath = path.join(home, ".config", "fish", "config.fish");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const profilePath = getShellProfilePath(shell);
|
||||
|
||||
if (!(await pathExists(profilePath))) {
|
||||
return false;
|
||||
@@ -175,6 +200,33 @@ export async function isCompletionInstalled(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the profile uses the slow dynamic completion pattern.
|
||||
* Returns true if profile has `source <(openclaw completion ...)` instead of cached file.
|
||||
*/
|
||||
export async function usesSlowDynamicCompletion(
|
||||
shell: CompletionShell,
|
||||
binName = "openclaw",
|
||||
): Promise<boolean> {
|
||||
const profilePath = getShellProfilePath(shell);
|
||||
|
||||
if (!(await pathExists(profilePath))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cachePath = resolveCompletionCachePath(shell, binName);
|
||||
const content = await fs.readFile(profilePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Check if any line has dynamic completion but NOT the cached path
|
||||
for (const line of lines) {
|
||||
if (isSlowDynamicCompletionLine(line, binName) && !line.includes(cachePath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function registerCompletionCli(program: Command) {
|
||||
program
|
||||
.command("completion")
|
||||
@@ -233,16 +285,26 @@ export async function installCompletion(shell: string, yes: boolean, binName = "
|
||||
const home = process.env.HOME || os.homedir();
|
||||
let profilePath = "";
|
||||
let sourceLine = "";
|
||||
let cachedPath: string | null = null;
|
||||
|
||||
const isShellSupported = isCompletionShell(shell);
|
||||
if (isShellSupported) {
|
||||
const candidate = resolveCompletionCachePath(shell, binName);
|
||||
cachedPath = (await pathExists(candidate)) ? candidate : null;
|
||||
if (!isShellSupported) {
|
||||
console.error(`Automated installation not supported for ${shell} yet.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the cache path - cache MUST exist for fast shell startup
|
||||
const cachePath = resolveCompletionCachePath(shell, binName);
|
||||
const cacheExists = await pathExists(cachePath);
|
||||
if (!cacheExists) {
|
||||
console.error(
|
||||
`Completion cache not found at ${cachePath}. Run \`${binName} completion --write-state\` first.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell === "zsh") {
|
||||
profilePath = path.join(home, ".zshrc");
|
||||
sourceLine = formatCompletionSourceLine("zsh", binName, cachedPath);
|
||||
sourceLine = formatCompletionSourceLine("zsh", binName, cachePath);
|
||||
} else if (shell === "bash") {
|
||||
// Try .bashrc first, then .bash_profile
|
||||
profilePath = path.join(home, ".bashrc");
|
||||
@@ -251,10 +313,10 @@ export async function installCompletion(shell: string, yes: boolean, binName = "
|
||||
} catch {
|
||||
profilePath = path.join(home, ".bash_profile");
|
||||
}
|
||||
sourceLine = formatCompletionSourceLine("bash", binName, cachedPath);
|
||||
sourceLine = formatCompletionSourceLine("bash", binName, cachePath);
|
||||
} else if (shell === "fish") {
|
||||
profilePath = path.join(home, ".config", "fish", "config.fish");
|
||||
sourceLine = formatCompletionSourceLine("fish", binName, cachedPath);
|
||||
sourceLine = formatCompletionSourceLine("fish", binName, cachePath);
|
||||
} else {
|
||||
console.error(`Automated installation not supported for ${shell} yet.`);
|
||||
return;
|
||||
@@ -273,7 +335,7 @@ export async function installCompletion(shell: string, yes: boolean, binName = "
|
||||
}
|
||||
|
||||
const content = await fs.readFile(profilePath, "utf-8");
|
||||
const update = updateCompletionProfile(content, binName, cachedPath, sourceLine);
|
||||
const update = updateCompletionProfile(content, binName, cachePath, sourceLine);
|
||||
if (!update.changed) {
|
||||
if (!yes) {
|
||||
console.log(`Completion already installed in ${profilePath}`);
|
||||
@@ -287,9 +349,8 @@ export async function installCompletion(shell: string, yes: boolean, binName = "
|
||||
}
|
||||
|
||||
await fs.writeFile(profilePath, update.next, "utf-8");
|
||||
console.log(`Completion installed. Restart your shell or run: source ${profilePath}`);
|
||||
if (!yes && cachedPath) {
|
||||
console.log(`Completion cache: ${cachedPath}`);
|
||||
if (!yes) {
|
||||
console.log(`Completion installed. Restart your shell or run: source ${profilePath}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to install completion: ${err as string}`);
|
||||
|
||||
@@ -4,6 +4,10 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
checkShellCompletionStatus,
|
||||
ensureCompletionCacheExists,
|
||||
} from "../commands/doctor-completion.js";
|
||||
import {
|
||||
formatUpdateAvailableHint,
|
||||
formatUpdateOneLiner,
|
||||
@@ -51,6 +55,7 @@ import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { replaceCliName, resolveCliName } from "./cli-name.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { installCompletion } from "./completion-cli.js";
|
||||
import { formatHelpExamples } from "./help-format.js";
|
||||
|
||||
export type UpdateCommandOptions = {
|
||||
@@ -224,6 +229,67 @@ async function tryWriteCompletionCache(root: string, jsonMode: boolean): Promise
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if shell completion is installed and prompt user to install if not. */
|
||||
async function tryInstallShellCompletion(opts: {
|
||||
jsonMode: boolean;
|
||||
skipPrompt: boolean;
|
||||
}): Promise<void> {
|
||||
if (opts.jsonMode || !process.stdin.isTTY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await checkShellCompletionStatus(CLI_NAME);
|
||||
|
||||
// Profile uses slow dynamic pattern - upgrade to cached version
|
||||
if (status.usesSlowPattern) {
|
||||
defaultRuntime.log(theme.muted("Upgrading shell completion to cached version..."));
|
||||
// Ensure cache exists first
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
|
||||
if (cacheGenerated) {
|
||||
await installCompletion(status.shell, true, CLI_NAME);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile has completion but no cache - auto-fix silently
|
||||
if (status.profileInstalled && !status.cacheExists) {
|
||||
defaultRuntime.log(theme.muted("Regenerating shell completion cache..."));
|
||||
await ensureCompletionCacheExists(CLI_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
// No completion at all - prompt to install
|
||||
if (!status.profileInstalled) {
|
||||
defaultRuntime.log("");
|
||||
defaultRuntime.log(theme.heading("Shell completion"));
|
||||
|
||||
const shouldInstall = await confirm({
|
||||
message: stylePromptMessage(`Enable ${status.shell} shell completion for ${CLI_NAME}?`),
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
if (isCancel(shouldInstall) || !shouldInstall) {
|
||||
if (!opts.skipPrompt) {
|
||||
defaultRuntime.log(
|
||||
theme.muted(
|
||||
`Skipped. Run \`${replaceCliName(formatCliCommand("openclaw completion --install"), CLI_NAME)}\` later to enable.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate cache first (required for fast shell startup)
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
|
||||
if (!cacheGenerated) {
|
||||
defaultRuntime.log(theme.warn("Failed to generate completion cache."));
|
||||
return;
|
||||
}
|
||||
|
||||
await installCompletion(status.shell, opts.skipPrompt, CLI_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
async function isEmptyDir(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
const entries = await fs.readdir(targetPath);
|
||||
@@ -985,6 +1051,12 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
|
||||
await tryWriteCompletionCache(root, Boolean(opts.json));
|
||||
|
||||
// Offer to install shell completion if not already installed
|
||||
await tryInstallShellCompletion({
|
||||
jsonMode: Boolean(opts.json),
|
||||
skipPrompt: Boolean(opts.yes),
|
||||
});
|
||||
|
||||
// Restart service if requested
|
||||
if (shouldRestart) {
|
||||
if (!opts.json) {
|
||||
|
||||
Reference in New Issue
Block a user