Skip to content

Commit d459c00

Browse files
authored
Add plugins 'add' command (#219)
* Add plugins 'add' command Implement 'mcpd config plugins add' command to add new plugins to a category.
1 parent 5ddc882 commit d459c00

File tree

5 files changed

+757
-29
lines changed

5 files changed

+757
-29
lines changed

cmd/config/plugins/add.go

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,167 @@ package plugins
22

33
import (
44
"fmt"
5+
"maps"
6+
"slices"
7+
"strings"
58

69
"github.com/spf13/cobra"
710

8-
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
9-
"github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
11+
internalcmd "github.com/mozilla-ai/mcpd/v2/internal/cmd"
12+
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
13+
"github.com/mozilla-ai/mcpd/v2/internal/config"
1014
)
1115

12-
// NewAddCmd creates the add command for plugins.
13-
// TODO: Implement in a future PR.
14-
func NewAddCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error) {
16+
const (
17+
flagFlow = "flow"
18+
flagRequired = "required"
19+
flagCommitHash = "commit-hash"
20+
)
21+
22+
// AddCmd represents the command for adding a new plugin entry.
23+
// NOTE: Use NewAddCmd to create instances of AddCmd.
24+
type AddCmd struct {
25+
*internalcmd.BaseCmd
26+
27+
// cfgLoader is used to load the configuration.
28+
cfgLoader config.Loader
29+
30+
// category is the category to add the plugin to.
31+
category config.Category
32+
33+
// flows is the list of flows.
34+
flows []string
35+
36+
// required indicates if the plugin is required.
37+
required bool
38+
39+
// commitHash is the optional commit hash for version validation.
40+
commitHash string
41+
}
42+
43+
// NewAddCmd creates a new add command for plugin entries.
44+
func NewAddCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
45+
opts, err := cmdopts.NewOptions(opt...)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
c := &AddCmd{
51+
BaseCmd: baseCmd,
52+
cfgLoader: opts.ConfigLoader,
53+
}
54+
1555
cobraCmd := &cobra.Command{
16-
Use: "add",
56+
Use: "add <plugin-name>",
1757
Short: "Add a new plugin entry to a category",
18-
Long: "Add a new plugin entry to a category. The configuration is saved automatically.",
19-
RunE: func(cmd *cobra.Command, args []string) error {
20-
return fmt.Errorf("not yet implemented")
21-
},
58+
Long: `Add a new plugin entry to a category. The configuration is saved automatically.
59+
60+
The plugin name must exactly match the name of the plugin binary file.
61+
62+
This command creates new plugin entries only. If a plugin with the same name already exists
63+
in the category, the command fails with an error. To update an existing plugin, use the 'set' command.`,
64+
Example: ` # Add new plugin with all fields
65+
mcpd config plugins add jwt-auth --category=authentication --flow=request --required
66+
67+
# Add plugin with multiple flows
68+
mcpd config plugins add metrics --category=observability --flow=request --flow=response --commit-hash=abc123
69+
70+
# Add without required flag (defaults to false)
71+
mcpd config plugins add rbac --category=authorization --flow=response`,
72+
RunE: c.run,
73+
Args: cobra.ExactArgs(1), // plugin-name
2274
}
2375

76+
allowedCategories := config.OrderedCategories()
77+
cobraCmd.Flags().Var(
78+
&c.category,
79+
flagCategory,
80+
fmt.Sprintf("Specify the category (one of: %s)", allowedCategories.String()),
81+
)
82+
_ = cobraCmd.MarkFlagRequired(flagCategory)
83+
84+
cobraCmd.Flags().StringArrayVar(
85+
&c.flows,
86+
flagFlow,
87+
nil,
88+
fmt.Sprintf(
89+
"Flow during which, the plugin should execute (%s) (can be repeated)",
90+
strings.Join(config.OrderedFlowNames(), ", "),
91+
),
92+
)
93+
_ = cobraCmd.MarkFlagRequired(flagFlow)
94+
95+
cobraCmd.Flags().BoolVar(
96+
&c.required,
97+
flagRequired,
98+
false,
99+
"Optional, mark plugin as required",
100+
)
101+
102+
cobraCmd.Flags().StringVar(
103+
&c.commitHash,
104+
flagCommitHash,
105+
"",
106+
"Optional, commit hash for runtime version validation",
107+
)
108+
24109
return cobraCmd, nil
25110
}
111+
112+
func (c *AddCmd) run(cmd *cobra.Command, args []string) error {
113+
pluginName := strings.TrimSpace(args[0])
114+
if pluginName == "" {
115+
return fmt.Errorf("plugin name cannot be empty")
116+
}
117+
118+
cfg, err := c.LoadConfig(c.cfgLoader)
119+
if err != nil {
120+
return err
121+
}
122+
123+
if _, exists := cfg.Plugin(c.category, pluginName); exists {
124+
return fmt.Errorf(
125+
"plugin '%s' already exists in category '%s'\n\n"+
126+
"To update an existing plugin, use: mcpd config plugins set %s --category=%s [flags]",
127+
pluginName,
128+
c.category,
129+
pluginName,
130+
c.category,
131+
)
132+
}
133+
134+
flows := config.ParseFlowsDistinct(c.flows)
135+
if len(flows) == 0 {
136+
return fmt.Errorf(
137+
"at least one valid flow is required (%s)",
138+
strings.Join(config.OrderedFlowNames(), ", "),
139+
)
140+
}
141+
142+
entry := config.PluginEntry{
143+
Name: pluginName,
144+
Flows: slices.Sorted(maps.Keys(flows)),
145+
}
146+
147+
// Set optional fields only if they were provided.
148+
if cmd.Flags().Changed(flagRequired) {
149+
entry.Required = &c.required
150+
}
151+
152+
if c.commitHash != "" {
153+
entry.CommitHash = &c.commitHash
154+
}
155+
156+
if _, err := cfg.UpsertPlugin(c.category, entry); err != nil {
157+
return err
158+
}
159+
160+
_, _ = fmt.Fprintf(
161+
cmd.OutOrStdout(),
162+
"✓ Plugin '%s' added to category '%s'\n",
163+
pluginName,
164+
c.category,
165+
)
166+
167+
return nil
168+
}

0 commit comments

Comments
 (0)