superthread 0.7.2
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 +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +492 -0
- data/exe/suth +19 -0
- data/lib/superthread/cli/accounts.rb +240 -0
- data/lib/superthread/cli/activity.rb +210 -0
- data/lib/superthread/cli/base.rb +355 -0
- data/lib/superthread/cli/boards.rb +131 -0
- data/lib/superthread/cli/cards.rb +530 -0
- data/lib/superthread/cli/checklists.rb +223 -0
- data/lib/superthread/cli/comments.rb +86 -0
- data/lib/superthread/cli/completion.rb +306 -0
- data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
- data/lib/superthread/cli/concerns/confirmable.rb +55 -0
- data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
- data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
- data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
- data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
- data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
- data/lib/superthread/cli/config.rb +129 -0
- data/lib/superthread/cli/formatter.rb +388 -0
- data/lib/superthread/cli/lists.rb +85 -0
- data/lib/superthread/cli/main.rb +121 -0
- data/lib/superthread/cli/members.rb +19 -0
- data/lib/superthread/cli/notes.rb +64 -0
- data/lib/superthread/cli/pages.rb +128 -0
- data/lib/superthread/cli/projects.rb +124 -0
- data/lib/superthread/cli/replies.rb +94 -0
- data/lib/superthread/cli/search.rb +34 -0
- data/lib/superthread/cli/setup.rb +253 -0
- data/lib/superthread/cli/spaces.rb +141 -0
- data/lib/superthread/cli/sprints.rb +32 -0
- data/lib/superthread/cli/tags.rb +86 -0
- data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
- data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
- data/lib/superthread/cli/ui.rb +263 -0
- data/lib/superthread/cli/workspaces.rb +105 -0
- data/lib/superthread/cli.rb +12 -0
- data/lib/superthread/client.rb +207 -0
- data/lib/superthread/configuration.rb +354 -0
- data/lib/superthread/connection.rb +57 -0
- data/lib/superthread/error.rb +164 -0
- data/lib/superthread/mention_formatter.rb +96 -0
- data/lib/superthread/model.rb +178 -0
- data/lib/superthread/models/board.rb +59 -0
- data/lib/superthread/models/card.rb +321 -0
- data/lib/superthread/models/checklist.rb +91 -0
- data/lib/superthread/models/checklist_item.rb +69 -0
- data/lib/superthread/models/comment.rb +71 -0
- data/lib/superthread/models/concerns/archivable.rb +32 -0
- data/lib/superthread/models/concerns/presentable.rb +113 -0
- data/lib/superthread/models/concerns/timestampable.rb +91 -0
- data/lib/superthread/models/list.rb +67 -0
- data/lib/superthread/models/member.rb +40 -0
- data/lib/superthread/models/note.rb +56 -0
- data/lib/superthread/models/page.rb +70 -0
- data/lib/superthread/models/project.rb +83 -0
- data/lib/superthread/models/space.rb +71 -0
- data/lib/superthread/models/sprint.rb +53 -0
- data/lib/superthread/models/tag.rb +52 -0
- data/lib/superthread/models/team.rb +68 -0
- data/lib/superthread/models/user.rb +76 -0
- data/lib/superthread/models.rb +12 -0
- data/lib/superthread/object.rb +285 -0
- data/lib/superthread/objects/collection.rb +179 -0
- data/lib/superthread/resources/base.rb +204 -0
- data/lib/superthread/resources/boards.rb +150 -0
- data/lib/superthread/resources/cards.rb +363 -0
- data/lib/superthread/resources/comments.rb +163 -0
- data/lib/superthread/resources/notes.rb +61 -0
- data/lib/superthread/resources/pages.rb +110 -0
- data/lib/superthread/resources/projects.rb +117 -0
- data/lib/superthread/resources/search.rb +46 -0
- data/lib/superthread/resources/spaces.rb +104 -0
- data/lib/superthread/resources/sprints.rb +37 -0
- data/lib/superthread/resources/tags.rb +52 -0
- data/lib/superthread/resources/users.rb +29 -0
- data/lib/superthread/version.rb +6 -0
- data/lib/superthread/version_checker.rb +174 -0
- data/lib/superthread.rb +30 -0
- metadata +259 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing card checklists.
|
|
6
|
+
#
|
|
7
|
+
# Checklists are task lists attached to cards with trackable items.
|
|
8
|
+
# This class provides commands to create, update, and delete checklists
|
|
9
|
+
# as well as manage checklist items.
|
|
10
|
+
class Checklists < Base
|
|
11
|
+
# Kebab-case aliases for commands
|
|
12
|
+
map "add-item" => :add_item,
|
|
13
|
+
"update-item" => :update_item,
|
|
14
|
+
"remove-item" => :remove_item
|
|
15
|
+
|
|
16
|
+
desc "list", "List all checklists on a card"
|
|
17
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
18
|
+
# List all checklists on a specified card.
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
def list
|
|
22
|
+
handle_error do
|
|
23
|
+
card = client.cards.find(workspace_id, options[:card])
|
|
24
|
+
if card.checklists.nil? || card.checklists.empty?
|
|
25
|
+
say "No checklists found on this card.", :yellow
|
|
26
|
+
else
|
|
27
|
+
output_list card.checklists, columns: %i[id title], headers: {id: "CHECKLIST_ID"}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "get CHECKLIST", "Get checklist details"
|
|
33
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
34
|
+
# Display detailed information about a specific checklist.
|
|
35
|
+
#
|
|
36
|
+
# @param checklist_id [String] the unique identifier of the checklist
|
|
37
|
+
# @return [void]
|
|
38
|
+
def get(checklist_id)
|
|
39
|
+
handle_error do
|
|
40
|
+
card = client.cards.find(workspace_id, options[:card])
|
|
41
|
+
checklist = card.checklists&.find { |c| c.id == checklist_id }
|
|
42
|
+
raise Thor::Error, "Checklist not found: #{checklist_id}" unless checklist
|
|
43
|
+
|
|
44
|
+
if json_output?
|
|
45
|
+
output_item checklist, fields: %i[id title card_id items time_created], labels: {
|
|
46
|
+
id: "Checklist ID",
|
|
47
|
+
card_id: "Card ID",
|
|
48
|
+
time_created: "Time Created"
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
output_item checklist, fields: %i[id title card_id time_created], labels: {
|
|
52
|
+
id: "Checklist ID",
|
|
53
|
+
card_id: "Card ID",
|
|
54
|
+
time_created: "Time Created"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if checklist.items&.any?
|
|
58
|
+
puts ""
|
|
59
|
+
Ui.section "Items"
|
|
60
|
+
checklist.items.each do |item|
|
|
61
|
+
marker = item.checked? ? "✓" : "○"
|
|
62
|
+
puts " #{marker} #{item.title} (#{item.id})"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "create", "Create a checklist on a card"
|
|
70
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
71
|
+
option :title, type: :string, required: true, desc: "Checklist title"
|
|
72
|
+
# Add a new checklist to a card.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def create
|
|
76
|
+
handle_error do
|
|
77
|
+
checklist = client.cards.create_checklist(workspace_id, options[:card], title: options[:title])
|
|
78
|
+
output_item checklist, fields: %i[id title card_id time_created], labels: {id: "Checklist ID"}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "update CHECKLIST", "Update a checklist"
|
|
83
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
84
|
+
option :title, type: :string, required: true, desc: "New checklist title"
|
|
85
|
+
# Rename an existing checklist on a card.
|
|
86
|
+
#
|
|
87
|
+
# @param checklist_id [String] the unique identifier of the checklist
|
|
88
|
+
# @return [void]
|
|
89
|
+
def update(checklist_id)
|
|
90
|
+
handle_error do
|
|
91
|
+
checklist = client.cards.update_checklist(
|
|
92
|
+
workspace_id, options[:card], checklist_id,
|
|
93
|
+
title: options[:title]
|
|
94
|
+
)
|
|
95
|
+
output_item checklist, fields: %i[id title card_id time_created], labels: {id: "Checklist ID"}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc "delete CHECKLIST", "Delete a checklist"
|
|
100
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
101
|
+
# Remove a checklist and all its items from a card after confirmation.
|
|
102
|
+
#
|
|
103
|
+
# @param checklist_id [String] the unique identifier of the checklist
|
|
104
|
+
# @return [void]
|
|
105
|
+
def delete(checklist_id)
|
|
106
|
+
handle_error do
|
|
107
|
+
card = client.cards.find(workspace_id, options[:card])
|
|
108
|
+
checklist = card.checklists&.find { |c| c.id == checklist_id }
|
|
109
|
+
checklist_name = checklist&.title || checklist_id
|
|
110
|
+
confirming("Delete checklist '#{checklist_name}' (#{checklist_id})?") do
|
|
111
|
+
client.cards.delete_checklist(workspace_id, options[:card], checklist_id)
|
|
112
|
+
output_success "Checklist '#{checklist_name}' deleted"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
desc "add-item CHECKLIST", "Add item to a checklist"
|
|
118
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
119
|
+
option :title, type: :string, required: true, desc: "Item title"
|
|
120
|
+
option :checked, type: :boolean, default: false, desc: "Create as checked"
|
|
121
|
+
# Add a new item to an existing checklist.
|
|
122
|
+
#
|
|
123
|
+
# @param checklist_id [String] the unique identifier of the checklist
|
|
124
|
+
# @return [void]
|
|
125
|
+
def add_item(checklist_id)
|
|
126
|
+
handle_error do
|
|
127
|
+
item = client.cards.add_checklist_item(
|
|
128
|
+
workspace_id, options[:card], checklist_id,
|
|
129
|
+
title: options[:title],
|
|
130
|
+
checked: options[:checked]
|
|
131
|
+
)
|
|
132
|
+
output_item item, fields: %i[id title checked checklist_id], labels: {
|
|
133
|
+
id: "Item ID",
|
|
134
|
+
checklist_id: "Checklist ID"
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
desc "update-item ITEM_ID", "Update a checklist item"
|
|
140
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
141
|
+
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
142
|
+
option :title, type: :string, desc: "New item title"
|
|
143
|
+
option :checked, type: :boolean, desc: "Mark as checked/unchecked"
|
|
144
|
+
# Update the title or checked state of a checklist item.
|
|
145
|
+
#
|
|
146
|
+
# @param item_id [String] the unique identifier of the item
|
|
147
|
+
# @return [void]
|
|
148
|
+
def update_item(item_id)
|
|
149
|
+
handle_error do
|
|
150
|
+
opts = symbolized_options(:title, :checked)
|
|
151
|
+
item = client.cards.update_checklist_item(
|
|
152
|
+
workspace_id, options[:card], options[:checklist], item_id,
|
|
153
|
+
**opts
|
|
154
|
+
)
|
|
155
|
+
output_item item, fields: %i[id title checked checklist_id], labels: {
|
|
156
|
+
id: "Item ID",
|
|
157
|
+
checklist_id: "Checklist ID"
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
desc "remove-item ITEM_ID", "Delete a checklist item"
|
|
163
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
164
|
+
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
165
|
+
# Remove an item from a checklist after confirmation.
|
|
166
|
+
#
|
|
167
|
+
# @param item_id [String] the unique identifier of the item
|
|
168
|
+
# @return [void]
|
|
169
|
+
def remove_item(item_id)
|
|
170
|
+
handle_error do
|
|
171
|
+
card = client.cards.find(workspace_id, options[:card])
|
|
172
|
+
checklist = card.checklists&.find { |c| c.id == options[:checklist] }
|
|
173
|
+
item = checklist&.items&.find { |i| i.id == item_id }
|
|
174
|
+
item_name = item&.title || item_id
|
|
175
|
+
confirming("Delete checklist item '#{item_name}' (#{item_id})?") do
|
|
176
|
+
client.cards.delete_checklist_item(workspace_id, options[:card], options[:checklist], item_id)
|
|
177
|
+
output_success "Checklist item '#{item_name}' deleted"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
desc "check ITEM_ID", "Mark a checklist item as checked"
|
|
183
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
184
|
+
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
185
|
+
# Mark a checklist item as completed.
|
|
186
|
+
#
|
|
187
|
+
# @param item_id [String] the unique identifier of the item
|
|
188
|
+
# @return [void]
|
|
189
|
+
def check(item_id)
|
|
190
|
+
handle_error do
|
|
191
|
+
item = client.cards.update_checklist_item(
|
|
192
|
+
workspace_id, options[:card], options[:checklist], item_id,
|
|
193
|
+
checked: true
|
|
194
|
+
)
|
|
195
|
+
output_item item, fields: %i[id title checked checklist_id], labels: {
|
|
196
|
+
id: "Item ID",
|
|
197
|
+
checklist_id: "Checklist ID"
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
desc "uncheck ITEM_ID", "Mark a checklist item as unchecked"
|
|
203
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
204
|
+
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
205
|
+
# Mark a checklist item as not completed.
|
|
206
|
+
#
|
|
207
|
+
# @param item_id [String] the unique identifier of the item
|
|
208
|
+
# @return [void]
|
|
209
|
+
def uncheck(item_id)
|
|
210
|
+
handle_error do
|
|
211
|
+
item = client.cards.update_checklist_item(
|
|
212
|
+
workspace_id, options[:card], options[:checklist], item_id,
|
|
213
|
+
checked: false
|
|
214
|
+
)
|
|
215
|
+
output_item item, fields: %i[id title checked checklist_id], labels: {
|
|
216
|
+
id: "Item ID",
|
|
217
|
+
checklist_id: "Checklist ID"
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing comments on Superthread cards and pages.
|
|
6
|
+
#
|
|
7
|
+
# Provides subcommands for creating, updating, and deleting comments.
|
|
8
|
+
class Comments < Base
|
|
9
|
+
desc "list", "List comments on a card"
|
|
10
|
+
option :card, type: :string, required: true, aliases: "-c", desc: "Card ID"
|
|
11
|
+
# List all comments on a specified card.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def list
|
|
15
|
+
handle_error do
|
|
16
|
+
comments = client.comments.list(workspace_id, card_id: options[:card])
|
|
17
|
+
output_list comments, columns: %i[id user_id content time_created], headers: {id: "COMMENT_ID"}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "get COMMENT_ID", "Get comment details"
|
|
22
|
+
# Display detailed information about a specific comment.
|
|
23
|
+
#
|
|
24
|
+
# @param comment_id [String] the unique identifier of the comment to retrieve
|
|
25
|
+
# @return [void]
|
|
26
|
+
def get(comment_id)
|
|
27
|
+
handle_error do
|
|
28
|
+
comment = with_not_found("Comment not found: '#{comment_id}'.") do
|
|
29
|
+
client.comments.find(workspace_id, comment_id)
|
|
30
|
+
end
|
|
31
|
+
output_item comment, fields: %i[id content user_id card_id time_created time_updated],
|
|
32
|
+
labels: {id: "Comment ID"}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "create", "Create a comment"
|
|
37
|
+
option :content, type: :string, required: true, desc: "Comment content (HTML)"
|
|
38
|
+
option :card, type: :string, aliases: "-c", desc: "Parent card ID (required unless --page)"
|
|
39
|
+
option :page, type: :string, aliases: "-p", desc: "Parent page ID (required unless --card)"
|
|
40
|
+
# Create a new comment on a card or page.
|
|
41
|
+
#
|
|
42
|
+
# @return [void]
|
|
43
|
+
def create
|
|
44
|
+
opts = symbolized_options(:content)
|
|
45
|
+
opts[:card_id] = options[:card] if options[:card]
|
|
46
|
+
opts[:page_id] = options[:page] if options[:page]
|
|
47
|
+
comment = client.comments.create(workspace_id, **opts)
|
|
48
|
+
output_item comment, labels: {id: "Comment ID"}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "update COMMENT_ID", "Update a comment"
|
|
52
|
+
option :content, type: :string, desc: "New content"
|
|
53
|
+
option :status, type: :string, enum: %w[resolved open orphaned], desc: "Comment status"
|
|
54
|
+
# Update an existing comment's content or status.
|
|
55
|
+
#
|
|
56
|
+
# @param comment_id [String] the unique identifier of the comment to update
|
|
57
|
+
# @return [void]
|
|
58
|
+
def update(comment_id)
|
|
59
|
+
handle_error do
|
|
60
|
+
comment = with_not_found("Comment not found: '#{comment_id}'.") do
|
|
61
|
+
client.comments.update(workspace_id, comment_id, **symbolized_options(:content, :status))
|
|
62
|
+
end
|
|
63
|
+
output_item comment, labels: {id: "Comment ID"}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "delete COMMENT_ID", "Delete a comment"
|
|
68
|
+
# Permanently delete a comment after confirmation.
|
|
69
|
+
#
|
|
70
|
+
# @param comment_id [String] the unique identifier of the comment to delete
|
|
71
|
+
# @return [void]
|
|
72
|
+
def delete(comment_id)
|
|
73
|
+
handle_error do
|
|
74
|
+
comment = with_not_found("Comment not found: '#{comment_id}'.") do
|
|
75
|
+
client.comments.find(workspace_id, comment_id)
|
|
76
|
+
end
|
|
77
|
+
preview = Formatter.truncate(Formatter.strip_html(comment.content), 50)
|
|
78
|
+
confirming("Delete comment '#{preview}' (#{comment.id})?") do
|
|
79
|
+
client.comments.destroy(workspace_id, comment.id)
|
|
80
|
+
output_success "Comment #{comment.id} deleted"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# Generate shell completion scripts for bash, zsh, and fish.
|
|
6
|
+
# Uses Thor introspection to automatically discover commands and options.
|
|
7
|
+
class Completion < Base
|
|
8
|
+
desc "bash", "Generate bash completion script"
|
|
9
|
+
long_desc <<~DESC
|
|
10
|
+
Generate the autocompletion script for the bash shell.
|
|
11
|
+
|
|
12
|
+
To load completions in your current shell session:
|
|
13
|
+
|
|
14
|
+
source <(suth completion bash)
|
|
15
|
+
|
|
16
|
+
To load completions for every new session, execute once:
|
|
17
|
+
|
|
18
|
+
# macOS (Homebrew):
|
|
19
|
+
suth completion bash > $(brew --prefix)/etc/bash_completion.d/suth
|
|
20
|
+
|
|
21
|
+
# Linux:
|
|
22
|
+
suth completion bash > /etc/bash_completion.d/suth
|
|
23
|
+
|
|
24
|
+
You will need to start a new shell for this setup to take effect.
|
|
25
|
+
DESC
|
|
26
|
+
# Outputs a bash completion script for the CLI.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def bash
|
|
30
|
+
puts bash_completion_script
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "zsh", "Generate zsh completion script"
|
|
34
|
+
long_desc <<~DESC
|
|
35
|
+
Generate the autocompletion script for the zsh shell.
|
|
36
|
+
|
|
37
|
+
To load completions in your current shell session:
|
|
38
|
+
|
|
39
|
+
source <(suth completion zsh)
|
|
40
|
+
|
|
41
|
+
To load completions for every new session, execute once:
|
|
42
|
+
|
|
43
|
+
# macOS (Homebrew):
|
|
44
|
+
suth completion zsh > $(brew --prefix)/share/zsh/site-functions/_suth
|
|
45
|
+
|
|
46
|
+
# Linux:
|
|
47
|
+
suth completion zsh > "${fpath[1]}/_suth"
|
|
48
|
+
|
|
49
|
+
You will need to start a new shell for this setup to take effect.
|
|
50
|
+
DESC
|
|
51
|
+
# Outputs a zsh completion script for the CLI.
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
54
|
+
def zsh
|
|
55
|
+
puts zsh_completion_script
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "fish", "Generate fish completion script"
|
|
59
|
+
long_desc <<~DESC
|
|
60
|
+
Generate the autocompletion script for the fish shell.
|
|
61
|
+
|
|
62
|
+
To load completions in your current shell session:
|
|
63
|
+
|
|
64
|
+
suth completion fish | source
|
|
65
|
+
|
|
66
|
+
To load completions for every new session, execute once:
|
|
67
|
+
|
|
68
|
+
suth completion fish > ~/.config/fish/completions/suth.fish
|
|
69
|
+
|
|
70
|
+
You will need to start a new shell for this setup to take effect.
|
|
71
|
+
DESC
|
|
72
|
+
# Outputs a fish completion script for the CLI.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def fish
|
|
76
|
+
puts fish_completion_script
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Get the cached command structure from Thor introspection.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash{String => Hash}] command names mapped to their metadata
|
|
84
|
+
def command_structure
|
|
85
|
+
@command_structure ||= build_command_structure
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build command structure by introspecting Thor classes.
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash{String => Hash}] command names with :desc, :subcommands, :options
|
|
91
|
+
def build_command_structure
|
|
92
|
+
structure = {}
|
|
93
|
+
main_class = Superthread::Cli::Main
|
|
94
|
+
|
|
95
|
+
main_class.commands.each do |name, command|
|
|
96
|
+
next if name == "help"
|
|
97
|
+
|
|
98
|
+
structure[name] = {
|
|
99
|
+
desc: command.description,
|
|
100
|
+
subcommands: {},
|
|
101
|
+
options: extract_options(command)
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Add subcommand details
|
|
106
|
+
main_class.subcommand_classes.each do |name, klass|
|
|
107
|
+
next unless structure[name]
|
|
108
|
+
|
|
109
|
+
klass.commands.each do |subcmd_name, subcmd|
|
|
110
|
+
next if subcmd_name == "help"
|
|
111
|
+
|
|
112
|
+
structure[name][:subcommands][subcmd_name] = {
|
|
113
|
+
desc: subcmd.description,
|
|
114
|
+
options: extract_options(subcmd)
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
structure
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Extract option definitions from a Thor command.
|
|
123
|
+
#
|
|
124
|
+
# @param command [Thor::Command] the command to extract options from
|
|
125
|
+
# @return [Array<Hash{Symbol => String}>] array of option hashes with :flag, :name, :desc
|
|
126
|
+
def extract_options(command)
|
|
127
|
+
command.options.map do |name, opt|
|
|
128
|
+
flag = "--#{name.to_s.tr("_", "-")}"
|
|
129
|
+
flag = "-#{opt.aliases.first}, #{flag}" if opt.aliases&.any?
|
|
130
|
+
{flag: flag, name: name.to_s.tr("_", "-"), desc: opt.description || name.to_s}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get the list of global CLI options available on all commands.
|
|
135
|
+
#
|
|
136
|
+
# @return [Array<Hash{Symbol => String}>] array of option hashes with :flag, :name, :desc
|
|
137
|
+
def global_options
|
|
138
|
+
[
|
|
139
|
+
{flag: "-v, --verbose", name: "verbose", desc: "Detailed logging"},
|
|
140
|
+
{flag: "-q, --quiet", name: "quiet", desc: "Minimal logging"},
|
|
141
|
+
{flag: "-w, --workspace", name: "workspace", desc: "Workspace (ID or name)"},
|
|
142
|
+
{flag: "--json", name: "json", desc: "Output as JSON"},
|
|
143
|
+
{flag: "-a, --account", name: "account", desc: "Use specific account"},
|
|
144
|
+
{flag: "-y, --yes", name: "yes", desc: "Auto-confirm prompts"}
|
|
145
|
+
]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Generate a bash completion script for the CLI.
|
|
149
|
+
#
|
|
150
|
+
# @return [String] the complete bash completion script
|
|
151
|
+
def bash_completion_script
|
|
152
|
+
cmds = command_structure
|
|
153
|
+
main_commands = cmds.keys.join(" ")
|
|
154
|
+
global_opts = global_options.map { |o| o[:flag].split(", ").last }.join(" ")
|
|
155
|
+
|
|
156
|
+
subcommand_cases = cmds.map do |cmd, data|
|
|
157
|
+
next if data[:subcommands].empty?
|
|
158
|
+
|
|
159
|
+
subcmds = data[:subcommands].keys.join(" ")
|
|
160
|
+
<<~CASE.chomp
|
|
161
|
+
#{cmd})
|
|
162
|
+
COMPREPLY=($(compgen -W "#{subcmds}" -- "${cur}"))
|
|
163
|
+
return 0
|
|
164
|
+
;;
|
|
165
|
+
CASE
|
|
166
|
+
end.compact.join("\n")
|
|
167
|
+
|
|
168
|
+
<<~BASH
|
|
169
|
+
# Bash completion for suth (Superthread CLI)
|
|
170
|
+
# Generated by: suth completion bash
|
|
171
|
+
|
|
172
|
+
_suth() {
|
|
173
|
+
local cur prev commands
|
|
174
|
+
COMPREPLY=()
|
|
175
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
176
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
177
|
+
|
|
178
|
+
commands="#{main_commands}"
|
|
179
|
+
|
|
180
|
+
case "${prev}" in
|
|
181
|
+
suth)
|
|
182
|
+
COMPREPLY=($(compgen -W "${commands}" -- "${cur}"))
|
|
183
|
+
return 0
|
|
184
|
+
;;
|
|
185
|
+
#{subcommand_cases}
|
|
186
|
+
esac
|
|
187
|
+
|
|
188
|
+
# Complete options
|
|
189
|
+
if [[ "${cur}" == -* ]]; then
|
|
190
|
+
COMPREPLY=($(compgen -W "#{global_opts}" -- "${cur}"))
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
complete -F _suth suth
|
|
196
|
+
BASH
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Generate a zsh completion script for the CLI.
|
|
200
|
+
#
|
|
201
|
+
# @return [String] the complete zsh completion script
|
|
202
|
+
def zsh_completion_script
|
|
203
|
+
cmds = command_structure
|
|
204
|
+
|
|
205
|
+
commands_list = cmds.map do |cmd, data|
|
|
206
|
+
" '#{cmd}:#{escape_zsh(data[:desc])}'"
|
|
207
|
+
end.join("\n")
|
|
208
|
+
|
|
209
|
+
subcommand_cases = cmds.map do |cmd, data|
|
|
210
|
+
next if data[:subcommands].empty?
|
|
211
|
+
|
|
212
|
+
subcmds_list = data[:subcommands].map do |subcmd, subcmd_data|
|
|
213
|
+
" '#{subcmd}:#{escape_zsh(subcmd_data[:desc])}'"
|
|
214
|
+
end.join("\n")
|
|
215
|
+
|
|
216
|
+
<<~CASE.chomp
|
|
217
|
+
#{cmd})
|
|
218
|
+
local -a subcommands
|
|
219
|
+
subcommands=(
|
|
220
|
+
#{subcmds_list}
|
|
221
|
+
)
|
|
222
|
+
_describe -t commands '#{cmd} subcommands' subcommands
|
|
223
|
+
;;
|
|
224
|
+
CASE
|
|
225
|
+
end.compact.join("\n")
|
|
226
|
+
|
|
227
|
+
<<~ZSH
|
|
228
|
+
#compdef suth
|
|
229
|
+
|
|
230
|
+
# Zsh completion for suth (Superthread CLI)
|
|
231
|
+
# Generated by: suth completion zsh
|
|
232
|
+
|
|
233
|
+
_suth() {
|
|
234
|
+
local -a commands
|
|
235
|
+
|
|
236
|
+
commands=(
|
|
237
|
+
#{commands_list}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if (( CURRENT == 2 )); then
|
|
241
|
+
_describe 'command' commands
|
|
242
|
+
return
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
case "${words[2]}" in
|
|
246
|
+
#{subcommand_cases}
|
|
247
|
+
esac
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_suth "$@"
|
|
251
|
+
ZSH
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Generate a fish completion script for the CLI.
|
|
255
|
+
#
|
|
256
|
+
# @return [String] the complete fish completion script
|
|
257
|
+
def fish_completion_script
|
|
258
|
+
cmds = command_structure
|
|
259
|
+
|
|
260
|
+
lines = [
|
|
261
|
+
"# Fish completion for suth (Superthread CLI)",
|
|
262
|
+
"# Generated by: suth completion fish",
|
|
263
|
+
"",
|
|
264
|
+
"# Disable file completion by default",
|
|
265
|
+
"complete -c suth -f",
|
|
266
|
+
"",
|
|
267
|
+
"# Main commands"
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
cmds.each do |cmd, data|
|
|
271
|
+
lines << "complete -c suth -n __fish_use_subcommand -a #{cmd} -d '#{escape_fish(data[:desc])}'"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
lines << ""
|
|
275
|
+
|
|
276
|
+
cmds.each do |cmd, data|
|
|
277
|
+
next if data[:subcommands].empty?
|
|
278
|
+
|
|
279
|
+
lines << "# #{cmd} subcommands"
|
|
280
|
+
data[:subcommands].each do |subcmd, subcmd_data|
|
|
281
|
+
lines << "complete -c suth -n '__fish_seen_subcommand_from #{cmd}' -a #{subcmd} -d '#{escape_fish(subcmd_data[:desc])}'"
|
|
282
|
+
end
|
|
283
|
+
lines << ""
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
lines.join("\n")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Escape a string for safe inclusion in zsh completion script.
|
|
290
|
+
#
|
|
291
|
+
# @param str [String] the string to escape
|
|
292
|
+
# @return [String] the escaped string with single quotes properly handled
|
|
293
|
+
def escape_zsh(str)
|
|
294
|
+
str.to_s.gsub("'", "'\\''")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Escape a string for safe inclusion in fish completion script.
|
|
298
|
+
#
|
|
299
|
+
# @param str [String] the string to escape
|
|
300
|
+
# @return [String] the escaped string with single quotes properly handled
|
|
301
|
+
def escape_fish(str)
|
|
302
|
+
str.to_s.gsub("'", "\\\\'")
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Superthread
|
|
6
|
+
module Cli
|
|
7
|
+
module Concerns
|
|
8
|
+
# Resolves board references (ID or name) to board IDs.
|
|
9
|
+
#
|
|
10
|
+
# Board resolution is context-dependent: when --space is provided, it
|
|
11
|
+
# searches only within that space. Otherwise, it searches across all
|
|
12
|
+
# spaces in the workspace.
|
|
13
|
+
module BoardResolvable
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Get the board ID from --board option, resolving name if needed.
|
|
19
|
+
#
|
|
20
|
+
# @return [String, nil] the resolved board ID, or nil if not specified
|
|
21
|
+
def board_id
|
|
22
|
+
resolve_board(options[:board])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Resolve a board reference (ID or name) to its ID.
|
|
26
|
+
#
|
|
27
|
+
# @param ref [String, nil] the board ID or name to resolve
|
|
28
|
+
# @return [String, nil] the resolved board ID
|
|
29
|
+
# @raise [Thor::Error] if name is provided but not found
|
|
30
|
+
def resolve_board(ref)
|
|
31
|
+
return ref if ref.nil?
|
|
32
|
+
return ref if looks_like_id?(ref)
|
|
33
|
+
|
|
34
|
+
board = find_board_by_name(ref)
|
|
35
|
+
return board.id if board
|
|
36
|
+
|
|
37
|
+
raise Thor::Error, "Board not found: '#{ref}'. Use 'suth boards list --space <space>' to see available boards."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find a board by name, searching within space if specified or all spaces.
|
|
41
|
+
#
|
|
42
|
+
# @param name [String] the board name to search for (case-insensitive)
|
|
43
|
+
# @return [Superthread::Models::Board, nil] the board object or nil if not found
|
|
44
|
+
def find_board_by_name(name)
|
|
45
|
+
# If space is specified, search only in that space
|
|
46
|
+
if options[:space]
|
|
47
|
+
@boards_cache ||= client.boards.list(workspace_id, space_id: space_id)
|
|
48
|
+
return @boards_cache.find { |b| b.title&.downcase == name.downcase }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Otherwise search across all spaces
|
|
52
|
+
@all_boards_cache ||= load_all_boards
|
|
53
|
+
@all_boards_cache.find { |b| b.title&.downcase == name.downcase }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Load all boards from all spaces in the workspace.
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<Superthread::Models::Board>] all accessible boards
|
|
59
|
+
def load_all_boards
|
|
60
|
+
@spaces_cache ||= client.spaces.list(workspace_id)
|
|
61
|
+
@spaces_cache.flat_map do |space|
|
|
62
|
+
client.boards.list(workspace_id, space_id: space.id).to_a
|
|
63
|
+
rescue Superthread::ApiError
|
|
64
|
+
[] # Skip spaces we can't access
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|