Skip to content
Merged
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
56 changes: 56 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,62 @@ func GetCommands() []components.Command {

The plugin needs to specify a list of all commands under their respective namespaces. Each command is defined using the `Command` structure.

### Nested subcommands

Some CLI paths include more than one command level after the namespace, for example `jf ai plugins publish`. Register the namespace as usual, then use `Command.Subcommands` on a parent command for intermediate groups and keep the executable work on the leaf command.

Embedded apps expose namespaces via `App.Subcommands` (`components.Namespace`). Commands listed under a namespace can define their own children through `Command.Subcommands`, which the conversion layer maps to urfave/cli subcommands.

For the `ai` → `plugins` → `publish` layout:

- `ai` is a `Namespace` on the embedded app (for example `components.Namespace{Name: "ai", Commands: aiCLI.GetAiCommands()}`).
- `plugins` is a parent `Command` with `Subcommands` only (a group, not a runnable verb).
- `publish` is the leaf `Command` with `Arguments`, `Flags`, and `Action`.

Parent group:

```go
func GetAiCommands() []components.Command {
return []components.Command{
{
Name: "plugins",
Description: "AI agent plugin commands.",
Subcommands: GetPluginSubCommands(),
},
}
}
```

Leaf subcommand:

```go
func GetPluginSubCommands() []components.Command {
return []components.Command{
{
Name: "publish",
Description: "Publish a plugin to Artifactory.",
Arguments: []components.Argument{
{
Name: "path",
Description: "Path to the plugin folder containing plugin.json.",
},
},
Flags: publishFlags,
Action: publish.RunPublish,
},
}
}
```

Keep the following on the parent group command:

- Do not set `Action` (users must run a child verb, such as `publish`; otherwise the parent `Action` runs when no child is specified).
- Do not set `Arguments` (the leaf command owns positional args so `jf ai plugins publish --help` shows publish-specific help).
- Do not set `SkipFlagParsing` when `Subcommands` is non-empty. The converter rejects that combination because urfave/cli v1.22+ will not route to child commands when `SkipFlagParsing` is true.
- Prefer not to set `Flags` on the parent; define flags on leaf subcommands unless you intentionally need flags shared across all children of the group.

Use `SkipFlagParsing` only on leaf commands that forward raw arguments to an external tool (for example `jf mvn`), not on command groups.

## Adding a Command

To add a command you need to insert an entry to the commands list. the entry is an instance of the `Command` structure that defines an `Action` to execute when triggered, as mentioned at this example:
Expand Down
22 changes: 22 additions & 0 deletions plugins/components/conversionlayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func convertCommands(commands []Command, nameSpaces ...string) ([]cli.Command, e
}

func convertCommand(cmd Command, namespaces ...string) (cli.Command, error) {
if err := validateCommandGroup(cmd, namespaces...); err != nil {
return cli.Command{}, err
}
if len(cmd.SupportedFormats) > 0 {
cmd.Flags = append([]Flag{GetFormatFlag(cmd.SupportedFormats, cmd.DefaultFormat)}, cmd.Flags...)
}
Expand All @@ -99,13 +102,32 @@ func convertCommand(cmd Command, namespaces ...string) (cli.Command, error) {
SkipFlagParsing: cmd.SkipFlagParsing,
Hidden: cmd.Hidden,
}
if len(cmd.Subcommands) > 0 {
Comment thread
udaykb2 marked this conversation as resolved.
subcommands, err := convertCommands(cmd.Subcommands, append(namespaces, cmd.Name)...)
if err != nil {
return cli.Command{}, err
}
cliCmd.Subcommands = subcommands
}
if cmd.Action != nil {
// Passing any other interface than 'cli.ActionFunc' will fail the command.
cliCmd.Action = getActionFunc(cmd)
}
return cliCmd, nil
}

// validateCommandGroup rejects SkipFlagParsing on commands with subcommands.
// urfave/cli v1.22+ skips subcommand routing when SkipFlagParsing is true (see command.Run).
func validateCommandGroup(cmd Command, namespaces ...string) error {
if len(cmd.Subcommands) == 0 || !cmd.SkipFlagParsing {
return nil
}
return fmt.Errorf(
"command %q: SkipFlagParsing cannot be used with subcommands; urfave/cli will not route to child commands",
getCmdUsageString(cmd, namespaces...),
)
}

func removeEmptyValues(slice []string) []string {
var result []string
for _, s := range slice {
Expand Down
35 changes: 35 additions & 0 deletions plugins/components/conversionlayer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,41 @@ func TestGetValueForBoolFlag(t *testing.T) {
})
}

func TestConvertCommandSkipFlagParsingWithSubcommands(t *testing.T) {
parent := Command{
Name: "plugins",
Description: "AI agent plugin commands.",
SkipFlagParsing: true,
Subcommands: []Command{
{Name: "publish", Description: "Publish a plugin."},
},
}
_, err := convertCommand(parent, "ai")
require.Error(t, err)
assert.Contains(t, err.Error(), "ai plugins")
assert.Contains(t, err.Error(), "SkipFlagParsing")
}

func TestConvertCommandNestedSubcommands(t *testing.T) {
parent := Command{
Name: "plugins",
Description: "AI agent plugin commands.",
Subcommands: []Command{
{
Name: "publish",
Description: "Publish a plugin.",
Arguments: []Argument{{Name: "path", Description: "Plugin folder."}},
},
},
}
converted, err := convertCommand(parent, "ai")
require.NoError(t, err)
assert.Equal(t, "plugins", converted.Name)
require.Len(t, converted.Subcommands, 1)
assert.Equal(t, "publish", converted.Subcommands[0].Name)
assert.Nil(t, converted.Action)
}

type DummyFlagValue struct {
Value string
}
Expand Down
20 changes: 11 additions & 9 deletions plugins/components/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ type Namespace struct {
}

type Command struct {
Name string
Description string
Category string
Aliases []string
UsageOptions *UsageOptions
Arguments []Argument
Flags []Flag
EnvVars []EnvVar
Action ActionFunc
Name string
Description string
Category string
Aliases []string
UsageOptions *UsageOptions
Arguments []Argument
Flags []Flag
EnvVars []EnvVar
Subcommands []Command
Action ActionFunc
// Must not be set when Subcommands is non-empty; convertCommand rejects that combination.
SkipFlagParsing bool
Hidden bool
// If set, a format flag will be added to the command. The format flag will be used to specify the output format of the command.
Expand Down
Loading