Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tidy-zsh-completions.md
Original file line number Diff line number Diff line change
@@ -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.
86 changes: 86 additions & 0 deletions packages/cli/src/__tests__/commands/completions.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
});
});
});
15 changes: 15 additions & 0 deletions patches/@bomb.sh__tab@0.0.22.patch
Original file line number Diff line number Diff line change
@@ -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 -*-
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading