11package main
22
33import (
4- "bytes"
54 "encoding/json"
65 "fmt"
76 "io"
87 "net/http"
9- "net/url"
10- "os"
11- "path"
12- "path/filepath"
138 "strings"
14- "unicode"
159
10+ "github.com/kosli-dev/cli/internal/docgen"
1611 "github.com/spf13/cobra"
1712 "github.com/spf13/cobra/doc"
1813)
@@ -27,6 +22,7 @@ type docsOptions struct {
2722 dest string
2823 topCmd * cobra.Command
2924 generateHeaders bool
25+ mintlify bool
3026}
3127
3228func newDocsCmd (out io.Writer ) * cobra.Command {
@@ -47,273 +43,76 @@ func newDocsCmd(out io.Writer) *cobra.Command {
4743 f := cmd .Flags ()
4844 f .StringVar (& o .dest , "dir" , "./" , "The directory to which documentation is written." )
4945 f .BoolVar (& o .generateHeaders , "generate-headers" , true , "Generate standard headers for markdown files." )
46+ f .BoolVar (& o .mintlify , "mintlify" , false , "Generate Mintlify-compatible MDX output instead of Hugo." )
5047
5148 return cmd
5249}
5350
5451func (o * docsOptions ) run () error {
5552 if o .generateHeaders {
56- linkHandler := func (name string ) string {
57- base := strings .TrimSuffix (name , path .Ext (name ))
58- return "/client_reference/" + strings .ToLower (base ) + "/"
59- }
60-
61- hdrFunc := func (filename string , beta , deprecated bool , summary string ) string {
62- base := filepath .Base (filename )
63- name := strings .TrimSuffix (base , path .Ext (base ))
64- title := strings .ToLower (strings .ReplaceAll (name , "_" , " " ))
65- return fmt .Sprintf ("---\n title: \" %s\" \n beta: %t\n deprecated: %t\n summary: \" %s\" \n ---\n \n " , title , beta , deprecated , summary )
66- }
67-
68- return MereklyGenMarkdownTreeCustom (o .topCmd , o .dest , hdrFunc , linkHandler )
69- }
70- return doc .GenMarkdownTree (o .topCmd , o .dest )
71- }
72-
73- func MereklyGenMarkdownTreeCustom (cmd * cobra.Command , dir string , filePrepender func (string , bool , bool , string ) string , linkHandler func (string ) string ) error {
74- for _ , c := range cmd .Commands () {
75- // skip all unavailable commands except deprecated ones
76- if (! c .IsAvailableCommand () || c .IsAdditionalHelpTopicCommand ()) && c .Deprecated == "" {
77- continue
78- }
79- if err := MereklyGenMarkdownTreeCustom (c , dir , filePrepender , linkHandler ); err != nil {
80- return err
81- }
82- }
83-
84- if ! cmd .HasParent () || ! cmd .HasSubCommands () {
85- basename := strings .ReplaceAll (cmd .CommandPath (), " " , "_" ) + ".md"
86- filename := filepath .Join (dir , basename )
87- summary := cmd .Short
88- f , err := os .Create (filename )
89- if err != nil {
90- return err
91- }
92- defer func () {
93- if err := f .Close (); err != nil {
94- logger .Warn ("failed to close file %s: %v" , filename , err )
95- }
96- }()
97-
98- if _ , err := io .WriteString (f , filePrepender (filename , isBeta (cmd ), isDeprecated (cmd ), summary )); err != nil {
99- return err
100- }
101- if err := KosliGenMarkdownCustom (cmd , f , linkHandler ); err != nil {
102- return err
103- }
104- }
105- return nil
106- }
107-
108- // KosliGenMarkdownCustom creates custom markdown output.
109- func KosliGenMarkdownCustom (cmd * cobra.Command , w io.Writer , linkHandler func (string ) string ) error {
110- cmd .InitDefaultHelpCmd ()
111- cmd .InitDefaultHelpFlag ()
112-
113- buf := new (bytes.Buffer )
114- name := cmd .CommandPath ()
115-
116- buf .WriteString ("# " + name + "\n \n " )
117-
118- if isBeta (cmd ) {
119- buf .WriteString ("{{% hint warning %}}\n " )
120- fmt .Fprintf (buf , "**%s** is a beta feature. " , name )
121- fmt .Fprintf (buf , "Beta features provide early access to product functionality. " )
122- fmt .Fprintf (buf , "These features may change between releases without warning, or can be removed in a " )
123- fmt .Fprintf (buf , "future release.\n " )
124- fmt .Fprintf (buf , "Please contact us to enable this feature for your organization.\n " )
125- // fmt.Fprintf(buf, "You can enable beta features by using the `kosli enable beta` command.")
126- buf .WriteString ("{{% /hint %}}\n " )
127- }
128-
129- if isDeprecated (cmd ) {
130- buf .WriteString ("{{% hint danger %}}\n " )
131- fmt .Fprintf (buf , "**%s** is deprecated. %s " , name , cmd .Deprecated )
132- fmt .Fprintf (buf , "Deprecated commands will be removed in a future release.\n " )
133- buf .WriteString ("{{% /hint %}}\n " )
134- }
135-
136- if len (cmd .Long ) > 0 {
137- buf .WriteString ("## Synopsis\n \n " )
138- if cmd .Runnable () {
139- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , cmd .UseLine ())
140- }
141- buf .WriteString (strings .ReplaceAll (cmd .Long , "^" , "`" ) + "\n \n " )
142- }
143-
144- if err := printOptions (buf , cmd , name ); err != nil {
145- return err
146- }
147-
148- urlSafeName := url .QueryEscape (name )
149- liveExamplesBuf := new (bytes.Buffer )
150- for _ , ci := range []string {"GitHub" , "GitLab" } {
151- if liveYamlDocExists (ci , urlSafeName ) {
152- fmt .Fprintf (liveExamplesBuf , "{{< tab \" %v\" >}}" , ci )
153- fmt .Fprintf (liveExamplesBuf , "View an example of the `%s` command in %s.\n \n " , name , ci )
154- fmt .Fprintf (liveExamplesBuf , "In [this YAML file](%v)" , yamlURL (ci , urlSafeName ))
155- if liveEventDocExists (ci , urlSafeName ) {
156- fmt .Fprintf (liveExamplesBuf , ", which created [this Kosli Event](%v)." , eventURL (ci , urlSafeName ))
157- }
158- liveExamplesBuf .WriteString ("{{< /tab >}}" )
159- }
160- }
161- liveExamples := liveExamplesBuf .String ()
162- if len (liveExamples ) > 0 {
163- buf .WriteString ("## Live Examples in different CI systems\n \n " )
164- buf .WriteString ("{{< tabs \" live-examples\" \" col-no-wrap\" >}}" )
165- buf .WriteString (liveExamples )
166- buf .WriteString ("{{< /tabs >}}\n \n " )
167- }
168-
169- liveCliFullCommand , liveCliURL , liveCliExists := liveCliDocExists (name )
170- if liveCliExists {
171- buf .WriteString ("## Live Example\n \n " )
172- buf .WriteString ("{{< raw-html >}}" )
173- fmt .Fprintf (buf , "To view a live example of '%s' you can run the commands below (for the <a href=\" https://app.kosli.com/cyber-dojo/environments/aws-prod/snapshots/\" >cyber-dojo</a> demo organization).<br/><a href=\" %s\" >Run the commands below and view the output.</a>" , name , liveCliURL )
174- buf .WriteString ("<pre>" )
175- buf .WriteString ("export KOSLI_ORG=cyber-dojo\n " )
176- buf .WriteString ("export KOSLI_API_TOKEN=Pj_XT2deaVA6V1qrTlthuaWsmjVt4eaHQwqnwqjRO3A # read-only\n " )
177- buf .WriteString (liveCliFullCommand )
178- buf .WriteString ("</pre>" )
179- buf .WriteString ("{{< / raw-html >}}\n \n " )
180- }
181-
182- if len (cmd .Example ) > 0 {
183- // This is an attempt to tidy up the non-live examples, so they each have their own title.
184- // Note: The contents of the title lines could also contain < and > characters which will
185- // be lost if simply embedded in a md ## section.
186- buf .WriteString ("## Examples Use Cases\n \n " )
187- url := "https://docs.kosli.com/getting_started/install/#assigning-flags-via-environment-variables"
188- message := fmt .Sprintf ("These examples all assume that the flags `--api-token`, `--org`, `--host`, (and `--flow`, `--trail` when required), are [set/provided](%v). \n \n " , url )
189- buf .WriteString (message )
190-
191- // Some non-title lines contain a # character, (eg in a snappish) so we have to
192- // split on newlines first and then only split on # in the first position
193- example := strings .TrimSpace (cmd .Example )
194- lines := strings .Split (example , "\n " )
195-
196- // Some commands have #titles spanning several lines (that is, each title line starts with a # character)
197- if name == "kosli report approval" {
198- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , example )
199- } else if name == "kosli request approval" {
200- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , example )
201- } else if name == "kosli snapshot server" {
202- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , example )
203- } else if lines [0 ][0 ] != '#' {
204- // Some commands, eg 'kosli assert snapshot' have no #title
205- // and their example starts immediately with the kosli command.
206- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , example )
53+ var formatter docgen.Formatter
54+ if o .mintlify {
55+ formatter = docgen.MintlifyFormatter {}
20756 } else {
208- // The rest we can format nicely
209- all := hashTitledExamples (lines )
210- for i := 0 ; i < len (all ); i ++ {
211- exampleLines := all [i ]
212- // Some titles have a trailing colon, some don't
213- title := strings .Trim (exampleLines [0 ], ":" )
214- if len (title ) > 0 {
215- fmt .Fprintf (buf , "##### %s\n \n " , strings .TrimSpace (title [1 :]))
216- fmt .Fprintf (buf , "```shell\n %s\n ```\n \n " , strings .Join (exampleLines [1 :], "\n " ))
217- }
218- }
57+ formatter = docgen.HugoFormatter {}
21958 }
220- }
22159
222- _ , err := buf .WriteTo (w )
223- return err
224- }
225-
226- func hashTitledExamples (lines []string ) [][]string {
227- // Some non-title lines contain a # character, so we have split on newlines first
228- // and then split on # which are the first character in their line
229- result := make ([][]string , 0 )
230- example := make ([]string , 0 )
231- for _ , line := range lines {
232- if strings .HasPrefix (line , "#" ) {
233- result = append (result , example ) // See result[1:] at end
234- example = make ([]string , 0 )
235- }
236- if ! isSetWithEnvVar (line ) {
237- example = append (example , choppedLineContinuation (line ))
60+ metaFn := func (cmd * cobra.Command ) docgen.CommandMeta {
61+ return docgen.CommandMeta {
62+ Name : cmd .CommandPath (),
63+ Beta : isBeta (cmd ),
64+ Deprecated : isDeprecated (cmd ),
65+ DeprecMsg : cmd .Deprecated ,
66+ Summary : cmd .Short ,
67+ Long : cmd .Long ,
68+ UseLine : cmd .UseLine (),
69+ Runnable : cmd .Runnable (),
70+ Example : cmd .Example ,
71+ }
23872 }
239- }
240- result = append (result , example )
241- return result [1 :]
242- }
24373
244- func isSetWithEnvVar (line string ) bool {
245- trimmed_line := strings .TrimSpace (line )
246- if strings .HasPrefix (trimmed_line , "--api-token " ) {
247- return true
248- } else if strings .HasPrefix (trimmed_line , "--host " ) {
249- return true
250- } else if strings .HasPrefix (trimmed_line , "--org " ) {
251- return true
252- } else if strings .HasPrefix (trimmed_line , "--flow " ) {
253- return true
254- } else if strings .HasPrefix (trimmed_line , "--trail " ) {
255- return true
256- } else {
257- return false
74+ return docgen .GenMarkdownTree (o .topCmd , o .dest , formatter , metaFn , & kosliLiveDocProvider {})
25875 }
76+ return doc .GenMarkdownTree (o .topCmd , o .dest )
25977}
26078
261- func choppedLineContinuation (line string ) string {
262- trimmed_line := strings .TrimRightFunc (line , unicode .IsSpace )
263- return strings .TrimSuffix (trimmed_line , "\\ " )
264- }
265-
266- func printOptions (buf * bytes.Buffer , cmd * cobra.Command , name string ) error {
267- flags := cmd .NonInheritedFlags ()
268- flags .SetOutput (buf )
269- if flags .HasAvailableFlags () {
270- buf .WriteString ("## Flags\n " )
271- buf .WriteString ("| Flag | Description |\n " )
272- buf .WriteString ("| :--- | :--- |\n " )
273- usages := CommandsInTable (flags )
274- fmt .Fprint (buf , usages )
275- buf .WriteString ("\n \n " )
276- }
277-
278- parentFlags := cmd .InheritedFlags ()
279- parentFlags .SetOutput (buf )
280- if parentFlags .HasAvailableFlags () {
281- buf .WriteString ("## Flags inherited from parent commands\n " )
282- buf .WriteString ("| Flag | Description |\n " )
283- buf .WriteString ("| :--- | :--- |\n " )
284- usages := CommandsInTable (parentFlags )
285- fmt .Fprint (buf , usages )
286- buf .WriteString ("\n \n " )
287- }
288- return nil
289- }
79+ // kosliLiveDocProvider implements docgen.LiveDocProvider using HTTP calls
80+ // to the Kosli live docs API.
81+ type kosliLiveDocProvider struct {}
29082
29183const baseURL = "https://app.kosli.com/api/v2/livedocs/cyber-dojo"
29284
293- func liveYamlDocExists ( ci string , command string ) bool {
85+ func ( p * kosliLiveDocProvider ) YamlDocExists ( ci , command string ) bool {
29486 url := fmt .Sprintf ("%s/yaml_exists?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
29587 return liveDocExists (url )
29688}
29789
298- func liveEventDocExists ( ci string , command string ) bool {
90+ func ( p * kosliLiveDocProvider ) EventDocExists ( ci , command string ) bool {
29991 url := fmt .Sprintf ("%s/event_exists?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
30092 return liveDocExists (url )
30193}
30294
303- func liveCliDocExists (command string ) (string , string , bool ) {
95+ func (p * kosliLiveDocProvider ) YamlURL (ci , command string ) string {
96+ return fmt .Sprintf ("%s/yaml?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
97+ }
98+
99+ func (p * kosliLiveDocProvider ) EventURL (ci , command string ) string {
100+ return fmt .Sprintf ("%s/event?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
101+ }
102+
103+ func (p * kosliLiveDocProvider ) CLIDocExists (command string ) (string , string , bool ) {
304104 fullCommand , ok := liveCliMap [command ]
305105 if ok {
306106 plussed := strings .ReplaceAll (fullCommand , " " , "+" )
307- exists_url := fmt .Sprintf ("%s/cli_exists?command=%s" , baseURL , plussed )
107+ existsURL := fmt .Sprintf ("%s/cli_exists?command=%s" , baseURL , plussed )
308108 url := fmt .Sprintf ("%s/cli?command=%s" , baseURL , plussed )
309- return fullCommand , url , liveDocExists (exists_url )
310- } else {
311- return "" , "" , false
109+ return fullCommand , url , liveDocExists (existsURL )
312110 }
111+ return "" , "" , false
313112}
314113
315114func liveDocExists (url string ) bool {
316- response , err := http .Get (url )
115+ response , err := http .Get (url ) //nolint:gosec
317116 if err != nil {
318117 return false
319118 }
@@ -331,14 +130,6 @@ func liveDocExists(url string) bool {
331130 return exists
332131}
333132
334- func yamlURL (ci string , command string ) string {
335- return fmt .Sprintf ("%s/yaml?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
336- }
337-
338- func eventURL (ci string , command string ) string {
339- return fmt .Sprintf ("%s/event?ci=%s&command=%s" , baseURL , strings .ToLower (ci ), command )
340- }
341-
342133var liveCliMap = map [string ]string {
343134 "kosli list environments" : "kosli list environments --output=json" ,
344135 "kosli get environment" : "kosli get environment aws-prod --output=json" ,
0 commit comments