cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 705 lines 23 kB view raw
1package main 2 3import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/spf13/cobra" 9 "github.com/stormlightlabs/noteleaf/internal/handlers" 10) 11 12// CommandGroup represents a group of related CLI commands 13type CommandGroup interface { 14 Create() *cobra.Command 15} 16 17// MovieCommand implements [CommandGroup] for movie-related commands 18type MovieCommand struct { 19 handler *handlers.MovieHandler 20} 21 22// NewMovieCommand creates a new MovieCommands with the given handler 23func NewMovieCommand(handler *handlers.MovieHandler) *MovieCommand { 24 return &MovieCommand{handler: handler} 25} 26 27func (c *MovieCommand) Create() *cobra.Command { 28 root := &cobra.Command{ 29 Use: "movie", 30 Short: "Manage movie watch queue", 31 Long: `Track movies you want to watch. 32 33Search for movies and add them to your queue. Mark movies as watched 34when completed. Maintains a history of your movie watching activity.`, 35 } 36 37 // TODO: add colors 38 // TODO: fix critic score parsing 39 addCmd := &cobra.Command{ 40 Use: "add [search query...]", 41 Short: "Search and add movie to watch queue", 42 Long: `Search for movies and add them to your watch queue. 43 44By default, shows search results in a simple list format where you can select by number. 45Use the -i flag for an interactive interface with navigation keys.`, 46 RunE: func(cmd *cobra.Command, args []string) error { 47 if len(args) == 0 { 48 return fmt.Errorf("search query cannot be empty") 49 } 50 interactive, _ := cmd.Flags().GetBool("interactive") 51 query := strings.Join(args, " ") 52 53 return c.handler.SearchAndAdd(cmd.Context(), query, interactive) 54 }, 55 } 56 addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for movie selection") 57 root.AddCommand(addCmd) 58 59 // TODO: add interactive list view 60 root.AddCommand(&cobra.Command{ 61 Use: "list [--all|--watched|--queued]", 62 Short: "List movies in queue with status filtering", 63 Long: `Display movies in your queue with optional status filters. 64 65Shows movie titles, release years, and current status. Filter by --all to show 66everything, --watched for completed movies, or --queued for unwatched items. 67Default shows queued movies only.`, 68 RunE: func(cmd *cobra.Command, args []string) error { 69 var status string 70 if len(args) > 0 { 71 switch args[0] { 72 case "--all": 73 status = "" 74 case "--watched": 75 status = "watched" 76 case "--queued": 77 status = "queued" 78 default: 79 return fmt.Errorf("invalid status filter: %s (use: --all, --watched, --queued)", args[0]) 80 } 81 } 82 83 return c.handler.List(cmd.Context(), status) 84 }, 85 }) 86 87 root.AddCommand(&cobra.Command{ 88 Use: "watched [id]", 89 Short: "Mark movie as watched", 90 Aliases: []string{"seen"}, 91 Long: "Mark a movie as watched with current timestamp. Moves the movie from queued to watched status.", 92 Args: cobra.ExactArgs(1), 93 RunE: func(cmd *cobra.Command, args []string) error { 94 return c.handler.MarkWatched(cmd.Context(), args[0]) 95 }, 96 }) 97 98 root.AddCommand(&cobra.Command{ 99 Use: "remove [id]", 100 Short: "Remove movie from queue", 101 Aliases: []string{"rm"}, 102 Long: "Remove a movie from your watch queue. Use this for movies you no longer want to track.", 103 Args: cobra.ExactArgs(1), 104 RunE: func(cmd *cobra.Command, args []string) error { 105 return c.handler.Remove(cmd.Context(), args[0]) 106 }, 107 }) 108 return root 109} 110 111// TVCommand implements [CommandGroup] for TV show-related commands 112type TVCommand struct { 113 handler *handlers.TVHandler 114} 115 116// NewTVCommand creates a new [TVCommand] with the given handler 117func NewTVCommand(handler *handlers.TVHandler) *TVCommand { 118 return &TVCommand{handler: handler} 119} 120 121func (c *TVCommand) Create() *cobra.Command { 122 root := &cobra.Command{ 123 Use: "tv", 124 Short: "Manage TV show watch queue", 125 Long: `Track TV shows and episodes. 126 127Search for TV shows and add them to your queue. Track which shows you're currently 128watching, mark episodes as watched, and maintain a complete history of your viewing 129activity.`, 130 } 131 132 addCmd := &cobra.Command{ 133 Use: "add [search query...]", 134 Short: "Search and add TV show to watch queue", 135 Long: `Search for TV shows and add them to your watch queue. 136 137By default, shows search results in a simple list format where you can select by number. 138Use the -i flag for an interactive interface with navigation keys.`, 139 RunE: func(cmd *cobra.Command, args []string) error { 140 if len(args) == 0 { 141 return fmt.Errorf("search query cannot be empty") 142 } 143 interactive, _ := cmd.Flags().GetBool("interactive") 144 query := strings.Join(args, " ") 145 146 return c.handler.SearchAndAdd(cmd.Context(), query, interactive) 147 }, 148 } 149 addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for TV show selection") 150 root.AddCommand(addCmd) 151 152 // TODO: Add interactive list view 153 root.AddCommand(&cobra.Command{ 154 Use: "list [--all|--queued|--watching|--watched]", 155 Short: "List TV shows in queue with status filtering", 156 Long: `Display TV shows in your queue with optional status filters. 157 158Shows show titles, air dates, and current status. Filter by --all, --queued, 159--watching for shows in progress, or --watched for completed series. Default 160shows queued shows only.`, 161 RunE: func(cmd *cobra.Command, args []string) error { 162 var status string 163 if len(args) > 0 { 164 switch args[0] { 165 case "--all": 166 status = "" 167 case "--queued": 168 status = "queued" 169 case "--watching": 170 status = "watching" 171 case "--watched": 172 status = "watched" 173 default: 174 return fmt.Errorf("invalid status filter: %s (use: --all, --queued, --watching, --watched)", args[0]) 175 } 176 } 177 178 return c.handler.List(cmd.Context(), status) 179 }, 180 }) 181 182 root.AddCommand(&cobra.Command{ 183 Use: "watching [id]", 184 Short: "Mark TV show as currently watching", 185 Long: "Mark a TV show as currently watching. Use this when you start watching a series.", 186 Args: cobra.ExactArgs(1), 187 RunE: func(cmd *cobra.Command, args []string) error { 188 return c.handler.MarkTVShowWatching(cmd.Context(), args[0]) 189 }, 190 }) 191 192 root.AddCommand(&cobra.Command{ 193 Use: "watched [id]", 194 Short: "Mark TV show/episodes as watched", 195 Aliases: []string{"seen"}, 196 Long: `Mark TV show episodes or entire series as watched. 197 198Updates episode tracking and completion status. Can mark individual episodes 199or complete seasons/series depending on ID format.`, 200 Args: cobra.ExactArgs(1), 201 RunE: func(cmd *cobra.Command, args []string) error { 202 return c.handler.MarkWatched(cmd.Context(), args[0]) 203 }, 204 }) 205 206 root.AddCommand(&cobra.Command{ 207 Use: "remove [id]", 208 Short: "Remove TV show from queue", 209 Aliases: []string{"rm"}, 210 Long: "Remove a TV show from your watch queue. Use this for shows you no longer want to track.", 211 Args: cobra.ExactArgs(1), 212 RunE: func(cmd *cobra.Command, args []string) error { 213 return c.handler.Remove(cmd.Context(), args[0]) 214 }, 215 }) 216 217 return root 218} 219 220// BookCommand implements [CommandGroup] for book-related commands 221type BookCommand struct { 222 handler *handlers.BookHandler 223} 224 225// NewBookCommand creates a new [BookCommand] with the given handler 226func NewBookCommand(handler *handlers.BookHandler) *BookCommand { 227 return &BookCommand{handler: handler} 228} 229 230func (c *BookCommand) Create() *cobra.Command { 231 root := &cobra.Command{ 232 Use: "book", 233 Short: "Manage reading list", 234 Long: `Track books and reading progress. 235 236Search Google Books API to add books to your reading list. Track which books 237you're reading, update progress percentages, and maintain a history of finished 238books.`, 239 } 240 241 addCmd := &cobra.Command{ 242 Use: "add [search query...]", 243 Short: "Search and add book to reading list", 244 Long: `Search for books and add them to your reading list. 245 246By default, shows search results in a simple list format where you can select by number. 247Use the -i flag for an interactive interface with navigation keys.`, 248 RunE: func(cmd *cobra.Command, args []string) error { 249 interactive, _ := cmd.Flags().GetBool("interactive") 250 query := strings.Join(args, " ") 251 return c.handler.SearchAndAdd(cmd.Context(), query, interactive) 252 }, 253 } 254 addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for book selection") 255 root.AddCommand(addCmd) 256 257 root.AddCommand(&cobra.Command{ 258 Use: "list [--all|--reading|--finished|--queued]", 259 Short: "Show reading queue with progress", 260 Long: `Display books in your reading list with progress indicators. 261 262Shows book titles, authors, and reading progress percentages. Filter by --all, 263--reading for books in progress, --finished for completed books, or --queued 264for books not yet started. Default shows queued books only.`, 265 RunE: func(cmd *cobra.Command, args []string) error { 266 var status string 267 if len(args) > 0 { 268 switch args[0] { 269 case "--all": 270 status = "" 271 case "--reading": 272 status = "reading" 273 case "--finished": 274 status = "finished" 275 case "--queued": 276 status = "queued" 277 default: 278 return fmt.Errorf("invalid status filter: %s (use: --all, --reading, --finished, --queued)", args[0]) 279 } 280 } 281 return c.handler.List(cmd.Context(), status) 282 }, 283 }) 284 285 root.AddCommand(&cobra.Command{ 286 Use: "reading <id>", 287 Short: "Mark book as currently reading", 288 Long: "Mark a book as currently reading. Use this when you start a book from your queue.", 289 Args: cobra.ExactArgs(1), 290 RunE: func(cmd *cobra.Command, args []string) error { 291 return c.handler.UpdateStatus(cmd.Context(), args[0], "reading") 292 }, 293 }) 294 295 root.AddCommand(&cobra.Command{ 296 Use: "finished <id>", 297 Short: "Mark book as completed", 298 Aliases: []string{"read"}, 299 Long: "Mark a book as finished with current timestamp. Sets reading progress to 100%.", 300 Args: cobra.ExactArgs(1), 301 RunE: func(cmd *cobra.Command, args []string) error { 302 return c.handler.UpdateStatus(cmd.Context(), args[0], "finished") 303 }, 304 }) 305 306 root.AddCommand(&cobra.Command{ 307 Use: "remove <id>", 308 Short: "Remove from reading list", 309 Aliases: []string{"rm"}, 310 Long: "Remove a book from your reading list. Use this for books you no longer want to track.", 311 Args: cobra.ExactArgs(1), 312 RunE: func(cmd *cobra.Command, args []string) error { 313 return c.handler.UpdateStatus(cmd.Context(), args[0], "removed") 314 }, 315 }) 316 317 root.AddCommand(&cobra.Command{ 318 Use: "progress <id> <percentage>", 319 Short: "Update reading progress percentage (0-100)", 320 Long: `Set reading progress for a book. 321 322Specify a percentage value between 0 and 100 to indicate how far you've 323progressed through the book. Automatically updates status to 'reading' if not 324already set.`, 325 Args: cobra.ExactArgs(2), 326 RunE: func(cmd *cobra.Command, args []string) error { 327 progress, err := strconv.Atoi(args[1]) 328 if err != nil { 329 return fmt.Errorf("invalid progress percentage: %s", args[1]) 330 } 331 return c.handler.UpdateProgress(cmd.Context(), args[0], progress) 332 }, 333 }) 334 335 root.AddCommand(&cobra.Command{ 336 Use: "update <id> <status>", 337 Short: "Update book status (queued|reading|finished|removed)", 338 Long: `Change a book's status directly. 339 340Valid statuses are: queued (not started), reading (in progress), finished 341(completed), or removed (no longer tracking).`, 342 Args: cobra.ExactArgs(2), 343 RunE: func(cmd *cobra.Command, args []string) error { 344 return c.handler.UpdateStatus(cmd.Context(), args[0], args[1]) 345 }, 346 }) 347 348 return root 349} 350 351// NoteCommand implements [CommandGroup] for note-related commands 352type NoteCommand struct { 353 handler *handlers.NoteHandler 354} 355 356// NewNoteCommand creates a new [NoteCommand] with the given handler 357func NewNoteCommand(handler *handlers.NoteHandler) *NoteCommand { 358 return &NoteCommand{handler: handler} 359} 360 361func (c *NoteCommand) Create() *cobra.Command { 362 root := &cobra.Command{ 363 Use: "note", 364 Short: "Manage notes", 365 Long: `Create and organize markdown notes with tags. 366 367Write notes in markdown format, organize them with tags, browse them in an 368interactive TUI, and edit them in your preferred editor. Notes are stored as 369files on disk with metadata tracked in the database.`, 370 } 371 372 createCmd := &cobra.Command{ 373 Use: "create [title] [content...]", 374 Short: "Create a new note", 375 Aliases: []string{"new"}, 376 Long: `Create a new markdown note. 377 378Provide a title and optional content inline, or use --interactive to open an 379editor. Use --file to import content from an existing markdown file. Notes 380support tags for organization and full-text search. 381 382Examples: 383 noteleaf note create "Meeting notes" "Discussed project timeline" 384 noteleaf note create -i 385 noteleaf note create --file ~/documents/draft.md`, 386 RunE: func(cmd *cobra.Command, args []string) error { 387 interactive, _ := cmd.Flags().GetBool("interactive") 388 editor, _ := cmd.Flags().GetBool("editor") 389 filePath, _ := cmd.Flags().GetString("file") 390 391 var title, content string 392 if len(args) > 0 { 393 title = args[0] 394 } 395 if len(args) > 1 { 396 content = strings.Join(args[1:], " ") 397 } 398 399 defer c.handler.Close() 400 return c.handler.CreateWithOptions(cmd.Context(), title, content, filePath, interactive, editor) 401 }, 402 } 403 createCmd.Flags().BoolP("interactive", "i", false, "Open interactive editor") 404 createCmd.Flags().BoolP("editor", "e", false, "Prompt to open note in editor after creation") 405 createCmd.Flags().StringP("file", "f", "", "Create note from markdown file") 406 root.AddCommand(createCmd) 407 408 listCmd := &cobra.Command{ 409 Use: "list [--archived] [--static] [--tags=tag1,tag2]", 410 Short: "Opens interactive TUI browser for navigating and viewing notes", 411 Aliases: []string{"ls"}, 412 RunE: func(cmd *cobra.Command, args []string) error { 413 archived, _ := cmd.Flags().GetBool("archived") 414 static, _ := cmd.Flags().GetBool("static") 415 tagsStr, _ := cmd.Flags().GetString("tags") 416 417 var tags []string 418 if tagsStr != "" { 419 tags = strings.Split(tagsStr, ",") 420 for i := range tags { 421 tags[i] = strings.TrimSpace(tags[i]) 422 } 423 } 424 425 defer c.handler.Close() 426 return c.handler.List(cmd.Context(), static, archived, tags) 427 }, 428 } 429 listCmd.Flags().BoolP("archived", "a", false, "Show archived notes") 430 listCmd.Flags().BoolP("static", "s", false, "Show static list instead of interactive TUI") 431 listCmd.Flags().String("tags", "", "Filter by tags (comma-separated)") 432 root.AddCommand(listCmd) 433 434 root.AddCommand(&cobra.Command{ 435 Use: "read [note-id]", 436 Short: "Display formatted note content with syntax highlighting", 437 Aliases: []string{"view"}, 438 Long: `Display note content with formatted markdown rendering. 439 440Shows the note with syntax highlighting, proper formatting, and metadata. 441Useful for quick viewing without opening an editor.`, 442 Args: cobra.ExactArgs(1), 443 RunE: func(cmd *cobra.Command, args []string) error { 444 if noteID, err := handlers.ParseID(args[0], "note"); err != nil { 445 return err 446 } else { 447 defer c.handler.Close() 448 return c.handler.View(cmd.Context(), noteID) 449 } 450 }, 451 }) 452 453 root.AddCommand(&cobra.Command{ 454 Use: "edit [note-id]", 455 Short: "Edit note in configured editor", 456 Long: `Open note in your configured text editor. 457 458Uses the editor specified in your noteleaf configuration or the EDITOR 459environment variable. Changes are automatically saved when you close the 460editor.`, 461 Args: cobra.ExactArgs(1), 462 RunE: func(cmd *cobra.Command, args []string) error { 463 if noteID, err := handlers.ParseID(args[0], "note"); err != nil { 464 return err 465 } else { 466 defer c.handler.Close() 467 return c.handler.Edit(cmd.Context(), noteID) 468 } 469 }, 470 }) 471 472 root.AddCommand(&cobra.Command{ 473 Use: "remove [note-id]", 474 Short: "Permanently removes the note file and metadata", 475 Aliases: []string{"rm", "delete", "del"}, 476 Long: `Delete a note permanently. 477 478Removes both the markdown file and database metadata. This operation cannot be 479undone. You will be prompted for confirmation before deletion.`, 480 Args: cobra.ExactArgs(1), 481 RunE: func(cmd *cobra.Command, args []string) error { 482 if noteID, err := handlers.ParseID(args[0], "note"); err != nil { 483 return err 484 } else { 485 defer c.handler.Close() 486 return c.handler.Delete(cmd.Context(), noteID) 487 } 488 }, 489 }) 490 491 return root 492} 493 494// ArticleCommand implements [CommandGroup] for article-related commands 495type ArticleCommand struct { 496 handler *handlers.ArticleHandler 497} 498 499// NewArticleCommand creates a new ArticleCommand with the given handler 500func NewArticleCommand(handler *handlers.ArticleHandler) *ArticleCommand { 501 return &ArticleCommand{handler: handler} 502} 503 504func (c *ArticleCommand) Create() *cobra.Command { 505 root := &cobra.Command{ 506 Use: "article", 507 Short: "Manage saved articles", 508 Long: `Save and archive web articles locally. 509 510Parse articles from supported websites, extract clean content, and save as 511both markdown and HTML. Maintains a searchable archive of articles with 512metadata including author, title, and publication date.`, 513 } 514 515 addCmd := &cobra.Command{ 516 Use: "add <url>", 517 Short: "Parse and save article from URL", 518 Long: `Parse and save article content from a supported website. 519 520The article will be parsed using domain-specific XPath rules and saved 521as both Markdown and HTML files. Article metadata is stored in the database.`, 522 Args: cobra.ExactArgs(1), 523 RunE: func(cmd *cobra.Command, args []string) error { 524 525 defer c.handler.Close() 526 return c.handler.Add(cmd.Context(), args[0]) 527 }, 528 } 529 root.AddCommand(addCmd) 530 531 listCmd := &cobra.Command{ 532 Use: "list [query]", 533 Short: "List saved articles", 534 Aliases: []string{"ls"}, 535 Long: `List saved articles with optional filtering. 536 537Use query to filter by title, or use flags for more specific filtering.`, 538 RunE: func(cmd *cobra.Command, args []string) error { 539 author, _ := cmd.Flags().GetString("author") 540 limit, _ := cmd.Flags().GetInt("limit") 541 542 var query string 543 if len(args) > 0 { 544 query = strings.Join(args, " ") 545 } 546 547 defer c.handler.Close() 548 return c.handler.List(cmd.Context(), query, author, limit) 549 }, 550 } 551 listCmd.Flags().String("author", "", "Filter by author") 552 listCmd.Flags().IntP("limit", "l", 0, "Limit number of results (0 = no limit)") 553 root.AddCommand(listCmd) 554 555 viewCmd := &cobra.Command{ 556 Use: "view <id>", 557 Short: "View article details and content preview", 558 Aliases: []string{"show"}, 559 Long: `Display article metadata and summary. 560 561Shows article title, author, publication date, URL, and a brief content 562preview. Use 'read' command to view the full article content.`, 563 Args: cobra.ExactArgs(1), 564 RunE: func(cmd *cobra.Command, args []string) error { 565 if articleID, err := handlers.ParseID(args[0], "article"); err != nil { 566 return err 567 } else { 568 defer c.handler.Close() 569 return c.handler.View(cmd.Context(), articleID) 570 } 571 }, 572 } 573 root.AddCommand(viewCmd) 574 575 readCmd := &cobra.Command{ 576 Use: "read <id>", 577 Short: "Read article content with formatted markdown", 578 Long: `Read the full markdown content of an article with beautiful formatting. 579 580This displays the complete article content using syntax highlighting and proper formatting.`, 581 Args: cobra.ExactArgs(1), 582 RunE: func(cmd *cobra.Command, args []string) error { 583 if articleID, err := handlers.ParseID(args[0], "article"); err != nil { 584 return err 585 } else { 586 defer c.handler.Close() 587 return c.handler.Read(cmd.Context(), articleID) 588 } 589 }, 590 } 591 root.AddCommand(readCmd) 592 593 removeCmd := &cobra.Command{ 594 Use: "remove <id>", 595 Short: "Remove article and associated files", 596 Aliases: []string{"rm", "delete"}, 597 Long: `Delete an article and its files permanently. 598 599Removes the article metadata from the database and deletes associated markdown 600and HTML files. This operation cannot be undone.`, 601 Args: cobra.ExactArgs(1), 602 RunE: func(cmd *cobra.Command, args []string) error { 603 if articleID, err := handlers.ParseID(args[0], "article"); err != nil { 604 return err 605 } else { 606 defer c.handler.Close() 607 return c.handler.Remove(cmd.Context(), articleID) 608 } 609 }, 610 } 611 root.AddCommand(removeCmd) 612 613 originalHelpFunc := root.HelpFunc() 614 root.SetHelpFunc(func(cmd *cobra.Command, args []string) { 615 originalHelpFunc(cmd, args) 616 617 fmt.Println() 618 defer c.handler.Close() 619 c.handler.Help() 620 }) 621 622 return root 623} 624 625// ConfigCommand implements [CommandGroup] for configuration management commands 626type ConfigCommand struct { 627 handler *handlers.ConfigHandler 628} 629 630// NewConfigCommand creates a new [ConfigCommand] with the given handler 631func NewConfigCommand(handler *handlers.ConfigHandler) *ConfigCommand { 632 return &ConfigCommand{handler: handler} 633} 634 635func (c *ConfigCommand) Create() *cobra.Command { 636 root := &cobra.Command{ 637 Use: "config", 638 Short: "Manage noteleaf configuration", 639 } 640 641 root.AddCommand(&cobra.Command{ 642 Use: "get [key]", 643 Short: "Get configuration value(s)", 644 Long: `Display configuration values. 645 646If no key is provided, displays all configuration values. 647Otherwise, displays the value for the specified key.`, 648 Args: cobra.MaximumNArgs(1), 649 RunE: func(cmd *cobra.Command, args []string) error { 650 var key string 651 if len(args) > 0 { 652 key = args[0] 653 } 654 return c.handler.Get(key) 655 }, 656 }) 657 658 root.AddCommand(&cobra.Command{ 659 Use: "set <key> <value>", 660 Short: "Set configuration value", 661 Long: `Update a configuration value. 662 663Available keys: 664 database_path - Custom database file path 665 data_dir - Custom data directory 666 date_format - Date format string (default: 2006-01-02) 667 color_scheme - Color scheme (default: default) 668 default_view - Default view mode (default: list) 669 default_priority - Default task priority 670 editor - Preferred text editor 671 articles_dir - Articles storage directory 672 notes_dir - Notes storage directory 673 auto_archive - Auto-archive completed items (true/false) 674 sync_enabled - Enable synchronization (true/false) 675 sync_endpoint - Synchronization endpoint URL 676 sync_token - Synchronization token 677 export_format - Default export format (default: json) 678 movie_api_key - API key for movie database 679 book_api_key - API key for book database`, 680 Args: cobra.ExactArgs(2), 681 RunE: func(cmd *cobra.Command, args []string) error { 682 return c.handler.Set(args[0], args[1]) 683 }, 684 }) 685 686 root.AddCommand(&cobra.Command{ 687 Use: "path", 688 Short: "Show configuration file path", 689 Long: "Display the path to the configuration file being used.", 690 RunE: func(cmd *cobra.Command, args []string) error { 691 return c.handler.Path() 692 }, 693 }) 694 695 root.AddCommand(&cobra.Command{ 696 Use: "reset", 697 Short: "Reset configuration to defaults", 698 Long: "Reset all configuration values to their defaults.", 699 RunE: func(cmd *cobra.Command, args []string) error { 700 return c.handler.Reset() 701 }, 702 }) 703 704 return root 705}