ruby_todo 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/ruby_todo/cli.rb CHANGED
@@ -7,6 +7,7 @@ require "tty-table"
7
7
  require "time"
8
8
  require "json"
9
9
  require "fileutils"
10
+ require "csv"
10
11
  require_relative "models/notebook"
11
12
  require_relative "models/task"
12
13
  require_relative "models/template"
@@ -14,8 +15,146 @@ require_relative "database"
14
15
  require_relative "commands/ai_assistant"
15
16
 
16
17
  module RubyTodo
18
+ module Export
19
+ private
20
+
21
+ def export_notebook(notebook)
22
+ {
23
+ "name" => notebook.name,
24
+ "created_at" => notebook.created_at,
25
+ "updated_at" => notebook.updated_at,
26
+ "tasks" => notebook.tasks.map { |task| task_to_hash(task) }
27
+ }
28
+ end
29
+
30
+ def export_all_notebooks(notebooks)
31
+ {
32
+ "notebooks" => notebooks.map { |notebook| export_notebook(notebook) }
33
+ }
34
+ end
35
+
36
+ def task_to_hash(task)
37
+ {
38
+ "title" => task.title,
39
+ "description" => task.description,
40
+ "status" => task.status,
41
+ "priority" => task.priority,
42
+ "tags" => task.tags,
43
+ "due_date" => task.due_date&.iso8601,
44
+ "created_at" => task.created_at&.iso8601,
45
+ "updated_at" => task.updated_at&.iso8601
46
+ }
47
+ end
48
+
49
+ def export_to_json(data, filename)
50
+ File.write(filename, JSON.pretty_generate(data))
51
+ end
52
+
53
+ def export_to_csv(data, filename)
54
+ CSV.open(filename, "wb") do |csv|
55
+ if data["notebooks"]
56
+ export_multiple_notebooks_to_csv(data, csv)
57
+ else
58
+ export_single_notebook_to_csv(data, csv)
59
+ end
60
+ end
61
+ end
62
+
63
+ def export_multiple_notebooks_to_csv(data, csv)
64
+ csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
65
+ "Created At", "Updated At"]
66
+
67
+ data["notebooks"].each do |notebook|
68
+ notebook_name = notebook["name"]
69
+ notebook["tasks"].each_with_index do |task, index|
70
+ csv << [
71
+ notebook_name,
72
+ index + 1,
73
+ task["title"],
74
+ task["description"],
75
+ task["status"],
76
+ task["priority"],
77
+ task["tags"],
78
+ task["due_date"],
79
+ task["created_at"],
80
+ task["updated_at"]
81
+ ]
82
+ end
83
+ end
84
+ end
85
+
86
+ def export_single_notebook_to_csv(data, csv)
87
+ csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
88
+ "Updated At"]
89
+
90
+ data["tasks"].each_with_index do |task, index|
91
+ csv << [
92
+ index + 1,
93
+ task["title"],
94
+ task["description"],
95
+ task["status"],
96
+ task["priority"],
97
+ task["tags"],
98
+ task["due_date"],
99
+ task["created_at"],
100
+ task["updated_at"]
101
+ ]
102
+ end
103
+ end
104
+ end
105
+
106
+ module Import
107
+ private
108
+
109
+ def import_tasks(notebook, tasks_data)
110
+ count = 0
111
+
112
+ tasks_data.each do |task_data|
113
+ # Convert ISO8601 string to Time object
114
+ due_date = Time.parse(task_data["due_date"]) if task_data["due_date"]
115
+
116
+ task = Task.create(
117
+ notebook: notebook,
118
+ title: task_data["title"],
119
+ description: task_data["description"],
120
+ status: task_data["status"] || "todo",
121
+ priority: task_data["priority"],
122
+ tags: task_data["tags"],
123
+ due_date: due_date
124
+ )
125
+
126
+ count += 1 if task.persisted?
127
+ end
128
+
129
+ count
130
+ end
131
+
132
+ def import_all_notebooks(data)
133
+ results = { notebooks: 0, tasks: 0 }
134
+
135
+ data["notebooks"].each do |notebook_data|
136
+ notebook_name = notebook_data["name"]
137
+ notebook = Notebook.find_by(name: notebook_name)
138
+
139
+ unless notebook
140
+ notebook = Notebook.create(name: notebook_name)
141
+ results[:notebooks] += 1 if notebook.persisted?
142
+ end
143
+
144
+ if notebook.persisted?
145
+ tasks_count = import_tasks(notebook, notebook_data["tasks"])
146
+ results[:tasks] += tasks_count
147
+ end
148
+ end
149
+
150
+ results
151
+ end
152
+ end
153
+
17
154
  class CLI < Thor
