Refactor help system to consolidate error messages and command-specific help

- Simplify main help to show brief command descriptions only
- Add --help support to all commands with detailed usage information
- Replace duplicated help text in error messages with error + help pattern
- Remove 'help' command in favor of consistent --help flag usage
- Add helper functions CheckForHelpFlag() and ShowErrorWithHelp() for standardization
- Refactor deploy command to reduce cognitive complexity and improve maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-07-28 19:20:36 +02:00
parent 59dd709d83
commit dee239a3d4
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
11 changed files with 370 additions and 80 deletions

View file

@ -19,10 +19,19 @@ func main() {
pluginctl.InitLogger() pluginctl.InitLogger()
var pluginPath string var pluginPath string
var showHelp bool
flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)") flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)")
flag.BoolVar(&showHelp, "help", false, "Show help information")
flag.Parse() flag.Parse()
// Show help if requested
if showHelp {
showUsage()
return
}
args := flag.Args() args := flag.Args()
if len(args) == 0 { if len(args) == 0 {
pluginctl.Logger.Error("No command specified") pluginctl.Logger.Error("No command specified")
@ -60,10 +69,6 @@ func runCommand(command string, args []string, pluginPath string) error {
return runManifestCommand(args, pluginPath) return runManifestCommand(args, pluginPath)
case "logs": case "logs":
return runLogsCommand(args, pluginPath) return runLogsCommand(args, pluginPath)
case "help":
showUsage()
return nil
case "version": case "version":
return runVersionCommand(args) return runVersionCommand(args)
case "create-plugin": case "create-plugin":
@ -123,41 +128,20 @@ Usage:
Global Options: Global Options:
--plugin-path PATH Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH) --plugin-path PATH Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)
--help Show this help message
Commands: Commands:
info Display plugin information info Display plugin information
enable Enable plugin from current directory in Mattermost server enable Enable plugin in Mattermost server
disable Disable plugin from current directory in Mattermost server disable Disable plugin in Mattermost server
reset Reset plugin from current directory (disable then enable) reset Reset plugin (disable then enable)
deploy Upload and enable plugin bundle to Mattermost server deploy Upload and enable plugin bundle
updateassets Update plugin files from embedded assets updateassets Update plugin files from embedded assets
manifest Get plugin manifest information using templates (get {{.field}}, apply, check) manifest Manage plugin manifest files
logs View plugin logs (use --watch to follow logs in real-time) logs View plugin logs
create-plugin Create a new plugin from the starter template (supports --name and --module flags) create-plugin Create a new plugin from template
help Show this help message
version Show version information version Show version information
Examples:
pluginctl info # Show info for plugin in current directory
pluginctl --plugin-path /path/to/plugin info # Show info for plugin at specific path
pluginctl enable # Enable plugin from current directory
pluginctl disable # Disable plugin from current directory
pluginctl reset # Reset plugin from current directory (disable then enable)
pluginctl deploy # Upload and enable plugin bundle from ./dist/
pluginctl deploy --bundle-path ./bundle.tar.gz # Deploy specific bundle file
pluginctl updateassets # Update plugin files from embedded assets
pluginctl manifest get '{{.id}}' # Get any manifest field using templates
pluginctl manifest apply # Generate manifest files for server/webapp
pluginctl manifest check # Validate plugin manifest
pluginctl logs # View recent plugin logs
pluginctl logs --watch # Watch plugin logs in real-time
pluginctl create-plugin # Create a new plugin from the starter template (interactive)
pluginctl create-plugin --name example \
--module github.com/user/mattermost-plugin-example # Create plugin non-interactively
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
pluginctl info # Show info using environment variable
pluginctl version # Show version information
Environment Variables: Environment Variables:
PLUGINCTL_PLUGIN_PATH Default plugin directory path PLUGINCTL_PLUGIN_PATH Default plugin directory path
MM_LOCALSOCKETPATH Path to Mattermost local socket MM_LOCALSOCKETPATH Path to Mattermost local socket
@ -166,6 +150,8 @@ Environment Variables:
MM_ADMIN_USERNAME Admin username for authentication MM_ADMIN_USERNAME Admin username for authentication
MM_ADMIN_PASSWORD Admin password for authentication MM_ADMIN_PASSWORD Admin password for authentication
Use 'pluginctl <command> --help' for detailed information about a command.
For more information about Mattermost plugin development, visit: For more information about Mattermost plugin development, visit:
https://developers.mattermost.com/integrate/plugins/ https://developers.mattermost.com/integrate/plugins/
` `

View file

@ -277,41 +277,75 @@ func promptForModuleName(pluginName string) (string, error) {
return result.input, nil return result.input, nil
} }
func parseCreatePluginFlags(args []string) (pluginName, moduleName string, err error) { func parseCreatePluginFlags(args []string, helpText string) (pluginName, moduleName string, err error) {
// Parse flags similar to how logs command handles --watch pluginName, moduleName, err = parseFlags(args, helpText)
if err != nil {
return "", "", err
}
pluginName, err = validatePluginName(pluginName, helpText)
if err != nil {
return "", "", err
}
moduleName, err = validateModuleNameWithHelp(moduleName, helpText)
if err != nil {
return "", "", err
}
return pluginName, moduleName, nil
}
func parseFlags(args []string, helpText string) (pluginName, moduleName string, err error) {
for i, arg := range args { for i, arg := range args {
switch arg { switch arg {
case "--name": case "--name":
if i+1 >= len(args) { if i+1 >= len(args) {
return "", "", fmt.Errorf("--name flag requires a value") return "", "", ShowErrorWithHelp("--name flag requires a value", helpText)
} }
pluginName = args[i+1] pluginName = args[i+1]
case "--module": case "--module":
if i+1 >= len(args) { if i+1 >= len(args) {
return "", "", fmt.Errorf("--module flag requires a value") return "", "", ShowErrorWithHelp("--module flag requires a value", helpText)
} }
moduleName = args[i+1] moduleName = args[i+1]
case HelpFlagLong, HelpFlagShort:
// Skip help flags as they're handled earlier
default:
if strings.HasPrefix(arg, "--") {
return "", "", ShowErrorWithHelp(fmt.Sprintf("unknown flag: %s", arg), helpText)
} }
} }
// Validate and process plugin name if provided
if pluginName != "" {
pluginName, err = validateAndProcessPluginName(pluginName)
if err != nil {
return "", "", err
}
}
// Validate module name if provided
if moduleName != "" {
if validationErr := validateModuleName(moduleName); validationErr != "" {
return "", "", fmt.Errorf("invalid module name: %s", validationErr)
}
} }
return pluginName, moduleName, nil return pluginName, moduleName, nil
} }
func validatePluginName(pluginName, helpText string) (string, error) {
if pluginName == "" {
return "", nil
}
validated, err := validateAndProcessPluginName(pluginName)
if err != nil {
return "", ShowErrorWithHelp(err.Error(), helpText)
}
return validated, nil
}
func validateModuleNameWithHelp(moduleName, helpText string) (string, error) {
if moduleName == "" {
return "", nil
}
if validationErr := validateModuleName(moduleName); validationErr != "" {
return "", ShowErrorWithHelp(fmt.Sprintf("invalid module name: %s", validationErr), helpText)
}
return moduleName, nil
}
// validateAndProcessPluginName checks if the plugin name has the correct prefix and adds it if necessary. // validateAndProcessPluginName checks if the plugin name has the correct prefix and adds it if necessary.
// Example: // Example:
// - If the input is "my-plugin", it returns "mattermost-plugin-my-plugin". // - If the input is "my-plugin", it returns "mattermost-plugin-my-plugin".
@ -337,13 +371,39 @@ func validateAndProcessPluginName(name string) (string, error) {
} }
func RunCreatePluginCommand(args []string, pluginPath string) error { func RunCreatePluginCommand(args []string, pluginPath string) error {
helpText := `Create a new plugin from template
Usage:
pluginctl create-plugin [options]
Options:
--name PLUGIN_NAME Plugin name (will be prefixed with 'mattermost-plugin-')
--module MODULE_NAME Go module name (e.g., github.com/user/mattermost-plugin-example)
--help, -h Show this help message
Description:
Creates a new Mattermost plugin from the starter template. If no options are
provided, the command will run in interactive mode prompting for values.
The plugin name will automatically be prefixed with 'mattermost-plugin-' if
not already present.
Examples:
pluginctl create-plugin # Interactive mode
pluginctl create-plugin --name example --module github.com/user/mattermost-plugin-example
pluginctl --plugin-path /path/to/parent create-plugin # Create in specific directory`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
// Parse flags // Parse flags
var pluginName, moduleName string var pluginName, moduleName string
var err error var err error
pluginName, moduleName, err = parseCreatePluginFlags(args) pluginName, moduleName, err = parseCreatePluginFlags(args, helpText)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse flags: %w", err) return err
} }
Logger.Info("Starting plugin creation process") Logger.Info("Starting plugin creation process")

