Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
110 changes: 104 additions & 6 deletions cmd/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
package docs

import (
"context"
"encoding/json"
"fmt"
"net/url"
"path/filepath"
"strings"

"github.com/slackapi/slack-cli/internal/search"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
Expand All @@ -27,6 +31,7 @@ import (
)

var searchMode bool
var outputFormat string

func NewCommand(clients *shared.ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Expand All @@ -43,26 +48,54 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
Command: "docs --search \"Block Kit\"",
},
{
Meaning: "Open Slack docs search page",
Command: "docs --search",
Meaning: "Search and get JSON results",
Command: "docs --search \"Block Kit\" --output=json",
},
{
Meaning: "Search and open in browser (default)",
Command: "docs --search \"Block Kit\" --output=browser",
},
}),
RunE: func(cmd *cobra.Command, args []string) error {
return runDocsCommand(clients, cmd, args)
},
}

cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page or search with query")
cmd.Flags().BoolVar(&searchMode, "search", false, "search Slack docs with optional query")
cmd.Flags().StringVar(&outputFormat, "output", "browser", "output format: browser, json")

return cmd
}

// runDocsCommand opens Slack developer docs in the browser
// DocsOutput represents the structured output for --json mode
type DocsOutput struct {
URL string `json:"url"`
Query string `json:"query,omitempty"`
Type string `json:"type"` // "homepage", "search", or "search_with_query"
}

// ProgrammaticSearchOutput represents the output from local docs search
type ProgrammaticSearchOutput = search.SearchResponse

// findDocsRepo tries to locate the docs repository
func findDocsRepo() string {
return search.FindDocsRepo()
}

// runProgrammaticSearch executes the local search
func runProgrammaticSearch(query string, docsPath string) (*ProgrammaticSearchOutput, error) {
contentDir := filepath.Join(docsPath, "content")
return search.SearchDocs(query, "", 20, contentDir)
}

// runDocsCommand opens Slack developer docs in the browser or performs programmatic search
func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

var docsURL string
var sectionText string
var query string
var docType string

// Validate: if there are arguments, --search flag must be used
if len(args) > 0 && !cmd.Flags().Changed("search") {
Expand All @@ -75,22 +108,58 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st

if cmd.Flags().Changed("search") {
if len(args) > 0 {
// --search "query" (space-separated) - join all args as the query
query := strings.Join(args, " ")
query = strings.Join(args, " ")

// Check output format
if outputFormat == "json" {
return runProgrammaticSearchCommand(clients, ctx, query)
}

// Default browser search
encodedQuery := url.QueryEscape(query)
docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery)
sectionText = "Docs Search"
docType = "search_with_query"
} else {
// --search (no argument) - open search page
docsURL = "https://docs.slack.dev/search/"
sectionText = "Docs Search"
docType = "search"
}
} else {
// No search flag: default homepage
docsURL = "https://docs.slack.dev"
sectionText = "Docs Open"
docType = "homepage"
}

// Handle JSON output mode (for browser-based results only)
if outputFormat == "json" && !cmd.Flags().Changed("search") {
output := DocsOutput{
URL: docsURL,
Query: query,
Type: docType,
}

jsonBytes, err := json.MarshalIndent(output, "", " ")
if err != nil {
return slackerror.New(slackerror.ErrDocsJSONEncodeFailed)
}

fmt.Println(string(jsonBytes))

// Still print trace for analytics
if cmd.Flags().Changed("search") {
traceValue := query
clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, traceValue)
} else {
clients.IO.PrintTrace(ctx, slacktrace.DocsSuccess)
}

return nil
}

// Standard browser-opening mode
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "books",
Text: sectionText,
Expand All @@ -113,3 +182,32 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st

return nil
}

// runProgrammaticSearchCommand handles local documentation search
func runProgrammaticSearchCommand(clients *shared.ClientFactory, ctx context.Context, query string) error {
// Find the docs repository
docsPath := findDocsRepo()
if docsPath == "" {
clients.IO.PrintError(ctx, "❌ Docs repository not found")
clients.IO.PrintInfo(ctx, false, "💡 Make sure the docs repository is cloned alongside slack-cli")
clients.IO.PrintInfo(ctx, false, " Expected structure:")
clients.IO.PrintInfo(ctx, false, " ├── slack-cli/")
clients.IO.PrintInfo(ctx, false, " └── docs/")
return fmt.Errorf("docs repository not found")
}

// Run the search
results, err := runProgrammaticSearch(query, docsPath)
if err != nil {
clients.IO.PrintError(ctx, "❌ Search failed: %v", err)
return err
}

// Always output JSON for programmatic search
jsonBytes, err := json.MarshalIndent(results, "", " ")
if err != nil {
return fmt.Errorf("failed to encode JSON: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}
Loading
Loading