18
155
  include Thor::Actions
156
+ include Export
157
+ include Import
19
158
 
20
159
  map %w[--version -v] => :version
21
160
  desc "version", "Show the Ruby Todo version"
@@ -204,23 +343,95 @@ module RubyTodo
204
343
  say "Ruby Todo has been initialized successfully!".green
205
344
  end
206
345
 
207
- # Register subcommands
208
- desc "notebook SUBCOMMAND", "Manage notebooks"
209
- subcommand "notebook", NotebookCommand
346
+ # Register subcommands with colon format
347
+ desc "notebook:create NAME", "Create a new notebook"
348
+ def notebook_create(name)
349
+ Notebook.create(name: name)
350
+ puts "Created notebook: #{name}".green
351
+ end
352
+
353
+ desc "notebook:list", "List all notebooks"
354
+ def notebook_list
355
+ notebooks = Notebook.all
356
+ return say "No notebooks found".yellow if notebooks.empty?
357
+
358
+ table = TTY::Table.new(
359
+ header: ["ID", "Name", "Tasks", "Created At", "Default"],
360
+ rows: notebooks.map do |notebook|
361
+ [
362
+ notebook.id,
363
+ notebook.name,
364
+ notebook.tasks.count,
365
+ notebook.created_at,
366
+ notebook.is_default? ? "✓" : ""
367
+ ]
368
+ end
369
+ )
370
+ puts table.render(:ascii)
371
+ end
372
+
373
+ desc "notebook:set_default NOTEBOOK", "Set a notebook as the default"
374
+ def notebook_set_default(name)
375
+ notebook = Notebook.find_by(name: name)
376
+ if notebook
377
+ notebook.make_default!
378
+ say "Successfully set '#{name}' as the default notebook".green
379
+ else
380
+ say "Notebook '#{name}' not found".red
381
+ end
382
+ end
383
+
384
+ desc "template:create NAME", "Create a new task template"
385
+ def template_create(name)
386
+ TemplateCommand.new.create(name)
387
+ end
388
+
389
+ desc "template:list", "List all templates"
390
+ def template_list
391
+ TemplateCommand.new.list
392
+ end
210
393
 
211
- desc "template SUBCOMMAND", "Manage task templates"
212
- subcommand "template", TemplateCommand
394
+ desc "template:show NAME", "Show details of a specific template"
395
+ def template_show(name)
396
+ TemplateCommand.new.show(name)
397
+ end
213
398
 
214
- # Register AI Assistant subcommand
215
- desc "ai SUBCOMMAND", "Use AI assistant"
216
- subcommand "ai", AIAssistantCommand
399
+ desc "template:delete NAME", "Delete a template"
400
+ def template_delete(name)
401
+ TemplateCommand.new.delete(name)
402
+ end
403
+
404
+ desc "template:use NAME NOTEBOOK", "Create a task from a template in the specified notebook"
405
+ def template_use(name, notebook)
406
+ TemplateCommand.new.use(name, notebook)
407
+ end
217
408
 
218
- desc "task add [NOTEBOOK] [TITLE]", "Add a new task to a notebook"
409
+ # Register AI Assistant subcommand with colon format
410
+ desc "ai:ask PROMPT", "Ask the AI assistant to perform tasks using natural language"
411
+ method_option :api_key, type: :string, desc: "OpenAI API key"
412
+ method_option :verbose, type: :boolean, default: false, desc: "Show detailed response"
413
+ def ai_ask(*prompt_args)
414
+ prompt = prompt_args.join(" ")
415
+ ai_command.ask(prompt)
416
+ end
417
+
418
+ desc "ai:configure", "Configure the AI assistant settings"
419
+ def ai_configure
420
+ ai_command.configure
421
+ end
422
+
423
+ # Map commands to use colon format
424
+ map "notebook:list" => :notebook_list
425
+ map "ai:ask" => :ai_ask
426
+ map "ai:configure" => :ai_configure
427
+
428
+ # Task commands
429
+ desc "task:add [NOTEBOOK] [TITLE]", "Add a new task to a notebook"
219
430
  method_option :description, type: :string, desc: "Task description"
