From 604e4f3eb22a5dcef67400f1678cd502f3f38202 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 2 Jul 2026 15:02:02 +0100 Subject: [PATCH 1/3] fix(completion): add --no-descriptions to bash/zsh/fish/powershell cobra auto-registers --no-descriptions on all four completion subcommands whenever descriptions are enabled (the Go CLI default). The TS proxies declared no flags at all, so the Effect CLI parser rejected the flag with UnrecognizedOption before it ever reached the Go binary. Hoisted the flag definition into completion.flags.ts since it's shared by all four leaves in the same family. Fixes CLI-1858 --- .../src/legacy/commands/completion/SIDE_EFFECTS.md | 4 ++++ .../legacy/commands/completion/bash/bash.command.ts | 5 ++++- .../legacy/commands/completion/bash/bash.handler.ts | 6 ++++-- .../completion/bash/bash.integration.test.ts | 10 +++++++++- .../legacy/commands/completion/completion.flags.ts | 12 ++++++++++++ .../legacy/commands/completion/fish/fish.command.ts | 5 ++++- .../legacy/commands/completion/fish/fish.handler.ts | 6 ++++-- .../completion/fish/fish.integration.test.ts | 10 +++++++++- .../completion/powershell/powershell.command.ts | 5 ++++- .../completion/powershell/powershell.handler.ts | 6 ++++-- .../powershell/powershell.integration.test.ts | 10 +++++++++- .../legacy/commands/completion/zsh/zsh.command.ts | 5 ++++- .../legacy/commands/completion/zsh/zsh.handler.ts | 6 ++++-- .../commands/completion/zsh/zsh.integration.test.ts | 10 +++++++++- 14 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 apps/cli/src/legacy/commands/completion/completion.flags.ts diff --git a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md index 9246628751..f64d01935e 100644 --- a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md @@ -58,3 +58,7 @@ which provides the matching hidden command. - The Go CLI exits non-zero when called without a shell subcommand (e.g. `supabase completion`). Effect CLI surfaces the same condition through its usual "missing subcommand" help-with-exit-1 behavior. +- Each of `bash`/`zsh`/`fish`/`powershell` declares `--no-descriptions` (cobra's + auto-registered flag, `completions.go` in `spf13/cobra`) and forwards it to the + Go binary, so the emitted script omits completion descriptions exactly as it + would with the Go CLI. diff --git a/apps/cli/src/legacy/commands/completion/bash/bash.command.ts b/apps/cli/src/legacy/commands/completion/bash/bash.command.ts index 5e8c1d9a10..a2810b19b7 100644 --- a/apps/cli/src/legacy/commands/completion/bash/bash.command.ts +++ b/apps/cli/src/legacy/commands/completion/bash/bash.command.ts @@ -1,8 +1,11 @@ import { Command } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; +import { LegacyCompletionNoDescriptionsFlagDef } from "../completion.flags.ts"; import { legacyCompletionBash } from "./bash.handler.ts"; -const config = {}; +const config = { + noDescriptions: LegacyCompletionNoDescriptionsFlagDef, +} as const; export type LegacyCompletionBashFlags = CliCommand.Command.Config.Infer; export const legacyCompletionBashCommand = Command.make("bash", config).pipe( diff --git a/apps/cli/src/legacy/commands/completion/bash/bash.handler.ts b/apps/cli/src/legacy/commands/completion/bash/bash.handler.ts index dce9331b4c..9ff371dd44 100644 --- a/apps/cli/src/legacy/commands/completion/bash/bash.handler.ts +++ b/apps/cli/src/legacy/commands/completion/bash/bash.handler.ts @@ -3,8 +3,10 @@ import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; import type { LegacyCompletionBashFlags } from "./bash.command.ts"; export const legacyCompletionBash = Effect.fn("legacy.completion.bash")(function* ( - _flags: LegacyCompletionBashFlags, + flags: LegacyCompletionBashFlags, ) { const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["completion", "bash"]); + const args: string[] = ["completion", "bash"]; + if (flags.noDescriptions) args.push("--no-descriptions"); + yield* proxy.exec(args); }); diff --git a/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts b/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts index 3506609072..4d11fa7558 100644 --- a/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts @@ -19,8 +19,16 @@ describe("legacy completion bash", () => { it.live("forwards `completion bash` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionBash(); return Effect.gen(function* () { - yield* legacyCompletionBash({}); + yield* legacyCompletionBash({ noDescriptions: false }); expect(calls).toEqual([["completion", "bash"]]); }).pipe(Effect.provide(layer)); }); + + it.live("forwards --no-descriptions when set", () => { + const { layer, calls } = setupLegacyCompletionBash(); + return Effect.gen(function* () { + yield* legacyCompletionBash({ noDescriptions: true }); + expect(calls).toEqual([["completion", "bash", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)); + }); }); diff --git a/apps/cli/src/legacy/commands/completion/completion.flags.ts b/apps/cli/src/legacy/commands/completion/completion.flags.ts new file mode 100644 index 0000000000..5f81f77b8e --- /dev/null +++ b/apps/cli/src/legacy/commands/completion/completion.flags.ts @@ -0,0 +1,12 @@ +import { Flag } from "effect/unstable/cli"; + +/** + * cobra auto-registers `--no-descriptions` on the bash/zsh/fish/powershell + * completion subcommands whenever descriptions are enabled (the default) — + * `compCmdNoDescFlagName`/`compCmdNoDescFlagDefault`/`compCmdNoDescFlagDesc` + * in `spf13/cobra@v1.10.2/completions.go:101-103`. Shared across all four + * leaves rather than redeclared per-file. + */ +export const LegacyCompletionNoDescriptionsFlagDef = Flag.boolean("no-descriptions").pipe( + Flag.withDescription("disable completion descriptions"), +); diff --git a/apps/cli/src/legacy/commands/completion/fish/fish.command.ts b/apps/cli/src/legacy/commands/completion/fish/fish.command.ts index 2d22d4cd6e..33ef664d5f 100644 --- a/apps/cli/src/legacy/commands/completion/fish/fish.command.ts +++ b/apps/cli/src/legacy/commands/completion/fish/fish.command.ts @@ -1,8 +1,11 @@ import { Command } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; +import { LegacyCompletionNoDescriptionsFlagDef } from "../completion.flags.ts"; import { legacyCompletionFish } from "./fish.handler.ts"; -const config = {}; +const config = { + noDescriptions: LegacyCompletionNoDescriptionsFlagDef, +} as const; export type LegacyCompletionFishFlags = CliCommand.Command.Config.Infer; export const legacyCompletionFishCommand = Command.make("fish", config).pipe( diff --git a/apps/cli/src/legacy/commands/completion/fish/fish.handler.ts b/apps/cli/src/legacy/commands/completion/fish/fish.handler.ts index c293d2e3b4..0157d9cc6a 100644 --- a/apps/cli/src/legacy/commands/completion/fish/fish.handler.ts +++ b/apps/cli/src/legacy/commands/completion/fish/fish.handler.ts @@ -3,8 +3,10 @@ import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; import type { LegacyCompletionFishFlags } from "./fish.command.ts"; export const legacyCompletionFish = Effect.fn("legacy.completion.fish")(function* ( - _flags: LegacyCompletionFishFlags, + flags: LegacyCompletionFishFlags, ) { const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["completion", "fish"]); + const args: string[] = ["completion", "fish"]; + if (flags.noDescriptions) args.push("--no-descriptions"); + yield* proxy.exec(args); }); diff --git a/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts b/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts index 2f441b4fbb..b345a1b63e 100644 --- a/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts @@ -19,8 +19,16 @@ describe("legacy completion fish", () => { it.live("forwards `completion fish` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionFish(); return Effect.gen(function* () { - yield* legacyCompletionFish({}); + yield* legacyCompletionFish({ noDescriptions: false }); expect(calls).toEqual([["completion", "fish"]]); }).pipe(Effect.provide(layer)); }); + + it.live("forwards --no-descriptions when set", () => { + const { layer, calls } = setupLegacyCompletionFish(); + return Effect.gen(function* () { + yield* legacyCompletionFish({ noDescriptions: true }); + expect(calls).toEqual([["completion", "fish", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)); + }); }); diff --git a/apps/cli/src/legacy/commands/completion/powershell/powershell.command.ts b/apps/cli/src/legacy/commands/completion/powershell/powershell.command.ts index 2034dc3f80..72dd479f3f 100644 --- a/apps/cli/src/legacy/commands/completion/powershell/powershell.command.ts +++ b/apps/cli/src/legacy/commands/completion/powershell/powershell.command.ts @@ -1,8 +1,11 @@ import { Command } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; +import { LegacyCompletionNoDescriptionsFlagDef } from "../completion.flags.ts"; import { legacyCompletionPowershell } from "./powershell.handler.ts"; -const config = {}; +const config = { + noDescriptions: LegacyCompletionNoDescriptionsFlagDef, +} as const; export type LegacyCompletionPowershellFlags = CliCommand.Command.Config.Infer; export const legacyCompletionPowershellCommand = Command.make("powershell", config).pipe( diff --git a/apps/cli/src/legacy/commands/completion/powershell/powershell.handler.ts b/apps/cli/src/legacy/commands/completion/powershell/powershell.handler.ts index 9410dccccb..8b1d056431 100644 --- a/apps/cli/src/legacy/commands/completion/powershell/powershell.handler.ts +++ b/apps/cli/src/legacy/commands/completion/powershell/powershell.handler.ts @@ -3,8 +3,10 @@ import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; import type { LegacyCompletionPowershellFlags } from "./powershell.command.ts"; export const legacyCompletionPowershell = Effect.fn("legacy.completion.powershell")(function* ( - _flags: LegacyCompletionPowershellFlags, + flags: LegacyCompletionPowershellFlags, ) { const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["completion", "powershell"]); + const args: string[] = ["completion", "powershell"]; + if (flags.noDescriptions) args.push("--no-descriptions"); + yield* proxy.exec(args); }); diff --git a/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts b/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts index 056c856dcb..62dbd11305 100644 --- a/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts @@ -19,8 +19,16 @@ describe("legacy completion powershell", () => { it.live("forwards `completion powershell` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionPowershell(); return Effect.gen(function* () { - yield* legacyCompletionPowershell({}); + yield* legacyCompletionPowershell({ noDescriptions: false }); expect(calls).toEqual([["completion", "powershell"]]); }).pipe(Effect.provide(layer)); }); + + it.live("forwards --no-descriptions when set", () => { + const { layer, calls } = setupLegacyCompletionPowershell(); + return Effect.gen(function* () { + yield* legacyCompletionPowershell({ noDescriptions: true }); + expect(calls).toEqual([["completion", "powershell", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)); + }); }); diff --git a/apps/cli/src/legacy/commands/completion/zsh/zsh.command.ts b/apps/cli/src/legacy/commands/completion/zsh/zsh.command.ts index fdda3df9e1..8951d805f6 100644 --- a/apps/cli/src/legacy/commands/completion/zsh/zsh.command.ts +++ b/apps/cli/src/legacy/commands/completion/zsh/zsh.command.ts @@ -1,8 +1,11 @@ import { Command } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; +import { LegacyCompletionNoDescriptionsFlagDef } from "../completion.flags.ts"; import { legacyCompletionZsh } from "./zsh.handler.ts"; -const config = {}; +const config = { + noDescriptions: LegacyCompletionNoDescriptionsFlagDef, +} as const; export type LegacyCompletionZshFlags = CliCommand.Command.Config.Infer; export const legacyCompletionZshCommand = Command.make("zsh", config).pipe( diff --git a/apps/cli/src/legacy/commands/completion/zsh/zsh.handler.ts b/apps/cli/src/legacy/commands/completion/zsh/zsh.handler.ts index 10ca64ae3d..472f1918bc 100644 --- a/apps/cli/src/legacy/commands/completion/zsh/zsh.handler.ts +++ b/apps/cli/src/legacy/commands/completion/zsh/zsh.handler.ts @@ -3,8 +3,10 @@ import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; import type { LegacyCompletionZshFlags } from "./zsh.command.ts"; export const legacyCompletionZsh = Effect.fn("legacy.completion.zsh")(function* ( - _flags: LegacyCompletionZshFlags, + flags: LegacyCompletionZshFlags, ) { const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["completion", "zsh"]); + const args: string[] = ["completion", "zsh"]; + if (flags.noDescriptions) args.push("--no-descriptions"); + yield* proxy.exec(args); }); diff --git a/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts b/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts index f15c1d9a41..a0ba04053e 100644 --- a/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts @@ -19,8 +19,16 @@ describe("legacy completion zsh", () => { it.live("forwards `completion zsh` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionZsh(); return Effect.gen(function* () { - yield* legacyCompletionZsh({}); + yield* legacyCompletionZsh({ noDescriptions: false }); expect(calls).toEqual([["completion", "zsh"]]); }).pipe(Effect.provide(layer)); }); + + it.live("forwards --no-descriptions when set", () => { + const { layer, calls } = setupLegacyCompletionZsh(); + return Effect.gen(function* () { + yield* legacyCompletionZsh({ noDescriptions: true }); + expect(calls).toEqual([["completion", "zsh", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)); + }); }); From dabd87b7a5de4ed5efd24c123d1b6ca3d9ebb964 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 2 Jul 2026 16:12:30 +0100 Subject: [PATCH 2/3] test(cli): guard --no-descriptions flag wiring for completion commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original fix only had handler-level tests, which call the handler function directly and never exercise the Effect CLI parser — so a dropped flag declaration in a command.ts config would silently regress without failing any test. - Add Command.runWith parser-level tests for bash/zsh/fish/powershell that parse real argv through the actual command definitions. - Add a real-subprocess e2e test proving --no-descriptions survives Effect's parser and is forwarded to the Go binary (asserted via the __completeNoDesc marker Cobra embeds in the generated script). - Note the --no-no-descriptions double-negation quirk from Effect's auto-derived --no- negation. - Fix a stale __complete/ directory reference in SIDE_EFFECTS.md and note the flag addition in the porting status doc. --- apps/cli/docs/go-cli-porting-status.md | 14 +++++------ .../commands/completion/SIDE_EFFECTS.md | 5 ++-- .../completion/bash/bash.integration.test.ts | 17 +++++++++++++ .../completion/completion.e2e.test.ts | 25 +++++++++++++++++++ .../commands/completion/completion.flags.ts | 5 ++++ .../completion/fish/fish.integration.test.ts | 17 +++++++++++++ .../powershell/powershell.integration.test.ts | 19 ++++++++++++++ .../completion/zsh/zsh.integration.test.ts | 17 +++++++++++++ 8 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 apps/cli/src/legacy/commands/completion/completion.e2e.test.ts diff --git a/apps/cli/docs/go-cli-porting-status.md b/apps/cli/docs/go-cli-porting-status.md index 2780e1c396..8bb5508448 100644 --- a/apps/cli/docs/go-cli-porting-status.md +++ b/apps/cli/docs/go-cli-porting-status.md @@ -193,13 +193,13 @@ These route-first equivalents are intentionally lower-level than the old Go comm ## Additional Commands -| Old command | TS status | TS command path or `missing` | Missing flags/params | Extra TS flags/params | Notes | -| ----------------------- | --------- | -------------------------------- | --------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ | -| `completion bash` | `ported` | `supabase completion bash` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). | -| `completion fish` | `ported` | `supabase completion fish` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). | -| `completion powershell` | `ported` | `supabase completion powershell` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). | -| `completion zsh` | `ported` | `supabase completion zsh` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). | -| `help` | `partial` | `supabase --help` | Go-style top-level `help` command shape | `-` | Feature parity exists via the framework-provided global `--help` flag instead of a dedicated `help` command. | +| Old command | TS status | TS command path or `missing` | Missing flags/params | Extra TS flags/params | Notes | +| ----------------------- | --------- | -------------------------------- | --------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `completion bash` | `ported` | `supabase completion bash` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). `--no-descriptions` added to match cobra's auto-registered flag (CLI-1858). | +| `completion fish` | `ported` | `supabase completion fish` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). `--no-descriptions` added to match cobra's auto-registered flag (CLI-1858). | +| `completion powershell` | `ported` | `supabase completion powershell` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). `--no-descriptions` added to match cobra's auto-registered flag (CLI-1858). | +| `completion zsh` | `ported` | `supabase completion zsh` | `-` | `-` | Proxies verbatim to the Go binary so the emitted script is byte-identical to Cobra's output (CLI-1532). `--no-descriptions` added to match cobra's auto-registered flag (CLI-1858). | +| `help` | `partial` | `supabase --help` | Go-style top-level `help` command shape | `-` | Feature parity exists via the framework-provided global `--help` flag instead of a dedicated `help` command. | ## Legacy Shell Wrapping Status diff --git a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md index f64d01935e..e25af12dbf 100644 --- a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md @@ -44,8 +44,9 @@ brew-managed `_supabase` files in their `fpath`, or analogous bash/fish/powershe artifacts. Drift would break tab completion for those users. The generated scripts call back to `supabase __complete ` on every tab press to -fetch dynamic completion candidates — see `apps/cli/src/legacy/commands/__complete/`, -which provides the matching hidden command. +fetch dynamic completion candidates — see `apps/cli/src/legacy/cli/complete-passthrough.ts`, +which intercepts `__complete` before Effect's argv parser and proxies it straight to +the Go binary. ## Notes diff --git a/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts b/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts index 4d11fa7558..3bb8f31137 100644 --- a/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/bash/bash.integration.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { Command } from "effect/unstable/cli"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { legacyCompletionBashCommand } from "./bash.command.ts"; import { legacyCompletionBash } from "./bash.handler.ts"; function setupLegacyCompletionBash() { @@ -15,6 +17,10 @@ function setupLegacyCompletionBash() { return { layer, calls }; } +function legacyTestRoot() { + return Command.make("supabase").pipe(Command.withSubcommands([legacyCompletionBashCommand])); +} + describe("legacy completion bash", () => { it.live("forwards `completion bash` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionBash(); @@ -31,4 +37,15 @@ describe("legacy completion bash", () => { expect(calls).toEqual([["completion", "bash", "--no-descriptions"]]); }).pipe(Effect.provide(layer)); }); + + it.live("accepts --no-descriptions from real argv via the command parser", () => { + const { layer, calls } = setupLegacyCompletionBash(); + return Effect.gen(function* () { + yield* Command.runWith(legacyTestRoot(), { version: "0.0.0-test" })([ + "bash", + "--no-descriptions", + ]); + expect(calls).toEqual([["completion", "bash", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)) as Effect.Effect; + }); }); diff --git a/apps/cli/src/legacy/commands/completion/completion.e2e.test.ts b/apps/cli/src/legacy/commands/completion/completion.e2e.test.ts new file mode 100644 index 0000000000..56c721e5f3 --- /dev/null +++ b/apps/cli/src/legacy/commands/completion/completion.e2e.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "vitest"; +import { runSupabase } from "../../../../tests/helpers/cli.ts"; + +const E2E_TIMEOUT_MS = 30_000; + +describe("supabase completion (legacy)", () => { + // Golden-path e2e for CLI-1858: `--no-descriptions` used to be rejected by + // Effect's argv parser (`UnrecognizedOption`) before the request ever + // reached the Go binary, because the flag wasn't declared on the TS leaf + // command. Only a real subprocess run proves both halves of the fix: the + // TS parser accepts the flag, and the Go binary actually receives it — it + // switches the generated script's completion callback from `__complete` to + // `__completeNoDesc` only when the flag is forwarded. + test( + "bash --no-descriptions is accepted and forwarded to the Go binary", + { timeout: E2E_TIMEOUT_MS }, + async () => { + const { exitCode, stdout } = await runSupabase(["completion", "bash", "--no-descriptions"], { + entrypoint: "legacy", + }); + expect(exitCode).toBe(0); + expect(stdout).toContain("__completeNoDesc"); + }, + ); +}); diff --git a/apps/cli/src/legacy/commands/completion/completion.flags.ts b/apps/cli/src/legacy/commands/completion/completion.flags.ts index 5f81f77b8e..29e1193022 100644 --- a/apps/cli/src/legacy/commands/completion/completion.flags.ts +++ b/apps/cli/src/legacy/commands/completion/completion.flags.ts @@ -6,6 +6,11 @@ import { Flag } from "effect/unstable/cli"; * `compCmdNoDescFlagName`/`compCmdNoDescFlagDefault`/`compCmdNoDescFlagDesc` * in `spf13/cobra@v1.10.2/completions.go:101-103`. Shared across all four * leaves rather than redeclared per-file. + * + * `Flag.boolean` auto-derives a `--no-` negation, so this flag name + * produces `--no-no-descriptions` as a working (if odd) way to re-enable + * descriptions. Harmless — it resolves to the same `false` default — and + * not something cobra does, so there's no parity requirement to remove it. */ export const LegacyCompletionNoDescriptionsFlagDef = Flag.boolean("no-descriptions").pipe( Flag.withDescription("disable completion descriptions"), diff --git a/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts b/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts index b345a1b63e..ba22430863 100644 --- a/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/fish/fish.integration.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { Command } from "effect/unstable/cli"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { legacyCompletionFishCommand } from "./fish.command.ts"; import { legacyCompletionFish } from "./fish.handler.ts"; function setupLegacyCompletionFish() { @@ -15,6 +17,10 @@ function setupLegacyCompletionFish() { return { layer, calls }; } +function legacyTestRoot() { + return Command.make("supabase").pipe(Command.withSubcommands([legacyCompletionFishCommand])); +} + describe("legacy completion fish", () => { it.live("forwards `completion fish` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionFish(); @@ -31,4 +37,15 @@ describe("legacy completion fish", () => { expect(calls).toEqual([["completion", "fish", "--no-descriptions"]]); }).pipe(Effect.provide(layer)); }); + + it.live("accepts --no-descriptions from real argv via the command parser", () => { + const { layer, calls } = setupLegacyCompletionFish(); + return Effect.gen(function* () { + yield* Command.runWith(legacyTestRoot(), { version: "0.0.0-test" })([ + "fish", + "--no-descriptions", + ]); + expect(calls).toEqual([["completion", "fish", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)) as Effect.Effect; + }); }); diff --git a/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts b/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts index 62dbd11305..da678deeb2 100644 --- a/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/powershell/powershell.integration.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { Command } from "effect/unstable/cli"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { legacyCompletionPowershellCommand } from "./powershell.command.ts"; import { legacyCompletionPowershell } from "./powershell.handler.ts"; function setupLegacyCompletionPowershell() { @@ -15,6 +17,12 @@ function setupLegacyCompletionPowershell() { return { layer, calls }; } +function legacyTestRoot() { + return Command.make("supabase").pipe( + Command.withSubcommands([legacyCompletionPowershellCommand]), + ); +} + describe("legacy completion powershell", () => { it.live("forwards `completion powershell` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionPowershell(); @@ -31,4 +39,15 @@ describe("legacy completion powershell", () => { expect(calls).toEqual([["completion", "powershell", "--no-descriptions"]]); }).pipe(Effect.provide(layer)); }); + + it.live("accepts --no-descriptions from real argv via the command parser", () => { + const { layer, calls } = setupLegacyCompletionPowershell(); + return Effect.gen(function* () { + yield* Command.runWith(legacyTestRoot(), { version: "0.0.0-test" })([ + "powershell", + "--no-descriptions", + ]); + expect(calls).toEqual([["completion", "powershell", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)) as Effect.Effect; + }); }); diff --git a/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts b/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts index a0ba04053e..9745c1a1e4 100644 --- a/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts +++ b/apps/cli/src/legacy/commands/completion/zsh/zsh.integration.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { Command } from "effect/unstable/cli"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { legacyCompletionZshCommand } from "./zsh.command.ts"; import { legacyCompletionZsh } from "./zsh.handler.ts"; function setupLegacyCompletionZsh() { @@ -15,6 +17,10 @@ function setupLegacyCompletionZsh() { return { layer, calls }; } +function legacyTestRoot() { + return Command.make("supabase").pipe(Command.withSubcommands([legacyCompletionZshCommand])); +} + describe("legacy completion zsh", () => { it.live("forwards `completion zsh` to the Go binary", () => { const { layer, calls } = setupLegacyCompletionZsh(); @@ -31,4 +37,15 @@ describe("legacy completion zsh", () => { expect(calls).toEqual([["completion", "zsh", "--no-descriptions"]]); }).pipe(Effect.provide(layer)); }); + + it.live("accepts --no-descriptions from real argv via the command parser", () => { + const { layer, calls } = setupLegacyCompletionZsh(); + return Effect.gen(function* () { + yield* Command.runWith(legacyTestRoot(), { version: "0.0.0-test" })([ + "zsh", + "--no-descriptions", + ]); + expect(calls).toEqual([["completion", "zsh", "--no-descriptions"]]); + }).pipe(Effect.provide(layer)) as Effect.Effect; + }); }); From e0a75d00c657d0070e3bdc03c36ca2383f67cdad Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 3 Jul 2026 21:17:40 +0100 Subject: [PATCH 3/3] fix(cli): proxy __completeNoDesc callback to match Go CLI (review: #5771) Cobra bakes the completion callback name into the generated script at generation time: scripts built with --no-descriptions call back into `__completeNoDesc` (spf13/cobra's alias for the hidden __complete command) on every tab press, not `__complete`. complete-passthrough.ts only bypassed Effect's parser for `__complete`, so installed --no-descriptions scripts would fail on every completion attempt instead of reaching the Go binary. --- apps/cli/src/legacy/cli/complete-passthrough.ts | 12 ++++++++---- .../src/legacy/cli/complete-passthrough.unit.test.ts | 11 +++++++++++ .../src/legacy/commands/completion/SIDE_EFFECTS.md | 8 +++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/legacy/cli/complete-passthrough.ts b/apps/cli/src/legacy/cli/complete-passthrough.ts index 59d4d27947..313868bc2c 100644 --- a/apps/cli/src/legacy/cli/complete-passthrough.ts +++ b/apps/cli/src/legacy/cli/complete-passthrough.ts @@ -16,9 +16,13 @@ export interface CompletePassthroughDeps { /** * Cobra-generated completion scripts (`supabase completion {bash,zsh,fish,powershell}`) - * call back into `supabase __complete ` on every tab press. The args may - * include partial flag tokens (e.g. `--de` while the user is mid-completion of a - * flag name) that Effect's structured parser would reject. Bypass Effect entirely + * call back into `supabase __complete ` on every tab press — or + * `supabase __completeNoDesc ` when the script was generated with + * `--no-descriptions` (`__completeNoDesc` is cobra's alias for the same hidden + * command, `ShellCompNoDescRequestCmd` in `spf13/cobra@v1.10.2/completions.go`, + * baked into the generated script at generation time). The args may include + * partial flag tokens (e.g. `--de` while the user is mid-completion of a flag + * name) that Effect's structured parser would reject. Bypass Effect entirely * for this code path and proxy the raw argv to the bundled Go binary, which is * the authority on completion behavior for the legacy shell. * @@ -26,7 +30,7 @@ export interface CompletePassthroughDeps { * otherwise. */ export function tryCompletePassthrough(deps: CompletePassthroughDeps): boolean { - if (deps.argv[0] !== "__complete") return false; + if (deps.argv[0] !== "__complete" && deps.argv[0] !== "__completeNoDesc") return false; const resolved = deps.resolveBinary(); if (!("found" in resolved)) { diff --git a/apps/cli/src/legacy/cli/complete-passthrough.unit.test.ts b/apps/cli/src/legacy/cli/complete-passthrough.unit.test.ts index 0e5f7c4801..09f578a801 100644 --- a/apps/cli/src/legacy/cli/complete-passthrough.unit.test.ts +++ b/apps/cli/src/legacy/cli/complete-passthrough.unit.test.ts @@ -68,6 +68,17 @@ describe("tryCompletePassthrough", () => { expect(exits).toEqual([0]); }); + it("forwards verbatim argv to the Go binary on __completeNoDesc (scripts generated with --no-descriptions)", () => { + const { deps, spawnCalls, exits } = makeDeps({ + argv: ["__completeNoDesc", "migration", "li"], + }); + expect(tryCompletePassthrough(deps)).toBe(true); + expect(spawnCalls).toEqual([ + { cmd: "/path/to/supabase-go", args: ["__completeNoDesc", "migration", "li"] }, + ]); + expect(exits).toEqual([0]); + }); + it("propagates the child's non-zero exit code", () => { const spawn = vi.fn(() => spawnResult(7)); const { deps, exits } = makeDeps({ spawn }); diff --git a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md index e25af12dbf..f25912c9c5 100644 --- a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md @@ -44,9 +44,11 @@ brew-managed `_supabase` files in their `fpath`, or analogous bash/fish/powershe artifacts. Drift would break tab completion for those users. The generated scripts call back to `supabase __complete ` on every tab press to -fetch dynamic completion candidates — see `apps/cli/src/legacy/cli/complete-passthrough.ts`, -which intercepts `__complete` before Effect's argv parser and proxies it straight to -the Go binary. +fetch dynamic completion candidates, or `supabase __completeNoDesc ` when the +script was generated with `--no-descriptions` (cobra's alias for the same hidden +command) — see `apps/cli/src/legacy/cli/complete-passthrough.ts`, which intercepts +both `__complete` and `__completeNoDesc` before Effect's argv parser and proxies them +straight to the Go binary. ## Notes