// Copyright 2022-2025 Salesforce, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.
This guide explains how command options (flags) are used in the Slack CLI project and provides step-by-step instructions for adding a new option to an existing command.
- @Understanding Command Options in Slack CLI
- @How Command Options are Defined
- @Step-by-Step Guide for Adding a New Option
- @Testing Your New Option
- @Best Practices
The Slack CLI uses the @Cobra library for command-line functionality. Command options (or flags) provide a way to modify the behavior of a command. For example, the platform run command includes options like --activity-level to specify the logging level, or --cleanup to uninstall the local app after exiting.
There are two main types of flags in Cobra:
-
Persistent Flags: Available to the command they're assigned to, as well as all its sub-commands.
rootCmd.PersistentFlags().StringVar(&config.APIHostFlag, "apihost", "", "Slack API host")
-
Local Flags: Only available to the specific command they're assigned to.
cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting")
In the Slack CLI project, command options follow a consistent pattern:
-
Flag Storage: Each command package defines a struct to store flag values.
type runCmdFlags struct { activityLevel string noActivity bool cleanup bool hideTriggers bool orgGrantWorkspaceID string } var runFlags runCmdFlags
-
Flag Definition: Options are defined in the command's constructor function.
cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting")
-
Flag Usage: The flag values are accessed in the command's run function through the struct variables.
runArgs := platform.RunArgs{ Activity: !runFlags.noActivity, ActivityLevel: runFlags.activityLevel, Cleanup: runFlags.cleanup, // ... }
-
Flag Helpers: The
cmdutilpackage provides helper functions for working with flags.cmdutil.IsFlagChanged(cmd, "flag-name")
Let's add a new flag called --watch-ignore to the platform run command to specify patterns to ignore while watching for changes.
Locate the command's flag struct in the command file: @run.go
type runCmdFlags struct {
activityLevel string
noActivity bool
cleanup bool
hideTriggers bool
orgGrantWorkspaceID string
watchIgnore []string // New flag for patterns to ignore
}Add the flag definition in the NewRunCommand function:
func NewRunCommand(clients *shared.ClientFactory) *cobra.Command {
// ... existing code
// Add flags
cmd.Flags().StringVar(&runFlags.activityLevel, "activity-level", platform.ActivityMinLevelDefault, "activity level to display")
cmd.Flags().BoolVar(&runFlags.noActivity, "no-activity", false, "hide Slack Platform log activity")
cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting")
cmd.Flags().StringVar(&runFlags.orgGrantWorkspaceID, cmdutil.OrgGrantWorkspaceFlag, "", cmdutil.OrgGrantWorkspaceDescription())
cmd.Flags().BoolVar(&runFlags.hideTriggers, "hide-triggers", false, "do not list triggers and skip trigger creation prompts")
// Add the new flag
cmd.Flags().StringSliceVar(&runFlags.watchIgnore, "watch-ignore", nil, "patterns to ignore while watching for changes")
// ... rest of the function
}Modify the RunRunCommand function to use the new flag:
func RunRunCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
// ... existing code
runArgs := platform.RunArgs{
Activity: !runFlags.noActivity,
ActivityLevel: runFlags.activityLevel,
App: selection.App,
Auth: selection.Auth,
Cleanup: runFlags.cleanup,
ShowTriggers: triggers.ShowTriggers(clients, runFlags.hideTriggers),
OrgGrantWorkspaceID: runFlags.orgGrantWorkspaceID,
WatchIgnore: runFlags.watchIgnore, // Pass the new flag value
}
// ... rest of the function
}Update the RunArgs struct in the internal platform package: @run.go
type RunArgs struct {
Activity bool
ActivityLevel string
App types.App
Auth types.SlackAuth
Cleanup bool
ShowTriggers bool
OrgGrantWorkspaceID string
WatchIgnore []string // New field for ignore patterns
}Update the Run function in the platform package to use the new flag value:
func Run(ctx context.Context, clients *shared.ClientFactory, log *logger.Logger, runArgs RunArgs) (*logger.LogEvent, types.InstallState, error) {
// ... existing code
watchOptions := &watcher.Options{
IgnorePatterns: runArgs.WatchIgnore, // Use the new flag value
}
// ... update the watcher setup to use the options
}Add an example for the new flag in the command constructor:
Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "platform run", Meaning: "Start a local development server"},
{Command: "platform run --activity-level debug", Meaning: "Run a local development server with debug activity"},
{Command: "platform run --cleanup", Meaning: "Run a local development server with cleanup"},
{Command: "platform run --watch-ignore '**/node_modules/**'", Meaning: "Ignore node_modules while watching for changes"},
}),For proper test coverage of your new flag, you need to:
- Update existing tests
- Add new test cases
In cmd/platform/run_test.go, update the test cases to include the new flag:
func TestRunCommand_Flags(t *testing.T) {
tests := map[string]struct {
cmdArgs []string
appFlag string
tokenFlag string
selectedAppAuth prompts.SelectedApp
selectedAppErr error
expectedRunArgs platform.RunArgs
expectedErr error
}{
// ... existing test cases
"Run with watch-ignore flag": {
cmdArgs: []string{"--watch-ignore", "**/node_modules/**,**/dist/**"},
selectedAppAuth: prompts.SelectedApp{
App: types.NewApp(),
Auth: types.SlackAuth{},
},
expectedRunArgs: platform.RunArgs{
Activity: true,
ActivityLevel: "info",
Auth: types.SlackAuth{},
App: types.NewApp(),
Cleanup: false,
ShowTriggers: true,
WatchIgnore: []string{"**/node_modules/**", "**/dist/**"}, // Check the flag is passed through
},
expectedErr: nil,
},
}
// ... test implementation
}Update the tests in the platform package (internal/pkg/platform/run_test.go) to test that the flag is used correctly:
func TestRun_WatchIgnore(t *testing.T) {
ctx := slackcontext.MockContext(context.Background())
clientsMock := shared.NewClientsMock()
clientsMock.AddDefaultMocks()
// Create test instance
clients := shared.NewClientFactory(clientsMock.MockClientFactory())
logger := logger.New(func(event *logger.LogEvent) {})
// Test with ignore patterns
runArgs := platform.RunArgs{
App: types.NewApp(),
Auth: types.SlackAuth{},
WatchIgnore: []string{"**/node_modules/**", "**/dist/**"},
}
// Run the function (may need to adapt to your testing approach)
_, _, err := platform.Run(ctx, clients, logger, runArgs)
// Assert that the ignore patterns were used correctly
// (how exactly depends on your implementation)
require.NoError(t, err)
// Add specific assertions about how the patterns should have been used
}Also test that the help text for the command includes the new flag:
func TestRunCommand_Help(t *testing.T) {
clients := shared.NewClientFactory()
cmd := NewRunCommand(clients)
var buf bytes.Buffer
cmd.SetOut(&buf)
err := cmd.Help()
require.NoError(t, err)
helpText := buf.String()
assert.Contains(t, helpText, "--watch-ignore")
assert.Contains(t, helpText, "patterns to ignore while watching for changes")
}When adding new command options, follow these best practices:
-
Meaningful Names: Choose clear, descriptive flag names.
- Good:
--watch-ignore - Avoid:
--wignoreor--wi
- Good:
-
Consistent Naming: Follow existing naming patterns.
- Use kebab-case for flag names (e.g.,
--org-workspace-grant). - Use camelCase for flag variables (e.g.,
orgGrantWorkspaceID).
- Use kebab-case for flag names (e.g.,
-
Good Descriptions: Write clear, concise descriptions.
- Use sentence fragments without ending periods.
- If needed, use
\nto add line breaks for complex descriptions.
-
Appropriate Flag Types: Choose the right type for each flag.
- For simple on/off settings, use
BoolVar. - For text values, use
StringVar. - For lists, use
StringSliceVar. - For numbers, use
IntVarorFloat64Var.
- For simple on/off settings, use
-
Default Values: Set sensible default values if applicable.
- For optional flags, consider what happens when the flag is not provided.
- Document default values in the help text.
-
Examples: Update command examples to showcase the new flag.
- Include realistic examples of how the flag might be used.
-
Thorough Testing: Test all combinations and edge cases.
- Test without the flag (default behavior).
- Test with the flag set.
- Test with invalid values, if applicable.
-
Updating documentation: Ensure you update any related documentation in
/docs- Check for any guides, tutorials, reference docs, or other documentation that may use the command.
- Ensure it's updated to include behavioral changes as well as any API changes.
- Follow existing docs patterns and best practices.
By following these steps and best practices, you can successfully add a new command option to the Slack CLI that integrates well with the existing codebase and provides value to users.