superthread 0.7.4 → 0.8.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.
- checksums.yaml +4 -4
- data/lib/superthread/cli/base.rb +1 -1
- data/lib/superthread/cli/cards.rb +70 -2
- data/lib/superthread/cli/checklists.rb +27 -25
- data/lib/superthread/cli/search.rb +17 -2
- data/lib/superthread/connection.rb +1 -1
- data/lib/superthread/models/checklist.rb +7 -0
- data/lib/superthread/models/checklist_item.rb +4 -0
- data/lib/superthread/resources/search.rb +35 -17
- data/lib/superthread/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb38a8ab1c8ba081e66e8bda3bc4fabcb8aa9732e1e8cfc5ce1bdff0b14f94b2
|
|
4
|
+
data.tar.gz: e97e7bffe8dc9bfbd86b424a6762455d1d9979b07a60651b3997ad5feaedddf5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2c5dfabc604f08c7aae0eedb7a94fb8f702912c5ccc2730c9d388d6f404dbdff8aa9f3fa835288732612f84d7755e4a2a5283f1d266747b3e9727aa09fdbe5f
|
|
7
|
+
data.tar.gz: 847b4c2868387c94b08bbbffab9db1d80dac9b4991884941258eb801347996f11257cdc1079d192848ae5208936352da2b894f95a1288914403279395b50cf61
|
data/lib/superthread/cli/base.rb
CHANGED
|
@@ -137,7 +137,7 @@ module Superthread
|
|
|
137
137
|
def output_list(items, columns: nil, headers: {})
|
|
138
138
|
all_items = items.respond_to?(:items) ? items.items : Array(items)
|
|
139
139
|
limit = effective_limit
|
|
140
|
-
truncated = all_items.length > limit
|
|
140
|
+
truncated = limit && all_items.length > limit
|
|
141
141
|
visible = truncated ? all_items.first(limit) : all_items
|
|
142
142
|
|
|
143
143
|
if json_output?
|
|
@@ -52,6 +52,52 @@ module Superthread
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
desc "search TERM", "Search for cards across boards and spaces"
|
|
56
|
+
option :space, type: :string, aliases: "-s", desc: "Space to filter by (ID or name)"
|
|
57
|
+
option :status, type: :string, desc: "Status filter (comma-separated)"
|
|
58
|
+
option :field, type: :string, enum: %w[title content], desc: "Search in title or content"
|
|
59
|
+
option :include_archived, type: :boolean, desc: "Include archived cards"
|
|
60
|
+
# Search for cards by keyword with rich output.
|
|
61
|
+
#
|
|
62
|
+
# @param term [String] the search term
|
|
63
|
+
# @return [void]
|
|
64
|
+
def search(term)
|
|
65
|
+
handle_error do
|
|
66
|
+
statuses = options[:status]&.split(",")&.map(&:strip)
|
|
67
|
+
results = client.search.query(
|
|
68
|
+
workspace_id,
|
|
69
|
+
query: term,
|
|
70
|
+
types: ["card"],
|
|
71
|
+
statuses: statuses,
|
|
72
|
+
field: options[:field],
|
|
73
|
+
space_id: (space_id if options[:space]),
|
|
74
|
+
archived: options[:include_archived],
|
|
75
|
+
limit: effective_limit
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
card_ids = results.map { |r| r[:id] }.compact
|
|
79
|
+
if card_ids.empty?
|
|
80
|
+
say "No cards found matching '#{term}'.", :yellow unless options[:quiet]
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
cards = card_ids.filter_map do |id|
|
|
85
|
+
client.cards.find(workspace_id, id)
|
|
86
|
+
rescue Superthread::NotFoundError, Superthread::ForbiddenError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if cards.empty?
|
|
91
|
+
say "No cards found matching '#{term}'.", :yellow unless options[:quiet]
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
enrich_members(cards)
|
|
96
|
+
output_list cards, columns: %i[id title priority list_title board_title members],
|
|
97
|
+
headers: {id: "CARD_ID", list_title: "LIST", board_title: "BOARD"}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
55
101
|
desc "get CARD", "Get card details"
|
|
56
102
|
option :raw, type: :boolean, desc: "Show raw content without markdown rendering"
|
|
57
103
|
option :no_content, type: :boolean, desc: "Hide content, show only metadata"
|
|
@@ -393,6 +439,16 @@ module Superthread
|
|
|
393
439
|
|
|
394
440
|
private
|
|
395
441
|
|
|
442
|
+
# Override Base#effective_limit for search-specific defaults.
|
|
443
|
+
# Returns nil for --limit 0 (unlimited), 30 for search default.
|
|
444
|
+
#
|
|
445
|
+
# @return [Integer, nil] the limit (nil = unlimited when --limit 0)
|
|
446
|
+
def effective_limit
|
|
447
|
+
limit = options[:limit]
|
|
448
|
+
return nil if limit == 0
|
|
449
|
+
(limit.is_a?(Integer) && limit > 0) ? limit : 30
|
|
450
|
+
end
|
|
451
|
+
|
|
396
452
|
# Enrich card members with display names from workspace users.
|
|
397
453
|
#
|
|
398
454
|
# The API returns members with only user_id. This fetches the workspace
|
|
@@ -441,11 +497,23 @@ module Superthread
|
|
|
441
497
|
sprint_obj = client.sprints.find(workspace_id, existing.sprint_id,
|
|
442
498
|
space_id: existing.project_id)
|
|
443
499
|
list = sprint_obj.lists&.find { |l| l.title&.downcase == list_ref.downcase }
|
|
500
|
+
unless list || looks_like_id?(list_ref)
|
|
501
|
+
available = sprint_obj.lists&.map(&:title)&.join(", ") || "none"
|
|
502
|
+
raise Thor::Error, "List '#{list_ref}' not found in sprint. Available: #{available}"
|
|
503
|
+
end
|
|
444
504
|
result[:list_id] = list ? list.id : list_ref
|
|
445
505
|
result[:sprint_id] = existing.sprint_id
|
|
446
506
|
result[:project_id] = existing.project_id
|
|
507
|
+
elsif looks_like_id?(list_ref)
|
|
508
|
+
result[:list_id] = list_ref
|
|
447
509
|
else
|
|
448
|
-
|
|
510
|
+
board = client.boards.find(workspace_id, existing.board_id)
|
|
511
|
+
list = board.lists&.find { |l| l.title&.downcase == list_ref.downcase }
|
|
512
|
+
unless list
|
|
513
|
+
available = board.lists&.map(&:title)&.join(", ") || "none"
|
|
514
|
+
raise Thor::Error, "List '#{list_ref}' not found on board. Available: #{available}"
|
|
515
|
+
end
|
|
516
|
+
result[:list_id] = list.id
|
|
449
517
|
end
|
|
450
518
|
end
|
|
451
519
|
|
|
@@ -518,7 +586,7 @@ module Superthread
|
|
|
518
586
|
card.checklists.each do |checklist|
|
|
519
587
|
progress = "(#{checklist.completed_count}/#{checklist.total_count})"
|
|
520
588
|
Ui.kv(checklist.title, progress)
|
|
521
|
-
checklist.
|
|
589
|
+
checklist.sorted_items.each do |item|
|
|
522
590
|
marker = item.checked? ? "✓" : "○"
|
|
523
591
|
title = item.title.gsub(/<[^>]*>/, "").strip
|
|
524
592
|
puts " #{marker} #{title}"
|
|
@@ -57,7 +57,7 @@ module Superthread
|
|
|
57
57
|
if checklist.items&.any?
|
|
58
58
|
puts ""
|
|
59
59
|
Ui.section "Items"
|
|
60
|
-
checklist.
|
|
60
|
+
checklist.sorted_items.each do |item|
|
|
61
61
|
marker = item.checked? ? "✓" : "○"
|
|
62
62
|
puts " #{marker} #{item.title} (#{item.id})"
|
|
63
63
|
end
|
|
@@ -179,43 +179,45 @@ module Superthread
|
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
-
desc "check ITEM_ID", "Mark
|
|
182
|
+
desc "check ITEM_ID [ITEM_ID...]", "Mark checklist item(s) as checked"
|
|
183
183
|
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
184
184
|
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
185
|
-
# Mark
|
|
185
|
+
# Mark one or more checklist items as completed.
|
|
186
186
|
#
|
|
187
|
-
# @param
|
|
187
|
+
# @param item_ids [Array<String>] the unique identifiers of the items
|
|
188
188
|
# @return [void]
|
|
189
|
-
def check(
|
|
189
|
+
def check(*item_ids)
|
|
190
190
|
handle_error do
|
|
191
|
-
item
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
raise Thor::Error, "No item IDs provided" if item_ids.empty?
|
|
192
|
+
|
|
193
|
+
item_ids.each do |item_id|
|
|
194
|
+
client.cards.update_checklist_item(
|
|
195
|
+
workspace_id, options[:card], options[:checklist], item_id,
|
|
196
|
+
checked: true
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
output_success "Checked #{item_ids.size} item(s)"
|
|
199
200
|
end
|
|
200
201
|
end
|
|
201
202
|
|
|
202
|
-
desc "uncheck ITEM_ID", "Mark
|
|
203
|
+
desc "uncheck ITEM_ID [ITEM_ID...]", "Mark checklist item(s) as unchecked"
|
|
203
204
|
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
204
205
|
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
205
|
-
# Mark
|
|
206
|
+
# Mark one or more checklist items as not completed.
|
|
206
207
|
#
|
|
207
|
-
# @param
|
|
208
|
+
# @param item_ids [Array<String>] the unique identifiers of the items
|
|
208
209
|
# @return [void]
|
|
209
|
-
def uncheck(
|
|
210
|
+
def uncheck(*item_ids)
|
|
210
211
|
handle_error do
|
|
211
|
-
item
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
212
|
+
raise Thor::Error, "No item IDs provided" if item_ids.empty?
|
|
213
|
+
|
|
214
|
+
item_ids.each do |item_id|
|
|
215
|
+
client.cards.update_checklist_item(
|
|
216
|
+
workspace_id, options[:card], options[:checklist], item_id,
|
|
217
|
+
checked: false
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
output_success "Unchecked #{item_ids.size} item(s)"
|
|
219
221
|
end
|
|
220
222
|
end
|
|
221
223
|
end
|
|
@@ -7,6 +7,7 @@ module Superthread
|
|
|
7
7
|
desc "query SEARCH_TERM", "Search across workspace"
|
|
8
8
|
option :field, type: :string, enum: %w[title content], desc: "Field to search in"
|
|
9
9
|
option :types, type: :string, desc: "Entity types to search (comma-separated: board,card,page,project,epic,note)"
|
|
10
|
+
option :status, type: :string, desc: "Status filter (comma-separated, e.g., open,started)"
|
|
10
11
|
option :space, type: :string, aliases: "-s", desc: "Space to filter by (ID or name)"
|
|
11
12
|
option :include_archived, type: :boolean, desc: "Include archived items"
|
|
12
13
|
option :grouped, type: :boolean, desc: "Group results by type"
|
|
@@ -17,18 +18,32 @@ module Superthread
|
|
|
17
18
|
def query(search_term)
|
|
18
19
|
handle_error do
|
|
19
20
|
types = options[:types]&.split(",")&.map(&:strip)
|
|
21
|
+
statuses = options[:status]&.split(",")&.map(&:strip)
|
|
20
22
|
results = client.search.query(
|
|
21
23
|
workspace_id,
|
|
22
24
|
query: search_term,
|
|
23
25
|
field: options[:field],
|
|
24
26
|
types: types,
|
|
25
|
-
|
|
27
|
+
statuses: statuses,
|
|
28
|
+
space_id: (space_id if options[:space]),
|
|
26
29
|
archived: options[:include_archived],
|
|
27
|
-
grouped: options[:grouped]
|
|
30
|
+
grouped: options[:grouped],
|
|
31
|
+
limit: effective_limit
|
|
28
32
|
)
|
|
29
33
|
output_list results, columns: %i[result_type id title], headers: {id: "ID"}
|
|
30
34
|
end
|
|
31
35
|
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Override Base#effective_limit for search-specific defaults.
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer, nil] the limit (nil = unlimited when --limit 0)
|
|
42
|
+
def effective_limit
|
|
43
|
+
limit = options[:limit]
|
|
44
|
+
return nil if limit == 0
|
|
45
|
+
(limit.is_a?(Integer) && limit > 0) ? limit : 30
|
|
46
|
+
end
|
|
32
47
|
end
|
|
33
48
|
end
|
|
34
49
|
end
|
|
@@ -57,6 +57,13 @@ module Superthread
|
|
|
57
57
|
|
|
58
58
|
timestamps :time_created, :time_updated
|
|
59
59
|
|
|
60
|
+
# Items sorted by position.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<ChecklistItem>] items in position order
|
|
63
|
+
def sorted_items
|
|
64
|
+
(items || []).sort_by { |i| i.position || 0 }
|
|
65
|
+
end
|
|
66
|
+
|
|
60
67
|
# Count of completed items.
|
|
61
68
|
#
|
|
62
69
|
# @return [Integer] Number of checked items
|
|
@@ -41,6 +41,10 @@ module Superthread
|
|
|
41
41
|
# @return [Boolean] whether this item is checked/completed
|
|
42
42
|
attribute :checked, Shale::Type::Boolean
|
|
43
43
|
|
|
44
|
+
# @!attribute [rw] position
|
|
45
|
+
# @return [Integer] position within the checklist for ordering
|
|
46
|
+
attribute :position, Shale::Type::Integer
|
|
47
|
+
|
|
44
48
|
# @!attribute [rw] time_created
|
|
45
49
|
# @return [Integer] Unix timestamp when the item was created
|
|
46
50
|
attribute :time_created, Shale::Type::Integer
|
|
@@ -7,10 +7,17 @@ module Superthread
|
|
|
7
7
|
# Provides methods for searching across workspace entities
|
|
8
8
|
# (cards, pages, boards, etc.) via the Superthread API.
|
|
9
9
|
class Search < Base
|
|
10
|
+
# Safety cap on pagination requests to prevent runaway loops.
|
|
11
|
+
MAX_PAGES = 100
|
|
12
|
+
|
|
10
13
|
# Searches across workspace entities.
|
|
11
14
|
#
|
|
15
|
+
# Follows pagination cursors automatically. When limit is provided,
|
|
16
|
+
# stops after accumulating that many results.
|
|
17
|
+
#
|
|
12
18
|
# @param workspace_id [String] the workspace identifier
|
|
13
19
|
# @param query [String] the search query string
|
|
20
|
+
# @param limit [Integer, nil] max results to return (nil = no limit)
|
|
14
21
|
# @param params [Hash{Symbol => Object}] optional search parameters
|
|
15
22
|
# @option params [String] :field the field to search (title, content)
|
|
16
23
|
# @option params [Array<String>] :types entity types to include (board, card, page, etc.)
|
|
@@ -18,28 +25,39 @@ module Superthread
|
|
|
18
25
|
# @option params [String] :space_id the space identifier to filter by
|
|
19
26
|
# @option params [Boolean] :archived when true, includes archived entities
|
|
20
27
|
# @option params [Boolean] :grouped when true, groups results by type (default: false)
|
|
21
|
-
# @option params [String] :cursor the pagination cursor for next page
|
|
22
28
|
# @return [Superthread::Objects::Collection] the search results
|
|
23
|
-
def query(workspace_id, query:, **params)
|
|
29
|
+
def query(workspace_id, query:, limit: nil, **params)
|
|
24
30
|
ws = safe_id("workspace_id", workspace_id)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
grouped = params.fetch(:grouped, false)
|
|
32
|
+
all_results = []
|
|
33
|
+
cursor = nil
|
|
34
|
+
pages = 0
|
|
35
|
+
|
|
36
|
+
loop do
|
|
37
|
+
search_params = compact_params(
|
|
38
|
+
query: query,
|
|
39
|
+
project_id: params[:space_id],
|
|
40
|
+
grouped: grouped,
|
|
41
|
+
cursor: cursor,
|
|
42
|
+
**params.except(:space_id, :grouped)
|
|
43
|
+
)
|
|
44
|
+
response = http_get("/#{ws}/search", params: search_params)
|
|
45
|
+
|
|
46
|
+
results = (response[:results] || []).map do |item|
|
|
47
|
+
result_type, data = item.first
|
|
48
|
+
data.merge(result_type: result_type.to_s)
|
|
49
|
+
end
|
|
50
|
+
all_results.concat(results)
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
cursor = response[:cursor]
|
|
53
|
+
pages += 1
|
|
54
|
+
break if cursor.nil? || cursor.empty?
|
|
55
|
+
break if limit && all_results.size >= limit
|
|
56
|
+
break if pages >= MAX_PAGES
|
|
40
57
|
end
|
|
41
58
|
|
|
42
|
-
|
|
59
|
+
all_results = all_results.first(limit) if limit
|
|
60
|
+
Objects::Collection.from_response(all_results)
|
|
43
61
|
end
|
|
44
62
|
end
|
|
45
63
|
end
|
data/lib/superthread/version.rb
CHANGED