@@ -19,7 +19,6 @@ import (
1919 "encoding/json"
2020 "fmt"
2121 "net/url"
22- "path/filepath"
2322 "strings"
2423
2524 "github.com/slackapi/slack-cli/internal/search"
@@ -30,192 +29,164 @@ import (
3029 "github.com/spf13/cobra"
3130)
3231
33- var searchMode bool
3432var outputFormat string
3533var searchLimit int
36- var searchOffset int
34+ var searchFilter string
3735
3836func NewCommand (clients * shared.ClientFactory ) * cobra.Command {
3937 cmd := & cobra.Command {
4038 Use : "docs" ,
4139 Short : "Open Slack developer docs" ,
42- Long : "Open the Slack developer docs in your browser, with optional search functionality " ,
40+ Long : "Open the Slack developer docs in your browser, or search docs with subcommands " ,
4341 Example : style .ExampleCommandsf ([]style.ExampleCommand {
4442 {
4543 Meaning : "Open Slack developer docs homepage" ,
4644 Command : "docs" ,
4745 },
4846 {
49- Meaning : "Search Slack developer docs for Block Kit " ,
50- Command : "docs -- search \" Block Kit\" " ,
47+ Meaning : "Search and return JSON (default) " ,
48+ Command : "docs search \" Block Kit\" " ,
5149 },
5250 {
53- Meaning : "Search and get JSON results" ,
54- Command : "docs --search \" Block Kit\" --output=json" ,
51+ Meaning : "Search and open in browser" ,
52+ Command : "docs search \" webhooks\" --output=browser" ,
53+ },
54+ }),
55+ RunE : func (cmd * cobra.Command , args []string ) error {
56+ return runDocsCommand (clients , cmd , args )
57+ },
58+ }
59+
60+ // Add search subcommand
61+ cmd .AddCommand (newSearchCommand (clients ))
62+
63+ return cmd
64+ }
65+
66+ // newSearchCommand creates the search subcommand
67+ func newSearchCommand (clients * shared.ClientFactory ) * cobra.Command {
68+ cmd := & cobra.Command {
69+ Use : "search [query]" ,
70+ Short : "Search Slack developer documentation" ,
71+ Long : "Search the Slack developer documentation and return results in JSON format (default) or open in browser. If no query provided, opens search page in browser." ,
72+ Args : cobra .MaximumNArgs (1 ),
73+ Example : style .ExampleCommandsf ([]style.ExampleCommand {
74+ {
75+ Meaning : "Open docs search page in browser" ,
76+ Command : "docs search" ,
77+ },
78+ {
79+ Meaning : "Search for Block Kit (returns JSON by default)" ,
80+ Command : "docs search \" Block Kit\" " ,
81+ },
82+ {
83+ Meaning : "Search and open in browser" ,
84+ Command : "docs search \" Block Kit\" --output=browser" ,
5585 },
5686 {
5787 Meaning : "Search with custom limit" ,
58- Command : "docs -- search \" Block Kit \" --output=json --limit=50" ,
88+ Command : "docs search \" webhooks \" --limit=50" ,
5989 },
6090 {
61- Meaning : "Search with pagination" ,
62- Command : "docs --search \" Block Kit\" --output=json --limit=20 --offset=20" ,
91+ Meaning : "Search with filter" ,
92+ Command : "docs search \" webhooks\" --filter=guides" ,
93+ },
94+ {
95+ Meaning : "Search Python documentation and open in browser" ,
96+ Command : "docs search \" bolt\" --filter=python --output=browser" ,
6397 },
6498 }),
6599 RunE : func (cmd * cobra.Command , args []string ) error {
66- return runDocsCommand (clients , cmd , args )
100+ if len (args ) == 0 {
101+ return runSearchBrowserCommand (clients , cmd )
102+ }
103+ return runSearchCommand (clients , cmd , args [0 ])
67104 },
68105 }
69106
70- cmd .Flags ().BoolVar (& searchMode , "search" , false , "search Slack docs with optional query" )
71- cmd .Flags ().StringVar (& outputFormat , "output" , "browser" , "output format: browser, json" )
107+ cmd .Flags ().StringVar (& outputFormat , "output" , "json" , "output format: json, browser" )
72108 cmd .Flags ().IntVar (& searchLimit , "limit" , 20 , "maximum number of results to return" )
73- cmd .Flags ().IntVar ( & searchOffset , "offset " , 0 , "number of results to skip (for pagination) " )
109+ cmd .Flags ().StringVar ( & searchFilter , "filter " , "" , "filter results by content type: guides, reference, changelog, python, javascript, java, slack_cli, slack_github_action, deno_slack_sdk " )
74110
75111 return cmd
76112}
77113
78- // DocsOutput represents the structured output for --json mode
79- type DocsOutput struct {
80- URL string `json:"url"`
81- Query string `json:"query,omitempty"`
82- Type string `json:"type"` // "homepage", "search", or "search_with_query"
83- }
84-
85- // ProgrammaticSearchOutput represents the output from local docs search
86- type ProgrammaticSearchOutput = search. SearchResponse
114+ // openSearchInBrowser opens the docs search page in browser
115+ func openSearchInBrowser ( clients * shared. ClientFactory , ctx context. Context , searchURL string ) error {
116+ clients . IO . PrintInfo ( ctx , false , " \n %s" , style . Sectionf (style. TextSection {
117+ Emoji : "books" ,
118+ Text : "Docs Search" ,
119+ Secondary : [] string {
120+ searchURL ,
121+ },
122+ }))
87123
88- // findDocsRepo tries to locate the docs repository
89- func findDocsRepo () string {
90- return search . FindDocsRepo ()
124+ clients . Browser (). OpenURL ( searchURL )
125+ clients . IO . PrintTrace ( ctx , slacktrace . DocsSearchSuccess , "" )
126+ return nil
91127}
92128
93- // runProgrammaticSearch executes the local search
94- func runProgrammaticSearch (query string , docsPath string ) (* ProgrammaticSearchOutput , error ) {
95- contentDir := filepath .Join (docsPath , "content" )
96- return search .SearchDocs (query , "" , searchLimit , searchOffset , contentDir )
129+ // runSearchBrowserCommand opens the docs search page in browser
130+ func runSearchBrowserCommand (clients * shared.ClientFactory , cmd * cobra.Command ) error {
131+ ctx := cmd .Context ()
132+ searchURL := "https://docs.slack.dev/search"
133+ return openSearchInBrowser (clients , ctx , searchURL )
97134}
98135
99- // runDocsCommand opens Slack developer docs in the browser or performs programmatic search
100- func runDocsCommand (clients * shared.ClientFactory , cmd * cobra.Command , args [] string ) error {
136+ // runSearchCommand handles the search subcommand
137+ func runSearchCommand (clients * shared.ClientFactory , cmd * cobra.Command , query string ) error {
101138 ctx := cmd .Context ()
102139
103- var docsURL string
104- var sectionText string
105- var query string
106- var docType string
107-
108- // Validate: if there are arguments, --search flag must be used
109- if len (args ) > 0 && ! cmd .Flags ().Changed ("search" ) {
110- query := strings .Join (args , " " )
111- return slackerror .New (slackerror .ErrDocsSearchFlagRequired ).WithRemediation (
112- "Use --search flag: %s" ,
113- style .Commandf (fmt .Sprintf ("docs --search \" %s\" " , query ), false ),
114- )
140+ results , err := search .SearchDocs (query , searchFilter , searchLimit )
141+ if err != nil {
142+ return fmt .Errorf ("search failed: %w" , err )
115143 }
116144
117- if cmd .Flags ().Changed ("search" ) {
118- if len (args ) > 0 {
119- query = strings .Join (args , " " )
120-
121- // Check output format
122- if outputFormat == "json" {
123- return runProgrammaticSearchCommand (clients , ctx , query )
124- }
125-
126- // Default browser search
127- encodedQuery := url .QueryEscape (query )
128- docsURL = fmt .Sprintf ("https://docs.slack.dev/search/?q=%s" , encodedQuery )
129- sectionText = "Docs Search"
130- docType = "search_with_query"
131- } else {
132- // --search (no argument) - open search page
133- docsURL = "https://docs.slack.dev/search/"
134- sectionText = "Docs Search"
135- docType = "search"
145+ // Output results
146+ if outputFormat == "json" {
147+ jsonBytes , err := json .MarshalIndent (results , "" , " " )
148+ if err != nil {
149+ return fmt .Errorf ("failed to encode JSON: %w" , err )
136150 }
151+ fmt .Println (string (jsonBytes ))
137152 } else {
138- // No search flag: default homepage
139- docsURL = "https://docs.slack.dev"
140- sectionText = "Docs Open"
141- docType = "homepage"
142- }
143-
144- // Handle JSON output mode (for browser-based results only)
145- if outputFormat == "json" && ! cmd .Flags ().Changed ("search" ) {
146- output := DocsOutput {
147- URL : docsURL ,
148- Query : query ,
149- Type : docType ,
150- }
151-
152- jsonBytes , err := json .MarshalIndent (output , "" , " " )
153- if err != nil {
154- return slackerror .New (slackerror .ErrDocsJSONEncodeFailed )
153+ // Browser output - open search page with query
154+ searchURL := fmt .Sprintf ("https://docs.slack.dev/search/?q=%s" , url .QueryEscape (query ))
155+ if searchFilter != "" {
156+ searchURL += fmt .Sprintf ("&filter=%s" , url .QueryEscape (searchFilter ))
155157 }
158+ return openSearchInBrowser (clients , ctx , searchURL )
159+ }
156160
157- fmt .Println (string (jsonBytes ))
161+ return nil
162+ }
158163
159- // Still print trace for analytics
160- if cmd .Flags ().Changed ("search" ) {
161- traceValue := query
162- clients .IO .PrintTrace (ctx , slacktrace .DocsSearchSuccess , traceValue )
163- } else {
164- clients .IO .PrintTrace (ctx , slacktrace .DocsSuccess )
165- }
164+ // runDocsCommand opens Slack developer docs in the browser
165+ func runDocsCommand (clients * shared.ClientFactory , cmd * cobra.Command , args []string ) error {
166+ ctx := cmd .Context ()
166167
167- return nil
168+ // If any arguments provided, suggest using search subcommand
169+ if len (args ) > 0 {
170+ query := strings .Join (args , " " )
171+ return slackerror .New (slackerror .ErrDocsSearchFlagRequired ).WithRemediation (
172+ "Use search subcommand: %s" ,
173+ style .Commandf (fmt .Sprintf ("docs search \" %s\" " , query ), false ),
174+ )
168175 }
169176
170- // Standard browser-opening mode
177+ // Open docs homepage
178+ docsURL := "https://docs.slack.dev"
179+
171180 clients .IO .PrintInfo (ctx , false , "\n %s" , style .Sectionf (style.TextSection {
172181 Emoji : "books" ,
173- Text : sectionText ,
182+ Text : "Docs Open" ,
174183 Secondary : []string {
175184 docsURL ,
176185 },
177186 }))
178187
179188 clients .Browser ().OpenURL (docsURL )
189+ clients .IO .PrintTrace (ctx , slacktrace .DocsSuccess )
180190
181- if cmd .Flags ().Changed ("search" ) {
182- traceValue := ""
183- if len (args ) > 0 {
184- traceValue = strings .Join (args , " " )
185- }
186- clients .IO .PrintTrace (ctx , slacktrace .DocsSearchSuccess , traceValue )
187- } else {
188- clients .IO .PrintTrace (ctx , slacktrace .DocsSuccess )
189- }
190-
191- return nil
192- }
193-
194- // runProgrammaticSearchCommand handles local documentation search
195- func runProgrammaticSearchCommand (clients * shared.ClientFactory , ctx context.Context , query string ) error {
196- // Find the docs repository
197- docsPath := findDocsRepo ()
198- if docsPath == "" {
199- clients .IO .PrintError (ctx , "❌ Docs repository not found" )
200- clients .IO .PrintInfo (ctx , false , "💡 Make sure the docs repository is cloned alongside slack-cli" )
201- clients .IO .PrintInfo (ctx , false , " Expected structure:" )
202- clients .IO .PrintInfo (ctx , false , " ├── slack-cli/" )
203- clients .IO .PrintInfo (ctx , false , " └── docs/" )
204- return fmt .Errorf ("docs repository not found" )
205- }
206-
207- // Run the search
208- results , err := runProgrammaticSearch (query , docsPath )
209- if err != nil {
210- clients .IO .PrintError (ctx , "❌ Search failed: %v" , err )
211- return err
212- }
213-
214- // Always output JSON for programmatic search
215- jsonBytes , err := json .MarshalIndent (results , "" , " " )
216- if err != nil {
217- return fmt .Errorf ("failed to encode JSON: %w" , err )
218- }
219- fmt .Println (string (jsonBytes ))
220191 return nil
221192}
0 commit comments