diff --git a/cmd/pluginctl/main.go b/cmd/pluginctl/main.go index 2baeb9f..0d3f2b8 100644 --- a/cmd/pluginctl/main.go +++ b/cmd/pluginctl/main.go @@ -19,10 +19,19 @@ func main() { pluginctl.InitLogger() var pluginPath string + var showHelp bool flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)") + flag.BoolVar(&showHelp, "help", false, "Show help information") flag.Parse() + // Show help if requested + if showHelp { + showUsage() + + return + } + args := flag.Args() if len(args) == 0 { pluginctl.Logger.Error("No command specified") @@ -60,10 +69,6 @@ func runCommand(command string, args []string, pluginPath string) error { return runManifestCommand(args, pluginPath) case "logs": return runLogsCommand(args, pluginPath) - case "help": - showUsage() - - return nil case "version": return runVersionCommand(args) case "create-plugin": @@ -123,41 +128,20 @@ Usage: Global Options: --plugin-path PATH Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH) + --help Show this help message Commands: info Display plugin information - enable Enable plugin from current directory in Mattermost server - disable Disable plugin from current directory in Mattermost server - reset Reset plugin from current directory (disable then enable) - deploy Upload and enable plugin bundle to Mattermost server + enable Enable plugin in Mattermost server + disable Disable plugin in Mattermost server + reset Reset plugin (disable then enable) + deploy Upload and enable plugin bundle updateassets Update plugin files from embedded assets - manifest Get plugin manifest information using templates (get {{.field}}, apply, check) - logs View plugin logs (use --watch to follow logs in real-time) - create-plugin Create a new plugin from the starter template (supports --name and --module flags) - help Show this help message + manifest Manage plugin manifest files + logs View plugin logs + create-plugin Create a new plugin from template 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: PLUGINCTL_PLUGIN_PATH Default plugin directory path MM_LOCALSOCKETPATH Path to Mattermost local socket @@ -166,6 +150,8 @@ Environment Variables: MM_ADMIN_USERNAME Admin username for authentication MM_ADMIN_PASSWORD Admin password for authentication +Use 'pluginctl --help' for detailed information about a command. + For more information about Mattermost plugin development, visit: https://developers.mattermost.com/integrate/plugins/ ` diff --git a/create-plugin.go b/create-plugin.go index 488fda7..1628e49 100644 --- a/create-plugin.go +++ b/create-plugin.go @@ -277,41 +277,75 @@ func promptForModuleName(pluginName string) (string, error) { return result.input, nil } -func parseCreatePluginFlags(args []string) (pluginName, moduleName string, err error) { - // Parse flags similar to how logs command handles --watch +func parseCreatePluginFlags(args []string, helpText string) (pluginName, moduleName string, err error) { + 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 { switch arg { case "--name": 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] case "--module": 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] - } - } - - // 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) + 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) + } } } 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. // Example: // - 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 { + 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 var pluginName, moduleName string var err error - pluginName, moduleName, err = parseCreatePluginFlags(args) + pluginName, moduleName, err = parseCreatePluginFlags(args, helpText) if err != nil { - return fmt.Errorf("failed to parse flags: %w", err) + return err } Logger.Info("Starting plugin creation process") diff --git a/deploy.go b/deploy.go index 8997c39..a068f89 100644 --- a/deploy.go +++ b/deploy.go @@ -10,15 +10,61 @@ import ( ) 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 - // Parse flags i := 0 for i < len(args) { switch args[i] { case "--bundle-path": 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] i += 2 @@ -27,37 +73,50 @@ func RunDeployCommand(args []string, pluginPath string) error { } } - // If no bundle path provided, auto-discover from dist folder - if bundlePath == "" { - manifest, err := LoadPluginManifestFromPath(pluginPath) - if err != nil { - return fmt.Errorf("failed to load plugin manifest: %w", err) - } - - expectedBundleName := fmt.Sprintf("%s-%s.tar.gz", manifest.Id, manifest.Version) - bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName) + return bundlePath, nil +} +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 not found at %s - run 'make bundle' to build the plugin first", bundlePath) + return "", fmt.Errorf("bundle file not found: %s", bundlePath) } + + return bundlePath, nil } - // Validate bundle file exists - if _, err := os.Stat(bundlePath); os.IsNotExist(err) { - return fmt.Errorf("bundle file not found: %s", bundlePath) - } - - // Load manifest to get plugin ID + // Auto-discover from dist folder manifest, err := LoadPluginManifestFromPath(pluginPath) 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) + bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName) + + 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 bundlePath, nil +} + +func getPluginIDFromManifest(pluginPath string) (string, error) { + manifest, err := LoadPluginManifestFromPath(pluginPath) + if err != nil { + return "", fmt.Errorf("failed to load plugin manifest: %w", err) } pluginID := manifest.Id 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) defer cancel() diff --git a/disable.go b/disable.go index 57cd2db..4fdae07 100644 --- a/disable.go +++ b/disable.go @@ -8,6 +8,27 @@ import ( ) 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) } diff --git a/enable.go b/enable.go index 208e6c1..34ad830 100644 --- a/enable.go +++ b/enable.go @@ -8,6 +8,27 @@ import ( ) 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) } diff --git a/info.go b/info.go index fb1e3b8..662dc85 100644 --- a/info.go +++ b/info.go @@ -109,6 +109,27 @@ func InfoCommandWithPath(path string) error { // RunInfoCommand implements the 'info' command functionality with plugin path. 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 absPath, err := filepath.Abs(pluginPath) if err != nil { diff --git a/logs.go b/logs.go index 4a19597..ddde6d5 100644 --- a/logs.go +++ b/logs.go @@ -20,6 +20,32 @@ const ( // RunLogsCommand executes the logs command with optional --watch flag. 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 watch := false if len(args) > 0 && args[0] == "--watch" { diff --git a/manifest.go b/manifest.go index e2347e6..c2ea165 100644 --- a/manifest.go +++ b/manifest.go @@ -91,8 +91,32 @@ func applyManifest(manifest *model.Manifest, pluginPath string) error { // RunManifestCommand implements the 'manifest' command functionality with subcommands. func RunManifestCommand(args []string, pluginPath string) error { + helpText := `Manage plugin manifest files + +Usage: + pluginctl manifest [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 { - 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 @@ -111,7 +135,7 @@ func RunManifestCommand(args []string, pluginPath string) error { switch subcommand { case "get": 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] @@ -139,8 +163,7 @@ func RunManifestCommand(args []string, pluginPath string) error { } Logger.Info("Plugin manifest is valid") default: - return fmt.Errorf("unknown subcommand: %s. Available subcommands: get {{.field_name}}, apply, check", - subcommand) + return ShowErrorWithHelp(fmt.Sprintf("unknown subcommand: %s", subcommand), helpText) } return nil diff --git a/plugin.go b/plugin.go index b8612ad..60df90c 100644 --- a/plugin.go +++ b/plugin.go @@ -114,3 +114,30 @@ func ParsePluginCtlConfig(manifest *model.Manifest) (*PluginCtlConfig, error) { 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) +} diff --git a/reset.go b/reset.go index 03f4ac7..73e7086 100644 --- a/reset.go +++ b/reset.go @@ -7,6 +7,27 @@ import ( ) 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) } diff --git a/updateassets.go b/updateassets.go index 9ce6222..472b691 100644 --- a/updateassets.go +++ b/updateassets.go @@ -25,8 +25,33 @@ const ( ) func RunUpdateAssetsCommand(args []string, pluginPath string) error { - if len(args) > 0 { - return fmt.Errorf("updateassets command does not accept arguments") + helpText := `Update plugin files from embedded assets + +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)