Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e9c0d42
feat(wrangler/cli): add tab completions
AmirSa12 Oct 27, 2025
f5f65e1
chore: update tab to version 0.0.9
AmirSa12 Nov 4, 2025
3e0e733
Merge remote-tracking branch 'upstream/main' into feat/tab-completions
AmirSa12 Nov 9, 2025
4b2b606
refactor(wrangler): use experimental_getWranglerCommands function for…
AmirSa12 Nov 9, 2025
b74ad8d
Update changelog for wrangler tab completions
ascorbic Dec 15, 2025
f0f739b
Merge branch 'main' into feat/tab-completions
ascorbic Dec 15, 2025
768ef7a
Format
ascorbic Dec 15, 2025
e38e836
Merge remote-tracking branch 'upstream/main' into feat/tab-completions
AmirSa12 Jan 5, 2026
be3b023
chore: update bomb.sh/tab to version 0.0.11
AmirSa12 Jan 5, 2026
15d5e4c
Merge branch 'main' into feat/tab-completions
NuroDev Jan 6, 2026
b1e60dd
Merge branch 'main' into feat/tab-completions
NuroDev Jan 7, 2026
412fa27
Merge branch 'main' into feat/tab-completions
NuroDev Jan 8, 2026
f49e16b
add tests
AmirSa12 Jan 8, 2026
20222b6
address Ben's review
AmirSa12 Jan 8, 2026
3437d21
update changeset .md file
AmirSa12 Jan 8, 2026
8c87c89
Merge branch 'main' into feat/tab-completions
NuroDev Jan 8, 2026
77a6ef3
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
89757a4
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
85ca3e7
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
4f5476c
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
7752c58
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
2f05cd5
Merge branch 'main' into feat/tab-completions
NuroDev Jan 9, 2026
c8d2486
Merge branch 'main' into feat/tab-completions
NuroDev Jan 12, 2026
e8a27f9
Merge branch 'main' into feat/tab-completions
NuroDev Jan 12, 2026
d18b46e
Merge branch 'main' into feat/tab-completions
NuroDev Jan 20, 2026
25bee9d
Removed self-explanitory doc comments
NuroDev Jan 21, 2026
ac25016
Switched to top-level `expect` import for completion tests
NuroDev Jan 21, 2026
149ae0f
Minor linting fixes
NuroDev Jan 21, 2026
137fb10
Added Fish shell to changeset
NuroDev Jan 21, 2026
6fd7f18
Refactored to use dedicated completion command namespace handler
NuroDev Jan 21, 2026
72e17bb
Minor help menu formatting fixes
NuroDev Jan 21, 2026
b4364b0
Minor status label fixes
NuroDev Jan 21, 2026
76fc750
Overhauled `complete` command logic
NuroDev Jan 21, 2026
5efb94b
Merge branch 'main' into feat/tab-completions
NuroDev Jan 21, 2026
a016b97
Fixed test help menu snapshots
NuroDev Jan 21, 2026
e9e79a1
Merge branch 'main' into feat/tab-completions
NuroDev Jan 21, 2026
b2fc39e
Minor `completionArgs` filtering tweak
NuroDev Jan 21, 2026
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
24 changes: 24 additions & 0 deletions .changeset/rude-cows-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"wrangler": minor
---

Add `wrangler complete` command for shell completion scripts (bash, zsh, powershell)

Usage:

```bash
# Bash
wrangler complete bash >> ~/.bashrc

# Zsh
wrangler complete zsh >> ~/.zshrc

# Fish
wrangler complete fish >> ~/.config/fish/completions/wrangler.fish

# PowerShell
wrangler complete powershell > $PROFILE
```

- Uses `@bomb.sh/tab` library for cross-shell compatibility
- Completions are dynamically generated from `experimental_getWranglerCommands()` API
1 change: 1 addition & 0 deletions packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.721.0",
"@bomb.sh/tab": "^0.0.11",
"@cloudflare/cli": "workspace:*",
"@cloudflare/containers-shared": "workspace:*",
"@cloudflare/eslint-config-shared": "workspace:*",
Expand Down
196 changes: 196 additions & 0 deletions packages/wrangler/src/__tests__/complete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { execSync } from "node:child_process";
import { describe, expect, test } from "vitest";
import { mockConsoleMethods } from "./helpers/mock-console";
import { runInTempDir } from "./helpers/run-in-tmp";
import { runWrangler } from "./helpers/run-wrangler";

