fix(cli): defer zsh compdef registration until compinit is available (#56555)

The generated zsh completion script called compdef at source time,
which fails with 'command not found: compdef' when loaded before
compinit. Replace with a deferred registration that tries immediately,
and if compdef is not yet available, queues a self-removing precmd hook
that retries on first prompt.

Handles repeated sourcing (deduped hook entry) and shells that never
run compinit (completion simply never registers, matching zsh model).

Add real zsh integration test verifying no compdef error on source and
successful registration after compinit.

Fixes #14289
This commit is contained in:
Robin Waslander 2026-03-28 19:35:32 +01:00 committed by GitHub
parent f32f7d0809
commit 6be14ab388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 1 deletions

View File

@ -1,3 +1,7 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import { getCompletionScript } from "./completion-cli.js";
@ -27,6 +31,56 @@ describe("completion-cli", () => {
expect(script).toContain("--force[Force the action]");
});
it("defers zsh registration until compinit is available", async () => {
if (process.platform === "win32") {
return;
}
const probe = spawnSync("zsh", ["-fc", "exit 0"], { encoding: "utf8" });
if (probe.error) {
if ("code" in probe.error && probe.error.code === "ENOENT") {
return;
}
throw probe.error;
}
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zsh-completion-"));
try {
const scriptPath = path.join(tempDir, "openclaw.zsh");
await fs.writeFile(scriptPath, getCompletionScript("zsh", createCompletionProgram()), "utf8");
const result = spawnSync(
"zsh",
[
"-fc",
`
source ${JSON.stringify(scriptPath)}
[[ -z "\${_comps[openclaw]-}" ]] || exit 10
[[ "\${precmd_functions[(r)_openclaw_register_completion]}" = "_openclaw_register_completion" ]] || exit 11
autoload -Uz compinit
compinit -C
_openclaw_register_completion
[[ -z "\${precmd_functions[(r)_openclaw_register_completion]}" ]] || exit 12
[[ "\${_comps[openclaw]-}" = "_openclaw_root_completion" ]]
`,
],
{
encoding: "utf8",
env: {
...process.env,
HOME: tempDir,
ZDOTDIR: tempDir,
},
},
);
expect(result.stderr).not.toContain("command not found: compdef");
expect(result.status).toBe(0);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("generates PowerShell command paths without the executable prefix", () => {
const script = getCompletionScript("powershell", createCompletionProgram());

View File

@ -411,7 +411,23 @@ _${rootCmd}_root_completion() {
${generateZshSubcommands(program, rootCmd)}
compdef _${rootCmd}_root_completion ${rootCmd}
_${rootCmd}_register_completion() {
if (( ! $+functions[compdef] )); then
return 0
fi
compdef _${rootCmd}_root_completion ${rootCmd}
precmd_functions=(\${precmd_functions:#_${rootCmd}_register_completion})
unfunction _${rootCmd}_register_completion 2>/dev/null
}
_${rootCmd}_register_completion
if (( ! $+functions[compdef] )); then
typeset -ga precmd_functions
if [[ -z "\${precmd_functions[(r)_${rootCmd}_register_completion]}" ]]; then
precmd_functions+=(_${rootCmd}_register_completion)
fi
fi
`;
return script;
}