From 90dff7c2a205885cbfd4465cd2ac9232824acade Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Feb 2026 13:54:18 -0800 Subject: [PATCH 01/10] adds command --- cmd/docs/docs.go | 91 +++++++++++++++++++++++ cmd/docs/docs_test.go | 119 ++++++++++++++++++++++++++++++ cmd/root.go | 3 + internal/slacktrace/slacktrace.go | 2 + 4 files changed, 215 insertions(+) create mode 100644 cmd/docs/docs.go create mode 100644 cmd/docs/docs_test.go diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go new file mode 100644 index 00000000..9edecbd3 --- /dev/null +++ b/cmd/docs/docs.go @@ -0,0 +1,91 @@ +// Copyright 2022-2026 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. + +package docs + +import ( + "fmt" + "net/url" + + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slacktrace" + "github.com/slackapi/slack-cli/internal/style" + "github.com/spf13/cobra" +) + +var searchFlag string + +func NewCommand(clients *shared.ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "docs", + Short: "Open Slack developer docs", + Long: "Open the Slack developer docs in your browser, with optional search functionality", + Example: style.ExampleCommandsf([]style.ExampleCommand{ + { + Meaning: "Open Slack developer docs homepage", + Command: "docs", + }, + { + Meaning: "Search Slack developer docs", + Command: "docs --search 'Block Kit'", + }, + }), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runDocsCommand(clients, cmd, args) + }, + } + + cmd.Flags().StringVar(&searchFlag, "search", "", "search query for Slack documentation") + + return cmd +} + +// runDocsCommand opens Slack developer documentation in the browser +func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + var docsURL string + var sectionText string + + if searchFlag != "" { + // Build search URL + searchQuery := url.QueryEscape(searchFlag) + docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", searchQuery) + sectionText = fmt.Sprintf("Searching Slack developer docs: \"%s\"", searchFlag) + } else { + // Default docs homepage + docsURL = "https://docs.slack.dev" + sectionText = "Slack developer docs" + } + + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: sectionText, + Secondary: []string{ + docsURL, + }, + })) + + clients.Browser().OpenURL(docsURL) + + // Add trace for analytics + if searchFlag != "" { + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, searchFlag) + } else { + clients.IO.PrintTrace(ctx, slacktrace.DocsSuccess) + } + + return nil +} diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go new file mode 100644 index 00000000..39f5c9ad --- /dev/null +++ b/cmd/docs/docs_test.go @@ -0,0 +1,119 @@ +// Copyright 2022-2026 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. + +package docs + +import ( + "context" + "testing" + + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slacktrace" + "github.com/slackapi/slack-cli/test/testutil" + "github.com/spf13/cobra" + "github.com/stretchr/testify/mock" +) + +func Test_Docs_DocsCommand(t *testing.T) { + testutil.TableTestCommand(t, testutil.CommandTests{ + "opens docs homepage without search": { + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + // No special setup needed for basic functionality + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Slack developer docs", + "https://docs.slack.dev", + }, + }, + "opens docs with basic search query": { + CmdArgs: []string{"--search", "Block Kit"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=Block+Kit" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Searching Slack developer docs: \"Block Kit\"", + "https://docs.slack.dev/search/?q=Block+Kit", + }, + }, + "handles search query with multiple words": { + CmdArgs: []string{"--search", "socket mode authentication"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=socket+mode+authentication" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Searching Slack developer docs: \"socket mode authentication\"", + "https://docs.slack.dev/search/?q=socket+mode+authentication", + }, + }, + "handles special characters in search query": { + CmdArgs: []string{"--search", "API & webhooks"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=API+%26+webhooks" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Searching Slack developer docs: \"API & webhooks\"", + "https://docs.slack.dev/search/?q=API+%26+webhooks", + }, + }, + "handles search query with quotes": { + CmdArgs: []string{"--search", "function \"hello world\""}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=function+%22hello+world%22" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Searching Slack developer docs: \"function \"hello world\"\"", + "https://docs.slack.dev/search/?q=function+%22hello+world%22", + }, + }, + "handles empty search query as homepage": { + CmdArgs: []string{"--search", ""}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Slack developer docs", + "https://docs.slack.dev", + }, + }, + "handles the exact user request example": { + CmdArgs: []string{"--search", "something example"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=something+example" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Searching Slack developer docs: \"something example\"", + "https://docs.slack.dev/search/?q=something+example", + }, + }, + }, func(cf *shared.ClientFactory) *cobra.Command { + return NewCommand(cf) + }) +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index e7358a35..84924b56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,7 @@ import ( "github.com/slackapi/slack-cli/cmd/collaborators" "github.com/slackapi/slack-cli/cmd/datastore" "github.com/slackapi/slack-cli/cmd/docgen" + "github.com/slackapi/slack-cli/cmd/docs" "github.com/slackapi/slack-cli/cmd/doctor" "github.com/slackapi/slack-cli/cmd/env" "github.com/slackapi/slack-cli/cmd/externalauth" @@ -94,6 +95,7 @@ func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.Up {Command: "init", Meaning: "Initialize an existing Slack app"}, {Command: "run", Meaning: "Start a local development server"}, {Command: "deploy", Meaning: "Deploy to the Slack Platform"}, + {Command: "docs", Meaning: "Open Slack developer docs"}, }), Long: strings.Join([]string{ `{{Emoji "sparkles"}}CLI to create, run, and deploy Slack apps`, @@ -179,6 +181,7 @@ func Init(ctx context.Context) (*cobra.Command, *shared.ClientFactory) { rootCmd.CompletionOptions.HiddenDefaultCmd = true topLevelCommands := []*cobra.Command{ + docs.NewCommand(clients), doctor.NewDoctorCommand(clients), feedback.NewFeedbackCommand(clients), } diff --git a/internal/slacktrace/slacktrace.go b/internal/slacktrace/slacktrace.go index 8bd9253f..a75bb904 100644 --- a/internal/slacktrace/slacktrace.go +++ b/internal/slacktrace/slacktrace.go @@ -74,6 +74,8 @@ const ( DatastoreCountDatastore = "SLACK_TRACE_DATASTORE_COUNT_DATASTORE" DatastoreCountSuccess = "SLACK_TRACE_DATASTORE_COUNT_SUCCESS" DatastoreCountTotal = "SLACK_TRACE_DATASTORE_COUNT_TOTAL" + DocsSearchSuccess = "SLACK_TRACE_DOCS_SEARCH_SUCCESS" + DocsSuccess = "SLACK_TRACE_DOCS_SUCCESS" EnvAddSuccess = "SLACK_TRACE_ENV_ADD_SUCCESS" EnvListCount = "SLACK_TRACE_ENV_LIST_COUNT" EnvListVariables = "SLACK_TRACE_ENV_LIST_VARIABLES" From 5fd56a2889ef2bc57e79aef9bdbb1d628fa9fbb4 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Feb 2026 13:56:14 -0800 Subject: [PATCH 02/10] docs not documentation --- cmd/docs/docs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 9edecbd3..ab72ef45 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -47,12 +47,12 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { }, } - cmd.Flags().StringVar(&searchFlag, "search", "", "search query for Slack documentation") + cmd.Flags().StringVar(&searchFlag, "search", "", "search query for Slack docs") return cmd } -// runDocsCommand opens Slack developer documentation in the browser +// runDocsCommand opens Slack developer docs in the browser func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error { ctx := cmd.Context() From 45396a663e8d36f7cb6cbf3901af007c186f09d3 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Feb 2026 14:57:02 -0800 Subject: [PATCH 03/10] go --- cmd/docs/docs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index 39f5c9ad..f2d73b7f 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -116,4 +116,4 @@ func Test_Docs_DocsCommand(t *testing.T) { }, func(cf *shared.ClientFactory) *cobra.Command { return NewCommand(cf) }) -} \ No newline at end of file +} From 2c2c5e8dcf08ea0b20c3dd22b8889c63bf1aed27 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Feb 2026 15:09:43 -0800 Subject: [PATCH 04/10] cleaning up tests --- cmd/docs/docs_test.go | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index f2d73b7f..de9eb134 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -42,39 +42,39 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "opens docs with basic search query": { - CmdArgs: []string{"--search", "Block Kit"}, + CmdArgs: []string{"--search", "messaging"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=Block+Kit" + expectedURL := "https://docs.slack.dev/search/?q=messaging" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"Block Kit\"", - "https://docs.slack.dev/search/?q=Block+Kit", + "Searching Slack developer docs: \"messaging\"", + "https://docs.slack.dev/search/?q=messaging", }, }, "handles search query with multiple words": { - CmdArgs: []string{"--search", "socket mode authentication"}, + CmdArgs: []string{"--search", "socket mode"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=socket+mode+authentication" + expectedURL := "https://docs.slack.dev/search/?q=socket+mode" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"socket mode authentication\"", - "https://docs.slack.dev/search/?q=socket+mode+authentication", + "Searching Slack developer docs: \"socket mode\"", + "https://docs.slack.dev/search/?q=socket+mode", }, }, "handles special characters in search query": { - CmdArgs: []string{"--search", "API & webhooks"}, + CmdArgs: []string{"--search", "messages & webhooks"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=API+%26+webhooks" + expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"API & webhooks\"", - "https://docs.slack.dev/search/?q=API+%26+webhooks", + "Searching Slack developer docs: \"messages & webhooks\"", + "https://docs.slack.dev/search/?q=messages+%26+webhooks", }, }, "handles search query with quotes": { @@ -101,18 +101,6 @@ func Test_Docs_DocsCommand(t *testing.T) { "https://docs.slack.dev", }, }, - "handles the exact user request example": { - CmdArgs: []string{"--search", "something example"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=something+example" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) - cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) - }, - ExpectedOutputs: []string{ - "Searching Slack developer docs: \"something example\"", - "https://docs.slack.dev/search/?q=something+example", - }, - }, }, func(cf *shared.ClientFactory) *cobra.Command { return NewCommand(cf) }) From 6af0af771c5841f626abf1822083389cd19993db Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Wed, 25 Feb 2026 08:59:36 -0800 Subject: [PATCH 05/10] better test value --- cmd/docs/docs_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index de9eb134..a2d5505e 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -29,7 +29,6 @@ func Test_Docs_DocsCommand(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "opens docs homepage without search": { Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { - // No special setup needed for basic functionality }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev" @@ -78,15 +77,15 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles search query with quotes": { - CmdArgs: []string{"--search", "function \"hello world\""}, + CmdArgs: []string{"--search", "webhook \"send message\""}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=function+%22hello+world%22" + expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"function \"hello world\"\"", - "https://docs.slack.dev/search/?q=function+%22hello+world%22", + "Searching Slack developer docs: \"webhook \"send message\"\"", + "https://docs.slack.dev/search/?q=webhook+%22send+message%22", }, }, "handles empty search query as homepage": { From aad04d0b346ccbc2f250f18b500e0288fde5aae5 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Feb 2026 08:50:15 -0800 Subject: [PATCH 06/10] feedback from edengod --- cmd/docs/docs.go | 5 ++--- cmd/docs/docs_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index ab72ef45..47f2cf99 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -63,11 +63,11 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st // Build search URL searchQuery := url.QueryEscape(searchFlag) docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", searchQuery) - sectionText = fmt.Sprintf("Searching Slack developer docs: \"%s\"", searchFlag) + sectionText = "Docs Search" } else { // Default docs homepage docsURL = "https://docs.slack.dev" - sectionText = "Slack developer docs" + sectionText = "Docs Open" } clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ @@ -80,7 +80,6 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st clients.Browser().OpenURL(docsURL) - // Add trace for analytics if searchFlag != "" { clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, searchFlag) } else { diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index a2d5505e..dcc34509 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -36,7 +36,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Slack developer docs", + "Docs Open", "https://docs.slack.dev", }, }, @@ -48,7 +48,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"messaging\"", + "Docs Search", "https://docs.slack.dev/search/?q=messaging", }, }, @@ -60,7 +60,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"socket mode\"", + "Docs Search", "https://docs.slack.dev/search/?q=socket+mode", }, }, @@ -72,7 +72,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"messages & webhooks\"", + "Docs Search", "https://docs.slack.dev/search/?q=messages+%26+webhooks", }, }, @@ -84,7 +84,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Searching Slack developer docs: \"webhook \"send message\"\"", + "Docs Search", "https://docs.slack.dev/search/?q=webhook+%22send+message%22", }, }, @@ -96,7 +96,7 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Slack developer docs", + "Docs Open", "https://docs.slack.dev", }, }, From 1d129273df5ee817d3668eb7e5a8915f355b7223 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Feb 2026 10:25:57 -0800 Subject: [PATCH 07/10] go --- cmd/docs/docs.go | 38 +++++++++++++++++++++++++++----------- cmd/docs/docs_test.go | 32 ++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 47f2cf99..5c001c4a 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -24,7 +24,7 @@ import ( "github.com/spf13/cobra" ) -var searchFlag string +var searchMode bool func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ @@ -37,17 +37,25 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { Command: "docs", }, { - Meaning: "Search Slack developer docs", - Command: "docs --search 'Block Kit'", + Meaning: "Open Slack docs search page", + Command: "docs --search", + }, + { + Meaning: "Search Slack docs", + Command: "docs --search \"Block Kit\"", + }, + { + Meaning: "Search Slack docs without search flag", + Command: "docs \"Block Kit\"", }, }), - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), // Allow 0-1 arguments for search query RunE: func(cmd *cobra.Command, args []string) error { return runDocsCommand(clients, cmd, args) }, } - cmd.Flags().StringVar(&searchFlag, "search", "", "search query for Slack docs") + cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page") return cmd } @@ -59,13 +67,17 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st var docsURL string var sectionText string - if searchFlag != "" { - // Build search URL - searchQuery := url.QueryEscape(searchFlag) + if len(args) > 0 { + // Search query provided as positional argument: slack docs "query" + searchQuery := url.QueryEscape(args[0]) docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", searchQuery) sectionText = "Docs Search" + } else if searchMode { + // Search flag provided without query: slack docs --search + docsURL = "https://docs.slack.dev/search/" + sectionText = "Docs Search" } else { - // Default docs homepage + // Default homepage: slack docs docsURL = "https://docs.slack.dev" sectionText = "Docs Open" } @@ -80,8 +92,12 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st clients.Browser().OpenURL(docsURL) - if searchFlag != "" { - clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, searchFlag) + if len(args) > 0 || searchMode { + traceValue := "" + if len(args) > 0 { + traceValue = args[0] + } + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, traceValue) } else { clients.IO.PrintTrace(ctx, slacktrace.DocsSuccess) } diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index dcc34509..b319bbd6 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -41,7 +41,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "opens docs with basic search query": { - CmdArgs: []string{"--search", "messaging"}, + CmdArgs: []string{"messaging"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=messaging" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -53,7 +53,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles search query with multiple words": { - CmdArgs: []string{"--search", "socket mode"}, + CmdArgs: []string{"socket mode"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=socket+mode" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -65,7 +65,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles special characters in search query": { - CmdArgs: []string{"--search", "messages & webhooks"}, + CmdArgs: []string{"messages & webhooks"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -77,7 +77,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles search query with quotes": { - CmdArgs: []string{"--search", "webhook \"send message\""}, + CmdArgs: []string{"webhook \"send message\""}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -88,16 +88,28 @@ func Test_Docs_DocsCommand(t *testing.T) { "https://docs.slack.dev/search/?q=webhook+%22send+message%22", }, }, - "handles empty search query as homepage": { - CmdArgs: []string{"--search", ""}, + "handles empty search query": { + CmdArgs: []string{""}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev" + expectedURL := "https://docs.slack.dev/search/?q=" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) - cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ - "Docs Open", - "https://docs.slack.dev", + "Docs Search", + "https://docs.slack.dev/search/?q=", + }, + }, + "handles search flag without argument": { + CmdArgs: []string{"--search"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/", }, }, }, func(cf *shared.ClientFactory) *cobra.Command { From 29ee8eed9c6ecd79f825da16622293c4e509abd6 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Feb 2026 13:47:14 -0800 Subject: [PATCH 08/10] updates to be better --- cmd/docs/docs.go | 48 ++++++++++++++++++++--------------- cmd/docs/docs_test.go | 30 ++++++++++------------ internal/slackerror/errors.go | 7 +++++ 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 5c001c4a..d9fbfa9d 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -19,6 +19,7 @@ import ( "net/url" "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/slacktrace" "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/cobra" @@ -37,25 +38,20 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { Command: "docs", }, { - Meaning: "Open Slack docs search page", - Command: "docs --search", - }, - { - Meaning: "Search Slack docs", + Meaning: "Search Slack developer docs for Block Kit", Command: "docs --search \"Block Kit\"", }, { - Meaning: "Search Slack docs without search flag", - Command: "docs \"Block Kit\"", + Meaning: "Open Slack docs search page", + Command: "docs --search", }, }), - Args: cobra.MaximumNArgs(1), // Allow 0-1 arguments for search query RunE: func(cmd *cobra.Command, args []string) error { return runDocsCommand(clients, cmd, args) }, } - cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page") + cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page or search with query") return cmd } @@ -67,17 +63,27 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st var docsURL string var sectionText string - if len(args) > 0 { - // Search query provided as positional argument: slack docs "query" - searchQuery := url.QueryEscape(args[0]) - docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", searchQuery) - sectionText = "Docs Search" - } else if searchMode { - // Search flag provided without query: slack docs --search - docsURL = "https://docs.slack.dev/search/" - sectionText = "Docs Search" + // Validate: if there are arguments, --search flag must be used + if len(args) > 0 && !cmd.Flags().Changed("search") { + return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithRemediation( + "Use --search flag: %s", + style.Commandf(fmt.Sprintf("docs --search \"%s\"", args[0]), false), + ) + } + + if cmd.Flags().Changed("search") { + if len(args) > 0 { + // --search "query" (space-separated) - use the first arg as the query + encodedQuery := url.QueryEscape(args[0]) + docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) + sectionText = "Docs Search" + } else { + // --search (no argument) - open search page + docsURL = "https://docs.slack.dev/search/" + sectionText = "Docs Search" + } } else { - // Default homepage: slack docs + // No search flag: default homepage docsURL = "https://docs.slack.dev" sectionText = "Docs Open" } @@ -92,10 +98,10 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st clients.Browser().OpenURL(docsURL) - if len(args) > 0 || searchMode { + if cmd.Flags().Changed("search") { traceValue := "" if len(args) > 0 { - traceValue = args[0] + traceValue = args[0] // For space-separated syntax } clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, traceValue) } else { diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index b319bbd6..f59bb919 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -40,8 +40,16 @@ func Test_Docs_DocsCommand(t *testing.T) { "https://docs.slack.dev", }, }, - "opens docs with basic search query": { - CmdArgs: []string{"messaging"}, + "fails when positional argument provided without search flag": { + CmdArgs: []string{"Block Kit"}, + ExpectedErrorStrings: []string{"Invalid docs command. Did you mean to search?"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + // No browser calls should be made when command fails + cm.Browser.AssertNotCalled(t, "OpenURL") + }, + }, + "opens docs with search query using space syntax": { + CmdArgs: []string{"--search", "messaging"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=messaging" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -53,7 +61,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles search query with multiple words": { - CmdArgs: []string{"socket mode"}, + CmdArgs: []string{"--search", "socket mode"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=socket+mode" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -65,7 +73,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles special characters in search query": { - CmdArgs: []string{"messages & webhooks"}, + CmdArgs: []string{"--search", "messages & webhooks"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -77,7 +85,7 @@ func Test_Docs_DocsCommand(t *testing.T) { }, }, "handles search query with quotes": { - CmdArgs: []string{"webhook \"send message\""}, + CmdArgs: []string{"--search", "webhook \"send message\""}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22" cm.Browser.AssertCalled(t, "OpenURL", expectedURL) @@ -88,18 +96,6 @@ func Test_Docs_DocsCommand(t *testing.T) { "https://docs.slack.dev/search/?q=webhook+%22send+message%22", }, }, - "handles empty search query": { - CmdArgs: []string{""}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) - cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) - }, - ExpectedOutputs: []string{ - "Docs Search", - "https://docs.slack.dev/search/?q=", - }, - }, "handles search flag without argument": { CmdArgs: []string{"--search"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { diff --git a/internal/slackerror/errors.go b/internal/slackerror/errors.go index 0ad6dd7e..05f24c88 100644 --- a/internal/slackerror/errors.go +++ b/internal/slackerror/errors.go @@ -96,6 +96,7 @@ const ( ErrDenoNotFound = "deno_not_found" ErrDeployedAppNotSupported = "deployed_app_not_supported" ErrDocumentationGenerationFailed = "documentation_generation_failed" + ErrDocsSearchFlagRequired = "docs_search_flag_required" ErrEnterpriseNotFound = "enterprise_not_found" ErrFailedAddingCollaborator = "failed_adding_collaborator" ErrFailedCreatingApp = "failed_creating_app" @@ -679,6 +680,12 @@ Otherwise start your app for local development with: %s`, Message: "Failed to generate documentation", }, + ErrDocsSearchFlagRequired: { + Code: ErrDocsSearchFlagRequired, + Message: "Invalid docs command. Did you mean to search?", + Remediation: fmt.Sprintf("Use --search flag: %s", style.Commandf("docs --search \"\"", false)), + }, + ErrEnterpriseNotFound: { Code: ErrEnterpriseNotFound, Message: "The `enterprise` was not found", From 35153a9f1217ace8a96735bca4abd6b905291dac Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Feb 2026 13:50:09 -0800 Subject: [PATCH 09/10] lint --- cmd/docs/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index d9fbfa9d..c7db4d04 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -66,7 +66,7 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st // Validate: if there are arguments, --search flag must be used if len(args) > 0 && !cmd.Flags().Changed("search") { return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithRemediation( - "Use --search flag: %s", + "Use --search flag: %s", style.Commandf(fmt.Sprintf("docs --search \"%s\"", args[0]), false), ) } From 7a2b2b93f8a51b319cdb1990f79c8fa873966409 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Feb 2026 17:31:33 -0800 Subject: [PATCH 10/10] takes all inputs --- cmd/docs/docs.go | 11 +++++++---- cmd/docs/docs_test.go | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index c7db4d04..9b47c3e8 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -17,6 +17,7 @@ package docs import ( "fmt" "net/url" + "strings" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackerror" @@ -65,16 +66,18 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st // Validate: if there are arguments, --search flag must be used if len(args) > 0 && !cmd.Flags().Changed("search") { + query := strings.Join(args, " ") return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithRemediation( "Use --search flag: %s", - style.Commandf(fmt.Sprintf("docs --search \"%s\"", args[0]), false), + style.Commandf(fmt.Sprintf("docs --search \"%s\"", query), false), ) } if cmd.Flags().Changed("search") { if len(args) > 0 { - // --search "query" (space-separated) - use the first arg as the query - encodedQuery := url.QueryEscape(args[0]) + // --search "query" (space-separated) - join all args as the query + query := strings.Join(args, " ") + encodedQuery := url.QueryEscape(query) docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) sectionText = "Docs Search" } else { @@ -101,7 +104,7 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st if cmd.Flags().Changed("search") { traceValue := "" if len(args) > 0 { - traceValue = args[0] // For space-separated syntax + traceValue = strings.Join(args, " ") } clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, traceValue) } else { diff --git a/cmd/docs/docs_test.go b/cmd/docs/docs_test.go index f59bb919..b2996c40 100644 --- a/cmd/docs/docs_test.go +++ b/cmd/docs/docs_test.go @@ -48,6 +48,14 @@ func Test_Docs_DocsCommand(t *testing.T) { cm.Browser.AssertNotCalled(t, "OpenURL") }, }, + "fails when multiple positional arguments provided without search flag": { + CmdArgs: []string{"webhook", "send", "message"}, + ExpectedErrorStrings: []string{"Invalid docs command. Did you mean to search?"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + // No browser calls should be made when command fails + cm.Browser.AssertNotCalled(t, "OpenURL") + }, + }, "opens docs with search query using space syntax": { CmdArgs: []string{"--search", "messaging"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -60,6 +68,18 @@ func Test_Docs_DocsCommand(t *testing.T) { "https://docs.slack.dev/search/?q=messaging", }, }, + "handles search with multiple arguments": { + CmdArgs: []string{"--search", "Block", "Kit", "Element"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=Block+Kit+Element" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=Block+Kit+Element", + }, + }, "handles search query with multiple words": { CmdArgs: []string{"--search", "socket mode"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {