@@ -5,16 +5,19 @@ package docs
55
66import (
77 "fmt"
8+ "regexp"
89
910 "github.com/gim-home/azldev-preview/internal/app/azldev"
11+ "github.com/gim-home/azldev-preview/internal/global/opctx"
1012 "github.com/gim-home/azldev-preview/internal/utils/fileutils"
11- "github.com/spf13/afero"
1213 "github.com/spf13/cobra"
1314 "github.com/spf13/cobra/doc"
1415)
1516
1617type GenerateMarkdownOptions struct {
17- OutputDir string
18+ OutputDir string
19+ IncludeHidden bool
20+ Force bool
1821}
1922
2023// Called once when the app is initialized; registers any commands or callbacks with the app.
@@ -29,12 +32,30 @@ func newMarkdownCmd() *cobra.Command {
2932 Use : "markdown" ,
3033 Aliases : []string {"md" },
3134 Short : "Generates Markdown (.md) docs for this tool" ,
32- RunE : func (cmd * cobra.Command , args []string ) error {
33- return GenerateMarkdownDocs (cmd .Root (), & options )
34- },
35+ Long : `Generate Markdown reference documentation for all azldev CLI commands.
36+
37+ Produces one .md file per command in the output directory. These files are
38+ auto-generated and should not be hand-edited; update the Cobra command
39+ definitions in Go source code instead, then re-run this command.` ,
40+ Example : ` # Generate docs into the user guide
41+ azldev docs markdown -o docs/user/reference/cli/
42+
43+ # Include hidden (advanced) commands
44+ azldev docs markdown -o docs/user/reference/cli/ --include-hidden
45+
46+ # Overwrite an existing non-empty output directory
47+ azldev docs markdown -o docs/user/reference/cli/ -f` ,
3548 }
3649
50+ cmd .RunE = azldev .RunFuncWithoutRequiredConfig (
51+ func (env * azldev.Env ) (interface {}, error ) {
52+ return nil , GenerateMarkdownDocs (env .FS (), cmd .Root (), & options )
53+ })
54+
3755 cmd .Flags ().StringVarP (& options .OutputDir , "output-dir" , "o" , "" , "directory markdown files will be written to" )
56+ cmd .Flags ().BoolVarP (& options .Force , "force" , "f" , false ,
57+ "delete and recreate the output directory if it already exists" )
58+ cmd .Flags ().BoolVar (& options .IncludeHidden , "include-hidden" , false , "include hidden commands in the generated docs" )
3859
3960 // NOTE: we ignore errors because we don't expect that they could fail at runtime, and because this function
4061 // is expected to be infallible.
@@ -46,17 +67,107 @@ func newMarkdownCmd() *cobra.Command {
4667 return cmd
4768}
4869
49- // Generates markdown documentation for the given command tree.
50- func GenerateMarkdownDocs (rootCmd * cobra.Command , options * GenerateMarkdownOptions ) error {
51- // Make sure the directory exists.
52- err := fileutils .MkdirAll (afero .NewOsFs (), options .OutputDir )
70+ const generatedFileHeader = `<!--- DO NOT EDIT: auto-generated by "azldev docs markdown" -->` + "\n \n "
71+
72+ // filePrepender returns a function that prepends the auto-generated header to each generated doc file.
73+ func filePrepender (_ string ) string {
74+ return generatedFileHeader
75+ }
76+
77+ // linkHandler converts cobra doc filenames to relative markdown file links.
78+ // Cobra passes in filenames like "azldev_component.md"; since each command gets
79+ // its own file we link directly to that file rather than using a fragment anchor.
80+ func linkHandler (name string ) string {
81+ return name
82+ }
83+
84+ // versionSuffix matches a trailing version string like " v1.2.3" or " v0.0.0-20260228210254-10ca5c6a64fb".
85+ var versionSuffix = regexp .MustCompile (` v\S+$` )
86+
87+ // stripVersionFromShort removes the trailing version string from the root command's
88+ // Short description so that generated docs don't change with every build.
89+ func stripVersionFromShort (short string ) string {
90+ return versionSuffix .ReplaceAllString (short , "" )
91+ }
92+
93+ // setHiddenRecursive sets or clears the [cobra.Command.Hidden] flag on a command and all its children.
94+ func setHiddenRecursive (cmd * cobra.Command , hidden bool ) {
95+ cmd .Hidden = hidden
96+
97+ for _ , child := range cmd .Commands () {
98+ setHiddenRecursive (child , hidden )
99+ }
100+ }
101+
102+ // CheckOutputDir verifies the output directory state before generation.
103+ // If the directory exists and is non-empty, it either removes it (when Force is set)
104+ // or returns an actionable error suggesting --force / -f.
105+ func CheckOutputDir (fs opctx.FS , options * GenerateMarkdownOptions ) error {
106+ dirExists , err := fileutils .DirExists (fs , options .OutputDir )
107+ if err != nil {
108+ return fmt .Errorf ("failed to check output directory %q:\n %w" , options .OutputDir , err )
109+ }
110+
111+ if ! dirExists {
112+ return nil
113+ }
114+
115+ empty , err := fileutils .IsDirEmpty (fs , options .OutputDir )
116+ if err != nil {
117+ return fmt .Errorf ("failed to check if output directory %q is empty:\n %w" , options .OutputDir , err )
118+ }
119+
120+ if empty {
121+ return nil
122+ }
123+
124+ if options .Force {
125+ if err := fs .RemoveAll (options .OutputDir ); err != nil {
126+ return fmt .Errorf ("failed to clean output directory %q:\n %w" , options .OutputDir , err )
127+ }
128+
129+ return nil
130+ }
131+
132+ return fmt .Errorf (
133+ "output directory %q already exists and is not empty;\n " +
134+ "use --force / -f to delete and recreate it" ,
135+ options .OutputDir )
136+ }
137+
138+ // GenerateMarkdownDocs generates markdown documentation for the given command tree.
139+ func GenerateMarkdownDocs (fs opctx.FS , rootCmd * cobra.Command , options * GenerateMarkdownOptions ) error {
140+ // Strip the dynamic version string from the root command's Short description and disable
141+ // the auto-generated date footer so that generated docs don't churn on every build.
142+ origShort := rootCmd .Short
143+ rootCmd .Short = stripVersionFromShort (origShort )
144+ rootCmd .DisableAutoGenTag = true
145+
146+ defer func () {
147+ rootCmd .Short = origShort
148+ rootCmd .DisableAutoGenTag = false
149+ }()
150+
151+ // Check the output directory state and handle force semantics.
152+ err := CheckOutputDir (fs , options )
153+ if err != nil {
154+ return err
155+ }
156+
157+ // Make sure the directory exists (it may have been removed by checkOutputDir, or may not exist yet).
158+ err = fileutils .MkdirAll (fs , options .OutputDir )
53159 if err != nil {
54160 return fmt .Errorf ("failed to ensure output directory %q exists:\n %w" , options .OutputDir , err )
55161 }
56162
163+ if options .IncludeHidden {
164+ // Unhide all commands so they appear in the generated docs.
165+ setHiddenRecursive (rootCmd , false )
166+ }
167+
57168 // NOTE: This function can't work with our [opctx.FS] filesystem abstraction, but there's not much
58169 // we can do about that.
59- err = doc .GenMarkdownTree (rootCmd , options .OutputDir )
170+ err = doc .GenMarkdownTreeCustom (rootCmd , options .OutputDir , filePrepender , linkHandler )
60171 if err != nil {
61172 return fmt .Errorf ("failed to generate markdown docs:\n %w" , err )
62173 }
0 commit comments