cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 338 lines 9.9 kB view raw
1//go:build !prod 2 3package tools 4 5import ( 6 "encoding/json" 7 "fmt" 8 "os" 9 "path/filepath" 10 "slices" 11 "strings" 12 13 "github.com/spf13/cobra" 14 "github.com/spf13/cobra/doc" 15) 16 17// NewDocGenCommand creates a hidden command for generating CLI documentation 18func NewDocGenCommand(root *cobra.Command) *cobra.Command { 19 cmd := &cobra.Command{ 20 Use: "docgen", 21 Short: "Generate CLI documentation", 22 Hidden: true, 23 RunE: func(cmd *cobra.Command, args []string) error { 24 out, _ := cmd.Flags().GetString("out") 25 format, _ := cmd.Flags().GetString("format") 26 front, _ := cmd.Flags().GetBool("frontmatter") 27 28 if err := os.MkdirAll(out, 0o755); err != nil { 29 return fmt.Errorf("failed to create output directory: %w", err) 30 } 31 32 root.DisableAutoGenTag = true 33 34 switch format { 35 case "docusaurus": 36 if err := generateDocusaurusDocs(root, out); err != nil { 37 return fmt.Errorf("failed to generate docusaurus documentation: %w", err) 38 } 39 case "markdown": 40 if front { 41 prep := func(filename string) string { 42 base := filepath.Base(filename) 43 name := strings.TrimSuffix(base, filepath.Ext(base)) 44 title := strings.ReplaceAll(name, "_", " ") 45 return fmt.Sprintf("---\ntitle: %q\nslug: %q\ndescription: \"CLI reference for %s\"\n---\n\n", title, name, title) 46 } 47 link := func(name string) string { return strings.ToLower(name) } 48 if err := doc.GenMarkdownTreeCustom(root, out, prep, link); err != nil { 49 return fmt.Errorf("failed to generate markdown documentation: %w", err) 50 } 51 } else { 52 if err := doc.GenMarkdownTree(root, out); err != nil { 53 return fmt.Errorf("failed to generate markdown documentation: %w", err) 54 } 55 } 56 case "man": 57 hdr := &doc.GenManHeader{Title: strings.ToUpper(root.Name()), Section: "1"} 58 if err := doc.GenManTree(root, hdr, out); err != nil { 59 return fmt.Errorf("failed to generate man pages: %w", err) 60 } 61 case "rest": 62 if err := doc.GenReSTTree(root, out); err != nil { 63 return fmt.Errorf("failed to generate ReStructuredText documentation: %w", err) 64 } 65 default: 66 return fmt.Errorf("unknown format: %s", format) 67 } 68 69 fmt.Fprintf(cmd.OutOrStdout(), "Documentation generated in %s\n", out) 70 return nil 71 }, 72 } 73 74 cmd.Flags().StringP("out", "o", "./docs/cli", "output directory") 75 cmd.Flags().StringP("format", "f", "markdown", "output format (docusaurus|markdown|man|rest)") 76 cmd.Flags().Bool("frontmatter", false, "prepend simple YAML front matter to markdown") 77 78 return cmd 79} 80 81// CategoryJSON represents the _category_.json structure for Docusaurus 82type CategoryJSON struct { 83 Label string `json:"label"` 84 Position int `json:"position"` 85 Link *Link `json:"link,omitempty"` 86 Collapsed bool `json:"collapsed,omitempty"` 87 Description string `json:"description,omitempty"` 88} 89 90// Link represents a link in _category_.json 91type Link struct { 92 Type string `json:"type"` 93 Description string `json:"description,omitempty"` 94} 95 96// generateDocusaurusDocs creates combined, Docusaurus-compatible documentation 97func generateDocusaurusDocs(root *cobra.Command, outDir string) error { 98 if err := os.MkdirAll(outDir, 0o755); err != nil { 99 return fmt.Errorf("failed to create output directory: %w", err) 100 } 101 102 category := CategoryJSON{ 103 Label: "CLI Reference", 104 Position: 3, 105 Link: &Link{ 106 Type: "generated-index", 107 Description: "Complete command-line reference for noteleaf", 108 }, 109 } 110 categoryJSON, err := json.MarshalIndent(category, "", " ") 111 if err != nil { 112 return fmt.Errorf("failed to marshal category json: %w", err) 113 } 114 if err := os.WriteFile(filepath.Join(outDir, "_category_.json"), categoryJSON, 0o644); err != nil { 115 return fmt.Errorf("failed to write category json: %w", err) 116 } 117 118 indexContent := generateIndexPage(root) 119 if err := os.WriteFile(filepath.Join(outDir, "index.md"), []byte(indexContent), 0o644); err != nil { 120 return fmt.Errorf("failed to write index.md: %w", err) 121 } 122 123 commandGroups := map[string]struct { 124 title string 125 position int 126 commands []string 127 description string 128 }{ 129 "tasks": { 130 title: "Task Management", 131 position: 1, 132 commands: []string{"todo", "task"}, 133 description: "Manage tasks with TaskWarrior-inspired features", 134 }, 135 "notes": { 136 title: "Notes", 137 position: 2, 138 commands: []string{"note"}, 139 description: "Create and organize markdown notes", 140 }, 141 "articles": { 142 title: "Articles", 143 position: 3, 144 commands: []string{"article"}, 145 description: "Save and archive web articles", 146 }, 147 "books": { 148 title: "Books", 149 position: 4, 150 commands: []string{"media book"}, 151 description: "Manage reading list and track progress", 152 }, 153 "movies": { 154 title: "Movies", 155 position: 5, 156 commands: []string{"media movie"}, 157 description: "Track movies in watch queue", 158 }, 159 "tv-shows": { 160 title: "TV Shows", 161 position: 6, 162 commands: []string{"media tv"}, 163 description: "Manage TV show watching", 164 }, 165 "configuration": { 166 title: "Configuration", 167 position: 7, 168 commands: []string{"config"}, 169 description: "Manage application configuration", 170 }, 171 "management": { 172 title: "Management", 173 position: 8, 174 commands: []string{"status", "setup", "reset"}, 175 description: "Application management commands", 176 }, 177 } 178 179 for filename, group := range commandGroups { 180 content := generateCombinedPage(root, group.title, group.position, group.commands, group.description) 181 outputFile := filepath.Join(outDir, filename+".md") 182 if err := os.WriteFile(outputFile, []byte(content), 0o644); err != nil { 183 return fmt.Errorf("failed to write %s: %w", outputFile, err) 184 } 185 } 186 187 return nil 188} 189 190// generateIndexPage creates the index/overview page 191func generateIndexPage(root *cobra.Command) string { 192 var b strings.Builder 193 194 b.WriteString("---\n") 195 b.WriteString("id: index\n") 196 b.WriteString("title: CLI Reference\n") 197 b.WriteString("sidebar_label: Overview\n") 198 b.WriteString("sidebar_position: 0\n") 199 b.WriteString("description: Complete command-line reference for noteleaf\n") 200 b.WriteString("---\n\n") 201 202 b.WriteString("# noteleaf CLI Reference\n\n") 203 204 if root.Long != "" { 205 b.WriteString(root.Long) 206 b.WriteString("\n\n") 207 } else if root.Short != "" { 208 b.WriteString(root.Short) 209 b.WriteString("\n\n") 210 } 211 212 b.WriteString("## Usage\n\n") 213 b.WriteString("```bash\n") 214 b.WriteString(root.UseLine()) 215 b.WriteString("\n```\n\n") 216 217 b.WriteString("## Command Groups\n\n") 218 b.WriteString("- **[Task Management](tasks)** - Manage todos, projects, and time tracking\n") 219 b.WriteString("- **[Notes](notes)** - Create and organize markdown notes\n") 220 b.WriteString("- **[Articles](articles)** - Save and archive web articles\n") 221 b.WriteString("- **[Books](books)** - Track reading list and progress\n") 222 b.WriteString("- **[Movies](movies)** - Manage movie watch queue\n") 223 b.WriteString("- **[TV Shows](tv-shows)** - Track TV show watching\n") 224 b.WriteString("- **[Configuration](configuration)** - Manage settings\n") 225 b.WriteString("- **[Management](management)** - Application management\n\n") 226 227 return b.String() 228} 229 230// generateCombinedPage creates a combined documentation page for a command group 231func generateCombinedPage(root *cobra.Command, title string, position int, commandPaths []string, description string) string { 232 var b strings.Builder 233 234 slug := strings.ToLower(strings.ReplaceAll(title, " ", "-")) 235 b.WriteString("---\n") 236 b.WriteString(fmt.Sprintf("id: %s\n", slug)) 237 b.WriteString(fmt.Sprintf("title: %s\n", title)) 238 b.WriteString(fmt.Sprintf("sidebar_position: %d\n", position)) 239 b.WriteString(fmt.Sprintf("description: %s\n", description)) 240 b.WriteString("---\n\n") 241 242 for _, cmdPath := range commandPaths { 243 cmd := findCommand(root, strings.Split(cmdPath, " ")) 244 if cmd == nil { 245 continue 246 } 247 248 b.WriteString(fmt.Sprintf("## %s\n\n", cmd.Name())) 249 if cmd.Long != "" { 250 b.WriteString(cmd.Long) 251 b.WriteString("\n\n") 252 } else if cmd.Short != "" { 253 b.WriteString(cmd.Short) 254 b.WriteString("\n\n") 255 } 256 257 b.WriteString("```bash\n") 258 b.WriteString(cmd.UseLine()) 259 b.WriteString("\n```\n\n") 260 261 if cmd.HasSubCommands() { 262 b.WriteString("### Subcommands\n\n") 263 for _, sub := range cmd.Commands() { 264 if sub.Hidden { 265 continue 266 } 267 generateSubcommandSection(&b, sub, 4) 268 } 269 } 270 271 if cmd.HasFlags() { 272 b.WriteString("### Options\n\n") 273 b.WriteString("```\n") 274 b.WriteString(cmd.Flags().FlagUsages()) 275 b.WriteString("```\n\n") 276 } 277 } 278 279 return b.String() 280} 281 282// generateSubcommandSection generates documentation for a subcommand 283func generateSubcommandSection(b *strings.Builder, cmd *cobra.Command, level int) { 284 prefix := strings.Repeat("#", level) 285 286 fmt.Fprintf(b, "%s %s\n\n", prefix, cmd.Name()) 287 288 if cmd.Long != "" { 289 b.WriteString(cmd.Long) 290 b.WriteString("\n\n") 291 } else if cmd.Short != "" { 292 b.WriteString(cmd.Short) 293 b.WriteString("\n\n") 294 } 295 296 b.WriteString("**Usage:**\n\n") 297 b.WriteString("```bash\n") 298 b.WriteString(cmd.UseLine()) 299 b.WriteString("\n```\n\n") 300 301 if cmd.HasLocalFlags() { 302 b.WriteString("**Options:**\n\n") 303 b.WriteString("```\n") 304 b.WriteString(cmd.LocalFlags().FlagUsages()) 305 b.WriteString("```\n\n") 306 } 307 308 if len(cmd.Aliases) > 0 { 309 fmt.Fprintf(b, "**Aliases:** %s\n\n", strings.Join(cmd.Aliases, ", ")) 310 } 311 312 if cmd.HasSubCommands() { 313 for _, sub := range cmd.Commands() { 314 if sub.Hidden { 315 continue 316 } 317 generateSubcommandSection(b, sub, level+1) 318 } 319 } 320} 321 322// findCommand finds a command by path 323func findCommand(root *cobra.Command, path []string) *cobra.Command { 324 if len(path) == 0 { 325 return root 326 } 327 328 for _, cmd := range root.Commands() { 329 if cmd.Name() == path[0] || slices.Contains(cmd.Aliases, path[0]) { 330 if len(path) == 1 { 331 return cmd 332 } 333 return findCommand(cmd, path[1:]) 334 } 335 } 336 337 return nil 338}