Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions cmd/docs/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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

Comment on lines +29 to +30
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var searchMode bool

🪓 suggesetion(non-blocking): Related to the earlier comment!

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 docs")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 suggestion: Related to the "required" search value if the "--search" flag is provided - I wonder if we can change this to be a "boolean" value?

This might allow this command to accept 1 argument instead of 0 for sake of being the search query? We might then search if that flag is provided or if an argument is used:

$ slack docs --search
$ slack docs "Block Kit"
$ slack docs --search "Block Kit"

We ought not need to document the second case IMHO since it's identical to the third, and I don't think such approach limits future changes. Not a blocker here but the examples might need updating before merge!


return cmd
}

// runDocsCommand opens Slack developer docs 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 = "Docs Search"
} else {
// Default docs homepage
docsURL = "https://docs.slack.dev"
sectionText = "Docs Open"
}

clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "books",
Text: sectionText,
Secondary: []string{
docsURL,
},
}))
Comment thread
lukegalbraithrussell marked this conversation as resolved.

clients.Browser().OpenURL(docsURL)

if searchFlag != "" {
clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, searchFlag)
} else {
clients.IO.PrintTrace(ctx, slacktrace.DocsSuccess)
}

return nil
}
106 changes: 106 additions & 0 deletions cmd/docs/docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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) {
Comment thread
lukegalbraithrussell marked this conversation as resolved.
testutil.TableTestCommand(t, testutil.CommandTests{
"opens docs homepage without search": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
},
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{
"Docs Open",
"https://docs.slack.dev",
},
},
"opens docs with basic search query": {
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)
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
},
ExpectedOutputs: []string{
"Docs Search",
"https://docs.slack.dev/search/?q=messaging",
},
},
"handles search query with multiple words": {
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)
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
},
ExpectedOutputs: []string{
"Docs Search",
"https://docs.slack.dev/search/?q=socket+mode",
},
},
"handles special characters in search query": {
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)
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
},
ExpectedOutputs: []string{
"Docs Search",
"https://docs.slack.dev/search/?q=messages+%26+webhooks",
},
},
"handles search query with quotes": {
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)
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything)
},
ExpectedOutputs: []string{
"Docs Search",
"https://docs.slack.dev/search/?q=webhook+%22send+message%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{
"Docs Open",
"https://docs.slack.dev",
},
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 issue: This case is a bit misleading to me! I tried using the search flag without an argument to find this error:

$ slack docs --search
Check /Users/eden.zimbelman/.slack/logs/slack-debug-20260226.log for error logs
flag needs an argument: --search

should this instead open docs.slack.dev/search?

I do think this would be ideal - perhaps "https://docs.slack.dev/search/" complete - if possible, without an argument?

}, func(cf *shared.ClientFactory) *cobra.Command {
return NewCommand(cf)
})
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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"
Expand Down Expand Up @@ -95,6 +96,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"},
Comment thread
lukegalbraithrussell marked this conversation as resolved.
}),
Long: strings.Join([]string{
`{{Emoji "sparkles"}}CLI to create, run, and deploy Slack apps`,
Expand Down Expand Up @@ -184,6 +186,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),
}
Expand Down
2 changes: 2 additions & 0 deletions internal/slacktrace/slacktrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading