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 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}