220
431
  method_option :due_date, type: :string, desc: "Due date (YYYY-MM-DD HH:MM)"
221
432
  method_option :priority, type: :string, desc: "Priority (high, medium, low)"
222
433
  method_option :tags, type: :string, desc: "Tags (comma-separated)"
223
- def add(notebook_name, title)
434
+ def task_add(notebook_name, title)
224
435
  notebook = Notebook.find_by(name: notebook_name)
225
436
  unless notebook
226
437
  say "Notebook '#{notebook_name}' not found".red
@@ -249,11 +460,11 @@ module RubyTodo
249
460
  say "Priority: #{priority}" if priority
250
461
  say "Tags: #{tags}" if tags
251
462
  else
252
- say "Error creating task: #{task.errors.full_messages.join(", ")}".red
463
+ say "Error adding task: #{task.errors.full_messages.join(", ")}".red
253
464
  end
254
465
  end
255
466
 
256
- desc "task list [NOTEBOOK]", "List all tasks in a notebook"
467
+ desc "task:list [NOTEBOOK]", "List all tasks in a notebook"
257
468
  method_option :status, type: :string, desc: "Filter by status (todo, in_progress, done, archived)"
258
469
  method_option :priority, type: :string, desc: "Filter by priority (high, medium, low)"
259
470
  method_option :due_soon, type: :boolean, desc: "Show only tasks due soon (within 24 hours)"
@@ -302,7 +513,7 @@ module RubyTodo
302
513
  puts table.render(:ascii)
303
514
  end
304
515
 
305
- desc "task show [NOTEBOOK] [TASK_ID]", "Show detailed information about a task"
516
+ desc "task:show [NOTEBOOK] [TASK_ID]", "Show detailed information about a task"
306
517
  def task_show(notebook_name, task_id)
307
518
  notebook = Notebook.find_by(name: notebook_name)
308
519
  unless notebook
@@ -328,14 +539,14 @@ module RubyTodo
328
539
  say "Updated: #{task.updated_at}"
329
540
  end
330
541
 
331
- desc "task edit [NOTEBOOK] [TASK_ID]", "Edit an existing task"
542
+ desc "task:edit [NOTEBOOK] [TASK_ID]", "Edit an existing task"
332
543
  method_option :title, type: :string, desc: "New title"
333
544
  method_option :description, type: :string, desc: "New description"
334
545
  method_option :due_date, type: :string, desc: "New due date (YYYY-MM-DD HH:MM)"
335
546
  method_option :priority, type: :string, desc: "New priority (high, medium, low)"
336
547
  method_option :tags, type: :string, desc: "New tags (comma-separated)"
337
548
  method_option :status, type: :string, desc: "New status (todo, in_progress, done, archived)"
338
- def edit(notebook_name, task_id)
549
+ def task_edit(notebook_name, task_id)
339
550
  notebook = Notebook.find_by(name: notebook_name)
340
551
  unless notebook
341
552
  say "Notebook '#{notebook_name}' not found".red
@@ -371,8 +582,8 @@ module RubyTodo
371
582
  end
372
583
  end
373
584
 
374
- desc "task move [NOTEBOOK] [TASK_ID] [STATUS]", "Move a task to a different status"
375
- def move(notebook_name, task_id, status)
585
+ desc "task:move [NOTEBOOK] [TASK_ID] [STATUS]", "Move a task to a different status"
586
+ def task_move(notebook_name, task_id, status)
376
587
  notebook = Notebook.find_by(name: notebook_name)
377
588
  unless notebook
378
589
  say "Notebook '#{notebook_name}' not found".red
@@ -392,8 +603,8 @@ module RubyTodo
392
603
  end
393
604
  end
394
605
 
