CLI/completion: fix generator OOM and harden plugin registries (#45537)

* fix: avoid OOM during completion script generation

* CLI/completion: fix PowerShell nested command paths

* CLI/completion: cover generated shell scripts

* Changelog: note completion generator follow-up

* Plugins: reserve shared registry names

---------

Co-authored-by: Xiaoyi <xiaoyi@example.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
xiaoyi
2026-03-16 03:14:30 +08:00
committed by GitHub
parent dd2eb29038
commit bbb0c3e5d7
5 changed files with 307 additions and 71 deletions

View File

@@ -69,7 +69,7 @@ export async function completionCacheExists(
return pathExists(cachePath);
}
function getCompletionScript(shell: CompletionShell, program: Command): string {
export function getCompletionScript(shell: CompletionShell, program: Command): string {
if (shell === "zsh") {
return generateZshCompletion(program);
}
@@ -442,17 +442,19 @@ function generateZshSubcmdList(cmd: Command): string {
}
function generateZshSubcommands(program: Command, prefix: string): string {
let script = "";
for (const cmd of program.commands) {
const cmdName = cmd.name();
const funcName = `_${prefix}_${cmdName.replace(/-/g, "_")}`;
const segments: string[] = [];
// Recurse first
script += generateZshSubcommands(cmd, `${prefix}_${cmdName.replace(/-/g, "_")}`);
const visit = (current: Command, currentPrefix: string) => {
for (const cmd of current.commands) {
const cmdName = cmd.name();
const nextPrefix = `${currentPrefix}_${cmdName.replace(/-/g, "_")}`;
const funcName = `_${nextPrefix}`;
const subCommands = cmd.commands;
if (subCommands.length > 0) {
script += `
visit(cmd, nextPrefix);
const subCommands = cmd.commands;
if (subCommands.length > 0) {
segments.push(`
${funcName}() {
local -a commands
local -a options
@@ -470,17 +472,21 @@ ${funcName}() {
;;
esac
}
`;
} else {
script += `
`);
continue;
}
segments.push(`
${funcName}() {
_arguments -C \\
${generateZshArgs(cmd)}
}
`;
`);
}
}
return script;
};
visit(program, prefix);
return segments.join("");
}
function generateBashCompletion(program: Command): string {
@@ -528,38 +534,34 @@ function generateBashSubcommand(cmd: Command): string {
function generatePowerShellCompletion(program: Command): string {
const rootCmd = program.name();
const segments: string[] = [];
const visit = (cmd: Command, parents: string[]): string => {
const cmdName = cmd.name();
const fullPath = [...parents, cmdName].join(" ");
let script = "";
const visit = (cmd: Command, pathSegments: string[]) => {
const fullPath = pathSegments.join(" ");
// Command completion for this level
const subCommands = cmd.commands.map((c) => c.name());
const options = cmd.options.map((o) => o.flags.split(/[ ,|]+/)[0]); // Take first flag
const allCompletions = [...subCommands, ...options].map((s) => `'${s}'`).join(",");
if (allCompletions.length > 0) {
script += `
if (fullPath.length > 0 && allCompletions.length > 0) {
segments.push(`
if ($commandPath -eq '${fullPath}') {
$completions = @(${allCompletions})
$completions | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
}
}
`;
`);
}
// Recurse
for (const sub of cmd.commands) {
script += visit(sub, [...parents, cmdName]);
visit(sub, [...pathSegments, sub.name()]);
}
return script;
};
const rootBody = visit(program, []);
visit(program, []);
const rootBody = segments.join("");
return `
Register-ArgumentCompleter -Native -CommandName ${rootCmd} -ScriptBlock {
@@ -593,65 +595,57 @@ Register-ArgumentCompleter -Native -CommandName ${rootCmd} -ScriptBlock {
function generateFishCompletion(program: Command): string {
const rootCmd = program.name();
let script = "";
const segments: string[] = [];
const visit = (cmd: Command, parents: string[]) => {
const cmdName = cmd.name();
const fullPath = [...parents];
if (parents.length > 0) {
fullPath.push(cmdName);
} // Only push if not root, or consistent root handling
// Fish uses 'seen_subcommand_from' to determine context.
// For root: complete -c openclaw -n "__fish_use_subcommand" -a "subcmd" -d "desc"
// Root logic
if (parents.length === 0) {
// Subcommands of root
for (const sub of cmd.commands) {
script += buildFishSubcommandCompletionLine({
rootCmd,
condition: "__fish_use_subcommand",
name: sub.name(),
description: sub.description(),
});
segments.push(
buildFishSubcommandCompletionLine({
rootCmd,
condition: "__fish_use_subcommand",
name: sub.name(),
description: sub.description(),
}),
);
}
// Options of root
for (const opt of cmd.options) {
script += buildFishOptionCompletionLine({
rootCmd,
condition: "__fish_use_subcommand",
flags: opt.flags,
description: opt.description,
});
segments.push(
buildFishOptionCompletionLine({
rootCmd,
condition: "__fish_use_subcommand",
flags: opt.flags,
description: opt.description,
}),
);
}
} else {
// Nested commands
// Logic: if seen subcommand matches parents...
// But fish completion logic is simpler if we just say "if we haven't seen THIS command yet but seen parent"
// Actually, a robust fish completion often requires defining a function to check current line.
// For simplicity, we'll assume standard fish helper __fish_seen_subcommand_from.
// To properly scope to 'openclaw gateway' and not 'openclaw other gateway', we need to check the sequence.
// A simplified approach:
// Subcommands
for (const sub of cmd.commands) {
script += buildFishSubcommandCompletionLine({
rootCmd,
condition: `__fish_seen_subcommand_from ${cmdName}`,
name: sub.name(),
description: sub.description(),
});
segments.push(
buildFishSubcommandCompletionLine({
rootCmd,
condition: `__fish_seen_subcommand_from ${cmdName}`,
name: sub.name(),
description: sub.description(),
}),
);
}
// Options
for (const opt of cmd.options) {
script += buildFishOptionCompletionLine({
rootCmd,
condition: `__fish_seen_subcommand_from ${cmdName}`,
flags: opt.flags,
description: opt.description,
});
segments.push(
buildFishOptionCompletionLine({
rootCmd,
condition: `__fish_seen_subcommand_from ${cmdName}`,
flags: opt.flags,
description: opt.description,
}),
);
}
}
@@ -661,5 +655,5 @@ function generateFishCompletion(program: Command): string {
};
visit(program, []);
return script;
return segments.join("");
}