cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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