ruby_todo 0.4.1 → 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,14 +7,154 @@ 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"
13
14
  require_relative "database"
15
+ require_relative "commands/ai_assistant"
14
16
 
15
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
+
16
154
  class CLI < Thor
17
155
  include Thor::Actions
156
+ include Export
157
+ include Import
18
158
 
19
159
  map %w[--version -v] => :version
20
160
  desc "version", "Show the Ruby Todo version"
@@ -203,19 +343,95 @@ module RubyTodo
203
343
  say "Ruby Todo has been initialized successfully!".green
204
344
  end
205
345
 
206
- # Register subcommands
207
- desc "notebook SUBCOMMAND", "Manage notebooks"
208
- 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
209
352
 
210
- desc "template SUBCOMMAND", "Manage task templates"
211
- subcommand "template", TemplateCommand
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
212
372
 
213
- desc "task add [NOTEBOOK] [TITLE]", "Add a new task to a notebook"
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
393
+
394
+ desc "template:show NAME", "Show details of a specific template"
395
+ def template_show(name)
396
+ TemplateCommand.new.show(name)
397
+ end
398
+
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
408
+
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"
214
430
  method_option :description, type: :string, desc: "Task description"
215
431
  method_option :due_date, type: :string, desc: "Due date (YYYY-MM-DD HH:MM)"
216
432
  method_option :priority, type: :string, desc: "Priority (high, medium, low)"
217
433
  method_option :tags, type: :string, desc: "Tags (comma-separated)"
218
- def add(notebook_name, title)
434
+ def task_add(notebook_name, title)
219
435
  notebook = Notebook.find_by(name: notebook_name)
220
436
  unless notebook
221
437
  say "Notebook '#{notebook_name}' not found".red
@@ -244,11 +460,11 @@ module RubyTodo
244
460
  say "Priority: #{priority}" if priority
245
461
  say "Tags: #{tags}" if tags
246
462
  else
247
- say "Error creating task: #{task.errors.full_messages.join(", ")}".red
463
+ say "Error adding task: #{task.errors.full_messages.join(", ")}".red
248
464
  end
249
465
  end
250
466
 
251
- desc "task list [NOTEBOOK]", "List all tasks in a notebook"
467
+ desc "task:list [NOTEBOOK]", "List all tasks in a notebook"
252
468
  method_option :status, type: :string, desc: "Filter by status (todo, in_progress, done, archived)"
253
469
  method_option :priority, type: :string, desc: "Filter by priority (high, medium, low)"
254
470
  method_option :due_soon, type: :boolean, desc: "Show only tasks due soon (within 24 hours)"
@@ -297,7 +513,7 @@ module RubyTodo
297
513
  puts table.render(:ascii)
298
514
  end
299
515
 
300
- 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"
301
517
  def task_show(notebook_name, task_id)
302
518
  notebook = Notebook.find_by(name: notebook_name)
303
519
  unless notebook
@@ -323,14 +539,14 @@ module RubyTodo
323
539
  say "Updated: #{task.updated_at}"
324
540
  end
325
541
 
326
- desc "task edit [NOTEBOOK] [TASK_ID]", "Edit an existing task"
542
+ desc "task:edit [NOTEBOOK] [TASK_ID]", "Edit an existing task"
327
543
  method_option :title, type: :string, desc: "New title"
328
544
  method_option :description, type: :string, desc: "New description"
329
545
  method_option :due_date, type: :string, desc: "New due date (YYYY-MM-DD HH:MM)"
330
546
  method_option :priority, type: :string, desc: "New priority (high, medium, low)"
331
547
  method_option :tags, type: :string, desc: "New tags (comma-separated)"
332
548
  method_option :status, type: :string, desc: "New status (todo, in_progress, done, archived)"
333
- def edit(notebook_name, task_id)
549
+ def task_edit(notebook_name, task_id)
334
550
  notebook = Notebook.find_by(name: notebook_name)
335
551
  unless notebook
336
552
  say "Notebook '#{notebook_name}' not found".red
@@ -366,8 +582,8 @@ module RubyTodo
366
582
  end
367
583
  end
368
584
 
369
- desc "task move [NOTEBOOK] [TASK_ID] [STATUS]", "Move a task to a different status"
370
- 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)
371
587
  notebook = Notebook.find_by(name: notebook_name)
372
588
  unless notebook
373
589
  say "Notebook '#{notebook_name}' not found".red
@@ -387,8 +603,8 @@ module RubyTodo
387
603
  end
388
604
  end
389
605
 
390
- desc "task delete [NOTEBOOK] [TASK_ID]", "Delete a task"
391
- def delete(notebook_name, task_id)
606
+ desc "task:delete [NOTEBOOK] [TASK_ID]", "Delete a task"
607
+ def task_delete(notebook_name, task_id)
392
608
  notebook = Notebook.find_by(name: notebook_name)
393
609
  unless notebook
394
610
  say "Notebook '#{notebook_name}' not found".red