395
- desc "task delete [NOTEBOOK] [TASK_ID]", "Delete a task"
396
- def delete(notebook_name, task_id)
606
+ desc "task:delete [NOTEBOOK] [TASK_ID]", "Delete a task"
607
+ def task_delete(notebook_name, task_id)
397
608
  notebook = Notebook.find_by(name: notebook_name)
398
609
  unless notebook
399
610
  say "Notebook '#{notebook_name}' not found".red
@@ -410,9 +621,9 @@ module RubyTodo
410
621
  say "Deleted task #{task_id}".green
411
622
  end
412
623
 
413
- desc "task search [QUERY]", "Search for tasks across all notebooks"
624
+ desc "task:search [QUERY]", "Search for tasks across all notebooks"
414
625
  method_option :notebook, type: :string, desc: "Limit search to a specific notebook"
415
- def search(query)
626
+ def task_search(query)
416
627
  notebooks = if options[:notebook]
417
628
  [Notebook.find_by(name: options[:notebook])].compact
418
629
  else
@@ -584,140 +795,70 @@ module RubyTodo
584
795
  end
585
796
  end
586
797
 
587
- # Task-related command aliases
588
- map "task:list" => "task_list"
589
- map "task:show" => "task_show"
590
- map "task:add" => "add"
591
- map "task:edit" => "edit"
592
- map "task:delete" => "delete"
593
- map "task:move" => "move"
594
- map "task:search" => "search"
595
-
596
- private
597
-
598
- def export_notebook(notebook)
599
- {
600
- "name" => notebook.name,
601
- "created_at" => notebook.created_at,
602
- "updated_at" => notebook.updated_at,
603
- "tasks" => notebook.tasks.map { |task| task_to_hash(task) }
604
- }
605
- end
606
-
607
- def export_all_notebooks(notebooks)
608
- {
609
- "notebooks" => notebooks.map { |notebook| export_notebook(notebook) }
610
- }
611
- end
612
-
613
- def task_to_hash(task)
614
- {
615
- "title" => task.title,
616
- "description" => task.description,
617
- "status" => task.status,
618
- "priority" => task.priority,
619
- "tags" => task.tags,
620
- "due_date" => task.due_date&.iso8601,
621
- "created_at" => task.created_at&.iso8601,
622
- "updated_at" => task.updated_at&.iso8601
623
- }
624
- end
625
-
626
- def export_to_json(data, filename)
627
- File.write(filename, JSON.pretty_generate(data))
628
- end
629
-
630
- def export_to_csv(data, filename)
631
- require "csv"
632
-
633
- CSV.open(filename, "wb") do |csv|
634
- if data["notebooks"]
635
- # Multiple notebooks export
636
- csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
637
- "Created At", "Updated At"]
638
-
639
- data["notebooks"].each do |notebook|
640
- notebook_name = notebook["name"]
641
- notebook["tasks"].each_with_index do |task, index|
642
- csv << [
643
- notebook_name,
644
- index + 1,
645
- task["title"],
646
- task["description"],
647
- task["status"],
648
- task["priority"],
649
- task["tags"],
650
- task["due_date"],
651
- task["created_at"],
652
- task["updated_at"]
653
- ]
654
- end
655
- end
656
- else
657
- # Single notebook export
658
- csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
659
- "Updated At"]
660
-
661
- data["tasks"].each_with_index do |task, index|
662
- csv << [
663
- index + 1,
664
- task["title"],
665
- task["description"],
666
- task["status"],
667
- task["priority"],
668
- task["tags"],
669
- task["due_date"],
670
- task["created_at"],
671
- task["updated_at"]
672
- ]
673
- end
674
- end
798
+ # Map all commands to use colon format
799
+ map "task:add" => :task_add
800
+ map "task:list" => :task_list
801
+ map "task:show" => :task_show
802
+ map "task:edit" => :task_edit
803
+ map "task:move" => :task_move
804
+ map "task:delete" => :task_delete
805
+ map "task:search" => :task_search
806
+ map "notebook:create" => :notebook_create
807
+ map "notebook:list" => :notebook_list
808
+ map "notebook:set_default" => :notebook_set_default
809
+ map "template:create" => :template_create
810
+ map "template:list" => :template_list
811
+ map "template:show" => :template_show
812
+ map "template:delete" => :template_delete
813
+ map "template:use" => :template_use
814
+
815
+ # Remove old command mappings
816
+ no_commands do
817
+ def self.remove_old_commands
818
+ remove_command :create
819
+ remove_command :list
820
+ remove_command :show
821
+ remove_command :delete
822
+ remove_command :use
823
+ remove_command :ai
675
824
  end
