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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,34 @@ var _ = Describe("CLI", func() {
})
})

Describe("completionPluginsFlag", func() {
It("should suggest available plugins and filter out entered/deprecated ones", func() {
p1 := newMockPlugin("p1.io", "v1", projectVersion)
p2 := newMockPlugin("p2.io", "v1", projectVersion)
p3 := newMockDeprecatedPlugin("p3.io", "v1-alpha", "deprecated", projectVersion)

c.plugins = makeMapFor(p1, p2, p3)

k1 := plugin.KeyFor(p1)
k2 := plugin.KeyFor(p2)

c.cmd = c.newRootCmd()
c.cmd.Flags().StringSlice(pluginsFlag, []string{}, "test usage")
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newRootCmd() already defines --plugins as a PersistentFlag; re-declaring the same flag via c.cmd.Flags().StringSlice risks a duplicate-flag panic and also means the test may not be exercising the real persistent-flag behavior. Remove the extra StringSlice() call and just Set() the existing flag (or set it on PersistentFlags()).

Suggested change
c.cmd.Flags().StringSlice(pluginsFlag, []string{}, "test usage")

Copilot uses AI. Check for mistakes.

err := c.cmd.Flags().Set(pluginsFlag, k1)
Expect(err).NotTo(HaveOccurred())

got, directive := c.completionPluginsFlag(c.cmd, []string{}, "")

Expect(directive).To(Equal(cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace))

expected := fmt.Sprintf("%s\tExternal or custom plugin", k2)

Expect(got).To(HaveLen(1))
Expect(got).To(ContainElement(expected))
})
})

Context("buildCmd", func() {
var projectFile string

Expand Down
46 changes: 46 additions & 0 deletions pkg/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (c CLI) newRootCmd() *cobra.Command {

// Global flags for all subcommands.
cmd.PersistentFlags().StringSlice(pluginsFlag, nil, "plugin keys to be used for this subcommand execution")
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(pluginsFlag, c.completionPluginsFlag))

// Register --project-version on the root command so that it shows up in help.
cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version")
Expand Down Expand Up @@ -252,3 +253,48 @@ func (c CLI) getPluginTableFilteredWithOptions(filter func(plugin.Plugin) bool,

return strings.Join(lines, "\n")
}

// completionPluginsFlag implements cobra.CompletionFunc and is registered
// as the flag completion function for --plugins
// We should note that flag completion does not work for comma-chained values
// but works fine when repeating the flag
func (c CLI) completionPluginsFlag(
cmd *cobra.Command,
_ []string,
_ string,
) ([]string, cobra.ShellCompDirective) {
// We filter strings that the user already passed to --plugins,
// in case the user chains the --plugins flag multiple times,
alreadyEntered, err := cmd.Flags().GetStringSlice(pluginsFlag)
if err != nil {
cobra.CheckErr(err)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a completion callback, calling cobra.CheckErr on GetStringSlice error can terminate the completion run (os.Exit), which can break shell completion unexpectedly. Prefer returning (nil, cobra.ShellCompDirectiveError) or treating the error as “no already-entered values” instead of exiting.

Suggested change
cobra.CheckErr(err)
return nil, cobra.ShellCompDirectiveError

Copilot uses AI. Check for mistakes.
}

comps := make([]string, 0, len(c.plugins))

for pluginKey, p := range c.plugins {
Comment on lines +274 to +275
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The completion list is built by ranging over a map, so the suggestion order will be non-deterministic between runs. For a stable UX (and deterministic tests if added later), collect keys and sort them before appending to comps.

Suggested change
for pluginKey, p := range c.plugins {
pluginKeys := make([]string, 0, len(c.plugins))
for pluginKey := range c.plugins {
pluginKeys = append(pluginKeys, pluginKey)
}
slices.Sort(pluginKeys)
for _, pluginKey := range pluginKeys {
p := c.plugins[pluginKey]

Copilot uses AI. Check for mistakes.
if slices.Contains(alreadyEntered, pluginKey) {
continue
}

// We also omit deprecated plugins from completion
if deprecated, ok := p.(plugin.Deprecated); ok {
if deprecated.DeprecationWarning() != "" {
continue
}
}

// If the plugin provides a description, we show that
// otherwise, we show the default description
desc := ""
if describable, ok := p.(plugin.Describable); ok {
desc = describable.Description()
} else {
desc = getPluginDescription(pluginKey)
}

comps = append(comps, cobra.CompletionWithDesc(pluginKey, desc))
}

return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
}