@@ -405,9 +621,9 @@ module RubyTodo
405
621
  say "Deleted task #{task_id}".green
406
622
  end
407
623
 
408
- desc "task search [QUERY]", "Search for tasks across all notebooks"
624
+ desc "task:search [QUERY]", "Search for tasks across all notebooks"
409
625
  method_option :notebook, type: :string, desc: "Limit search to a specific notebook"
410
- def search(query)
626
+ def task_search(query)
411
627
  notebooks = if options[:notebook]
412
628
  [Notebook.find_by(name: options[:notebook])].compact
413
629
  else
@@ -579,140 +795,70 @@ module RubyTodo
579
795
  end
580
796
  end
581
797
 
582
- # Task-related command aliases
583
- map "task:list" => "task_list"
584
- map "task:show" => "task_show"
585
- map "task:add" => "add"
586
- map "task:edit" => "edit"
587
- map "task:delete" => "delete"
588
- map "task:move" => "move"
589
- map "task:search" => "search"
590
-
591
- private
592
-
593
- def export_notebook(notebook)
594
- {
595
- "name" => notebook.name,
596
- "created_at" => notebook.created_at,
597
- "updated_at" => notebook.updated_at,
598
- "tasks" => notebook.tasks.map { |task| task_to_hash(task) }
599
- }
600
- end
601
-
602
- def export_all_notebooks(notebooks)
603
- {
604
- "notebooks" => notebooks.map { |notebook| export_notebook(notebook) }
605
- }
606
- end
607
-
608
- def task_to_hash(task)
609
- {
610
- "title" => task.title,
611
- "description" => task.description,
612
- "status" => task.status,
613
- "priority" => task.priority,
614
- "tags" => task.tags,
615
- "due_date" => task.due_date&.iso8601,
616
- "created_at" => task.created_at&.iso8601,
617
- "updated_at" => task.updated_at&.iso8601
618
- }
619
- end
620
-
621
- def export_to_json(data, filename)
622
- File.write(filename, JSON.pretty_generate(data))
623
- end
624
-
625
- def export_to_csv(data, filename)
626
- require "csv"
627
-
628
- CSV.open(filename, "wb") do |csv|
629
- if data["notebooks"]
630
- # Multiple notebooks export
631
- csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
632
- "Created At", "Updated At"]
633
-
634
- data["notebooks"].each do |notebook|
635
- notebook_name = notebook["name"]
636
- notebook["tasks"].each_with_index do |task, index|
637
- csv << [
638
- notebook_name,
639
- index + 1,
640
- task["title"],
641
- task["description"],
642
- task["status"],
643
- task["priority"],
644
- task["tags"],
645
- task["due_date"],
646
- task["created_at"],
647
- task["updated_at"]
648
- ]
649
- end
650
- end
651
- else
652
- # Single notebook export
653
- csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
654
- "Updated At"]
655
-
656
- data["tasks"].each_with_index do |task, index|
657
- csv << [
658
- index + 1,
659
- task["title"],
660
- task["description"],
661
- task["status"],
662
- task["priority"],
663
- task["tags"],
664
- task["due_date"],
665
- task["created_at"],
666
- task["updated_at"]
667
- ]
668
- end
669
- 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
670
824
  end
671
825
  end
672
826
 
673
- def import_tasks(notebook, tasks_data)
674
- count = 0
675
-
676
- tasks_data.each do |task_data|
677
- # Convert ISO8601 string to Time object
678
- due_date = Time.parse(task_data["due_date"]) if task_data["due_date"]
679
-
680
- task = Task.create(
681
- notebook: notebook,
682
- title: task_data["title"],
683
- description: task_data["description"],
684
- status: task_data["status"] || "todo",
685
- priority: task_data["priority"],
686
- tags: task_data["tags"],
687
- due_date: due_date
688
- )
689
-
690
- 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
691
858
  end
692
-
693
- count
694
859
  end
695
860
 
696
- def import_all_notebooks(data)
697
- results = { notebooks: 0, tasks: 0 }
698
-
699
- data["notebooks"].each do |notebook_data|
700
- notebook_name = notebook_data["name"]
701
- notebook = Notebook.find_by(name: notebook_name)
702
-
703
- unless notebook
704
- notebook = Notebook.create(name: notebook_name)
705
- results[:notebooks] += 1 if notebook.persisted?
706
- end
707
-
708
- if notebook.persisted?
709
- tasks_count = import_tasks(notebook, notebook_data["tasks"])
710
- results[:tasks] += tasks_count
711
- end
712
- end
713
-
714
- results
715
- end
861
+ private
716
862
 
717
863
  def display_notebook_stats(notebook)
718
864
  stats = notebook.statistics
@@ -856,5 +1002,9 @@ module RubyTodo
856
1002
 
857
1003
  text.length > length ? "#{text[0...length]}..." : text
858
1004
  end
1005
+
1006
+ def ai_command
1007
+ @ai_command ||= AIAssistantCommand.new
1008
+ end
859
1009
  end
860
1010
  end