function shellAvailable(shell: string): boolean {
try {
execSync(`which ${shell}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}

describe("wrangler", () => {
describe("complete", () => {
const std = mockConsoleMethods();
runInTempDir();

describe("complete --", () => {
test("should return top-level commands", async () => {
await runWrangler('complete -- ""');

expect(std.out).toContain("deploy\t");
expect(std.out).toContain("dev\t");
expect(std.out).toContain("kv\t");
});

test("should return subcommands for namespace", async () => {
await runWrangler('complete -- kv ""');

expect(std.out).toContain("namespace\t");
expect(std.out).toContain("key\t");
});

test("should return flags for a command", async () => {
await runWrangler("complete -- dev --");

expect(std.out).toContain("--port\t");
expect(std.out).toContain("--ip\t");
});

test("should not include internal commands", async () => {
await runWrangler('complete -- ""');

expect(std.out).toContain("deploy\t");
expect(std.out).toContain("dev\t");
// Internal commands like "_dev" should not be exposed
expect(std.out).not.toMatch(/^_dev\t/m);
});

test("should handle deeply nested commands", async () => {
await runWrangler('complete -- queues consumer http ""');

expect(std.out).toContain("add\t");
expect(std.out).toContain("remove\t");
});

test("should output tab-separated format", async () => {
await runWrangler('complete -- ""');

// Most lines should be "value\tdescription" format
const lines = std.out.trim().split("\n");
let tabSeparatedCount = 0;
for (const line of lines) {
if (line.trim() && !line.startsWith(":")) {
if (line.includes("\t")) {
tabSeparatedCount++;
}
}
}
// Most commands should have descriptions
expect(tabSeparatedCount).toBeGreaterThan(10);
});

test("should return options with choices", async () => {
await runWrangler('complete -- dev --log-level=""');

expect(std.out).toContain("debug\t");
expect(std.out).toContain("info\t");
expect(std.out).toContain("warn\t");
expect(std.out).toContain("error\t");
});
});

const shells = ["bash", "zsh", "fish"] as const;

describe.each(shells)("%s", (shell) => {
test("should output valid shell script", async () => {
await runWrangler(`complete ${shell}`);

expect(std.out.length).toBeGreaterThan(100);
});

test("should reference wrangler complete", async () => {
await runWrangler(`complete ${shell}`);

expect(std.out).toContain("wrangler complete --");
});
});

describe("bash", () => {
test.skipIf(!shellAvailable("bash"))(
"should generate valid bash syntax",
async () => {
await runWrangler("complete bash");

// bash -n checks syntax without executing
expect(() => {
execSync("bash -n", { input: std.out });
}).not.toThrow();
}
);

test("should define __wrangler_complete function", async () => {
await runWrangler("complete bash");

expect(std.out).toContain("__wrangler_complete()");
});

test("should register completion with complete builtin", async () => {
await runWrangler("complete bash");

expect(std.out).toContain("complete -F __wrangler_complete wrangler");
});
});

describe("zsh", () => {
test.skipIf(!shellAvailable("zsh"))(
"should generate valid zsh syntax",
async () => {
await runWrangler("complete zsh");

// zsh -n checks syntax without executing
expect(() => {
execSync("zsh -n", { input: std.out });
}).not.toThrow();
}
);

test("should start with #compdef directive", async () => {
await runWrangler("complete zsh");

expect(std.out).toContain("#compdef wrangler");
});

test("should define _wrangler function", async () => {
await runWrangler("complete zsh");

expect(std.out).toContain("_wrangler()");
});

test("should register with compdef", async () => {
await runWrangler("complete zsh");

expect(std.out).toContain("compdef _wrangler wrangler");
});
});

describe("fish", () => {
test.skipIf(!shellAvailable("fish"))(
"should generate valid fish syntax",
async () => {
await runWrangler("complete fish");

// fish -n checks syntax without executing
expect(() => {
execSync("fish -n", { input: std.out });
}).not.toThrow();
}
);

test("should define __wrangler_perform_completion function", async () => {
await runWrangler("complete fish");

expect(std.out).toContain("function __wrangler_perform_completion");
});

test("should register completion with complete builtin", async () => {
await runWrangler("complete fish");

expect(std.out).toContain("complete -c wrangler");
});

test("should use commandline for token extraction", async () => {
await runWrangler("complete fish");

// commandline -opc gets completed tokens
expect(std.out).toContain("commandline -opc");
// commandline -ct gets current token being typed
expect(std.out).toContain("commandline -ct");
});
});
});
});
1 change: 0 additions & 1 deletion packages/wrangler/src/__tests__/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ describe("wrangler docs", () => {

📚 Open Wrangler's command documentation in your browser


POSITIONALS
search Enter search terms (e.g. the wrangler command) you want to know more about [array] [default: []]

Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("wrangler", () => {

COMMANDS
wrangler docs [search..] 📚 Open Wrangler's command documentation in your browser
wrangler complete [shell] ⌨️ Generate and handle shell completions

ACCOUNT
wrangler auth 🔐 Manage authentication
Expand Down Expand Up @@ -108,6 +109,7 @@ describe("wrangler", () => {

COMMANDS
wrangler docs [search..] 📚 Open Wrangler's command documentation in your browser
wrangler complete [shell] ⌨️ Generate and handle shell completions

ACCOUNT
wrangler auth 🔐 Manage authentication
Expand Down
Loading
Loading