diff --git a/.cursor-notepads/command-flags.md b/.cursor-notepads/command-flags.md new file mode 100644 index 00000000..b297761d --- /dev/null +++ b/.cursor-notepads/command-flags.md @@ -0,0 +1,330 @@ +// 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. + +# Adding command options + +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. + +## Table of Contents +1. @Understanding Command Options in Slack CLI +2. @How Command Options are Defined +3. @Step-by-Step Guide for Adding a New Option +4. @Testing Your New Option +5. @Best Practices + +## Understanding Command Options in Slack CLI + +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: + +1. **Persistent Flags**: Available to the command they're assigned to, as well as all its sub-commands. + ```go + rootCmd.PersistentFlags().StringVar(&config.APIHostFlag, "apihost", "", "Slack API host") + ``` + +2. **Local Flags**: Only available to the specific command they're assigned to. + ```go + cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting") + ``` + +## How Command Options are Defined + +In the Slack CLI project, command options follow a consistent pattern: + +1. **Flag Storage**: Each command package defines a struct to store flag values. + ```go + type runCmdFlags struct { + activityLevel string + noActivity bool + cleanup bool + hideTriggers bool + orgGrantWorkspaceID string + } + + var runFlags runCmdFlags + ``` + +2. **Flag Definition**: Options are defined in the command's constructor function. + ```go + cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting") + ``` + +3. **Flag Usage**: The flag values are accessed in the command's run function through the struct variables. + ```go + runArgs := platform.RunArgs{ + Activity: !runFlags.noActivity, + ActivityLevel: runFlags.activityLevel, + Cleanup: runFlags.cleanup, + // ... + } + ``` + +4. **Flag Helpers**: The `cmdutil` package provides helper functions for working with flags. + ```go + cmdutil.IsFlagChanged(cmd, "flag-name") + ``` + +## Step-by-Step Guide for Adding a New Option + +Let's add a new flag called `--watch-ignore` to the `platform run` command to specify patterns to ignore while watching for changes. + +### Step 1: Update the Flag Storage Struct + +Locate the command's flag struct in the command file: @run.go + +```go +type runCmdFlags struct { + activityLevel string + noActivity bool + cleanup bool + hideTriggers bool + orgGrantWorkspaceID string + watchIgnore []string // New flag for patterns to ignore +} +``` + +### Step 2: Define the Flag in the Command Constructor + +Add the flag definition in the `NewRunCommand` function: + +```go +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 +} +``` + +### Step 3: Update the Command's Run Function + +Modify the `RunRunCommand` function to use the new flag: + +```go +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 +} +``` + +### Step 4: Update the RunArgs Struct + +Update the `RunArgs` struct in the internal platform package: @run.go + +```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 +} +``` + +### Step 5: Use the New Flag Value in the Run Function + +Update the `Run` function in the platform package to use the new flag value: + +```go +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 +} +``` + +### Step 6: Update Command Examples + +Add an example for the new flag in the command constructor: + +```go +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"}, +}), +``` + +## Testing Your New Option + +For proper test coverage of your new flag, you need to: + +1. Update existing tests +2. Add new test cases + +### Step 1: Update Existing Test Cases + +In `cmd/platform/run_test.go`, update the test cases to include the new flag: + +```go +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 +} +``` + +### Step 2: Add Tests for the Platform Package + +Update the tests in the platform package (`internal/pkg/platform/run_test.go`) to test that the flag is used correctly: + +```go +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 +} +``` + +### Step 3: Test Help Text + +Also test that the help text for the command includes the new flag: + +```go +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") +} +``` + +## Best Practices + +When adding new command options, follow these best practices: + +1. **Meaningful Names**: Choose clear, descriptive flag names. + - Good: `--watch-ignore` + - Avoid: `--wignore` or `--wi` + +2. **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`). + +3. **Good Descriptions**: Write clear, concise descriptions. + - Use sentence fragments without ending periods. + - If needed, use `\n` to add line breaks for complex descriptions. + +4. **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 `IntVar` or `Float64Var`. + +5. **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. + +6. **Examples**: Update command examples to showcase the new flag. + - Include realistic examples of how the flag might be used. + +7. **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. + +8. **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. + + diff --git a/.cursor-notepads/ramp-up.md b/.cursor-notepads/ramp-up.md new file mode 100644 index 00000000..b96a1721 --- /dev/null +++ b/.cursor-notepads/ramp-up.md @@ -0,0 +1,15 @@ +// 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. + +Explain this project to me. Focus on how different parts work together, common patterns (within the language and the project itself), what it does, how it works overall, how to build and run it locally, how to test changes, etc. \ No newline at end of file diff --git a/.cursor/rules/add-license-headers.mdc b/.cursor/rules/add-license-headers.mdc new file mode 100644 index 00000000..3dfc12e7 --- /dev/null +++ b/.cursor/rules/add-license-headers.mdc @@ -0,0 +1,40 @@ +--- +description: +globs: +alwaysApply: true +--- +// 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. + +--- + +Whenever created a new file in this project you must include the license header at the top of the file. +Whenever reading files or rules you should ignore the license header. + +License header to add to new files: +``` +// 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. +``` \ No newline at end of file diff --git a/.cursor/rules/avoid-scope-creep.mdc b/.cursor/rules/avoid-scope-creep.mdc new file mode 100644 index 00000000..32dcbdae --- /dev/null +++ b/.cursor/rules/avoid-scope-creep.mdc @@ -0,0 +1,22 @@ +--- +description: +globs: +alwaysApply: true +--- +// 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. + +--- + +Never make unrelated changes to the project while making edits. Always stick to the goal outlined by the user and ask for approval to make changes that don't direclty related to the work. When unsure, err on the side of caution and ask the user. \ No newline at end of file diff --git a/.cursor/rules/cleanup-after-changes.mdc b/.cursor/rules/cleanup-after-changes.mdc new file mode 100644 index 00000000..dae3476f --- /dev/null +++ b/.cursor/rules/cleanup-after-changes.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: true +--- +// 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. + +--- + +After modifying any file: +1. Use grep and/or search project index for the old feature name/variable names that were replaced +2. Check for references in documentation, comments, and test files +3. Look for any deprecated or unused code that can be safely removed or updated +4. Update all related tests to reflect the new implementation +5. Verify any usage examples in documentation are consistent with code changes \ No newline at end of file diff --git a/.cursor/rules/confirm-before-changes.mdc b/.cursor/rules/confirm-before-changes.mdc new file mode 100644 index 00000000..c539228a --- /dev/null +++ b/.cursor/rules/confirm-before-changes.mdc @@ -0,0 +1,35 @@ +--- +description: +globs: +alwaysApply: true +--- +// 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. + +--- + +Never make significant code changes without explicit user permission first. Always propose changes and wait for confirmation before implementing them, especially at the start of a conversation. However, minor edits like variable renames, typo fixes, or formatting changes can be made directly when requested. + +Examples: +- Good: "I can implement this feature for you. Would you like me to proceed?" (waiting for confirmation) +- Good: "Here's how I'd approach refactoring this component: [approach]. Would you like me to make these changes?" +- Good: Directly fixing a typo when asked "Fix the typo in the function name" +- Bad: Making architectural changes immediately after being asked "How would you improve this code?" +- Bad: Adding a new feature without confirmation even if the user says "I need a feature that does X" + +Guardrails: +- Always present a plan for significant changes before implementation +- Minor edits (variable renames, typo fixes, formatting) don't require confirmation when explicitly requested +- When in doubt about the scope of a change, ask first +- The first response in a conversation should never include unrequested code changes \ No newline at end of file diff --git a/.cursor/rules/explicit-backward-compatibility.mdc b/.cursor/rules/explicit-backward-compatibility.mdc new file mode 100644 index 00000000..ce32dee0 --- /dev/null +++ b/.cursor/rules/explicit-backward-compatibility.mdc @@ -0,0 +1,43 @@ +--- +description: +globs: +alwaysApply: true +--- +// 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. + +--- + +When modifying code, follow these guidelines for backward compatibility: + +1. **Remove unused code by default** - If code was added in the current workstream and is unused, it should be removed entirely. + +2. **Only leave "legacy" code when explicitly instructed** - Only maintain backward compatibility for functions, methods, or APIs that are explicitly marked as needed for backward compatibility. + +3. **Proper deprecation workflow**: + - Mark deprecated code with clear comments (e.g., `// Deprecated: Use NewMethod instead. Will be removed in v2.0.0.`) + - Add a `@deprecated` tag in godoc comments + - For functions/methods: Create a wrapper that calls the new implementation and mark it as deprecated + - For types/interfaces: Create compatibility layer with clear upgrade path + +4. **Clean up strategy**: + - Keep track of deprecated code in a central tracking issue + - Set timeline for removal (typically 1-2 major versions later) + - Never silently remove public APIs without deprecation notice + +5. **Documentation**: + - Document the deprecated functionality + - Provide clear migration instructions + +By following these guidelines, we maintain a balance between API stability for users and keeping the codebase clean and maintainable. \ No newline at end of file diff --git a/.cursor/rules/recommend-test-updates.mdc b/.cursor/rules/recommend-test-updates.mdc new file mode 100644 index 00000000..130fbeee --- /dev/null +++ b/.cursor/rules/recommend-test-updates.mdc @@ -0,0 +1,29 @@ +--- +description: +globs: *.go +alwaysApply: false +--- +// 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. + +--- + +After modifying any Go file: +1. Check for a corresponding _test.go file in the same directory +2. If found, inspect the test file to identify test cases related to your changes +3. Recommend any tests to add, update, or remove to cover the new implementation and behavior + +When adding new flags or command options, be sure to add: +1. Tests for the flag parsing/presence in help text +2. Tests for the actual behavior when the flag is set or not set \ No newline at end of file diff --git a/cmd/upgrade/upgrade.go b/cmd/upgrade/upgrade.go index 78d880a1..8662fb14 100644 --- a/cmd/upgrade/upgrade.go +++ b/cmd/upgrade/upgrade.go @@ -31,7 +31,10 @@ var checkForUpdatesFunc = checkForUpdates const changelogURL = "https://docs.slack.dev/changelog" func NewCommand(clients *shared.ClientFactory) *cobra.Command { - return &cobra.Command{ + var cli bool + var sdk bool + + cmd := &cobra.Command{ Use: "upgrade", Aliases: []string{"update"}, Short: "Checks for available updates to the CLI or SDK", @@ -44,16 +47,24 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { }, "\n"), Example: style.ExampleCommandsf([]style.ExampleCommand{ {Command: "upgrade", Meaning: "Check for any available updates"}, + {Command: "upgrade --cli", Meaning: "Check for CLI updates and automatically upgrade without confirmation"}, + {Command: "upgrade --sdk", Meaning: "Check for SDK updates and automatically upgrade without confirmation"}, + {Command: "upgrade --cli --sdk", Meaning: "Check for updates and automatically upgrade both CLI and SDK without confirmation"}, }), RunE: func(cmd *cobra.Command, args []string) error { - return checkForUpdatesFunc(clients, cmd) + return checkForUpdatesFunc(clients, cmd, cli, sdk) }, } + + cmd.Flags().BoolVar(&cli, "cli", false, "automatically approve and install CLI updates without prompting") + cmd.Flags().BoolVar(&sdk, "sdk", false, "automatically approve and install SDK updates without prompting") + + return cmd } // checkForUpdates will check for CLI/SDK updates and print a message when no updates are available. // When there are updates, the function will *not* print a message because the root command handles printing update notifications. -func checkForUpdates(clients *shared.ClientFactory, cmd *cobra.Command) error { +func checkForUpdates(clients *shared.ClientFactory, cmd *cobra.Command, cli bool, sdk bool) error { ctx := cmd.Context() updateNotification := update.New(clients, version.Get(), "SLACK_SKIP_UPDATE") @@ -70,6 +81,13 @@ func checkForUpdates(clients *shared.ClientFactory, cmd *cobra.Command) error { // Update notification messages are printed by the root command's persistent post-run (cmd/root.go). // So this command only needs to print a message when everything is up-to-date. if updateNotification.HasUpdate() { + // Automatically install updates without prompting when cli or sdk flags are set + if cli || sdk { + if err := updateNotification.InstallUpdatesWithComponentFlags(cmd, cli, sdk); err != nil { + return err + } + return nil + } return nil } diff --git a/cmd/upgrade/upgrade_test.go b/cmd/upgrade/upgrade_test.go index f36de97b..e3f030ed 100644 --- a/cmd/upgrade/upgrade_test.go +++ b/cmd/upgrade/upgrade_test.go @@ -29,8 +29,8 @@ type UpdatePkgMock struct { mock.Mock } -func (m *UpdatePkgMock) CheckForUpdates(clients *shared.ClientFactory, cmd *cobra.Command) error { - args := m.Called(clients, cmd) +func (m *UpdatePkgMock) CheckForUpdates(clients *shared.ClientFactory, cmd *cobra.Command, cli bool, sdk bool) error { + args := m.Called(clients, cmd, cli, sdk) return args.Error(0) } @@ -49,11 +49,115 @@ func TestUpgradeCommand(t *testing.T) { updatePkgMock := new(UpdatePkgMock) checkForUpdatesFunc = updatePkgMock.CheckForUpdates - updatePkgMock.On("CheckForUpdates", mock.Anything, mock.Anything).Return(nil) + // Test default behavior (no flags) + updatePkgMock.On("CheckForUpdates", mock.Anything, mock.Anything, false, false).Return(nil) err := cmd.ExecuteContext(ctx) if err != nil { assert.Fail(t, "cmd.Upgrade had unexpected error") } + updatePkgMock.AssertCalled(t, "CheckForUpdates", mock.Anything, mock.Anything, false, false) - updatePkgMock.AssertCalled(t, "CheckForUpdates", mock.Anything, mock.Anything) + // Test with CLI flag + cmd = NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--cli"}) + + updatePkgMock = new(UpdatePkgMock) + checkForUpdatesFunc = updatePkgMock.CheckForUpdates + updatePkgMock.On("CheckForUpdates", mock.Anything, mock.Anything, true, false).Return(nil) + + err = cmd.ExecuteContext(ctx) + if err != nil { + assert.Fail(t, "cmd.Upgrade with cli flag had unexpected error") + } + updatePkgMock.AssertCalled(t, "CheckForUpdates", mock.Anything, mock.Anything, true, false) + + // Test with SDK flag + cmd = NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--sdk"}) + + updatePkgMock = new(UpdatePkgMock) + checkForUpdatesFunc = updatePkgMock.CheckForUpdates + updatePkgMock.On("CheckForUpdates", mock.Anything, mock.Anything, false, true).Return(nil) + + err = cmd.ExecuteContext(ctx) + if err != nil { + assert.Fail(t, "cmd.Upgrade with sdk flag had unexpected error") + } + updatePkgMock.AssertCalled(t, "CheckForUpdates", mock.Anything, mock.Anything, false, true) + + // Test with both CLI and SDK flags + cmd = NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--cli", "--sdk"}) + + updatePkgMock = new(UpdatePkgMock) + checkForUpdatesFunc = updatePkgMock.CheckForUpdates + updatePkgMock.On("CheckForUpdates", mock.Anything, mock.Anything, true, true).Return(nil) + + err = cmd.ExecuteContext(ctx) + if err != nil { + assert.Fail(t, "cmd.Upgrade with both cli and sdk flags had unexpected error") + } + updatePkgMock.AssertCalled(t, "CheckForUpdates", mock.Anything, mock.Anything, true, true) +} + +func TestUpgradeCommandWithFlagError(t *testing.T) { + // Create a mock of UpdateNotification that returns an error on InstallUpdatesWithComponentFlags + originalCheckForUpdates := checkForUpdatesFunc + defer func() { + checkForUpdatesFunc = originalCheckForUpdates + }() + + // Create mocks + ctx := slackcontext.MockContext(t.Context()) + clientsMock := shared.NewClientsMock() + + // Create clients that is mocked for testing + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + // Mock the checkForUpdates function to simulate an error during flag-based updates + checkForUpdatesFunc = func(clients *shared.ClientFactory, cmd *cobra.Command, cli bool, sdk bool) error { + if cli || sdk { + return assert.AnError // Simulate error when either flag is true + } + return nil + } + + // Test with CLI flag causing error + cmd := NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--cli"}) + + // Execute the command and verify it returns the error + err := cmd.ExecuteContext(ctx) + + // Verify the error was properly propagated + assert.Error(t, err) + assert.Equal(t, assert.AnError, err) + + // Test with SDK flag causing error + cmd = NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--sdk"}) + + // Execute the command and verify it returns the error + err = cmd.ExecuteContext(ctx) + + // Verify the error was properly propagated + assert.Error(t, err) + assert.Equal(t, assert.AnError, err) + + // Test with both flags causing error + cmd = NewCommand(clients) + testutil.MockCmdIO(clients.IO, cmd) + cmd.SetArgs([]string{"--cli", "--sdk"}) + + // Execute the command and verify it returns the error + err = cmd.ExecuteContext(ctx) + + // Verify the error was properly propagated + assert.Error(t, err) + assert.Equal(t, assert.AnError, err) } diff --git a/docs/reference/commands/slack_upgrade.md b/docs/reference/commands/slack_upgrade.md index 99858cbd..257301a8 100644 --- a/docs/reference/commands/slack_upgrade.md +++ b/docs/reference/commands/slack_upgrade.md @@ -18,12 +18,17 @@ slack upgrade [flags] ``` $ slack upgrade # Check for any available updates +$ slack upgrade --cli # Check for CLI updates and automatically upgrade without confirmation +$ slack upgrade --sdk # Check for SDK updates and automatically upgrade without confirmation +$ slack upgrade --cli --sdk # Check for updates and automatically upgrade both CLI and SDK without confirmation ``` ### Options ``` - -h, --help help for upgrade + --cli automatically approve and install CLI updates without prompting + -h, --help help for upgrade + --sdk automatically approve and install SDK updates without prompting ``` ### Options inherited from parent commands diff --git a/internal/update/cli.go b/internal/update/cli.go index e0407cbf..72f8db40 100644 --- a/internal/update/cli.go +++ b/internal/update/cli.go @@ -87,17 +87,25 @@ func (c *CLIDependency) PrintUpdateNotification(cmd *cobra.Command) (bool, error "\n To update with Homebrew, run: %s\n\n", style.CommandText(fmt.Sprintf("brew update && brew upgrade %s", processName)), ) + return false, nil } else { cmd.Printf( "\n To manually update, visit the download page:\n %s\n\n", style.CommandText("https://tools.slack.dev/slack-cli"), ) + + // Check for cli flag from upgrade command + if cmd.Name() == "upgrade" && cmd.Flags().Changed("cli") { + cli, _ := cmd.Flags().GetBool("cli") + if cli { + return true, nil + } + } + selfUpdatePrompt := fmt.Sprintf("%sDo you want to auto-update to the latest version now?", style.Emoji("rocket")) return c.clients.IO.ConfirmPrompt(ctx, selfUpdatePrompt, false) } - return false, nil - // TODO: Uncomment when open sourced to display the latest release URL that includes release notes // cmd.Printf( // "\n%s\n\n", diff --git a/internal/update/sdk.go b/internal/update/sdk.go index d502bb6a..cf85ffbc 100644 --- a/internal/update/sdk.go +++ b/internal/update/sdk.go @@ -319,6 +319,14 @@ func (c *SDKDependency) PrintUpdateNotification(cmd *cobra.Command) (bool, error // If `install-update` hook available, prompt to auto-update if c.clients.SDKConfig.Hooks.InstallUpdate.IsAvailable() { + // Check for sdk flag from upgrade command + if cmd.Name() == "upgrade" && cmd.Flags().Changed("sdk") { + sdk, _ := cmd.Flags().GetBool("sdk") + if sdk { + return true, nil + } + } + autoUpdatePrompt := fmt.Sprintf("%sDo you want to auto-update to the latest versions now?", style.Emoji("rocket")) return c.clients.IO.ConfirmPrompt(ctx, autoUpdatePrompt, false) } diff --git a/internal/update/update.go b/internal/update/update.go index c323a102..7eed72f7 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -26,9 +26,21 @@ import ( "github.com/slackapi/slack-cli/internal/goutils" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackerror" + "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/cobra" ) +// Functions for type checking that can be replaced in tests +var isDependencyCLI = func(d Dependency) bool { + _, isCLI := d.(*CLIDependency) + return isCLI +} + +var isDependencySDK = func(d Dependency) bool { + _, isSDK := d.(*SDKDependency) + return isSDK +} + // UpdateNotification checks for an update (non-blocking in the background or blocking). // It provides the release information for the latest update of each dependency. type UpdateNotification struct { @@ -251,3 +263,40 @@ func (u *UpdateNotification) isLastUpdateCheckedAtGreaterThan(ctx context.Contex func newHTTPClient() (*http.Client, error) { return api.NewHTTPClient(api.HTTPClientOptions{TotalTimeOut: 60 * time.Second}), nil } + +// InstallUpdatesWithComponentFlags automatically installs updates for specified components +// without prompting the user. This is used by the upgrade command when the --cli +// or --sdk flags are set. +func (u *UpdateNotification) InstallUpdatesWithComponentFlags(cmd *cobra.Command, cli bool, sdk bool) error { + ctx := cmd.Context() + + for _, dependency := range u.Dependencies() { + // Skip dependencies that don't match the specified flags + if isDependencyCLI(dependency) && !cli { + continue + } + if isDependencySDK(dependency) && !sdk { + continue + } + + hasUpdate, err := dependency.HasUpdate() + if err != nil { + return slackerror.Wrap(err, "An error occurred while fetching a dependency") + } + + if hasUpdate { + // Print update notification but skip the confirmation prompt + _, err := dependency.PrintUpdateNotification(cmd) + if err != nil { + return err + } + + // Install the update without prompting + cmd.Printf("%s Installing update automatically...\n", style.Styler().Green("✓").String()) + if err := dependency.InstallUpdate(ctx); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 90bf999c..3be18330 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -23,6 +23,7 @@ import ( "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackdeps" "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -109,3 +110,179 @@ func Test_Update_HasUpdate(t *testing.T) { }) } } + +func Test_Update_InstallUpdatesWithComponentFlags(t *testing.T) { + // Save original type checking functions to restore later + originalIsCLI := isDependencyCLI + originalIsSDK := isDependencySDK + defer func() { + isDependencyCLI = originalIsCLI + isDependencySDK = originalIsSDK + }() + + // Create mock dependencies - one CLI, one SDK + cliDep := new(mockDependency) + sdkDep := new(mockDependency) + + // Setup test cases + for name, tt := range map[string]struct { + cli bool + sdk bool + cliHasUpdate bool + sdkHasUpdate bool + cliInstallError error + sdkInstallError error + expectedErrorContains string + shouldInstallCLI bool + shouldInstallSDK bool + }{ + "Both flags false, both have updates": { + cli: false, + sdk: false, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: false, + shouldInstallSDK: false, + }, + "Only CLI flag set, both have updates": { + cli: true, + sdk: false, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: true, + shouldInstallSDK: false, + }, + "Only SDK flag set, both have updates": { + cli: false, + sdk: true, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: false, + shouldInstallSDK: true, + }, + "Both flags set, both have updates": { + cli: true, + sdk: true, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: true, + shouldInstallSDK: true, + }, + "CLI flag set, CLI fails to install": { + cli: true, + sdk: false, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: assert.AnError, + sdkInstallError: nil, + expectedErrorContains: "general error for testing", + shouldInstallCLI: true, + shouldInstallSDK: false, + }, + "SDK flag set, SDK fails to install": { + cli: false, + sdk: true, + cliHasUpdate: true, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: assert.AnError, + expectedErrorContains: "general error for testing", + shouldInstallCLI: false, + shouldInstallSDK: true, + }, + "CLI flag set, CLI has no update": { + cli: true, + sdk: false, + cliHasUpdate: false, + sdkHasUpdate: true, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: false, + shouldInstallSDK: false, + }, + "SDK flag set, SDK has no update": { + cli: false, + sdk: true, + cliHasUpdate: true, + sdkHasUpdate: false, + cliInstallError: nil, + sdkInstallError: nil, + expectedErrorContains: "", + shouldInstallCLI: false, + shouldInstallSDK: false, + }, + } { + t.Run(name, func(t *testing.T) { + // Create clients + clients := shared.ClientFactory{ + Config: config.NewConfig(slackdeps.NewFsMock(), slackdeps.NewOsMock()), + SDKConfig: hooks.NewSDKConfigMock(), + } + + // Reset mocks + cliDep = new(mockDependency) + sdkDep = new(mockDependency) + + // Setup mock CLI dependency - allowing any number of calls to HasUpdate + cliDep.On("HasUpdate").Return(tt.cliHasUpdate, nil).Maybe() + if tt.cliHasUpdate && tt.shouldInstallCLI { + cliDep.On("PrintUpdateNotification", mock.Anything).Return(false, nil) + cliDep.On("InstallUpdate", mock.Anything).Return(tt.cliInstallError) + } + + // Setup mock SDK dependency - allowing any number of calls to HasUpdate + sdkDep.On("HasUpdate").Return(tt.sdkHasUpdate, nil).Maybe() + if tt.sdkHasUpdate && tt.shouldInstallSDK { + sdkDep.On("PrintUpdateNotification", mock.Anything).Return(false, nil) + sdkDep.On("InstallUpdate", mock.Anything).Return(tt.sdkInstallError) + } + + // Override type checking functions for this test + isDependencyCLI = func(d Dependency) bool { + return d == cliDep + } + isDependencySDK = func(d Dependency) bool { + return d == sdkDep + } + + // Create updateNotification with our dependencies + updateNotification = &UpdateNotification{ + clients: &clients, + enabled: true, + envDisabled: "SLACK_SKIP_UPDATE", + hoursToWait: defaultHoursToWait, + dependencies: []Dependency{cliDep, sdkDep}, + } + + // Create test cmd + cmd := &cobra.Command{} + + // Test + err := updateNotification.InstallUpdatesWithComponentFlags(cmd, tt.cli, tt.sdk) + + // Verify the error + if tt.expectedErrorContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErrorContains) + } else { + require.NoError(t, err) + } + + // Don't assert expectations since we've used .Maybe() + // This avoids strictness in the number of times HasUpdate is called + }) + } +}