Skip to content

Commit 36b3af3

Browse files
feat: adds docs command with optional search flag (#352)
Co-authored-by: Ale Mercado <104795114+srtaalej@users.noreply.github.com>
1 parent 33287a3 commit 36b3af3

5 files changed

Lines changed: 261 additions & 0 deletions

File tree

cmd/docs/docs.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2022-2026 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+
package docs
16+
17+
import (
18+
"fmt"
19+
"net/url"
20+
"strings"
21+
22+
"github.com/slackapi/slack-cli/internal/shared"
23+
"github.com/slackapi/slack-cli/internal/slackerror"
24+
"github.com/slackapi/slack-cli/internal/slacktrace"
25+
"github.com/slackapi/slack-cli/internal/style"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
var searchMode bool
30+
31+
func NewCommand(clients *shared.ClientFactory) *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: "docs",
34+
Short: "Open Slack developer docs",
35+
Long: "Open the Slack developer docs in your browser, with optional search functionality",
36+
Example: style.ExampleCommandsf([]style.ExampleCommand{
37+
{
38+
Meaning: "Open Slack developer docs homepage",
39+
Command: "docs",
40+
},
41+
{
42+
Meaning: "Search Slack developer docs for Block Kit",
43+
Command: "docs --search \"Block Kit\"",
44+
},
45+
{
46+
Meaning: "Open Slack docs search page",
47+
Command: "docs --search",
48+
},
49+
}),
50+
RunE: func(cmd *cobra.Command, args []string) error {
51+
return runDocsCommand(clients, cmd, args)
52+
},
53+
}
54+
55+
cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page or search with query")
56+
57+
return cmd
58+
}
59+
60+
// runDocsCommand opens Slack developer docs in the browser
61+
func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
62+
ctx := cmd.Context()
63+
64+
var docsURL string
65+
var sectionText string
66+
67+
// Validate: if there are arguments, --search flag must be used
68+
if len(args) > 0 && !cmd.Flags().Changed("search") {
69+
query := strings.Join(args, " ")
70+
return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithRemediation(
71+
"Use --search flag: %s",
72+
style.Commandf(fmt.Sprintf("docs --search \"%s\"", query), false),
73+
)
74+
}
75+
76+
if cmd.Flags().Changed("search") {
77+
if len(args) > 0 {
78+
// --search "query" (space-separated) - join all args as the query
79+
query := strings.Join(args, " ")
80+
encodedQuery := url.QueryEscape(query)
81+
docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery)
82+
sectionText = "Docs Search"
83+
} else {
84+
// --search (no argument) - open search page
85+
docsURL = "https://docs.slack.dev/search/"
86+
sectionText = "Docs Search"
87+
}
88+
} else {
89+
// No search flag: default homepage
90+
docsURL = "https://docs.slack.dev"
91+
sectionText = "Docs Open"
92+
}
93+
94+
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
95+
Emoji: "books",
96+
Text: sectionText,
97+
Secondary: []string{
98+
docsURL,
99+
},
100+
}))
101+
102+
clients.Browser().OpenURL(docsURL)
103+
104+
if cmd.Flags().Changed("search") {
105+
traceValue := ""
106+
if len(args) > 0 {
107+
traceValue = strings.Join(args, " ")
108+
}
109+
clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, traceValue)
110+
} else {
111+
clients.IO.PrintTrace(ctx, slacktrace.DocsSuccess)
112+
}
113+
114+
return nil
115+
}

