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