fizzy-cli 0.1.0

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.
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Cards < Thor
6
+ include Base
7
+
8
+ desc "list", "List cards"
9
+ option :board, desc: "Board ID (filter)"
10
+ option :column, desc: "Column ID (filter)"
11
+ option :status, desc: "Filter: open, closed, all", default: "open"
12
+ option :assignee, desc: "Assignee user ID (filter)"
13
+ option :tag, desc: "Tag ID (filter, repeatable)", type: :array
14
+ def list
15
+ params = {}
16
+ params[:status] = options[:status] if options[:status]
17
+ params[:board_id] = options[:board] if options[:board]
18
+ params[:column_id] = options[:column] if options[:column]
19
+ params[:assignee_id] = options[:assignee] if options[:assignee]
20
+ options[:tag]&.each { |t| (params[:"tag_ids[]"] ||= []) << t }
21
+
22
+ data = paginator.all("#{slug}/cards", params: params)
23
+ output_list(data, headers: %w[# Title Board Status Column]) do |c|
24
+ [
25
+ c["number"],
26
+ Formatter.truncate(c["title"], 50),
27
+ c.dig("board", "name") || "",
28
+ c["status"],
29
+ c.dig("column", "name") || ""
30
+ ]
31
+ end
32
+ end
33
+
34
+ desc "get NUMBER", "Show a card"
35
+ def get(number)
36
+ resp = client.get("#{slug}/cards/#{number}")
37
+ c = resp.body
38
+ output_detail(c, pairs: [
39
+ ["Number", "##{c["number"]}"],
40
+ ["Title", c["title"]],
41
+ ["Board", c.dig("board", "name")],
42
+ ["Column", c.dig("column", "name")],
43
+ ["Status", c["status"]],
44
+ ["Creator", c.dig("creator", "name")],
45
+ ["Assignees", format_assignees(c["assignees"])],
46
+ ["Tags", format_tags(c["tags"])],
47
+ ["Steps", format_steps(c["steps"])],
48
+ ["Created", c["created_at"]],
49
+ ["URL", c["url"]]
50
+ ])
51
+ end
52
+
53
+ desc "create TITLE", "Create a card"
54
+ option :board, required: true, desc: "Board ID"
55
+ option :body, desc: "Card body (HTML)"
56
+ option :column, desc: "Column ID"
57
+ def create(title)
58
+ body = { title: title }
59
+ body[:body] = options[:body] if options[:body]
60
+ body[:column_id] = options[:column] if options[:column]
61
+ resp = client.post("#{slug}/boards/#{options[:board]}/cards", body: body)
62
+ c = resp.body
63
+ output_detail(c, pairs: [
64
+ ["Number", "##{c["number"]}"],
65
+ ["Title", c["title"]],
66
+ ["URL", c["url"]]
67
+ ])
68
+ end
69
+
70
+ desc "update NUMBER", "Update a card"
71
+ option :title, desc: "New title"
72
+ option :body, desc: "New body (HTML)"
73
+ def update(number)
74
+ resp = client.put("#{slug}/cards/#{number}", body: build_body(:title, :body))
75
+ c = resp.body
76
+ output_detail(c, pairs: [
77
+ ["Number", "##{c["number"]}"],
78
+ ["Title", c["title"]]
79
+ ])
80
+ end
81
+
82
+ desc "delete NUMBER", "Delete a card"
83
+ def delete(number)
84
+ client.delete("#{slug}/cards/#{number}")
85
+ puts "Card ##{number} deleted."
86
+ end
87
+
88
+ desc "close NUMBER", "Close a card"
89
+ def close(number)
90
+ client.post("#{slug}/cards/#{number}/closure")
91
+ puts "Card ##{number} closed."
92
+ end
93
+
94
+ desc "reopen NUMBER", "Reopen a closed card"
95
+ def reopen(number)
96
+ client.delete("#{slug}/cards/#{number}/closure")
97
+ puts "Card ##{number} reopened."
98
+ end
99
+
100
+ desc "not-now NUMBER", "Mark a card as not-now"
101
+ map "not-now" => :not_now
102
+ def not_now(number)
103
+ client.post("#{slug}/cards/#{number}/not_now")
104
+ puts "Card ##{number} marked not-now."
105
+ end
106
+
107
+ desc "triage NUMBER", "Triage a card into a column"
108
+ option :column, required: true, desc: "Column ID"
109
+ def triage(number)
110
+ client.post("#{slug}/cards/#{number}/triage", body: { column_id: options[:column] })
111
+ puts "Card ##{number} triaged."
112
+ end
113
+
114
+ desc "untriage NUMBER", "Remove a card from triage"
115
+ def untriage(number)
116
+ client.delete("#{slug}/cards/#{number}/triage")
117
+ puts "Card ##{number} untriaged."
118
+ end
119
+
120
+ desc "tag NUMBER TAG_TITLE", "Add a tag to a card"
121
+ def tag(number, tag_title)
122
+ client.post("#{slug}/cards/#{number}/taggings", body: { tag_title: tag_title })
123
+ puts "Tag '#{tag_title}' added to card ##{number}."
124
+ end
125
+
126
+ desc "assign NUMBER ASSIGNEE_ID", "Toggle assignment on a card"
127
+ def assign(number, assignee_id)
128
+ client.post("#{slug}/cards/#{number}/assignments", body: { assignee_id: assignee_id })
129
+ puts "Assignment toggled for card ##{number}."
130
+ end
131
+
132
+ desc "watch NUMBER", "Watch a card"
133
+ def watch(number)
134
+ client.post("#{slug}/cards/#{number}/watch")
135
+ puts "Watching card ##{number}."
136
+ end
137
+
138
+ desc "unwatch NUMBER", "Unwatch a card"
139
+ def unwatch(number)
140
+ client.delete("#{slug}/cards/#{number}/watch")
141
+ puts "Unwatched card ##{number}."
142
+ end
143
+
144
+ desc "golden NUMBER", "Mark a card as golden"
145
+ def golden(number)
146
+ client.post("#{slug}/cards/#{number}/goldness")
147
+ puts "Card ##{number} marked golden."
148
+ end
149
+
150
+ desc "ungolden NUMBER", "Remove golden from a card"
151
+ def ungolden(number)
152
+ client.delete("#{slug}/cards/#{number}/goldness")
153
+ puts "Card ##{number} ungolden."
154
+ end
155
+
156
+ private
157
+
158
+ def format_assignees(assignees)
159
+ return "" unless assignees&.any?
160
+
161
+ assignees.map { |a| a["name"] }.join(", ")
162
+ end
163
+
164
+ def format_tags(tags)
165
+ return "" unless tags&.any?
166
+
167
+ tags.map { |t| t["title"] || t["name"] }.join(", ")
168
+ end
169
+
170
+ def format_steps(steps)
171
+ return "" unless steps&.any?
172
+
173
+ done = steps.count { |s| s["completed"] }
174
+ "#{done}/#{steps.size}"
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Columns < Thor
6
+ include Base
7
+
8
+ desc "list", "List columns for a board"
9
+ option :board, required: true, desc: "Board ID"
10
+ def list
11
+ data = paginator.all("#{slug}/boards/#{options[:board]}/columns")
12
+ output_list(data, headers: %w[ID Name Position]) do |c|
13
+ [c["id"], c["name"], c["position"]]
14
+ end
15
+ end
16
+
17
+ desc "get COLUMN_ID", "Show a column"
18
+ option :board, required: true, desc: "Board ID"
19
+ def get(column_id)
20
+ resp = client.get("#{slug}/boards/#{options[:board]}/columns/#{column_id}")
21
+ c = resp.body
22
+ output_detail(c, pairs: [
23
+ ["ID", c["id"]],
24
+ ["Name", c["name"]],
25
+ ["Position", c["position"]],
26
+ ["Created", c["created_at"]]
27
+ ])
28
+ end
29
+
30
+ desc "create NAME", "Create a column"
31
+ option :board, required: true, desc: "Board ID"
32
+ def create(name)
33
+ resp = client.post("#{slug}/boards/#{options[:board]}/columns", body: { name: name })
34
+ c = resp.body
35
+ output_detail(c, pairs: [
36
+ ["ID", c["id"]],
37
+ ["Name", c["name"]]
38
+ ])
39
+ end
40
+
41
+ desc "update COLUMN_ID", "Update a column"
42
+ option :board, required: true, desc: "Board ID"
43
+ option :name, required: true, desc: "New column name"
44
+ def update(column_id)
45
+ resp = client.put("#{slug}/boards/#{options[:board]}/columns/#{column_id}", body: { name: options[:name] })
46
+ c = resp.body
47
+ output_detail(c, pairs: [
48
+ ["ID", c["id"]],
49
+ ["Name", c["name"]]
50
+ ])
51
+ end
52
+
53
+ desc "delete COLUMN_ID", "Delete a column"
54
+ option :board, required: true, desc: "Board ID"
55
+ def delete(column_id)
56
+ client.delete("#{slug}/boards/#{options[:board]}/columns/#{column_id}")
57
+ puts "Column #{column_id} deleted."
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Comments < Thor
6
+ include Base
7
+
8
+ desc "list", "List comments on a card"
9
+ option :card, required: true, type: :numeric, desc: "Card number"
10
+ def list
11
+ data = paginator.all("#{slug}/cards/#{options[:card]}/comments")
12
+ output_list(data, headers: %w[ID Author Created]) do |c|
13
+ [c["id"], c.dig("creator", "name"), c["created_at"]]
14
+ end
15
+ end
16
+
17
+ desc "get COMMENT_ID", "Show a comment"
18
+ option :card, required: true, type: :numeric, desc: "Card number"
19
+ def get(comment_id)
20
+ resp = client.get("#{slug}/cards/#{options[:card]}/comments/#{comment_id}")
21
+ c = resp.body
22
+ output_detail(c, pairs: [
23
+ ["ID", c["id"]],
24
+ ["Author", c.dig("creator", "name")],
25
+ ["Body", c["body"]],
26
+ ["Created", c["created_at"]]
27
+ ])
28
+ end
29
+
30
+ desc "create BODY", "Add a comment to a card"
31
+ option :card, required: true, type: :numeric, desc: "Card number"
32
+ def create(body_text)
33
+ resp = client.post("#{slug}/cards/#{options[:card]}/comments", body: { body: body_text })
34
+ c = resp.body
35
+ output_detail(c, pairs: [
36
+ ["ID", c["id"]],
37
+ ["Body", c["body"]]
38
+ ])
39
+ end
40
+
41
+ desc "update COMMENT_ID", "Update a comment"
42
+ option :card, required: true, type: :numeric, desc: "Card number"
43
+ option :body, required: true, desc: "New body"
44
+ def update(comment_id)
45
+ resp = client.put("#{slug}/cards/#{options[:card]}/comments/#{comment_id}", body: build_body(:body))
46
+ c = resp.body
47
+ output_detail(c, pairs: [
48
+ ["ID", c["id"]],
49
+ ["Body", c["body"]]
50
+ ])
51
+ end
52
+
53
+ desc "delete COMMENT_ID", "Delete a comment"
54
+ option :card, required: true, type: :numeric, desc: "Card number"
55
+ def delete(comment_id)
56
+ client.delete("#{slug}/cards/#{options[:card]}/comments/#{comment_id}")
57
+ puts "Comment #{comment_id} deleted."
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Notifications < Thor
6
+ include Base
7
+
8
+ desc "list", "List notifications"
9
+ def list
10
+ data = paginator.all("#{slug}/notifications")
11
+ output_list(data, headers: %w[ID Type Card Read Created]) do |n|
12
+ [
13
+ n["id"],
14
+ n["event_type"],
15
+ n.dig("card", "title") || "",
16
+ n["read_at"] ? "yes" : "no",
17
+ n["created_at"]
18
+ ]
19
+ end
20
+ end
21
+
22
+ desc "read ID", "Mark notification as read"
23
+ def read(id)
24
+ client.post("#{slug}/notifications/#{id}/reading")
25
+ puts "Notification #{id} marked read."
26
+ end
27
+
28
+ desc "unread ID", "Mark notification as unread"
29
+ def unread(id)
30
+ client.delete("#{slug}/notifications/#{id}/reading")
31
+ puts "Notification #{id} marked unread."
32
+ end
33
+
34
+ desc "mark-all-read", "Mark all notifications as read"
35
+ map "mark-all-read" => :mark_all_read
36
+ def mark_all_read
37
+ client.post("#{slug}/notifications/bulk_reading")
38
+ puts "All notifications marked read."
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Pins < Thor
6
+ include Base
7
+
8
+ desc "list", "List pinned cards"
9
+ def list
10
+ data = paginator.all("/my/pins")
11
+ output_list(data, headers: %w[# Title Board]) do |p|
12
+ card = p["card"] || p
13
+ [card["number"], card["title"], card.dig("board", "name") || ""]
14
+ end
15
+ end
16
+
17
+ desc "pin NUMBER", "Pin a card"
18
+ def pin(number)
19
+ client.post("#{slug}/cards/#{number}/pin")
20
+ puts "Card ##{number} pinned."
21
+ end
22
+
23
+ desc "unpin NUMBER", "Unpin a card"
24
+ def unpin(number)
25
+ client.delete("#{slug}/cards/#{number}/pin")
26
+ puts "Card ##{number} unpinned."
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Reactions < Thor
6
+ include Base
7
+
8
+ desc "list", "List reactions on a card"
9
+ option :card, required: true, type: :numeric, desc: "Card number"
10
+ option :comment, desc: "Comment ID (for comment reactions)"
11
+ def list
12
+ path = reaction_path(options[:card], options[:comment])
13
+ data = paginator.all(path)
14
+ output_list(data, headers: %w[ID Content User]) do |r|
15
+ [r["id"], r["content"], r.dig("creator", "name")]
16
+ end
17
+ end
18
+
19
+ desc "create CONTENT", "Add a reaction"
20
+ option :card, required: true, type: :numeric, desc: "Card number"
21
+ option :comment, desc: "Comment ID (for comment reactions)"
22
+ def create(content)
23
+ path = reaction_path(options[:card], options[:comment])
24
+ resp = client.post(path, body: { content: content })
25
+ r = resp.body
26
+ output_detail(r, pairs: [
27
+ ["ID", r["id"]],
28
+ ["Content", r["content"]]
29
+ ])
30
+ end
31
+
32
+ desc "delete REACTION_ID", "Remove a reaction"
33
+ option :card, required: true, type: :numeric, desc: "Card number"
34
+ option :comment, desc: "Comment ID (for comment reactions)"
35
+ def delete(reaction_id)
36
+ path = "#{reaction_path(options[:card], options[:comment])}/#{reaction_id}"
37
+ client.delete(path)
38
+ puts "Reaction #{reaction_id} deleted."
39
+ end
40
+
41
+ private
42
+
43
+ def reaction_path(card_number, comment_id = nil)
44
+ base = "#{slug}/cards/#{card_number}"
45
+ comment_id ? "#{base}/comments/#{comment_id}/reactions" : "#{base}/reactions"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Steps < Thor
6
+ include Base
7
+
8
+ desc "get STEP_ID", "Show a step"
9
+ option :card, required: true, type: :numeric, desc: "Card number"
10
+ def get(step_id)
11
+ resp = client.get("#{slug}/cards/#{options[:card]}/steps/#{step_id}")
12
+ s = resp.body
13
+ output_detail(s, pairs: [
14
+ ["ID", s["id"]],
15
+ ["Description", s["description"]],
16
+ ["Completed", s["completed"]],
17
+ ["Position", s["position"]]
18
+ ])
19
+ end
20
+
21
+ desc "create DESCRIPTION", "Add a step to a card"
22
+ option :card, required: true, type: :numeric, desc: "Card number"
23
+ def create(description)
24
+ resp = client.post("#{slug}/cards/#{options[:card]}/steps", body: { description: description })
25
+ s = resp.body
26
+ output_detail(s, pairs: [
27
+ ["ID", s["id"]],
28
+ ["Description", s["description"]]
29
+ ])
30
+ end
31
+
32
+ desc "update STEP_ID", "Update a step"
33
+ option :card, required: true, type: :numeric, desc: "Card number"
34
+ option :description, desc: "New description"
35
+ option :completed, type: :boolean, desc: "Mark completed"
36
+ def update(step_id)
37
+ path = "#{slug}/cards/#{options[:card]}/steps/#{step_id}"
38
+ resp = client.put(path, body: build_body(:description, :completed))
39
+ s = resp.body
40
+ output_detail(s, pairs: [
41
+ ["ID", s["id"]],
42
+ ["Description", s["description"]],
43
+ ["Completed", s["completed"]]
44
+ ])
45
+ end
46
+
47
+ desc "delete STEP_ID", "Delete a step"
48
+ option :card, required: true, type: :numeric, desc: "Card number"
49
+ def delete(step_id)
50
+ client.delete("#{slug}/cards/#{options[:card]}/steps/#{step_id}")
51
+ puts "Step #{step_id} deleted."
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Tags < Thor
6
+ include Base
7
+
8
+ desc "list", "List all tags"
9
+ def list
10
+ data = paginator.all("#{slug}/tags")
11
+ output_list(data, headers: %w[ID Title Color]) do |t|
12
+ [t["id"], t["title"] || t["name"], t["color"]]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class CLI < Thor
5
+ class Users < Thor
6
+ include Base
7
+
8
+ desc "list", "List users"
9
+ def list
10
+ data = paginator.all("#{slug}/users")
11
+ output_list(data, headers: %w[ID Name Email Role Active]) do |u|
12
+ [u["id"], u["name"], u["email_address"], u["role"], u["active"]]
13
+ end
14
+ end
15
+
16
+ desc "get USER_ID", "Show a user"
17
+ def get(user_id)
18
+ resp = client.get("#{slug}/users/#{user_id}")
19
+ u = resp.body
20
+ output_detail(u, pairs: [
21
+ ["ID", u["id"]],
22
+ ["Name", u["name"]],
23
+ ["Email", u["email_address"]],
24
+ ["Role", u["role"]],
25
+ ["Active", u["active"]],
26
+ ["Created", u["created_at"]]
27
+ ])
28
+ end
29
+
30
+ desc "update USER_ID", "Update a user"
31
+ option :name, desc: "New name"
32
+ option :role, desc: "New role"
33
+ def update(user_id)
34
+ resp = client.put("#{slug}/users/#{user_id}", body: build_body(:name, :role))
35
+ u = resp.body
36
+ output_detail(u, pairs: [
37
+ ["ID", u["id"]],
38
+ ["Name", u["name"]],
39
+ ["Role", u["role"]]
40
+ ])
41
+ end
42
+
43
+ desc "deactivate USER_ID", "Deactivate a user"
44
+ def deactivate(user_id)
45
+ client.delete("#{slug}/users/#{user_id}")
46
+ puts "User #{user_id} deactivated."
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/fizzy/cli.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "cli/base"
5
+ require_relative "cli/boards"
6
+ require_relative "cli/cards"
7
+ require_relative "cli/columns"
8
+ require_relative "cli/steps"
9
+ require_relative "cli/comments"
10
+ require_relative "cli/reactions"
11
+ require_relative "cli/tags"
12
+ require_relative "cli/users"
13
+ require_relative "cli/notifications"
14
+ require_relative "cli/pins"
15
+ require_relative "cli/auth"
16
+
17
+ module Fizzy
18
+ class CLI < Thor
19
+ class_option :json, type: :boolean, desc: "Output as JSON"
20
+ class_option :account, type: :string, desc: "Account slug override"
21
+
22
+ desc "version", "Print version"
23
+ def version
24
+ puts "fizzy-cli #{VERSION}"
25
+ end
26
+
27
+ desc "boards SUBCOMMAND ...ARGS", "Manage boards"
28
+ subcommand "boards", CLI::Boards
29
+
30
+ desc "cards SUBCOMMAND ...ARGS", "Manage cards"
31
+ subcommand "cards", CLI::Cards
32
+
33
+ desc "columns SUBCOMMAND ...ARGS", "Manage columns"
34
+ subcommand "columns", CLI::Columns
35
+
36
+ desc "steps SUBCOMMAND ...ARGS", "Manage card steps"
37
+ subcommand "steps", CLI::Steps
38
+
39
+ desc "comments SUBCOMMAND ...ARGS", "Manage card comments"
40
+ subcommand "comments", CLI::Comments
41
+
42
+ desc "reactions SUBCOMMAND ...ARGS", "Manage reactions"
43
+ subcommand "reactions", CLI::Reactions
44
+
45
+ desc "tags SUBCOMMAND ...ARGS", "List tags"
46
+ subcommand "tags", CLI::Tags
47
+
48
+ desc "users SUBCOMMAND ...ARGS", "Manage users"
49
+ subcommand "users", CLI::Users
50
+
51
+ desc "notifications SUBCOMMAND ...ARGS", "Manage notifications"
52
+ subcommand "notifications", CLI::Notifications
53
+
54
+ desc "pins SUBCOMMAND ...ARGS", "Manage pinned cards"
55
+ subcommand "pins", CLI::Pins
56
+
57
+ desc "auth SUBCOMMAND ...ARGS", "Authentication commands"
58
+ subcommand "auth", CLI::AuthCommands
59
+
60
+ def self.exit_on_failure? = true
61
+ end
62
+ end