View file

@ -10,15 +10,61 @@ import (
) )
func RunDeployCommand(args []string, pluginPath string) error { func RunDeployCommand(args []string, pluginPath string) error {
helpText := getDeployHelpText()
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
bundlePath, err := parseDeployFlags(args, helpText)
if err != nil {
return err
}
bundlePath, err = resolveBundlePath(bundlePath, pluginPath)
if err != nil {
return err
}
pluginID, err := getPluginIDFromManifest(pluginPath)
if err != nil {
return err
}
return deployPluginBundle(pluginID, bundlePath)
}
func getDeployHelpText() string {
return `Upload and enable plugin bundle
Usage:
pluginctl deploy [options]
Options:
--bundle-path PATH Path to plugin bundle file (.tar.gz)
--help, -h Show this help message
Description:
Uploads a plugin bundle to the Mattermost server and enables it. If no
bundle path is specified, it will auto-discover the bundle from the dist/
directory based on the plugin manifest.
Examples:
pluginctl deploy # Deploy bundle from ./dist/
pluginctl deploy --bundle-path ./bundle.tar.gz # Deploy specific bundle file
pluginctl --plugin-path /path/to/plugin deploy # Deploy plugin at specific path`
}
func parseDeployFlags(args []string, helpText string) (string, error) {
var bundlePath string var bundlePath string
// Parse flags
i := 0 i := 0
for i < len(args) { for i < len(args) {
switch args[i] { switch args[i] {
case "--bundle-path": case "--bundle-path":
if i+1 >= len(args) { if i+1 >= len(args) {
return fmt.Errorf("--bundle-path flag requires a value") return "", ShowErrorWithHelp("--bundle-path flag requires a value", helpText)
} }
bundlePath = args[i+1] bundlePath = args[i+1]
i += 2 i += 2
@ -27,37 +73,50 @@ func RunDeployCommand(args []string, pluginPath string) error {
} }
} }
// If no bundle path provided, auto-discover from dist folder return bundlePath, nil
if bundlePath == "" { }
func resolveBundlePath(bundlePath, pluginPath string) (string, error) {
// If bundle path provided, validate it exists
if bundlePath != "" {
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
return "", fmt.Errorf("bundle file not found: %s", bundlePath)
}
return bundlePath, nil
}
// Auto-discover from dist folder
manifest, err := LoadPluginManifestFromPath(pluginPath) manifest, err := LoadPluginManifestFromPath(pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load plugin manifest: %w", err) return "", fmt.Errorf("failed to load plugin manifest: %w", err)
} }
expectedBundleName := fmt.Sprintf("%s-%s.tar.gz", manifest.Id, manifest.Version) expectedBundleName := fmt.Sprintf("%s-%s.tar.gz", manifest.Id, manifest.Version)
bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName) bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName)
if _, err := os.Stat(bundlePath); os.IsNotExist(err) { if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
return fmt.Errorf("bundle not found at %s - run 'make bundle' to build the plugin first", bundlePath) return "", fmt.Errorf("bundle not found at %s - run 'make bundle' to build the plugin first", bundlePath)
}
} }
// Validate bundle file exists return bundlePath, nil
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
return fmt.Errorf("bundle file not found: %s", bundlePath)
} }
// Load manifest to get plugin ID func getPluginIDFromManifest(pluginPath string) (string, error) {
manifest, err := LoadPluginManifestFromPath(pluginPath) manifest, err := LoadPluginManifestFromPath(pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load plugin manifest: %w", err) return "", fmt.Errorf("failed to load plugin manifest: %w", err)
} }
pluginID := manifest.Id pluginID := manifest.Id
if pluginID == "" { if pluginID == "" {
return fmt.Errorf("plugin ID not found in manifest") return "", fmt.Errorf("plugin ID not found in manifest")
} }
return pluginID, nil
}
func deployPluginBundle(pluginID, bundlePath string) error {
ctx, cancel := context.WithTimeout(context.Background(), commandTimeout) ctx, cancel := context.WithTimeout(context.Background(), commandTimeout)
defer cancel() defer cancel()

View file

@ -8,6 +8,27 @@ import (
) )
func RunDisableCommand(args []string, pluginPath string) error { func RunDisableCommand(args []string, pluginPath string) error {
helpText := `Disable plugin in Mattermost server
Usage:
pluginctl disable [options]
Options:
--help, -h Show this help message
Description:
Disables the plugin in the connected Mattermost server. The plugin will
remain uploaded but will be inactive.
Examples:
pluginctl disable # Disable plugin from current directory
pluginctl --plugin-path /path/to/plugin disable # Disable plugin at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
return runPluginCommand(args, pluginPath, disablePlugin) return runPluginCommand(args, pluginPath, disablePlugin)
} }

View file

@ -8,6 +8,27 @@ import (
) )
func RunEnableCommand(args []string, pluginPath string) error { func RunEnableCommand(args []string, pluginPath string) error {
helpText := `Enable plugin in Mattermost server
Usage:
pluginctl enable [options]
Options:
--help, -h Show this help message
Description:
Enables the plugin in the connected Mattermost server. The plugin must already
be uploaded to the server for this command to work.
Examples:
pluginctl enable # Enable plugin from current directory
pluginctl --plugin-path /path/to/plugin enable # Enable plugin at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
return runPluginCommand(args, pluginPath, enablePlugin) return runPluginCommand(args, pluginPath, enablePlugin)
} }

