|
| 1 | +// Copyright 2022-2025 Salesforce, Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +# Adding command options |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +## Table of Contents |
| 20 | +1. @Understanding Command Options in Slack CLI |
| 21 | +2. @How Command Options are Defined |
| 22 | +3. @Step-by-Step Guide for Adding a New Option |
| 23 | +4. @Testing Your New Option |
| 24 | +5. @Best Practices |
| 25 | + |
| 26 | +## Understanding Command Options in Slack CLI |
| 27 | + |
| 28 | +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. |
| 29 | + |
| 30 | +There are two main types of flags in Cobra: |
| 31 | + |
| 32 | +1. **Persistent Flags**: Available to the command they're assigned to, as well as all its sub-commands. |
| 33 | + ```go |
| 34 | + rootCmd.PersistentFlags().StringVar(&config.APIHostFlag, "apihost", "", "Slack API host") |
| 35 | + ``` |
| 36 | + |
| 37 | +2. **Local Flags**: Only available to the specific command they're assigned to. |
| 38 | + ```go |
| 39 | + cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting") |
| 40 | + ``` |
| 41 | + |
| 42 | +## How Command Options are Defined |
| 43 | + |
| 44 | +In the Slack CLI project, command options follow a consistent pattern: |
| 45 | + |
| 46 | +1. **Flag Storage**: Each command package defines a struct to store flag values. |
| 47 | + ```go |
| 48 | + type runCmdFlags struct { |
| 49 | + activityLevel string |
| 50 | + noActivity bool |
| 51 | + cleanup bool |
| 52 | + hideTriggers bool |
| 53 | + orgGrantWorkspaceID string |
| 54 | + } |
| 55 | + |
| 56 | + var runFlags runCmdFlags |
| 57 | + ``` |
| 58 | + |
| 59 | +2. **Flag Definition**: Options are defined in the command's constructor function. |
| 60 | + ```go |
| 61 | + cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting") |
| 62 | + ``` |
| 63 | + |
| 64 | +3. **Flag Usage**: The flag values are accessed in the command's run function through the struct variables. |
| 65 | + ```go |
| 66 | + runArgs := platform.RunArgs{ |
| 67 | + Activity: !runFlags.noActivity, |
| 68 | + ActivityLevel: runFlags.activityLevel, |
| 69 | + Cleanup: runFlags.cleanup, |
| 70 | + // ... |
| 71 | + } |
| 72 | + ``` |
| 73 | + |
| 74 | +4. **Flag Helpers**: The `cmdutil` package provides helper functions for working with flags. |
| 75 | + ```go |
| 76 | + cmdutil.IsFlagChanged(cmd, "flag-name") |
| 77 | + ``` |
| 78 | + |
| 79 | +## Step-by-Step Guide for Adding a New Option |
| 80 | + |
| 81 | +Let's add a new flag called `--watch-ignore` to the `platform run` command to specify patterns to ignore while watching for changes. |
| 82 | + |
| 83 | +### Step 1: Update the Flag Storage Struct |
| 84 | + |
| 85 | +Locate the command's flag struct in the command file: @run.go |
| 86 | + |
| 87 | +```go |
| 88 | +type runCmdFlags struct { |
| 89 | + activityLevel string |
| 90 | + noActivity bool |
| 91 | + cleanup bool |
| 92 | + hideTriggers bool |
| 93 | + orgGrantWorkspaceID string |
| 94 | + watchIgnore []string // New flag for patterns to ignore |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### Step 2: Define the Flag in the Command Constructor |
| 99 | + |
| 100 | +Add the flag definition in the `NewRunCommand` function: |
| 101 | + |
| 102 | +```go |
| 103 | +func NewRunCommand(clients *shared.ClientFactory) *cobra.Command { |
| 104 | + // ... existing code |
| 105 | + |
| 106 | + // Add flags |
| 107 | + cmd.Flags().StringVar(&runFlags.activityLevel, "activity-level", platform.ActivityMinLevelDefault, "activity level to display") |
| 108 | + cmd.Flags().BoolVar(&runFlags.noActivity, "no-activity", false, "hide Slack Platform log activity") |
| 109 | + cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting") |
| 110 | + cmd.Flags().StringVar(&runFlags.orgGrantWorkspaceID, cmdutil.OrgGrantWorkspaceFlag, "", cmdutil.OrgGrantWorkspaceDescription()) |
| 111 | + cmd.Flags().BoolVar(&runFlags.hideTriggers, "hide-triggers", false, "do not list triggers and skip trigger creation prompts") |
| 112 | + |
| 113 | + // Add the new flag |
| 114 | + cmd.Flags().StringSliceVar(&runFlags.watchIgnore, "watch-ignore", nil, "patterns to ignore while watching for changes") |
| 115 | + |
| 116 | + // ... rest of the function |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +### Step 3: Update the Command's Run Function |
| 121 | + |
| 122 | +Modify the `RunRunCommand` function to use the new flag: |
| 123 | + |
| 124 | +```go |
| 125 | +func RunRunCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error { |
| 126 | + // ... existing code |
| 127 | + |
| 128 | + runArgs := platform.RunArgs{ |
| 129 | + Activity: !runFlags.noActivity, |
| 130 | + ActivityLevel: runFlags.activityLevel, |
| 131 | + App: selection.App, |
| 132 | + Auth: selection.Auth, |
| 133 | + Cleanup: runFlags.cleanup, |
| 134 | + ShowTriggers: triggers.ShowTriggers(clients, runFlags.hideTriggers), |
| 135 | + OrgGrantWorkspaceID: runFlags.orgGrantWorkspaceID, |
| 136 | + WatchIgnore: runFlags.watchIgnore, // Pass the new flag value |
| 137 | + } |
| 138 | + |
| 139 | + // ... rest of the function |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +### Step 4: Update the RunArgs Struct |
| 144 | + |
| 145 | +Update the `RunArgs` struct in the internal platform package: @run.go |
| 146 | + |
| 147 | +```go |
| 148 | +type RunArgs struct { |
| 149 | + Activity bool |
| 150 | + ActivityLevel string |
| 151 | + App types.App |
| 152 | + Auth types.SlackAuth |
| 153 | + Cleanup bool |
| 154 | + ShowTriggers bool |
| 155 | + OrgGrantWorkspaceID string |
| 156 | + WatchIgnore []string // New field for ignore patterns |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +### Step 5: Use the New Flag Value in the Run Function |
| 161 | + |
| 162 | +Update the `Run` function in the platform package to use the new flag value: |
| 163 | + |
| 164 | +```go |
| 165 | +func Run(ctx context.Context, clients *shared.ClientFactory, log *logger.Logger, runArgs RunArgs) (*logger.LogEvent, types.InstallState, error) { |
| 166 | + // ... existing code |
| 167 | + |
| 168 | + watchOptions := &watcher.Options{ |
| 169 | + IgnorePatterns: runArgs.WatchIgnore, // Use the new flag value |
| 170 | + } |
| 171 | + |
| 172 | + // ... update the watcher setup to use the options |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +### Step 6: Update Command Examples |
| 177 | + |
| 178 | +Add an example for the new flag in the command constructor: |
| 179 | + |
| 180 | +```go |
| 181 | +Example: style.ExampleCommandsf([]style.ExampleCommand{ |
| 182 | + {Command: "platform run", Meaning: "Start a local development server"}, |
| 183 | + {Command: "platform run --activity-level debug", Meaning: "Run a local development server with debug activity"}, |
| 184 | + {Command: "platform run --cleanup", Meaning: "Run a local development server with cleanup"}, |
| 185 | + {Command: "platform run --watch-ignore '**/node_modules/**'", Meaning: "Ignore node_modules while watching for changes"}, |
| 186 | +}), |
| 187 | +``` |
| 188 | + |
| 189 | +## Testing Your New Option |
| 190 | + |
| 191 | +For proper test coverage of your new flag, you need to: |
| 192 | + |
| 193 | +1. Update existing tests |
| 194 | +2. Add new test cases |
| 195 | + |
| 196 | +### Step 1: Update Existing Test Cases |
| 197 | + |
| 198 | +In `cmd/platform/run_test.go`, update the test cases to include the new flag: |
| 199 | + |
| 200 | +```go |
| 201 | +func TestRunCommand_Flags(t *testing.T) { |
| 202 | + tests := map[string]struct { |
| 203 | + cmdArgs []string |
| 204 | + appFlag string |
| 205 | + tokenFlag string |
| 206 | + selectedAppAuth prompts.SelectedApp |
| 207 | + selectedAppErr error |
| 208 | + expectedRunArgs platform.RunArgs |
| 209 | + expectedErr error |
| 210 | + }{ |
| 211 | + // ... existing test cases |
| 212 | + |
| 213 | + "Run with watch-ignore flag": { |
| 214 | + cmdArgs: []string{"--watch-ignore", "**/node_modules/**,**/dist/**"}, |
| 215 | + selectedAppAuth: prompts.SelectedApp{ |
| 216 | + App: types.NewApp(), |
| 217 | + Auth: types.SlackAuth{}, |
| 218 | + }, |
| 219 | + expectedRunArgs: platform.RunArgs{ |
| 220 | + Activity: true, |
| 221 | + ActivityLevel: "info", |
| 222 | + Auth: types.SlackAuth{}, |
| 223 | + App: types.NewApp(), |
| 224 | + Cleanup: false, |
| 225 | + ShowTriggers: true, |
| 226 | + WatchIgnore: []string{"**/node_modules/**", "**/dist/**"}, // Check the flag is passed through |
| 227 | + }, |
| 228 | + expectedErr: nil, |
| 229 | + }, |
| 230 | + } |
| 231 | + |
| 232 | + // ... test implementation |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +### Step 2: Add Tests for the Platform Package |
| 237 | + |
| 238 | +Update the tests in the platform package (`internal/pkg/platform/run_test.go`) to test that the flag is used correctly: |
| 239 | + |
| 240 | +```go |
| 241 | +func TestRun_WatchIgnore(t *testing.T) { |
| 242 | + ctx := slackcontext.MockContext(context.Background()) |
| 243 | + clientsMock := shared.NewClientsMock() |
| 244 | + clientsMock.AddDefaultMocks() |
| 245 | + |
| 246 | + // Create test instance |
| 247 | + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) |
| 248 | + logger := logger.New(func(event *logger.LogEvent) {}) |
| 249 | + |
| 250 | + // Test with ignore patterns |
| 251 | + runArgs := platform.RunArgs{ |
| 252 | + App: types.NewApp(), |
| 253 | + Auth: types.SlackAuth{}, |
| 254 | + WatchIgnore: []string{"**/node_modules/**", "**/dist/**"}, |
| 255 | + } |
| 256 | + |
| 257 | + // Run the function (may need to adapt to your testing approach) |
| 258 | + _, _, err := platform.Run(ctx, clients, logger, runArgs) |
| 259 | + |
| 260 | + // Assert that the ignore patterns were used correctly |
| 261 | + // (how exactly depends on your implementation) |
| 262 | + require.NoError(t, err) |
| 263 | + // Add specific assertions about how the patterns should have been used |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +### Step 3: Test Help Text |
| 268 | + |
| 269 | +Also test that the help text for the command includes the new flag: |
| 270 | + |
| 271 | +```go |
| 272 | +func TestRunCommand_Help(t *testing.T) { |
| 273 | + clients := shared.NewClientFactory() |
| 274 | + cmd := NewRunCommand(clients) |
| 275 | + |
| 276 | + var buf bytes.Buffer |
| 277 | + cmd.SetOut(&buf) |
| 278 | + err := cmd.Help() |
| 279 | + |
| 280 | + require.NoError(t, err) |
| 281 | + helpText := buf.String() |
| 282 | + |
| 283 | + assert.Contains(t, helpText, "--watch-ignore") |
| 284 | + assert.Contains(t, helpText, "patterns to ignore while watching for changes") |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +## Best Practices |
| 289 | + |
| 290 | +When adding new command options, follow these best practices: |
| 291 | + |
| 292 | +1. **Meaningful Names**: Choose clear, descriptive flag names. |
| 293 | + - Good: `--watch-ignore` |
| 294 | + - Avoid: `--wignore` or `--wi` |
| 295 | + |
| 296 | +2. **Consistent Naming**: Follow existing naming patterns. |
| 297 | + - Use kebab-case for flag names (e.g., `--org-workspace-grant`). |
| 298 | + - Use camelCase for flag variables (e.g., `orgGrantWorkspaceID`). |
| 299 | + |
| 300 | +3. **Good Descriptions**: Write clear, concise descriptions. |
| 301 | + - Use sentence fragments without ending periods. |
| 302 | + - If needed, use `\n` to add line breaks for complex descriptions. |
| 303 | + |
| 304 | +4. **Appropriate Flag Types**: Choose the right type for each flag. |
| 305 | + - For simple on/off settings, use `BoolVar`. |
| 306 | + - For text values, use `StringVar`. |
| 307 | + - For lists, use `StringSliceVar`. |
| 308 | + - For numbers, use `IntVar` or `Float64Var`. |
| 309 | + |
| 310 | +5. **Default Values**: Set sensible default values if applicable. |
| 311 | + - For optional flags, consider what happens when the flag is not provided. |
| 312 | + - Document default values in the help text. |
| 313 | + |
| 314 | +6. **Examples**: Update command examples to showcase the new flag. |
| 315 | + - Include realistic examples of how the flag might be used. |
| 316 | + |
| 317 | +7. **Thorough Testing**: Test all combinations and edge cases. |
| 318 | + - Test without the flag (default behavior). |
| 319 | + - Test with the flag set. |
| 320 | + - Test with invalid values, if applicable. |
| 321 | + |
| 322 | +8. **Updating documentation**: Ensure you update any related documentation in `/docs` |
| 323 | + - Check for any guides, tutorials, reference docs, or other documentation that may use the command. |
| 324 | + - Ensure it's updated to include behavioral changes as well as any API changes. |
| 325 | + - Follow existing docs patterns and best practices. |
| 326 | + |
| 327 | + |
| 328 | +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. |
| 329 | + |
| 330 | + |
0 commit comments