tasks_cli 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/Gemfile +5 -3
- data/Gemfile.lock +61 -0
- data/LICENSE +21 -0
- data/bin/tasks +4 -3
- data/example.png +0 -0
- data/lib/tasks_cli/cli.rb +38 -19
- data/lib/tasks_cli/task.rb +17 -13
- data/lib/tasks_cli/task_manager.rb +18 -11
- data/lib/tasks_cli/version.rb +4 -2
- data/lib/tasks_cli.rb +8 -7
- data/readme.md +106 -0
- data/tasks_cli.gemspec +27 -21
- metadata +68 -8
- data/tasks +0 -248
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23f5574360b1def72fa621127d325779afb0ae22b260755686ff3dd87cf8bef0
|
4
|
+
data.tar.gz: dd4090622a552cde4d2bf01b774f1e2ae53980fae66b767491f7dcc36d9643c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1dcf2eef4863b35bb212cec9dbf4e090670d3c84ff734fb8e231eaed9151fd1780dc4ef7a95d640d8f7eeb377779f7fe89d3ea64a3cfadb47f69dbf154d5860c
|
7
|
+
data.tar.gz: f0d031c7ede706093e0ef8f5980ac7c64973a1b230e16a2e7557540267b1a1a3a38773ef1f9464d94f1e817076dcb934561bd1de1b150de6b9f496778728c70a
|
data/Gemfile
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in tasks_cli.gemspec
|
4
6
|
gemspec
|
5
7
|
|
6
|
-
gem
|
7
|
-
gem
|
8
|
+
gem 'rake', '~> 12.0'
|
9
|
+
gem 'rspec', '~> 3.0'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
tasks_cli (0.1.3)
|
5
|
+
csv (~> 3.0)
|
6
|
+
logger (~> 1.5)
|
7
|
+
ostruct (~> 0.3.0)
|
8
|
+
rainbow (~> 3.0)
|
9
|
+
thor (~> 1.0)
|
10
|
+
tty-table (~> 0.12.0)
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
csv (3.2.8)
|
16
|
+
diff-lcs (1.5.1)
|
17
|
+
logger (1.6.1)
|
18
|
+
ostruct (0.3.3)
|
19
|
+
pastel (0.8.0)
|
20
|
+
tty-color (~> 0.5)
|
21
|
+
rainbow (3.1.1)
|
22
|
+
rake (12.3.3)
|
23
|
+
rspec (3.13.0)
|
24
|
+
rspec-core (~> 3.13.0)
|
25
|
+
rspec-expectations (~> 3.13.0)
|
26
|
+
rspec-mocks (~> 3.13.0)
|
27
|
+
rspec-core (3.13.1)
|
28
|
+
rspec-support (~> 3.13.0)
|
29
|
+
rspec-expectations (3.13.3)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.13.0)
|
32
|
+
rspec-mocks (3.13.1)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.13.0)
|
35
|
+
rspec-support (3.13.1)
|
36
|
+
strings (0.2.1)
|
37
|
+
strings-ansi (~> 0.2)
|
38
|
+
unicode-display_width (>= 1.5, < 3.0)
|
39
|
+
unicode_utils (~> 1.4)
|
40
|
+
strings-ansi (0.2.0)
|
41
|
+
thor (1.3.2)
|
42
|
+
tty-color (0.6.0)
|
43
|
+
tty-screen (0.8.2)
|
44
|
+
tty-table (0.12.0)
|
45
|
+
pastel (~> 0.8)
|
46
|
+
strings (~> 0.2.0)
|
47
|
+
tty-screen (~> 0.8)
|
48
|
+
unicode-display_width (2.6.0)
|
49
|
+
unicode_utils (1.4.0)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
arm64-darwin-23
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
rake (~> 12.0)
|
57
|
+
rspec (~> 3.0)
|
58
|
+
tasks_cli!
|
59
|
+
|
60
|
+
BUNDLED WITH
|
61
|
+
2.5.16
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 adilw3nomad
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/bin/tasks
CHANGED
data/example.png
ADDED
Binary file
|
data/lib/tasks_cli/cli.rb
CHANGED
@@ -1,34 +1,48 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'rainbow'
|
5
|
+
require 'tty-table'
|
3
6
|
|
4
7
|
module TasksCLI
|
5
8
|
class CLI < Thor
|
6
|
-
desc
|
9
|
+
desc 'list', 'List all tasks'
|
7
10
|
def list
|
8
11
|
manager = TaskManager.new
|
9
12
|
print_tasks(manager.all_tasks)
|
10
13
|
end
|
11
14
|
|
12
|
-
desc
|
13
|
-
|
15
|
+
desc 'filter FIELD:VALUE [FIELD:VALUE ...]',
|
16
|
+
'Filter tasks by one or more field:value pairs. Example: filter status:in_progress priority:high'
|
17
|
+
def filter(*args)
|
18
|
+
criteria = {}
|
19
|
+
args.flatten.each do |arg|
|
20
|
+
field, value = arg.split(':', 2)
|
21
|
+
criteria[field.to_sym] ||= []
|
22
|
+
criteria[field.to_sym] << value
|
23
|
+
end
|
14
24
|
manager = TaskManager.new
|
15
|
-
|
16
|
-
filtered_tasks
|
17
|
-
|
25
|
+
filtered_tasks = manager.filter_tasks(criteria)
|
26
|
+
if filtered_tasks.empty?
|
27
|
+
puts Rainbow('No tasks found matching the given criteria.').yellow
|
28
|
+
else
|
29
|
+
print_tasks(filtered_tasks)
|
30
|
+
puts "\nFiltered tasks: #{Rainbow(filtered_tasks.count).green}"
|
31
|
+
end
|
18
32
|
end
|
19
33
|
|
20
|
-
desc
|
34
|
+
desc 'view NUMBER', 'View details of a specific task'
|
21
35
|
def view(number)
|
22
36
|
manager = TaskManager.new
|
23
37
|
task = manager.find_task(number)
|
24
38
|
if task
|
25
39
|
puts task.detailed_view
|
26
40
|
else
|
27
|
-
puts Rainbow(
|
41
|
+
puts Rainbow('Task not found').red
|
28
42
|
end
|
29
43
|
end
|
30
44
|
|
31
|
-
desc
|
45
|
+
desc 'update NUMBER STATUS', 'Update the status of a task'
|
32
46
|
def update(number, status)
|
33
47
|
manager = TaskManager.new
|
34
48
|
manager.update_status(number, status)
|
@@ -38,21 +52,26 @@ module TasksCLI
|
|
38
52
|
|
39
53
|
def print_tasks(tasks)
|
40
54
|
if tasks.empty?
|
41
|
-
puts Rainbow(
|
55
|
+
puts Rainbow('No tasks found.').yellow
|
42
56
|
return
|
43
57
|
end
|
44
58
|
|
45
|
-
|
46
|
-
tasks.
|
47
|
-
|
59
|
+
header = ['Number', 'Epic', 'Name', 'Status', 'Priority', 'Last Updated']
|
60
|
+
rows = tasks.map do |task|
|
61
|
+
[
|
48
62
|
Rainbow(task.number).cyan,
|
49
|
-
Rainbow(task.
|
50
|
-
task.
|
63
|
+
Rainbow(task.epic).yellow,
|
64
|
+
Rainbow(task.name).magenta,
|
65
|
+
task.status_color,
|
51
66
|
task.priority_colored,
|
52
|
-
Rainbow(task.updated_at).blue
|
67
|
+
Rainbow(task.updated_at.to_s).blue
|
53
68
|
]
|
54
69
|
end
|
70
|
+
|
71
|
+
table = TTY::Table.new(header: header, rows: rows)
|
72
|
+
puts table.render(:unicode, padding: [0, 1], alignment: [:left])
|
73
|
+
|
55
74
|
puts "\nTotal tasks: #{Rainbow(tasks.count).green}"
|
56
75
|
end
|
57
76
|
end
|
58
|
-
end
|
77
|
+
end
|
data/lib/tasks_cli/task.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rainbow'
|
2
4
|
|
3
5
|
module TasksCLI
|
4
6
|
class Task
|
5
7
|
attr_reader :epic, :number, :name, :description, :priority, :status, :relates_to, :blocked_by, :updated_at
|
6
|
-
|
8
|
+
|
7
9
|
def initialize(row)
|
8
10
|
@epic = extract_value(row[0])
|
9
11
|
@number = extract_value(row[1])
|
@@ -17,25 +19,27 @@ module TasksCLI
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def extract_value(field)
|
22
|
+
return '' if field.nil?
|
20
23
|
return field if field.is_a?(String)
|
24
|
+
|
21
25
|
field.is_a?(Array) ? field[1].to_s.strip : field.to_s.strip
|
22
26
|
end
|
23
27
|
|
24
28
|
def to_s
|
25
|
-
"#{Rainbow(@number).cyan}: #{Rainbow(@name).bright} (#{
|
29
|
+
"#{Rainbow(@number).cyan}: #{Rainbow(@name).bright} (#{status_color}) - Priority: #{priority_colored} - Updated: #{@updated_at}"
|
26
30
|
end
|
27
31
|
|
28
32
|
def detailed_view
|
29
33
|
<<~TASK
|
30
|
-
#{Rainbow(
|
31
|
-
#{Rainbow(
|
32
|
-
#{Rainbow(
|
33
|
-
#{Rainbow(
|
34
|
-
#{Rainbow(
|
35
|
-
#{Rainbow(
|
36
|
-
#{Rainbow(
|
37
|
-
#{Rainbow(
|
38
|
-
#{Rainbow(
|
34
|
+
#{Rainbow('Epic:').bright} #{@epic}
|
35
|
+
#{Rainbow('Number:').bright} #{Rainbow(@number).cyan}
|
36
|
+
#{Rainbow('Name:').bright} #{Rainbow(@name).bright}
|
37
|
+
#{Rainbow('Description:').bright} #{@description}
|
38
|
+
#{Rainbow('Priority:').bright} #{priority_colored}
|
39
|
+
#{Rainbow('Status:').bright} #{status_color}
|
40
|
+
#{Rainbow('Relates To:').bright} #{@relates_to}
|
41
|
+
#{Rainbow('Blocked By:').bright} #{@blocked_by}
|
42
|
+
#{Rainbow('Last Updated:').bright} #{@updated_at}
|
39
43
|
TASK
|
40
44
|
end
|
41
45
|
|
@@ -53,7 +57,7 @@ module TasksCLI
|
|
53
57
|
@updated_at = Time.now.iso8601
|
54
58
|
end
|
55
59
|
|
56
|
-
def
|
60
|
+
def status_color
|
57
61
|
case @status.downcase
|
58
62
|
when 'to do' then Rainbow(@status).red
|
59
63
|
when 'in progress' then Rainbow(@status).yellow
|
@@ -71,4 +75,4 @@ module TasksCLI
|
|
71
75
|
end
|
72
76
|
end
|
73
77
|
end
|
74
|
-
end
|
78
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'csv'
|
2
4
|
require 'json'
|
3
5
|
require 'rainbow'
|
@@ -12,11 +14,13 @@ module TasksCLI
|
|
12
14
|
def load_tasks
|
13
15
|
@tasks = []
|
14
16
|
CSV.foreach(@file_path, headers: false) do |row|
|
15
|
-
next if row == [
|
17
|
+
next if row == ['Epic Name', 'Ticket Number', 'Ticket Name', 'Ticket Description', 'Priority', 'Status',
|
18
|
+
'Relates To', 'Blocked By']
|
19
|
+
|
16
20
|
begin
|
17
21
|
parsed_row = row.map { |field| parse_field(field) }
|
18
22
|
@tasks << Task.new(parsed_row)
|
19
|
-
rescue => e
|
23
|
+
rescue StandardError => e
|
20
24
|
puts "Error processing row: #{row.inspect}"
|
21
25
|
puts "Error message: #{e.message}"
|
22
26
|
end
|
@@ -24,13 +28,14 @@ module TasksCLI
|
|
24
28
|
rescue CSV::MalformedCSVError => e
|
25
29
|
puts Rainbow("Error reading CSV: #{e.message}. Trying to parse with alternative method...").red
|
26
30
|
parse_csv_manually
|
27
|
-
rescue => e
|
31
|
+
rescue StandardError => e
|
28
32
|
puts Rainbow("Unexpected error while loading tasks: #{e.message}").red
|
29
33
|
puts e.backtrace
|
30
34
|
end
|
31
35
|
|
32
36
|
def parse_field(field)
|
33
|
-
return field
|
37
|
+
return field if field.nil? || !(field.start_with?('[') && field.end_with?(']'))
|
38
|
+
|
34
39
|
parsed = JSON.parse(field.gsub('=>', ':'))
|
35
40
|
[parsed[0], parsed[1]]
|
36
41
|
rescue JSON::ParserError
|
@@ -40,11 +45,11 @@ module TasksCLI
|
|
40
45
|
def parse_csv_manually
|
41
46
|
@tasks = []
|
42
47
|
lines = File.readlines(@file_path)
|
43
|
-
lines[1
|
48
|
+
lines[1..].each do |line|
|
44
49
|
fields = line.strip.split(',')
|
45
50
|
parsed_fields = fields.map { |field| parse_field(field) }
|
46
51
|
@tasks << Task.new(parsed_fields)
|
47
|
-
rescue => e
|
52
|
+
rescue StandardError => e
|
48
53
|
puts "Error processing line: #{line}"
|
49
54
|
puts "Error message: #{e.message}"
|
50
55
|
end
|
@@ -54,7 +59,7 @@ module TasksCLI
|
|
54
59
|
lines = File.readlines(@file_path)
|
55
60
|
File.open(@file_path, 'w') do |file|
|
56
61
|
file.puts lines[0].chomp # Write header as is
|
57
|
-
lines[1
|
62
|
+
lines[1..].each do |line|
|
58
63
|
fields = line.strip.split(',').map { |f| parse_field(f) }
|
59
64
|
if fields[1][1] == task_number
|
60
65
|
task = find_task(task_number)
|
@@ -68,12 +73,14 @@ module TasksCLI
|
|
68
73
|
end
|
69
74
|
|
70
75
|
def all_tasks
|
71
|
-
@tasks
|
76
|
+
@tasks[1..]
|
72
77
|
end
|
73
78
|
|
74
79
|
def filter_tasks(criteria)
|
75
80
|
@tasks.select do |task|
|
76
|
-
criteria.all?
|
81
|
+
criteria.all? do |field, values|
|
82
|
+
values.any? { |value| task.matches?(field, value) }
|
83
|
+
end
|
77
84
|
end
|
78
85
|
end
|
79
86
|
|
@@ -93,8 +100,8 @@ module TasksCLI
|
|
93
100
|
puts Rainbow("Last updated: #{TasksCLI.format_date(task.updated_at)}").cyan
|
94
101
|
else
|
95
102
|
TasksCLI.logger.warn("Task not found: #{number}")
|
96
|
-
puts Rainbow(
|
103
|
+
puts Rainbow('Task not found').red
|
97
104
|
end
|
98
105
|
end
|
99
106
|
end
|
100
|
-
end
|
107
|
+
end
|
data/lib/tasks_cli/version.rb
CHANGED
data/lib/tasks_cli.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tasks_cli/version'
|
4
|
+
require 'tasks_cli/cli'
|
5
|
+
require 'tasks_cli/task'
|
6
|
+
require 'tasks_cli/task_manager'
|
7
|
+
require 'logger'
|
6
8
|
|
7
9
|
module TasksCLI
|
8
10
|
class Error < StandardError; end
|
@@ -37,9 +39,8 @@ module TasksCLI
|
|
37
39
|
|
38
40
|
# Logging
|
39
41
|
def self.logger
|
40
|
-
@logger ||= Logger.new(
|
42
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
41
43
|
log.progname = 'TasksCLI'
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
45
|
-
```
|
data/readme.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# TasksCLI
|
2
|
+
|
3
|
+
TasksCLI is a command-line interface tool for managing tasks stored in a CSV file. It allows you to list, filter, view, and update tasks efficiently from your terminal.
|
4
|
+
|
5
|
+
|
6
|
+
## Features
|
7
|
+
|
8
|
+
- List all tasks
|
9
|
+
- Filter tasks by field and value
|
10
|
+
- View detailed information for a specific task
|
11
|
+
- Update task status
|
12
|
+
- Colorful output for better readability
|
13
|
+
|
14
|
+

|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Install the gem by running:
|
21
|
+
|
22
|
+
```
|
23
|
+
gem install tasks_cli
|
24
|
+
```
|
25
|
+
|
26
|
+
## Configuration
|
27
|
+
|
28
|
+
TasksCLI expects a CSV file located at `~/tasks.csv`. Ensure this file exists and has the correct format before using the tool.
|
29
|
+
|
30
|
+
### CSV Format
|
31
|
+
|
32
|
+
The CSV file should have the following columns:
|
33
|
+
|
34
|
+
1. Epic Name
|
35
|
+
2. Ticket Number
|
36
|
+
3. Ticket Name
|
37
|
+
4. Ticket Description
|
38
|
+
5. Priority
|
39
|
+
6. Status
|
40
|
+
7. Relates To [Array]
|
41
|
+
8. Blocked By [Array]
|
42
|
+
9. Updated At (optional)
|
43
|
+
|
44
|
+
```
|
45
|
+
Epic Name,Ticket Number,Ticket Name,Ticket Description,Priority,Status,Relates To,Blocked By,Updated At
|
46
|
+
Epic 1,1,Task 1,Description 1,High,To Do,,,
|
47
|
+
Epic 1,2,Task 2,Description 2,Medium,In Progress,,,
|
48
|
+
Epic 1,3,Task 3,Description 3,Low,Done,,,
|
49
|
+
```
|
50
|
+
|
51
|
+
What I find best is to use the following process;
|
52
|
+
1. Loosely define the project with an LLM of your choice.
|
53
|
+
2. Ask LLM to generate a list of tasks for the project.
|
54
|
+
3. Iterate on the LLM generated tasks until you are satisfied.
|
55
|
+
4. Ask LLM to generate the CSV of the tasks using the above format.
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
### Listing Tasks
|
60
|
+
|
61
|
+
To list all tasks, run:
|
62
|
+
|
63
|
+
```
|
64
|
+
tasks list
|
65
|
+
```
|
66
|
+
|
67
|
+
### Filtering Tasks
|
68
|
+
|
69
|
+
To filter tasks by a specific field and value, run:
|
70
|
+
|
71
|
+
```
|
72
|
+
tasks filter FIELD:VALUE
|
73
|
+
```
|
74
|
+
|
75
|
+
Replace `FIELD` with the column name and `VALUE` with the value you want to filter by.
|
76
|
+
|
77
|
+
For example, to filter tasks by Epic 1:
|
78
|
+
|
79
|
+
```
|
80
|
+
tasks filter "Epic Name":"Epic 1"
|
81
|
+
```
|
82
|
+
|
83
|
+
### Viewing Task Details
|
84
|
+
|
85
|
+
To view details of a specific task, run:
|
86
|
+
|
87
|
+
```
|
88
|
+
tasks view NUMBER
|
89
|
+
```
|
90
|
+
|
91
|
+
### Updating Task Status
|
92
|
+
|
93
|
+
To update the status of a task, run:
|
94
|
+
|
95
|
+
```
|
96
|
+
tasks update NUMBER STATUS
|
97
|
+
```
|
98
|
+
|
99
|
+
Replace `NUMBER` with the ticket number and `STATUS` with the new status you want to set.
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
This project is licensed under the MIT License. See the `LICENSE` file for more details.
|
104
|
+
|
105
|
+
|
106
|
+
|
data/tasks_cli.gemspec
CHANGED
@@ -1,35 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'lib/tasks_cli/version'
|
2
4
|
|
3
5
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
6
|
+
spec.name = 'tasks_cli'
|
5
7
|
spec.version = TasksCLI::VERSION
|
6
|
-
spec.authors = [
|
7
|
-
spec.email = [
|
8
|
+
spec.authors = ['adilw3nomad']
|
9
|
+
spec.email = ['adil@w3nomad.com']
|
8
10
|
|
9
|
-
spec.summary =
|
10
|
-
spec.description =
|
11
|
-
spec.homepage =
|
12
|
-
spec.license =
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new(
|
11
|
+
spec.summary = 'A simple CLI for managing tasks'
|
12
|
+
spec.description = 'A command-line interface for managing tasks stored in a CSV file'
|
13
|
+
spec.homepage = 'https://github.com/adilw3nomad/tasks_cli'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
14
16
|
|
15
|
-
spec.metadata[
|
16
|
-
spec.metadata[
|
17
|
-
spec.metadata[
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/adilw3nomad/tasks_cli'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/adilw3nomad/tasks_cli/blob/master/CHANGELOG.md'
|
18
20
|
|
19
21
|
# Specify which files should be added to the gem when it is released.
|
20
22
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
-
spec.files
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
24
|
`git ls-files -z`.split("\x0").reject do |f|
|
23
25
|
f.match(%r{^(test|spec|features)/}) ||
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
f.match(/^tasks_cli-.*\.gem$/) ||
|
27
|
+
f.match(/^\.git/) ||
|
28
|
+
f.match(/^\.github/)
|
27
29
|
end
|
28
30
|
end
|
29
|
-
spec.bindir =
|
30
|
-
spec.executables =
|
31
|
-
spec.require_paths = [
|
31
|
+
spec.bindir = 'bin'
|
32
|
+
spec.executables = ['tasks']
|
33
|
+
spec.require_paths = ['lib']
|
32
34
|
|
33
|
-
spec.add_dependency
|
34
|
-
spec.add_dependency
|
35
|
-
|
35
|
+
spec.add_dependency 'csv', '~> 3.0'
|
36
|
+
spec.add_dependency 'logger', '~> 1.5'
|
37
|
+
spec.add_dependency 'ostruct', '~> 0.3.0'
|
38
|
+
spec.add_dependency 'rainbow', '~> 3.0'
|
39
|
+
spec.add_dependency 'thor', '~> 1.0'
|
40
|
+
spec.add_dependency 'tty-table', '~> 0.12.0'
|
41
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tasks_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- adilw3nomad
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-09-
|
11
|
+
date: 2024-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: csv
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ostruct
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.3.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.3.0
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: rainbow
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,21 +66,53 @@ dependencies:
|
|
38
66
|
- - "~>"
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thor
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: tty-table
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.12.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.12.0
|
41
97
|
description: A command-line interface for managing tasks stored in a CSV file
|
42
98
|
email:
|
43
99
|
- adil@w3nomad.com
|
44
|
-
executables:
|
100
|
+
executables:
|
101
|
+
- tasks
|
45
102
|
extensions: []
|
46
103
|
extra_rdoc_files: []
|
47
104
|
files:
|
48
105
|
- Gemfile
|
106
|
+
- Gemfile.lock
|
107
|
+
- LICENSE
|
49
108
|
- bin/tasks
|
109
|
+
- example.png
|
50
110
|
- lib/tasks_cli.rb
|
51
111
|
- lib/tasks_cli/cli.rb
|
52
112
|
- lib/tasks_cli/task.rb
|
53
113
|
- lib/tasks_cli/task_manager.rb
|
54
114
|
- lib/tasks_cli/version.rb
|
55
|
-
-
|
115
|
+
- readme.md
|
56
116
|
- tasks_cli.gemspec
|
57
117
|
homepage: https://github.com/adilw3nomad/tasks_cli
|
58
118
|
licenses:
|
data/tasks
DELETED
@@ -1,248 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'csv'
|
4
|
-
require 'optparse'
|
5
|
-
require 'rainbow'
|
6
|
-
require 'time'
|
7
|
-
require 'json'
|
8
|
-
|
9
|
-
Rainbow.enabled = true
|
10
|
-
|
11
|
-
class Task
|
12
|
-
attr_reader :epic, :number, :name, :description, :priority, :status, :relates_to, :blocked_by, :updated_at
|
13
|
-
|
14
|
-
def initialize(row)
|
15
|
-
@epic = extract_value(row[0])
|
16
|
-
@number = extract_value(row[1])
|
17
|
-
@name = extract_value(row[2])
|
18
|
-
@description = extract_value(row[3])
|
19
|
-
@priority = extract_value(row[4])
|
20
|
-
@status = extract_value(row[5])
|
21
|
-
@relates_to = extract_value(row[6])
|
22
|
-
@blocked_by = extract_value(row[7])
|
23
|
-
@updated_at = extract_value(row[8]) || Time.now.iso8601
|
24
|
-
end
|
25
|
-
|
26
|
-
def extract_value(field)
|
27
|
-
return field if field.is_a?(String)
|
28
|
-
field.is_a?(Array) ? field[1].to_s.strip : field.to_s.strip
|
29
|
-
end
|
30
|
-
|
31
|
-
def to_s
|
32
|
-
"#{Rainbow(@number).cyan}: #{Rainbow(@name).bright} (#{status_colored}) - Priority: #{priority_colored} - Updated: #{@updated_at}"
|
33
|
-
end
|
34
|
-
|
35
|
-
def detailed_view
|
36
|
-
<<~TASK
|
37
|
-
#{Rainbow("Epic:").bright} #{@epic}
|
38
|
-
#{Rainbow("Number:").bright} #{Rainbow(@number).cyan}
|
39
|
-
#{Rainbow("Name:").bright} #{Rainbow(@name).bright}
|
40
|
-
#{Rainbow("Description:").bright} #{@description}
|
41
|
-
#{Rainbow("Priority:").bright} #{priority_colored}
|
42
|
-
#{Rainbow("Status:").bright} #{status_colored}
|
43
|
-
#{Rainbow("Relates To:").bright} #{@relates_to}
|
44
|
-
#{Rainbow("Blocked By:").bright} #{@blocked_by}
|
45
|
-
#{Rainbow("Last Updated:").bright} #{@updated_at}
|
46
|
-
TASK
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_csv
|
50
|
-
[@epic, @number, @name, @description, @priority, @status, @relates_to, @blocked_by, @updated_at]
|
51
|
-
end
|
52
|
-
|
53
|
-
def matches?(field, value)
|
54
|
-
send(field.to_sym).to_s.downcase.include?(value.to_s.downcase)
|
55
|
-
rescue NoMethodError
|
56
|
-
false
|
57
|
-
end
|
58
|
-
|
59
|
-
def update_timestamp
|
60
|
-
@updated_at = Time.now.iso8601
|
61
|
-
end
|
62
|
-
|
63
|
-
def status_colored
|
64
|
-
case @status.downcase
|
65
|
-
when 'to do' then Rainbow(@status).red
|
66
|
-
when 'in progress' then Rainbow(@status).yellow
|
67
|
-
when 'done' then Rainbow(@status).green
|
68
|
-
else Rainbow(@status).white
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def priority_colored
|
73
|
-
case @priority.downcase
|
74
|
-
when 'high' then Rainbow(@priority).red
|
75
|
-
when 'medium' then Rainbow(@priority).yellow
|
76
|
-
when 'low' then Rainbow(@priority).green
|
77
|
-
else Rainbow(@priority).white
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
class TaskManager
|
83
|
-
def initialize(file_path)
|
84
|
-
@file_path = file_path
|
85
|
-
load_tasks
|
86
|
-
end
|
87
|
-
|
88
|
-
def load_tasks
|
89
|
-
@tasks = []
|
90
|
-
CSV.foreach(@file_path, headers: false) do |row|
|
91
|
-
next if row == ["Epic Name", "Ticket Number", "Ticket Name", "Ticket Description", "Priority", "Status", "Relates To", "Blocked By"]
|
92
|
-
begin
|
93
|
-
parsed_row = row.map do |field|
|
94
|
-
parse_field(field)
|
95
|
-
end
|
96
|
-
@tasks << Task.new(parsed_row)
|
97
|
-
rescue => e
|
98
|
-
puts "Error processing row: #{row.inspect}"
|
99
|
-
puts "Error message: #{e.message}"
|
100
|
-
end
|
101
|
-
end
|
102
|
-
rescue CSV::MalformedCSVError => e
|
103
|
-
puts Rainbow("Error reading CSV: #{e.message}. Trying to parse with alternative method...").red
|
104
|
-
parse_csv_manually
|
105
|
-
rescue => e
|
106
|
-
puts Rainbow("Unexpected error while loading tasks: #{e.message}").red
|
107
|
-
puts e.backtrace
|
108
|
-
end
|
109
|
-
|
110
|
-
def parse_field(field)
|
111
|
-
return field unless field.start_with?('[') && field.end_with?(']')
|
112
|
-
parsed = JSON.parse(field.gsub('=>', ':'))
|
113
|
-
[parsed[0], parsed[1]]
|
114
|
-
rescue JSON::ParserError
|
115
|
-
[nil, field]
|
116
|
-
end
|
117
|
-
|
118
|
-
def parse_csv_manually
|
119
|
-
@tasks = []
|
120
|
-
lines = File.readlines(@file_path)
|
121
|
-
lines[1..-1].each do |line|
|
122
|
-
fields = line.strip.split(',')
|
123
|
-
parsed_fields = fields.map { |field| parse_field(field) }
|
124
|
-
@tasks << Task.new(parsed_fields)
|
125
|
-
rescue => e
|
126
|
-
puts "Error processing line: #{line}"
|
127
|
-
puts "Error message: #{e.message}"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def save_tasks(task_number)
|
132
|
-
lines = File.readlines(@file_path)
|
133
|
-
File.open(@file_path, 'w') do |file|
|
134
|
-
file.puts lines[0].chomp # Write header as is
|
135
|
-
lines[1..-1].each do |line|
|
136
|
-
fields = line.strip.split(',').map { |f| parse_field(f) }
|
137
|
-
if fields[1][1] == task_number
|
138
|
-
task = find_task(task_number)
|
139
|
-
updated_line = task.to_csv.map { |value| [fields[_1][0], value].to_s }.join(',')
|
140
|
-
file.puts updated_line
|
141
|
-
else
|
142
|
-
file.puts line.chomp # Write unchanged lines as is
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def all_tasks
|
149
|
-
@tasks
|
150
|
-
end
|
151
|
-
|
152
|
-
def filter_tasks(criteria)
|
153
|
-
@tasks.select do |task|
|
154
|
-
criteria.all? { |field, value| task.matches?(field, value) }
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def find_task(number)
|
159
|
-
@tasks.find { |task| task.number == number }
|
160
|
-
end
|
161
|
-
|
162
|
-
def update_status(number, new_status)
|
163
|
-
task = find_task(number)
|
164
|
-
if task
|
165
|
-
old_status = task.status
|
166
|
-
task.instance_variable_set(:@status, new_status)
|
167
|
-
task.update_timestamp
|
168
|
-
save_tasks(number)
|
169
|
-
puts Rainbow("Updated status of task #{number} from #{old_status} to #{new_status}").green
|
170
|
-
puts Rainbow("Last updated: #{task.updated_at}").cyan
|
171
|
-
else
|
172
|
-
puts Rainbow("Task not found").red
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def print_tasks(tasks)
|
178
|
-
if tasks.empty?
|
179
|
-
puts Rainbow("No tasks found.").yellow
|
180
|
-
return
|
181
|
-
end
|
182
|
-
|
183
|
-
puts Rainbow("\n%-10s %-30s %-15s %-10s %-25s" % ["Number", "Name", "Status", "Priority", "Last Updated"]).underline
|
184
|
-
tasks.each do |task|
|
185
|
-
puts "%-10s %-30s %-15s %-10s %-25s" % [
|
186
|
-
Rainbow(task.number).cyan,
|
187
|
-
Rainbow(task.name.to_s.truncate(30)).magenta,
|
188
|
-
task.status_colored,
|
189
|
-
task.priority_colored,
|
190
|
-
Rainbow(task.updated_at).blue
|
191
|
-
]
|
192
|
-
end
|
193
|
-
puts "\nTotal tasks: #{Rainbow(tasks.count).green}"
|
194
|
-
end
|
195
|
-
|
196
|
-
# Main program
|
197
|
-
if __FILE__ == $0
|
198
|
-
begin
|
199
|
-
file_path = File.expand_path('~/tasks.csv') # Store the CSV in the user's home directory
|
200
|
-
manager = TaskManager.new(file_path)
|
201
|
-
|
202
|
-
options = {}
|
203
|
-
OptionParser.new do |opts|
|
204
|
-
opts.banner = "Usage: tasks [options]"
|
205
|
-
|
206
|
-
opts.on("-a", "--all", "View all tasks") do
|
207
|
-
options[:all] = true
|
208
|
-
end
|
209
|
-
|
210
|
-
opts.on("-f FIELD:VALUE", "--filter FIELD:VALUE", "Filter tasks by field and value (can be used multiple times)") do |f|
|
211
|
-
options[:filter] ||= {}
|
212
|
-
key, value = f.split(':')
|
213
|
-
options[:filter][key] = value
|
214
|
-
end
|
215
|
-
|
216
|
-
opts.on("-v NUMBER", "--view NUMBER", "View a single task") do |v|
|
217
|
-
options[:view] = v
|
218
|
-
end
|
219
|
-
|
220
|
-
opts.on("-u NUMBER,STATUS", "--update NUMBER,STATUS", Array, "Update a task's status") do |u|
|
221
|
-
options[:update] = u
|
222
|
-
end
|
223
|
-
end.parse!
|
224
|
-
|
225
|
-
if options[:all]
|
226
|
-
print_tasks(manager.all_tasks)
|
227
|
-
elsif options[:filter]
|
228
|
-
print_tasks(manager.filter_tasks(options[:filter]))
|
229
|
-
elsif options[:view]
|
230
|
-
task = manager.find_task(options[:view])
|
231
|
-
if task
|
232
|
-
puts task.detailed_view
|
233
|
-
else
|
234
|
-
puts Rainbow("Task not found").red
|
235
|
-
end
|
236
|
-
elsif options[:update]
|
237
|
-
number, status = options[:update]
|
238
|
-
manager.update_status(number, status)
|
239
|
-
else
|
240
|
-
puts Rainbow("No valid option provided. Use -h for help.").yellow
|
241
|
-
end
|
242
|
-
rescue Errno::ENOENT
|
243
|
-
puts Rainbow("Error: CSV file '#{file_path}' not found. Please ensure the file exists.").red
|
244
|
-
rescue => e
|
245
|
-
puts Rainbow("An unexpected error occurred: #{e.message}").red
|
246
|
-
puts e.backtrace
|
247
|
-
end
|
248
|
-
end
|