Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
91 changes: 91 additions & 0 deletions cmd/docs/docs.go
Original file line number Diff line number Diff line change
@@ -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

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 = 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,
},
}))
Comment thread
lukegalbraithrussell marked this conversation as resolved.

clients.Browser().OpenURL(docsURL)

// Add trace for analytics
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.

🧪 note: We use these traces for testing certain paths are reached in our E2E tests:

$ SLACK_TEST_TRACE=1 slack docs

It might be nice to update or remove this comment to avoid ongoing confusion toward this!

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{
"Slack developer docs",
"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{
"Searching Slack developer docs: \"messaging\"",
"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{
"Searching Slack developer docs: \"socket mode\"",
"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{
"Searching Slack developer docs: \"messages & webhooks\"",
"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{
"Searching Slack developer docs: \"webhook \"send message\"\"",
"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{
"Slack developer docs",
"https://docs.slack.dev",
},
},
}, 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