676
825
  end
677
826
 
678
- def import_tasks(notebook, tasks_data)
679
- count = 0
680
-
681
- tasks_data.each do |task_data|
682
- # Convert ISO8601 string to Time object
683
- due_date = Time.parse(task_data["due_date"]) if task_data["due_date"]
684
-
685
- task = Task.create(
686
- notebook: notebook,
687
- title: task_data["title"],
688
- description: task_data["description"],
689
- status: task_data["status"] || "todo",
690
- priority: task_data["priority"],
691
- tags: task_data["tags"],
692
- due_date: due_date
693
- )
694
-
695
- count += 1 if task.persisted?
827
+ remove_old_commands
828
+
829
+ # Override the help command to show only colon-formatted commands
830
+ def help(command = nil)
831
+ if command.nil?
832
+ puts "Commands:"
833
+ puts " ruby_todo ai:ask PROMPT # Ask the AI assistant to perform tasks"
834
+ puts " ruby_todo ai:configure # Configure the AI assistant settings"
835
+ puts " ruby_todo notebook:create NAME # Create a new notebook"
836
+ puts " ruby_todo notebook:list # List all notebooks"
837
+ puts " ruby_todo notebook:set_default NAME # Set a notebook as the default"
838
+ puts " ruby_todo task:add [NOTEBOOK] [TITLE] # Add a new task to a notebook"
839
+ puts " ruby_todo task:list [NOTEBOOK] # List all tasks in a notebook"
840
+ puts " ruby_todo task:show [NOTEBOOK] [TASK_ID] # Show task details"
841
+ puts " ruby_todo task:edit [NOTEBOOK] [TASK_ID] # Edit a task"
842
+ puts " ruby_todo task:move [NOTEBOOK] [TASK_ID] STATUS # Move a task to a different status"
843
+ puts " ruby_todo task:delete [NOTEBOOK] [TASK_ID] # Delete a task"
844
+ puts " ruby_todo task:search [QUERY] # Search for tasks"
845
+ puts " ruby_todo template:create NAME # Create a task template"
846
+ puts " ruby_todo template:list # List all templates"
847
+ puts " ruby_todo template:show NAME # Show template details"
848
+ puts " ruby_todo template:delete NAME # Delete a template"
849
+ puts " ruby_todo template:use NAME NOTEBOOK # Use a template"
850
+ puts " ruby_todo export [NOTEBOOK] [FILENAME] # Export tasks"
851
+ puts " ruby_todo import [FILENAME] # Import tasks"
852
+ puts " ruby_todo init # Initialize todo list"
853
+ puts " ruby_todo version # Show version"
854
+ puts "\nOptions:"
855
+ puts " [--notebook=NOTEBOOK] # Specify the notebook to use"
856
+ else
857
+ super
696
858
  end
697
-
698
- count
699
859
  end
700
860
 
701
- def import_all_notebooks(data)
702
- results = { notebooks: 0, tasks: 0 }
703
-
704
- data["notebooks"].each do |notebook_data|
705
- notebook_name = notebook_data["name"]
706
- notebook = Notebook.find_by(name: notebook_name)
707
-
708
- unless notebook
709
- notebook = Notebook.create(name: notebook_name)
710
- results[:notebooks] += 1 if notebook.persisted?
711
- end
712
-
713
- if notebook.persisted?
714
- tasks_count = import_tasks(notebook, notebook_data["tasks"])
715
- results[:tasks] += tasks_count
716
- end
717
- end
718
-
719
- results
720
- end
861
+ private
721
862
 
722
863
  def display_notebook_stats(notebook)
723
864
  stats = notebook.statistics
@@ -861,5 +1002,9 @@ module RubyTodo
861
1002
 
862
1003
  text.length > length ? "#{text[0...length]}..." : text
863
1004
  end
1005
+
1006
+ def ai_command
1007
+ @ai_command ||= AIAssistantCommand.new
1008
+ end
864
1009
  end
865
1010
  end