21
info.go
View file

@ -109,6 +109,27 @@ func InfoCommandWithPath(path string) error {
// RunInfoCommand implements the 'info' command functionality with plugin path. // RunInfoCommand implements the 'info' command functionality with plugin path.
func RunInfoCommand(args []string, pluginPath string) error { func RunInfoCommand(args []string, pluginPath string) error {
helpText := `Display plugin information
Usage:
pluginctl info [options]
Options:
--help, -h Show this help message
Description:
Shows detailed information about the plugin including ID, name, version,
supported components (server/webapp), and settings schema status.
Examples:
pluginctl info # Show info for plugin in current directory
pluginctl --plugin-path /path/to/plugin info # Show info for plugin at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
// Convert to absolute path // Convert to absolute path
absPath, err := filepath.Abs(pluginPath) absPath, err := filepath.Abs(pluginPath)
if err != nil { if err != nil {

26
logs.go
View file

@ -20,6 +20,32 @@ const (
// RunLogsCommand executes the logs command with optional --watch flag. // RunLogsCommand executes the logs command with optional --watch flag.
func RunLogsCommand(args []string, pluginPath string) error { func RunLogsCommand(args []string, pluginPath string) error {
helpText := `View plugin logs
Usage:
pluginctl logs [options]
Options:
--watch Follow logs in real-time
--help, -h Show this help message
Description:
Views plugin logs from the Mattermost server. By default, shows recent log
entries. Use --watch to follow logs in real-time.
Note: JSON output for file logs must be enabled in Mattermost configuration
(LogSettings.FileJson) for this command to work.
Examples:
pluginctl logs # View recent plugin logs
pluginctl logs --watch # Watch plugin logs in real-time
pluginctl --plugin-path /path/to/plugin logs # View logs for plugin at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
// Check for --watch flag // Check for --watch flag
watch := false watch := false
if len(args) > 0 && args[0] == "--watch" { if len(args) > 0 && args[0] == "--watch" {

View file

@ -91,8 +91,32 @@ func applyManifest(manifest *model.Manifest, pluginPath string) error {
// RunManifestCommand implements the 'manifest' command functionality with subcommands. // RunManifestCommand implements the 'manifest' command functionality with subcommands.
func RunManifestCommand(args []string, pluginPath string) error { func RunManifestCommand(args []string, pluginPath string) error {
helpText := `Manage plugin manifest files
Usage:
pluginctl manifest <subcommand> [options]
Subcommands:
get TEMPLATE Get manifest field using template syntax (e.g., {{.id}}, {{.version}})
apply Generate manifest files for server/webapp
check Validate plugin manifest
Options:
--help, -h Show this help message
Examples:
pluginctl manifest get '{{.id}}' # Get plugin ID
pluginctl manifest get '{{.version}}' # Get plugin version
pluginctl manifest apply # Generate server/webapp manifest files
pluginctl manifest check # Validate manifest`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("manifest command requires a subcommand: get {{.field_name}}, apply, check") return ShowErrorWithHelp("manifest command requires a subcommand", helpText)
} }
// Convert to absolute path // Convert to absolute path
@ -111,7 +135,7 @@ func RunManifestCommand(args []string, pluginPath string) error {
switch subcommand { switch subcommand {
case "get": case "get":
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("get subcommand requires a template expression (e.g., {{.id}}, {{.version}})") return ShowErrorWithHelp("get subcommand requires a template expression", helpText)
} }
templateStr := args[1] templateStr := args[1]
@ -139,8 +163,7 @@ func RunManifestCommand(args []string, pluginPath string) error {
} }
Logger.Info("Plugin manifest is valid") Logger.Info("Plugin manifest is valid")
default: default:
return fmt.Errorf("unknown subcommand: %s. Available subcommands: get {{.field_name}}, apply, check", return ShowErrorWithHelp(fmt.Sprintf("unknown subcommand: %s", subcommand), helpText)
subcommand)
} }
return nil return nil

View file

@ -114,3 +114,30 @@ func ParsePluginCtlConfig(manifest *model.Manifest) (*PluginCtlConfig, error) {
return config, nil return config, nil
} }
const (
HelpFlagLong = "--help"
HelpFlagShort = "-h"
)
// CheckForHelpFlag checks if --help is in the arguments and shows help if found.
// Returns true if help was shown, false otherwise.
func CheckForHelpFlag(args []string, helpText string) bool {
for _, arg := range args {
if arg == HelpFlagLong || arg == HelpFlagShort {
Logger.Info(helpText)
return true
}
}
return false
}
// ShowErrorWithHelp displays an error message followed by command help.
func ShowErrorWithHelp(errorMsg, helpText string) error {
Logger.Error(errorMsg)
Logger.Info(helpText)
return fmt.Errorf("%s", errorMsg)
}

View file

@ -7,6 +7,27 @@ import (
) )
func RunResetCommand(args []string, pluginPath string) error { func RunResetCommand(args []string, pluginPath string) error {
helpText := `Reset plugin (disable then enable)
Usage:
pluginctl reset [options]
Options:
--help, -h Show this help message
Description:
Resets the plugin by first disabling it and then enabling it. This is useful
for restarting a plugin without having to redeploy it.
Examples:
pluginctl reset # Reset plugin from current directory
pluginctl --plugin-path /path/to/plugin reset # Reset plugin at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
return runPluginCommand(args, pluginPath, resetPlugin) return runPluginCommand(args, pluginPath, resetPlugin)
} }

View file

@ -25,8 +25,33 @@ const (
) )
func RunUpdateAssetsCommand(args []string, pluginPath string) error { func RunUpdateAssetsCommand(args []string, pluginPath string) error {
if len(args) > 0 { helpText := `Update plugin files from embedded assets
return fmt.Errorf("updateassets command does not accept arguments")
Usage:
pluginctl updateassets [options]
Options:
--help, -h Show this help message
Description:
Updates plugin development files such as Makefile, .editorconfig, build
configurations, and other assets from the embedded templates. This ensures
your plugin uses the latest development tooling and configurations.
Examples:
pluginctl updateassets # Update assets in current directory
pluginctl --plugin-path /path/to/plugin updateassets # Update assets at specific path`
// Check for help flag
if CheckForHelpFlag(args, helpText) {
return nil
}
// Check for unexpected arguments
for _, arg := range args {
if arg != "--help" && arg != "-h" {
return ShowErrorWithHelp(fmt.Sprintf("unknown argument: %s", arg), helpText)
}
} }
Logger.Info("Updating assets in plugin directory", "path", pluginPath) Logger.Info("Updating assets in plugin directory", "path", pluginPath)