1515package docs
1616
1717import (
18+ "context"
19+ "encoding/json"
1820 "fmt"
1921 "net/url"
22+ "path/filepath"
2023 "strings"
2124
25+ "github.com/slackapi/slack-cli/internal/search"
2226 "github.com/slackapi/slack-cli/internal/shared"
2327 "github.com/slackapi/slack-cli/internal/slackerror"
2428 "github.com/slackapi/slack-cli/internal/slacktrace"
@@ -27,6 +31,7 @@ import (
2731)
2832
2933var searchMode bool
34+ var outputFormat string
3035
3136func NewCommand (clients * shared.ClientFactory ) * cobra.Command {
3237 cmd := & cobra.Command {
@@ -43,26 +48,54 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
4348 Command : "docs --search \" Block Kit\" " ,
4449 },
4550 {
46- Meaning : "Open Slack docs search page" ,
47- Command : "docs --search" ,
51+ Meaning : "Search and get JSON results" ,
52+ Command : "docs --search \" Block Kit\" --output=json" ,
53+ },
54+ {
55+ Meaning : "Search and open in browser (default)" ,
56+ Command : "docs --search \" Block Kit\" --output=browser" ,
4857 },
4958 }),
5059 RunE : func (cmd * cobra.Command , args []string ) error {
5160 return runDocsCommand (clients , cmd , args )
5261 },
5362 }
5463
55- cmd .Flags ().BoolVar (& searchMode , "search" , false , "open Slack docs search page or search with query" )
64+ cmd .Flags ().BoolVar (& searchMode , "search" , false , "search Slack docs with optional query" )
65+ cmd .Flags ().StringVar (& outputFormat , "output" , "browser" , "output format: browser, json" )
5666
5767 return cmd
5868}
5969
60- // runDocsCommand opens Slack developer docs in the browser
70+ // DocsOutput represents the structured output for --json mode
71+ type DocsOutput struct {
72+ URL string `json:"url"`
73+ Query string `json:"query,omitempty"`
74+ Type string `json:"type"` // "homepage", "search", or "search_with_query"
75+ }
76+
77+ // ProgrammaticSearchOutput represents the output from local docs search
78+ type ProgrammaticSearchOutput = search.SearchResponse
79+
80+ // findDocsRepo tries to locate the docs repository
81+ func findDocsRepo () string {
82+ return search .FindDocsRepo ()
83+ }
84+
85+ // runProgrammaticSearch executes the local search
86+ func runProgrammaticSearch (query string , docsPath string ) (* ProgrammaticSearchOutput , error ) {
87+ contentDir := filepath .Join (docsPath , "content" )
88+ return search .SearchDocs (query , "" , 20 , contentDir )
89+ }
90+
91+ // runDocsCommand opens Slack developer docs in the browser or performs programmatic search
6192func runDocsCommand (clients * shared.ClientFactory , cmd * cobra.Command , args []string ) error {
6293 ctx := cmd .Context ()
6394
6495 var docsURL string
6596 var sectionText string
97+ var query string
98+ var docType string
6699
67100 // Validate: if there are arguments, --search flag must be used
68101 if len (args ) > 0 && ! cmd .Flags ().Changed ("search" ) {
@@ -75,22 +108,58 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st
75108
76109 if cmd .Flags ().Changed ("search" ) {
77110 if len (args ) > 0 {
78- // --search "query" (space-separated) - join all args as the query
79- query := strings .Join (args , " " )
111+ query = strings .Join (args , " " )
112+
113+ // Check output format
114+ if outputFormat == "json" {
115+ return runProgrammaticSearchCommand (clients , ctx , query )
116+ }
117+
118+ // Default browser search
80119 encodedQuery := url .QueryEscape (query )
81120 docsURL = fmt .Sprintf ("https://docs.slack.dev/search/?q=%s" , encodedQuery )
82121 sectionText = "Docs Search"
122+ docType = "search_with_query"
83123 } else {
84124 // --search (no argument) - open search page
85125 docsURL = "https://docs.slack.dev/search/"
86126 sectionText = "Docs Search"
127+ docType = "search"
87128 }
88129 } else {
89130 // No search flag: default homepage
90131 docsURL = "https://docs.slack.dev"
91132 sectionText = "Docs Open"
133+ docType = "homepage"
134+ }
135+
136+ // Handle JSON output mode (for browser-based results only)
137+ if outputFormat == "json" && ! cmd .Flags ().Changed ("search" ) {
138+ output := DocsOutput {
139+ URL : docsURL ,
140+ Query : query ,
141+ Type : docType ,
142+ }
143+
144+ jsonBytes , err := json .MarshalIndent (output , "" , " " )
145+ if err != nil {
146+ return slackerror .New (slackerror .ErrDocsJSONEncodeFailed )
147+ }
148+
149+ fmt .Println (string (jsonBytes ))
150+
151+ // Still print trace for analytics
152+ if cmd .Flags ().Changed ("search" ) {
153+ traceValue := query
154+ clients .IO .PrintTrace (ctx , slacktrace .DocsSearchSuccess , traceValue )
155+ } else {
156+ clients .IO .PrintTrace (ctx , slacktrace .DocsSuccess )
157+ }
158+
159+ return nil
92160 }
93161
162+ // Standard browser-opening mode
94163 clients .IO .PrintInfo (ctx , false , "\n %s" , style .Sectionf (style.TextSection {
95164 Emoji : "books" ,
96165 Text : sectionText ,
@@ -113,3 +182,32 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st
113182
114183 return nil
115184}
185+
186+ // runProgrammaticSearchCommand handles local documentation search
187+ func runProgrammaticSearchCommand (clients * shared.ClientFactory , ctx context.Context , query string ) error {
188+ // Find the docs repository
189+ docsPath := findDocsRepo ()
190+ if docsPath == "" {
191+ clients .IO .PrintError (ctx , "❌ Docs repository not found" )
192+ clients .IO .PrintInfo (ctx , false , "💡 Make sure the docs repository is cloned alongside slack-cli" )
193+ clients .IO .PrintInfo (ctx , false , " Expected structure:" )
194+ clients .IO .PrintInfo (ctx , false , " ├── slack-cli/" )
195+ clients .IO .PrintInfo (ctx , false , " └── docs/" )
196+ return fmt .Errorf ("docs repository not found" )
197+ }
198+
199+ // Run the search
200+ results , err := runProgrammaticSearch (query , docsPath )
201+ if err != nil {
202+ clients .IO .PrintError (ctx , "❌ Search failed: %v" , err )
203+ return err
204+ }
205+
206+ // Always output JSON for programmatic search
207+ jsonBytes , err := json .MarshalIndent (results , "" , " " )
208+ if err != nil {
209+ return fmt .Errorf ("failed to encode JSON: %w" , err )
210+ }
211+ fmt .Println (string (jsonBytes ))
212+ return nil
213+ }
0 commit comments