-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(wrangler/cli): add @bomb.sh/tab completions
#11113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+421
−2
Merged
Changes from 26 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
e9c0d42
feat(wrangler/cli): add tab completions
AmirSa12 f5f65e1
chore: update tab to version 0.0.9
AmirSa12 3e0e733
Merge remote-tracking branch 'upstream/main' into feat/tab-completions
AmirSa12 4b2b606
refactor(wrangler): use experimental_getWranglerCommands function for…
AmirSa12 b74ad8d
Update changelog for wrangler tab completions
ascorbic f0f739b
Merge branch 'main' into feat/tab-completions
ascorbic 768ef7a
Format
ascorbic e38e836
Merge remote-tracking branch 'upstream/main' into feat/tab-completions
AmirSa12 be3b023
chore: update bomb.sh/tab to version 0.0.11
AmirSa12 15d5e4c
Merge branch 'main' into feat/tab-completions
NuroDev b1e60dd
Merge branch 'main' into feat/tab-completions
NuroDev 412fa27
Merge branch 'main' into feat/tab-completions
NuroDev f49e16b
add tests
AmirSa12 20222b6
address Ben's review
AmirSa12 3437d21
update changeset .md file
AmirSa12 8c87c89
Merge branch 'main' into feat/tab-completions
NuroDev 77a6ef3
Merge branch 'main' into feat/tab-completions
NuroDev 89757a4
Merge branch 'main' into feat/tab-completions
NuroDev 85ca3e7
Merge branch 'main' into feat/tab-completions
NuroDev 4f5476c
Merge branch 'main' into feat/tab-completions
NuroDev 7752c58
Merge branch 'main' into feat/tab-completions
NuroDev 2f05cd5
Merge branch 'main' into feat/tab-completions
NuroDev c8d2486
Merge branch 'main' into feat/tab-completions
NuroDev e8a27f9
Merge branch 'main' into feat/tab-completions
NuroDev d18b46e
Merge branch 'main' into feat/tab-completions
NuroDev 25bee9d
Removed self-explanitory doc comments
NuroDev ac25016
Switched to top-level `expect` import for completion tests
NuroDev 149ae0f
Minor linting fixes
NuroDev 137fb10
Added Fish shell to changeset
NuroDev 6fd7f18
Refactored to use dedicated completion command namespace handler
NuroDev 72e17bb
Minor help menu formatting fixes
NuroDev b4364b0
Minor status label fixes
NuroDev 76fc750
Overhauled `complete` command logic
NuroDev 5efb94b
Merge branch 'main' into feat/tab-completions
NuroDev a016b97
Fixed test help menu snapshots
NuroDev e9e79a1
Merge branch 'main' into feat/tab-completions
NuroDev b2fc39e
Minor `completionArgs` filtering tweak
NuroDev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| "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 | ||
|
|
||
| # PowerShell | ||
| wrangler complete powershell > $PROFILE | ||
| ``` | ||
|
|
||
| - Uses `@bomb.sh/tab` library for cross-shell compatibility | ||
| - Completions are dynamically generated from `experimental_getWranglerCommands()` API |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import { execSync } from "node:child_process"; | ||
| import { describe, 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 ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| await runWrangler('complete -- kv ""'); | ||
|
|
||
| expect(std.out).toContain("namespace\t"); | ||
| expect(std.out).toContain("key\t"); | ||
| }); | ||
|
|
||
| test("should return flags for a command", async ({ expect }) => { | ||
| await runWrangler("complete -- dev --"); | ||
|
|
||
| expect(std.out).toContain("--port\t"); | ||
| expect(std.out).toContain("--ip\t"); | ||
| }); | ||
|
|
||
| test("should not include internal commands", async ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| await runWrangler(`complete ${shell}`); | ||
|
|
||
| expect(std.out.length).toBeGreaterThan(100); | ||
| }); | ||
|
|
||
| test("should reference wrangler complete", async ({ expect }) => { | ||
| await runWrangler(`complete ${shell}`); | ||
|
|
||
| expect(std.out).toContain("wrangler complete --"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("bash", () => { | ||
| test.skipIf(!shellAvailable("bash"))( | ||
| "should generate valid bash syntax", | ||
| async ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| await runWrangler("complete bash"); | ||
|
|
||
| expect(std.out).toContain("__wrangler_complete()"); | ||
| }); | ||
|
|
||
| test("should register completion with complete builtin", async ({ | ||
| expect, | ||
| }) => { | ||
| 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 ({ expect }) => { | ||
| 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 ({ expect }) => { | ||
| await runWrangler("complete zsh"); | ||
|
|
||
| expect(std.out).toContain("#compdef wrangler"); | ||
| }); | ||
|
|
||
| test("should define _wrangler function", async ({ expect }) => { | ||
| await runWrangler("complete zsh"); | ||
|
|
||
| expect(std.out).toContain("_wrangler()"); | ||
| }); | ||
|
|
||
| test("should register with compdef", async ({ expect }) => { | ||
| await runWrangler("complete zsh"); | ||
|
|
||
| expect(std.out).toContain("compdef _wrangler wrangler"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("fish", () => { | ||
| test.skipIf(!shellAvailable("fish"))( | ||
| "should generate valid fish syntax", | ||
| async ({ expect }) => { | ||
| 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 ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("complete fish"); | ||
|
|
||
| expect(std.out).toContain("function __wrangler_perform_completion"); | ||
| }); | ||
|
|
||
| test("should register completion with complete builtin", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("complete fish"); | ||
|
|
||
| expect(std.out).toContain("complete -c wrangler"); | ||
| }); | ||
|
|
||
| test("should use commandline for token extraction", async ({ | ||
| expect, | ||
| }) => { | ||
| 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"); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import t from "@bomb.sh/tab"; | ||
| import { experimental_getWranglerCommands } from "./experimental-commands-api"; | ||
| import type { DefinitionTreeNode } from "./core/types"; | ||
|
|
||
| function setupCompletions() { | ||
| const { registry, globalFlags } = experimental_getWranglerCommands(); | ||
|
|
||
| // global flags that work on every command | ||
| for (const [flagName, flagDef] of Object.entries(globalFlags)) { | ||
| // skip hidden flags | ||
| if ("hidden" in flagDef && flagDef.hidden) continue; | ||
|
|
||
| const description = flagDef.describe || ""; | ||
| t.option(flagName, description); | ||
|
|
||
| if ("alias" in flagDef && flagDef.alias) { | ||
| const aliases = Array.isArray(flagDef.alias) | ||
| ? flagDef.alias | ||
| : [flagDef.alias]; | ||
| for (const alias of aliases) { | ||
| t.option(alias, `Alias for --${flagName}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // recursively add commands from the registry tree | ||
| function addCommandsFromTree( | ||
| node: DefinitionTreeNode, | ||
| parentPath: string[] = [] | ||
| ) { | ||
| for (const [name, childNode] of node.subtree.entries()) { | ||
| const commandPath = [...parentPath, name]; | ||
| const commandName = commandPath.join(" "); | ||
|
|
||
| if (childNode.definition) { | ||
| const def = childNode.definition; | ||
| let description = ""; | ||
|
|
||
| if (def.metadata?.description) { | ||
| description = def.metadata.description; | ||
| } | ||
|
|
||
| if ( | ||
| def.metadata?.status && | ||
| def.metadata.status !== "stable" && | ||
| !def.metadata.hidden | ||
| ) { | ||
| const statusLabels = { | ||
| experimental: "[experimental]", | ||
| alpha: "[alpha]", | ||
| "private-beta": "[private-beta]", | ||
| "open-beta": "[open-beta]", | ||
NuroDev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
| const statusLabel = | ||
| statusLabels[def.metadata.status as keyof typeof statusLabels]; | ||
| if (statusLabel) { | ||
| description = `${description} ${statusLabel}`; | ||
| } | ||
| } | ||
|
|
||
| if (!def.metadata?.hidden) { | ||
| const cmd = t.command(commandName, description); | ||
|
|
||
| if (def.type === "command" && "args" in def) { | ||
| const args = def.args || {}; | ||
| for (const [argName, argDef] of Object.entries(args)) { | ||
| if (argDef.hidden) continue; | ||
|
|
||
| const argDescription = argDef.describe || ""; | ||
|
|
||
| if (argDef.choices && Array.isArray(argDef.choices)) { | ||
| cmd.option(argName, argDescription, (complete) => { | ||
| for (const choice of argDef.choices as string[]) { | ||
| complete(choice, choice); | ||
| } | ||
| }); | ||
| } else { | ||
| cmd.option(argName, argDescription); | ||
| } | ||
|
|
||
| if (argDef.alias) { | ||
| const aliases = Array.isArray(argDef.alias) | ||
| ? argDef.alias | ||
| : [argDef.alias]; | ||
| for (const alias of aliases) { | ||
| cmd.option(alias, `Alias for --${argName}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (childNode.subtree.size > 0) { | ||
| addCommandsFromTree(childNode, commandPath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| addCommandsFromTree(registry); | ||
|
|
||
| return t; | ||
| } | ||
| // Handle completion requests from the shell | ||
| export function handleCompletion(args: string[]) { | ||
| const shell = args[0]; | ||
|
|
||
| if (shell === "--") { | ||
| // Parse completion request from shell | ||
| setupCompletions(); | ||
| t.parse(args.slice(1)); | ||
| } else { | ||
| // Generate shell completion script | ||
| setupCompletions(); | ||
| t.setup("wrangler", "wrangler", shell); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ import { | |
| } from "./cert/cert"; | ||
| import { checkNamespace, checkStartupCommand } from "./check/commands"; | ||
| import { cloudchamber } from "./cloudchamber"; | ||
| import { handleCompletion } from "./complete"; | ||
| import { getDefaultEnvFiles, loadDotEnv } from "./config/dot-env"; | ||
| import { containers } from "./containers"; | ||
| import { demandSingleValue } from "./core"; | ||
|
|
@@ -1730,6 +1731,12 @@ export function createCLIParser(argv: string[]) { | |
| export async function main(argv: string[]): Promise<void> { | ||
| setupSentry(); | ||
|
|
||
| // Handle shell completion requests | ||
| if (argv[0] === "complete") { | ||
| handleCompletion(argv.slice(1)); | ||
| return; | ||
| } | ||
|
||
|
|
||
| checkMacOSVersion({ shouldThrow: false }); | ||
|
|
||
| const startTime = Date.now(); | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.