cmd/docs/docs_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2022-2026 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+
package docs
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/slackapi/slack-cli/internal/shared"
22+
"github.com/slackapi/slack-cli/internal/slacktrace"
23+
"github.com/slackapi/slack-cli/test/testutil"
24+
"github.com/spf13/cobra"
25+
"github.com/stretchr/testify/mock"
26+
)
27+
28+
func Test_Docs_DocsCommand(t *testing.T) {
29+
testutil.TableTestCommand(t, testutil.CommandTests{
30+
"opens docs homepage without search": {
31+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
32+
},
33+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
34+
expectedURL := "https://docs.slack.dev"
35+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
36+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSuccess, mock.Anything)
37+
},
38+
ExpectedOutputs: []string{
39+
"Docs Open",
40+
"https://docs.slack.dev",
41+
},
42+
},
43+
"fails when positional argument provided without search flag": {
44+
CmdArgs: []string{"Block Kit"},
45+
ExpectedErrorStrings: []string{"Invalid docs command. Did you mean to search?"},
46+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
47+
// No browser calls should be made when command fails
48+
cm.Browser.AssertNotCalled(t, "OpenURL")
49+
},
50+
},
51+
"fails when multiple positional arguments provided without search flag": {
52+
CmdArgs: []string{"webhook", "send", "message"},
53+
ExpectedErrorStrings: []string{"Invalid docs command. Did you mean to search?"},
54+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
55+
// No browser calls should be made when command fails
56+
cm.Browser.AssertNotCalled(t, "OpenURL")
57+
},
58+
},
59+
"opens docs with search query using space syntax": {
60+
CmdArgs: []string{"--search", "messaging"},
61+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
62+
expectedURL := "https://docs.slack.dev/search/?q=messaging"
63+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
64+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
65+
},
66+
ExpectedOutputs: []string{
67+
"Docs Search",
68+
"https://docs.slack.dev/search/?q=messaging",
69+
},
70+
},
71+
"handles search with multiple arguments": {
72+
CmdArgs: []string{"--search", "Block", "Kit", "Element"},
73+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
74+
expectedURL := "https://docs.slack.dev/search/?q=Block+Kit+Element"
75+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
76+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
77+
},
78+
ExpectedOutputs: []string{
79+
"Docs Search",
80+
"https://docs.slack.dev/search/?q=Block+Kit+Element",
81+
},
82+
},
83+
"handles search query with multiple words": {
84+
CmdArgs: []string{"--search", "socket mode"},
85+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
86+
expectedURL := "https://docs.slack.dev/search/?q=socket+mode"
87+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
88+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
89+
},
90+
ExpectedOutputs: []string{
91+
"Docs Search",
92+
"https://docs.slack.dev/search/?q=socket+mode",
93+
},
94+
},
95+
"handles special characters in search query": {
96+
CmdArgs: []string{"--search", "messages & webhooks"},
97+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
98+
expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks"
99+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
100+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
101+
},
102+
ExpectedOutputs: []string{
103+
"Docs Search",
104+
"https://docs.slack.dev/search/?q=messages+%26+webhooks",
105+
},
106+
},
107+
"handles search query with quotes": {
108+
CmdArgs: []string{"--search", "webhook \"send message\""},
109+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
110+
expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22"
111+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
112+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
113+
},
114+
ExpectedOutputs: []string{
115+
"Docs Search",
116+
"https://docs.slack.dev/search/?q=webhook+%22send+message%22",
117+
},
118+
},
119+
"handles search flag without argument": {
120+
CmdArgs: []string{"--search"},
121+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
122+
expectedURL := "https://docs.slack.dev/search/"
123+
cm.Browser.AssertCalled(t, "OpenURL", expectedURL)
124+
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
125+
},
126+
ExpectedOutputs: []string{
127+
"Docs Search",
128+
"https://docs.slack.dev/search/",
129+
},
130+
},
131+
}, func(cf *shared.ClientFactory) *cobra.Command {
132+
return NewCommand(cf)
133+
})
134+
}

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/slackapi/slack-cli/cmd/collaborators"
2929
"github.com/slackapi/slack-cli/cmd/datastore"
3030
"github.com/slackapi/slack-cli/cmd/docgen"
31+
"github.com/slackapi/slack-cli/cmd/docs"
3132
"github.com/slackapi/slack-cli/cmd/doctor"
3233
"github.com/slackapi/slack-cli/cmd/env"
3334
"github.com/slackapi/slack-cli/cmd/externalauth"
@@ -95,6 +96,7 @@ func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.Up
9596
{Command: "init", Meaning: "Initialize an existing Slack app"},
9697
{Command: "run", Meaning: "Start a local development server"},
9798
{Command: "deploy", Meaning: "Deploy to the Slack Platform"},
99+
{Command: "docs", Meaning: "Open Slack developer docs"},
98100
}),
99101
Long: strings.Join([]string{
100102
`{{Emoji "sparkles"}}CLI to create, run, and deploy Slack apps`,
@@ -184,6 +186,7 @@ func Init(ctx context.Context) (*cobra.Command, *shared.ClientFactory) {
184186
rootCmd.CompletionOptions.HiddenDefaultCmd = true
185187

186188
topLevelCommands := []*cobra.Command{
189+
docs.NewCommand(clients),
187190
doctor.NewDoctorCommand(clients),
188191
feedback.NewFeedbackCommand(clients),
189192
}

internal/slackerror/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const (
9696
ErrDenoNotFound = "deno_not_found"
9797
ErrDeployedAppNotSupported = "deployed_app_not_supported"
9898
ErrDocumentationGenerationFailed = "documentation_generation_failed"
99+
ErrDocsSearchFlagRequired = "docs_search_flag_required"
99100
ErrEnterpriseNotFound = "enterprise_not_found"
100101
ErrFailedAddingCollaborator = "failed_adding_collaborator"
101102
ErrFailedCreatingApp = "failed_creating_app"
@@ -680,6 +681,12 @@ Otherwise start your app for local development with: %s`,
680681
Message: "Failed to generate documentation",
681682
},
682683

684+
ErrDocsSearchFlagRequired: {
685+
Code: ErrDocsSearchFlagRequired,
686+
Message: "Invalid docs command. Did you mean to search?",
687+
Remediation: fmt.Sprintf("Use --search flag: %s", style.Commandf("docs --search \"<query>\"", false)),
688+
},
689+
683690
ErrEnterpriseNotFound: {
684691
Code: ErrEnterpriseNotFound,
685692
Message: "The `enterprise` was not found",

internal/slacktrace/slacktrace.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ const (
7474
DatastoreCountDatastore = "SLACK_TRACE_DATASTORE_COUNT_DATASTORE"
7575
DatastoreCountSuccess = "SLACK_TRACE_DATASTORE_COUNT_SUCCESS"
7676
DatastoreCountTotal = "SLACK_TRACE_DATASTORE_COUNT_TOTAL"
77+
DocsSearchSuccess = "SLACK_TRACE_DOCS_SEARCH_SUCCESS"
78+
DocsSuccess = "SLACK_TRACE_DOCS_SUCCESS"
7779
EnvAddSuccess = "SLACK_TRACE_ENV_ADD_SUCCESS"
7880
EnvListCount = "SLACK_TRACE_ENV_LIST_COUNT"
7981
EnvListVariables = "SLACK_TRACE_ENV_LIST_VARIABLES"

0 commit comments

Comments
 (0)