From 189032097e067db5ecd52ec53cd913ba6262b343 Mon Sep 17 00:00:00 2001 From: Connor Couetil Date: Fri, 16 Oct 2026 20:43:12 -0500 Subject: [PATCH] fix: initialize Zsh completions before registering cf --- .changeset/tidy-zsh-completions.md | 7 ++ .../__tests__/commands/completions.test.ts | 86 +++++++++++++++++++ patches/@bomb.sh__tab@0.0.22.patch | 15 ++++ pnpm-lock.yaml | 5 +- pnpm-workspace.yaml | 1 + 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-zsh-completions.md create mode 100644 patches/@bomb.sh__tab@0.0.22.patch diff --git a/.changeset/tidy-zsh-completions.md b/.changeset/tidy-zsh-completions.md new file mode 100644 index 0000000000..e83e9bc83b --- /dev/null +++ b/.changeset/tidy-zsh-completions.md @@ -0,0 +1,7 @@ +--- +"cf": patch +--- + +Initialize Zsh completions before registering `cf` + +Fix `cf complete zsh >> ~/.zshrc` in shells that have not initialized Zsh's completion system. The generated script now runs `compinit` only when `compdef` is unavailable, and preserves the first-line `#compdef cf` directive for autoloaded completion files. diff --git a/packages/cli/src/__tests__/commands/completions.test.ts b/packages/cli/src/__tests__/commands/completions.test.ts index 6418c35b5f..f83e30e5a1 100644 --- a/packages/cli/src/__tests__/commands/completions.test.ts +++ b/packages/cli/src/__tests__/commands/completions.test.ts @@ -1,3 +1,7 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { RootCommand } from "@bomb.sh/tab"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { registerCompletions } from "../../commands/completions/index.js"; @@ -67,3 +71,85 @@ describe("metadata-backed completions", () => { ); }); }); + +describe("Zsh completion script", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function generateScript(): string { + const output = vi.spyOn(console, "log").mockImplementation(() => {}); + new RootCommand().setup("cf", "cf", "zsh"); + return output.mock.calls.map(([line]) => String(line)).join("\n"); + } + + function runZsh(commands: string) { + const directory = mkdtempSync(join(tmpdir(), "cf zsh-")); + try { + const scriptPath = join(directory, "completions.zsh"); + writeFileSync(scriptPath, generateScript()); + return spawnSync( + "zsh", + ["-f", "-c", commands, "cf-zsh-test", scriptPath], + { + encoding: "utf8", + env: { ...process.env, ZDOTDIR: directory }, + timeout: 10_000, + } + ); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + } + + it("preserves the compdef directive as the first line", () => { + expect(generateScript().split("\n")[0]).toBe("#compdef cf"); + }); + + // Zsh is optional on Linux and Windows; script generation is tested everywhere. + describe.skipIf( + spawnSync("zsh", ["-f", "-c", "exit 0"], { timeout: 5_000 }).status !== 0 + )("when Zsh is available", () => { + it("initializes a clean shell and registers the cf handler", () => { + const result = runZsh(` +(( ! $+functions[compdef] )) || exit 1 +source "$1" || exit $? +(( $+functions[_cf] )) || exit 1 +print -r -- "$_comps[cf]" +`); + expect(result.error).toBeUndefined(); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + expect(result.stdout).toBe("_cf\n"); + }); + + it("does not reinitialize completion when sourced again", () => { + const result = runZsh(` +source "$1" || exit $? +compinit() { print -u2 -- "unexpected compinit"; return 1; } +source "$1" || exit $? +print -r -- "$_comps[cf]" +`); + expect(result.error).toBeUndefined(); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + expect(result.stdout).toBe("_cf\n"); + }); + + it("skips compinit in an initialized shell, including repeated sourcing", () => { + const result = runZsh(` +autoload -Uz compinit +compinit || exit $? +compinit() { print -u2 -- "unexpected compinit"; return 1; } +source "$1" || exit $? +source "$1" || exit $? +(( $+functions[_cf] )) || exit 1 +print -r -- "$_comps[cf]" +`); + expect(result.error).toBeUndefined(); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + expect(result.stdout).toBe("_cf\n"); + }); + }); +}); diff --git a/patches/@bomb.sh__tab@0.0.22.patch b/patches/@bomb.sh__tab@0.0.22.patch new file mode 100644 index 0000000000..6078879820 --- /dev/null +++ b/patches/@bomb.sh__tab@0.0.22.patch @@ -0,0 +1,15 @@ +diff --git a/dist/powershell-C1q3FW2e.mjs b/dist/powershell-C1q3FW2e.mjs +index bfe3d3da6c336f831129eac39cc60896e3d8bc1e..4e72b13ee22af14ea8901ea22ff62751df2a5fa1 100644 +--- a/dist/powershell-C1q3FW2e.mjs ++++ b/dist/powershell-C1q3FW2e.mjs +@@ -1,4 +1,10 @@ + import{ShellCompDirective as e}from"./index.mjs";function t(t,n){return`#compdef ${t} ++ ++if (( ! $+functions[compdef] )); then ++ autoload -Uz compinit ++ compinit ++fi ++ + compdef _${t} ${t} + + # zsh completion for ${t} -*- shell-script -*- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10283153b4..cd1f2bf6c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,7 @@ overrides: vitest@*: 5.0.3 patchedDependencies: + '@bomb.sh/tab@0.0.22': b794937f159e94af4d2cd4d070607a4a83df0fd4db9ccbbf318b1974a6a1d9c1 '@changesets/cli@2.31.0': 059d085279022eeb7f92871a182f71310e4891b62871cfedccb21b34f4bfcd27 '@cloudflare/containers-shared@*': ecd632c350c22e92f11dd426d4aec675be6515b0aa7e21f2ff1367e315e3ac59 '@cloudflare/deploy-helpers@*': 664a40a198d157b2798e2f45cf713cb4d5926bc697603116b32a25c9ab92879d @@ -271,7 +272,7 @@ importers: devDependencies: '@bomb.sh/tab': specifier: 0.0.22 - version: 0.0.22(commander@14.0.3) + version: 0.0.22(patch_hash=b794937f159e94af4d2cd4d070607a4a83df0fd4db9ccbbf318b1974a6a1d9c1)(commander@14.0.3) '@bytecodealliance/jco': specifier: 1.37.0 version: 1.37.0 @@ -4081,7 +4082,7 @@ snapshots: '@blazediff/core@1.10.0': {} - '@bomb.sh/tab@0.0.22(commander@14.0.3)': + '@bomb.sh/tab@0.0.22(patch_hash=b794937f159e94af4d2cd4d070607a4a83df0fd4db9ccbbf318b1974a6a1d9c1)(commander@14.0.3)': optionalDependencies: commander: 14.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9e32d9258a..1c74336eae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - fixtures/* patchedDependencies: + "@bomb.sh/tab@0.0.22": patches/@bomb.sh__tab@0.0.22.patch "@changesets/cli@2.31.0": patches/@changesets__cli@2.31.0.patch "@cloudflare/containers-shared@*": patches/@cloudflare__containers-shared@0.21.1.patch "@cloudflare/deploy-helpers@*": patches/@cloudflare__deploy-helpers@0.20.1.patch