diff --git a/docs/official-plugins/plugin-completions.md b/docs/official-plugins/plugin-completions.md index 26196bb1..35e28d96 100644 --- a/docs/official-plugins/plugin-completions.md +++ b/docs/official-plugins/plugin-completions.md @@ -4,7 +4,7 @@ title: Completions Plugin # @clerc/plugin-completions -A plugin that adds command-line auto-completion functionality to your CLI. Based on [@pnpm/tabtab](https://github.com/pnpm/tabtab) +A plugin that adds command-line auto-completion functionality to your CLI. Based on [@bomb.sh/tab](https://github.com/bombshell-dev/tab). ## 📦 Installation diff --git a/docs/zh/official-plugins/plugin-completions.md b/docs/zh/official-plugins/plugin-completions.md index 2cce44df..ef3bd9d6 100644 --- a/docs/zh/official-plugins/plugin-completions.md +++ b/docs/zh/official-plugins/plugin-completions.md @@ -4,7 +4,7 @@ title: 自动补全插件 # @clerc/plugin-completions -为您的 CLI 添加命令行自动补全功能的插件。基于 [@pnpm/tabtab](https://github.com/pnpm/tabtab) +为您的 CLI 添加命令行自动补全功能的插件。基于 [@bomb.sh/tab](https://github.com/bombshell-dev/tab) ## 📦 安装 diff --git a/packages/plugin-completions/debug.txt b/packages/plugin-completions/debug.txt new file mode 100644 index 00000000..2133796d --- /dev/null +++ b/packages/plugin-completions/debug.txt @@ -0,0 +1,254 @@ +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: zsh +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["a"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["versi"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: powershell +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: powershell +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["asdfs"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["a"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: powershell +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: powershell +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["test","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["test","-a",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["test",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["test",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["test","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["complete","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["help","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["help","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["help","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["help","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["help",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete",""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete","-V="] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete","-V="] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["complete","-V="] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["version","-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: ["--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +flagName: version, short: V, desc: Prints current version, boolean: true +flagName: help, short: h, desc: Show help, boolean: true +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: [] +ctx.flags.shell: undefined +ctx.parameters.shell: powershell +extra: [""] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["-"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined +extra: ["--"] +ctx.flags.shell: undefined +ctx.parameters.shell: undefined diff --git a/packages/plugin-completions/package.json b/packages/plugin-completions/package.json index 8339d713..4446eecd 100644 --- a/packages/plugin-completions/package.json +++ b/packages/plugin-completions/package.json @@ -45,7 +45,7 @@ "access": "public" }, "dependencies": { - "@pnpm/tabtab": "^0.5.4" + "@bomb.sh/tab": "^0.0.10" }, "devDependencies": { "@clerc/core": "workspace:*", diff --git a/packages/plugin-completions/src/complete.ts b/packages/plugin-completions/src/complete.ts deleted file mode 100644 index e1260782..00000000 --- a/packages/plugin-completions/src/complete.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { Clerc, ClercFlagsDefinition } from "@clerc/core"; -import { DOUBLE_DASH, normalizeFlagValue, resolveCommand } from "@clerc/core"; -import { formatFlagName } from "@clerc/utils"; -import type { CompletionItem, ParseEnvResult } from "@pnpm/tabtab"; - -function splitCommand(cmd: string) { - const args: string[] = []; - let current = ""; - let quote: string | null = null; - let escape = false; - - for (const char of cmd) { - if (escape) { - current += char; - escape = false; - continue; - } - if (char === "\\") { - escape = true; - continue; - } - if (quote) { - if (char === quote) { - quote = null; - } else { - current += char; - } - } else { - if (char === '"' || char === "'") { - quote = char; - } else if (/\s/.test(char)) { - if (current) { - args.push(current); - current = ""; - } - } else { - current += char; - } - } - } - if (current) { - args.push(current); - } - - return args; -} - -export async function getCompletion( - cli: Clerc, - env: ParseEnvResult, -): Promise { - const finishedArgv = env.partial.slice( - 0, - env.partial.length - env.lastPartial.length, - ); - const inputArgv = splitCommand(finishedArgv).slice(1); - - if (inputArgv.includes(DOUBLE_DASH)) { - return []; - } - - const [command, commandName] = resolveCommand(cli._commands, inputArgv); - - const lastPartial = env.lastPartial; - const isOption = lastPartial.startsWith("-"); - - if (isOption) { - const flags: ClercFlagsDefinition = command - ? { ...cli._globalFlags, ...command.flags } - : cli._globalFlags; - - const candidates: CompletionItem[] = []; - for (const [name, def] of Object.entries(flags)) { - const normalized = normalizeFlagValue(def); - const names = [name]; - if (normalized.short) { - names.push(normalized.short); - } - for (const name of names) { - candidates.push({ - name: formatFlagName(name), - description: normalized.description, - }); - } - } - - return candidates; - } - - const candidates: CompletionItem[] = []; - let prefix = ""; - if (commandName) { - const matchedParts = commandName.split(" "); - const remainingArgs = inputArgv.slice(matchedParts.length); - prefix = `${[command.name, ...remainingArgs].join(" ")} `; - } else { - prefix = inputArgv.length > 0 ? `${inputArgv.join(" ")} ` : ""; - } - - for (const command of cli._commands.values()) { - if (command.completions?.show === false) { - continue; - } - - if (command.name.startsWith(prefix)) { - const remaining = command.name.slice(prefix.length); - // Only suggest the next word - const nextWord = remaining.split(" ")[0]; - if (nextWord) { - candidates.push({ - name: nextWord, - description: command.description, - }); - } - } - } - - // Deduplicate - const uniqueCandidates = new Map(); - for (const c of candidates) { - uniqueCandidates.set(c.name, c); - } - - return [...uniqueCandidates.values()]; -} diff --git a/packages/plugin-completions/src/index.ts b/packages/plugin-completions/src/index.ts index f5cd7275..529b5dfc 100644 --- a/packages/plugin-completions/src/index.ts +++ b/packages/plugin-completions/src/index.ts @@ -1,8 +1,8 @@ +import t from "@bomb.sh/tab"; import type { Plugin } from "@clerc/core"; import { Types, definePlugin } from "@clerc/core"; -import tabtab, { getShellFromEnv } from "@pnpm/tabtab"; -import { getCompletion } from "./complete"; +import { buildTabModel } from "./t"; declare module "@clerc/core" { export interface CommandCustomOptions { @@ -20,86 +20,18 @@ declare module "@clerc/core" { } } -export interface CompletionsPluginOptions { - /** - * Whether to register the `completions install` and `completions uninstall` commands. - * @default true - */ - managementCommands?: boolean; -} - -export const completionsPlugin = ( - options: CompletionsPluginOptions = {}, -): Plugin => +export const completionsPlugin = (): Plugin => definePlugin({ setup: (cli) => { - const { managementCommands = true } = options; - - cli.store.help?.addGroup({ - commands: [["completions", "Completions"]], - }); - - const supportedShellEnum = Types.Enum(...tabtab.SUPPORTED_SHELLS); - - if (managementCommands) { - cli - .command("completions install", "Install shell completions", { - help: { - group: "completions", - }, - flags: { - shell: { - description: "Shell type", - type: supportedShellEnum, - }, - }, - parameters: [ - { - key: "[shell]", - description: "Shell type", - type: supportedShellEnum, - }, - ], - }) - .on("completions install", async (ctx) => { - const shell = ctx.parameters.shell ?? ctx.flags.shell; - if (!shell) { - throw new Error( - "Please specify the shell type via the --shell flag or the [shell] parameter.", - ); - } - if (!tabtab.SUPPORTED_SHELLS.includes(shell)) { - throw new Error( - `Unsupported shell: ${shell}. Supported shells are: ${tabtab.SUPPORTED_SHELLS.join( - ", ", - )}.`, - ); - } - await tabtab.install({ - name: cli._scriptName, - completer: cli._scriptName, - shell, - }); - }); - - cli - .command("completions uninstall", "Uninstall shell completions", { - help: { - group: "completions", - }, - }) - .on("completions uninstall", async () => { - await tabtab.uninstall({ - name: cli._scriptName, - }); - }); - } + const supportedShellEnum = Types.Enum( + "zsh", + "bash", + "fish", + "powershell", + ); cli - .command("completions", "Generate completions script", { - help: { - group: "completions", - }, + .command("completions", "Generate shell completion scripts", { flags: { shell: { description: "Shell type", @@ -109,52 +41,36 @@ export const completionsPlugin = ( parameters: [ { key: "[shell]", - description: "Shell type", + description: "Shell type (zsh, bash, fish, powershell)", type: supportedShellEnum, }, ], }) .on("completions", async (ctx) => { const shell = ctx.parameters.shell ?? ctx.flags.shell; + if (!shell) { throw new Error( - "Please specify the shell type via the --shell flag or the [shell] parameter.", + "Shell type is required. Please provide it via --shell flag or [shell] parameter.", ); } - if (!tabtab.SUPPORTED_SHELLS.includes(shell)) { - throw new Error( - `Unsupported shell: ${shell}. Supported shells are: ${tabtab.SUPPORTED_SHELLS.join( - ", ", - )}.`, - ); - } - const script = await tabtab.getCompletionScript({ - name: cli._scriptName, - completer: cli._scriptName, - shell, - }); - console.log(script); + + buildTabModel(cli._globalFlags, cli._commands); + + t.setup(cli._scriptName, cli._scriptName, shell); }); cli - .command("completion-server", "Handle completions", { - help: { - show: false, - }, - completions: { - show: false, - }, + .command("complete", { + help: { show: false }, + completions: { show: false }, + parameters: ["--", "[input...]"], }) - .on("completion-server", async () => { - const env = tabtab.parseEnv(process.env); - if (!env.complete) { - return; - } - - const shell = getShellFromEnv(process.env); - const candidates = await getCompletion(cli, env); + .on("complete", async (ctx) => { + buildTabModel(cli._globalFlags, cli._commands); - tabtab.log(candidates, shell); + const { input } = ctx.parameters; + t.parse(input); }); return cli; diff --git a/packages/plugin-completions/src/t.ts b/packages/plugin-completions/src/t.ts new file mode 100644 index 00000000..06f6dc14 --- /dev/null +++ b/packages/plugin-completions/src/t.ts @@ -0,0 +1,46 @@ +import type { Command } from "@bomb.sh/tab"; +import t from "@bomb.sh/tab"; +import type { ClercFlagsDefinition, CommandsMap } from "clerc"; +import { normalizeFlagValue } from "clerc"; + +export function resetTab(): void { + t.commands.clear(); + t.options.clear(); + t.arguments.clear(); + t.completions = []; +} + +function registerGlobalFlags( + globalFlags: ClercFlagsDefinition, + tc: Command, +): void { + for (const [flagName, def] of Object.entries(globalFlags)) { + const normalized = normalizeFlagValue(def); + const desc = normalized.description ?? ""; + tc.option(flagName, desc, normalized.short); + } +} + +export function buildTabModel( + globalFlags: ClercFlagsDefinition, + commands: CommandsMap, +): void { + resetTab(); + + registerGlobalFlags(globalFlags, t); + + for (const cmd of commands.values()) { + if (cmd.completions?.show === false) { + continue; + } + + const command = t.command(cmd.name, cmd.description ?? ""); + registerGlobalFlags(globalFlags, command); + + for (const [flagName, def] of Object.entries(cmd.flags ?? {})) { + const normalized = normalizeFlagValue(def); + const desc = normalized.description ?? ""; + command.option(flagName, desc, normalized.short); + } + } +} diff --git a/packages/plugin-completions/test/commands.test.ts b/packages/plugin-completions/test/commands.test.ts index 5a5467c2..f9f3cb51 100644 --- a/packages/plugin-completions/test/commands.test.ts +++ b/packages/plugin-completions/test/commands.test.ts @@ -1,19 +1,22 @@ +import t from "@bomb.sh/tab"; import { TestBaseCli } from "@clerc/test-utils"; -import tabtab, { getShellFromEnv } from "@pnpm/tabtab"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { completionsPlugin } from "../src"; -vi.mock("@pnpm/tabtab", () => ({ +vi.mock("@bomb.sh/tab", () => ({ default: { - install: vi.fn(), - uninstall: vi.fn(), - getCompletionScript: vi.fn().mockResolvedValue("mock script"), - parseEnv: vi.fn(), - log: vi.fn(), - SUPPORTED_SHELLS: ["bash", "zsh", "fish"], + setup: vi.fn(), + parse: vi.fn(), + commands: new Map(), + options: new Map(), + arguments: new Map(), + completions: [], + command: vi.fn().mockReturnValue({ + option: vi.fn(), + }), + option: vi.fn(), }, - getShellFromEnv: vi.fn(), })); describe("plugin-completions/commands", () => { @@ -21,71 +24,29 @@ describe("plugin-completions/commands", () => { vi.clearAllMocks(); }); - describe("completions install", () => { - it("should install completions", async () => { - const cli = TestBaseCli().use(completionsPlugin()); - - await cli.parse(["completions", "install", "--shell", "bash"]); - - expect(tabtab.install).toHaveBeenCalledWith({ - name: "test", - completer: "test", - shell: "bash", - }); - }); - - it("should throw error if shell is missing", async () => { - await expect(async () => { - await TestBaseCli() - .use(completionsPlugin()) - .parse(["completions", "install"]); - }).rejects.toThrow("Please specify the shell type"); - }); - - it("should throw error if shell is unsupported", async () => { - await expect(async () => { - await TestBaseCli() - .use(completionsPlugin()) - .parse(["completions", "install", "--shell", "invalid"]); - }).rejects.toThrowErrorMatchingInlineSnapshot( - "[Error: Invalid value: invalid. Must be one of: bash, zsh, fish]", - ); - }); - }); - - describe("completions uninstall", () => { - it("should uninstall completions", async () => { + describe("completions", () => { + it("should setup completions with shell parameter", async () => { const cli = TestBaseCli().use(completionsPlugin()); - await cli.parse(["completions", "uninstall"]); + await cli.parse(["completions", "bash"]); - expect(tabtab.uninstall).toHaveBeenCalledWith({ - name: "test", - }); + expect(t.setup).toHaveBeenCalledWith("test", "test", "bash"); }); - }); - describe("completions", () => { - it("should generate completion script", async () => { - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + it("should setup completions with shell flag", async () => { const cli = TestBaseCli().use(completionsPlugin()); - await cli.parse(["completions", "--shell", "bash"]); + await cli.parse(["completions", "--shell", "zsh"]); - expect(tabtab.getCompletionScript).toHaveBeenCalledWith({ - name: "test", - completer: "test", - shell: "bash", - }); - expect(consoleSpy).toHaveBeenCalledWith("mock script"); - - consoleSpy.mockRestore(); + expect(t.setup).toHaveBeenCalledWith("test", "test", "zsh"); }); it("should throw error if shell is missing", async () => { await expect(async () => { await TestBaseCli().use(completionsPlugin()).parse(["completions"]); - }).rejects.toThrow("Please specify the shell type"); + }).rejects.toThrow( + "Shell type is required. Please provide it via --shell flag or [shell] parameter.", + ); }); it("should throw error if shell is unsupported", async () => { @@ -94,47 +55,28 @@ describe("plugin-completions/commands", () => { .use(completionsPlugin()) .parse(["completions", "--shell", "invalid"]); }).rejects.toThrowErrorMatchingInlineSnapshot( - "[Error: Invalid value: invalid. Must be one of: bash, zsh, fish]", + "[Error: Invalid value: invalid. Must be one of: zsh, bash, fish, powershell]", ); }); }); - describe("completion-server", () => { - it("should handle completion request", async () => { + describe("complete", () => { + it("should parse completion input", async () => { const cli = TestBaseCli() .use(completionsPlugin()) .command("commit", "Commit changes"); - vi.mocked(tabtab.parseEnv).mockReturnValue({ - complete: true, - words: ["test", "com"], - point: 8, - partial: "test com", - lastPartial: "com", - line: "test com", - } as any); - - vi.mocked(getShellFromEnv).mockReturnValue("bash"); - - await cli.parse(["completion-server"]); + await cli.parse(["complete", "--", "test", "com"]); - expect(tabtab.log).toHaveBeenCalled(); - - const candidates = vi.mocked(tabtab.log).mock.calls[0][0] as any[]; - - expect(candidates.some((c) => c.name === "commit")).toBeTruthy(); + expect(t.parse).toHaveBeenCalledWith(["test", "com"]); }); - it("should do nothing if not in completion mode", async () => { + it("should handle empty input", async () => { const cli = TestBaseCli().use(completionsPlugin()); - vi.mocked(tabtab.parseEnv).mockReturnValue({ - complete: false, - } as any); - - await cli.parse(["completion-server"]); + await cli.parse(["complete", "--"]); - expect(tabtab.log).not.toHaveBeenCalled(); + expect(t.parse).toHaveBeenCalledWith([]); }); }); }); diff --git a/packages/plugin-completions/test/complete.test.ts b/packages/plugin-completions/test/complete.test.ts deleted file mode 100644 index 7df6390c..00000000 --- a/packages/plugin-completions/test/complete.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { TestBaseCli } from "@clerc/test-utils"; -import { describe, expect, it } from "vitest"; - -import { getCompletion } from "../src/complete"; - -describe("plugin-completions/complete", () => { - describe("getCompletion", () => { - it("should return empty array when -- is present", async () => { - const cli = TestBaseCli(); - const env = { - partial: "test --", - lastPartial: "", - complete: true, - words: ["test", "--"], - point: 8, - }; - // TODO: remove type assertion after fixing types - const result = await getCompletion(cli, env as any); - - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(0); - }); - - it("should return flag options when input starts with -", async () => { - const cli = TestBaseCli() - .globalFlag("verbose", "Enable verbose mode", { - type: Boolean, - short: "v", - }) - .globalFlag("output", "Output file", { - type: String, - short: "o", - }) - .command("build", "Build the project", { - flags: { - minify: { - description: "Minify output", - type: Boolean, - }, - }, - }); - - const env = { - partial: "test build -", - lastPartial: "-", - complete: true, - words: ["test", "build", "-"], - point: 12, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("--verbose"); - expect(names).toContain("-v"); - expect(names).toContain("--output"); - expect(names).toContain("-o"); - expect(names).toContain("--minify"); - }); - - it("should return command completions for non-option input", async () => { - const cli = TestBaseCli() - .command("build", "Build the project") - .command("dev", "Start dev server") - .command("test", "Run tests"); - - const env = { - partial: "test ", - lastPartial: "", - complete: true, - words: ["test"], - point: 5, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("build"); - expect(names).toContain("dev"); - expect(names).toContain("test"); - }); - - it("should filter commands by prefix", async () => { - const cli = TestBaseCli() - .command("build", "Build the project") - .command("bundle", "Bundle the project") - .command("dev", "Start dev server"); - - const env = { - partial: "test bu", - lastPartial: "bu", - complete: true, - words: ["test", "bu"], - point: 8, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("build"); - expect(names).toContain("bundle"); - }); - - it("should exclude commands with show: false", async () => { - const cli = TestBaseCli() - .command("build", "Build the project") - .command("hidden", "Hidden command", { - completions: { - show: false, - }, - }); - - const env = { - partial: "test ", - lastPartial: "", - complete: true, - words: ["test"], - point: 5, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("build"); - expect(names).not.toContain("hidden"); - }); - - it("should deduplicate completion candidates", async () => { - const cli = TestBaseCli() - .command("build", "Build the project") - .command("build:prod", "Build for production") - .command("build:dev", "Build for development"); - - const env = { - partial: "test build", - lastPartial: "build", - complete: true, - words: ["test", "build"], - point: 10, - }; - - const result = await getCompletion(cli, env as any); - - // Count how many times "build" appears - const buildCount = result.filter((c) => c.name === "build").length; - - expect(buildCount).toBe(1); - }); - - it("should return global flags for root level option completion", async () => { - const cli = TestBaseCli() - .globalFlag("help", "Show help", { - type: Boolean, - short: "h", - }) - .command("test", "Test command"); - - const env = { - partial: "test -", - lastPartial: "-", - complete: true, - words: ["test", "-"], - point: 6, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("--help"); - expect(names).toContain("-h"); - }); - - it("should handle multi-word command names", async () => { - const cli = TestBaseCli() - .command("git commit", "Commit changes") - .command("git push", "Push changes") - .command("npm install", "Install packages"); - - const env = { - partial: "test git ", - lastPartial: "", - complete: true, - words: ["test", "git", ""], - point: 9, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("commit"); - expect(names).toContain("push"); - expect(names).not.toContain("install"); - }); - - it("should return suggestions when partial matches multi-word commands", async () => { - const cli = TestBaseCli() - .command("git commit", "Commit changes") - .command("git push", "Push changes"); - - const env = { - partial: "test git c", - lastPartial: "c", - complete: true, - words: ["test", "git", "c"], - point: 9, - }; - - const result = await getCompletion(cli, env as any); - - // Should suggest "commit" as the next word - const names = result.map((c) => c.name); - - expect(names.length).toBeGreaterThan(0); - }); - - it("should handle quoted strings in input", async () => { - const cli = TestBaseCli().command("commit", "Commit"); - - const env = { - partial: 'test "some arg" ', - lastPartial: "", - complete: true, - words: ["test", '"some arg"', ""], - point: 16, - }; - - await getCompletion(cli, env as any); - }); - - it("should suggest subcommands after resolved command", async () => { - const cli = TestBaseCli() - .command("git", "Git") - .command("git commit", "Commit"); - - const env = { - partial: "test git ", - lastPartial: "", - complete: true, - words: ["test", "git", ""], - point: 9, - }; - - const result = await getCompletion(cli, env as any); - - const names = result.map((c) => c.name); - - expect(names).toContain("commit"); - }); - - it("should handle escaped characters", async () => { - const cli = TestBaseCli().command("commit", "Commit"); - - const env = { - partial: 'test "some\\ arg" ', - lastPartial: "", - complete: true, - words: ["test", '"some\\ arg"', ""], - point: 17, - }; - - await getCompletion(cli, env as any); - }); - }); -}); diff --git a/packages/plugin-completions/test/plugin-completions.test.ts b/packages/plugin-completions/test/plugin-completions.test.ts index 045d344d..98ef285f 100644 --- a/packages/plugin-completions/test/plugin-completions.test.ts +++ b/packages/plugin-completions/test/plugin-completions.test.ts @@ -8,21 +8,8 @@ describe("plugin-completions", () => { const cli = TestBaseCli().use(completionsPlugin()); // Check if commands are registered - expect(cli._commands.has("completions install")).toBeTruthy(); - expect(cli._commands.has("completions uninstall")).toBeTruthy(); + expect(cli._commands.has("complete")).toBeTruthy(); expect(cli._commands.has("completions")).toBeTruthy(); - expect(cli._commands.has("completion-server")).toBeTruthy(); - }); - - it("should not register management commands when disabled", () => { - const cli = TestBaseCli().use( - completionsPlugin({ managementCommands: false }), - ); - - expect(cli._commands.has("completions install")).toBeFalsy(); - expect(cli._commands.has("completions uninstall")).toBeFalsy(); - expect(cli._commands.has("completions")).toBeTruthy(); - expect(cli._commands.has("completion-server")).toBeTruthy(); }); it("should have completions command with proper configuration", () => { @@ -33,48 +20,4 @@ describe("plugin-completions", () => { expect(completionsCmd?.flags).toBeDefined(); expect(completionsCmd?.flags?.shell).toBeDefined(); }); - - it("should have completion-server command hidden from help and completion itself", () => { - const cli = TestBaseCli().use(completionsPlugin()); - const completionServerCmd = cli._commands.get("completion-server"); - - expect(completionServerCmd).toBeDefined(); - expect(completionServerCmd?.help?.show).toBeFalsy(); - expect(completionServerCmd?.completions?.show).toBeFalsy(); - }); - - it("should support command completions options", () => { - const cli = TestBaseCli() - .use(completionsPlugin()) - .command("test", "test command", { - completions: { - show: false, - }, - }); - - const cmd = cli._commands.get("test"); - - expect(cmd?.completions?.show).toBeFalsy(); - }); - - it("should have install and uninstall commands with proper configuration", () => { - const cli = TestBaseCli().use(completionsPlugin()); - - const installCmd = cli._commands.get("completions install"); - - expect(installCmd).toBeDefined(); - expect(installCmd?.flags).toBeDefined(); - expect(installCmd?.flags?.shell).toBeDefined(); - - const uninstallCmd = cli._commands.get("completions uninstall"); - - expect(uninstallCmd).toBeDefined(); - }); - - it("should return cli instance from plugin setup", () => { - const cli = TestBaseCli(); - const result = cli.use(completionsPlugin()); - - expect(result).toBe(cli); - }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82f364f9..32565c2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,9 +201,9 @@ importers: packages/plugin-completions: dependencies: - '@pnpm/tabtab': - specifier: ^0.5.4 - version: 0.5.4 + '@bomb.sh/tab': + specifier: ^0.0.10 + version: 0.0.10(cac@6.7.14)(citty@0.1.6) devDependencies: '@clerc/core': specifier: workspace:* @@ -391,6 +391,21 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bomb.sh/tab@0.0.10': + resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==} + hasBin: true + peerDependencies: + cac: ^6.7.14 + citty: ^0.1.6 + commander: ^13.1.0 + peerDependenciesMeta: + cac: + optional: true + citty: + optional: true + commander: + optional: true + '@clerc/core@0.44.0': resolution: {integrity: sha512-o8RgXNcMRoHRujSw9OPDMxqrmoNk7HG0XAZkjZgOrSyIfRXCf85VLyHGBT3XmaOrPEGY964h02ZxMVFdp8RnNQ==} @@ -1032,10 +1047,6 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@pnpm/tabtab@0.5.4': - resolution: {integrity: sha512-bWLDlHsBlgKY/05wDN/V3ETcn5G2SV/SiA2ZmNvKGGlmVX4G5li7GRDhHcgYvHJHyJ8TUStqg2xtHmCs0UbAbg==} - engines: {node: '>=18'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1898,10 +1909,6 @@ packages: alien-signals@3.1.1: resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2298,10 +2305,6 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -4243,10 +4246,6 @@ packages: synckit: optional: true - untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -4638,6 +4637,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bomb.sh/tab@0.0.10(cac@6.7.14)(citty@0.1.6)': + optionalDependencies: + cac: 6.7.14 + citty: 0.1.6 + '@clerc/core@0.44.0': dependencies: '@clerc/utils': 0.44.0(@clerc/core@0.44.0) @@ -5171,15 +5175,6 @@ snapshots: '@pkgr/core@0.2.9': {} - '@pnpm/tabtab@0.5.4': - dependencies: - debug: 4.4.3 - enquirer: 2.4.1 - minimist: 1.2.8 - untildify: 4.0.0 - transitivePeerDependencies: - - supports-color - '@polka/url@1.0.0-next.29': {} '@quansync/fs@0.1.5': @@ -6160,8 +6155,6 @@ snapshots: alien-signals@3.1.1: {} - ansi-colors@4.1.3: {} - ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -6549,11 +6542,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - entities@4.5.0: {} entities@6.0.1: {} @@ -8947,8 +8935,6 @@ snapshots: optionalDependencies: synckit: 0.11.11 - untildify@4.0.0: {} - update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0