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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ff6a32a6f48d0010c39659ee7fca19ff5d136ad8ffdb236a9e44e32108b65c2
4
- data.tar.gz: d22ea63399a42dbbb6f79e02ccbb5983ac732c981bfa38af16756a775c037185
3
+ metadata.gz: eb38a8ab1c8ba081e66e8bda3bc4fabcb8aa9732e1e8cfc5ce1bdff0b14f94b2
4
+ data.tar.gz: e97e7bffe8dc9bfbd86b424a6762455d1d9979b07a60651b3997ad5feaedddf5
5
5
  SHA512:
6
- metadata.gz: da133f0ba26e4f5b4b16cac769408133f4cf143965948efb5d933d3cecf32ea478891fc9a1fa93dc590b80acc1be46d30aa3611e6d951c9c636abb5a566a59e2
7
- data.tar.gz: c3dfc65b6e9927a9112a33194258c73b239a3238e3446dcba9b929a5b15281d06398d26c86b27dc714a9a9378ff0455280295d33154a4f119628a6a04dfa1208
6
+ metadata.gz: b2c5dfabc604f08c7aae0eedb7a94fb8f702912c5ccc2730c9d388d6f404dbdff8aa9f3fa835288732612f84d7755e4a2a5283f1d266747b3e9727aa09fdbe5f
7
+ data.tar.gz: 847b4c2868387c94b08bbbffab9db1d80dac9b4991884941258eb801347996f11257cdc1079d192848ae5208936352da2b894f95a1288914403279395b50cf61
@@ -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
- result[:list_id] = resolve_list(list_ref)
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.items&.each do |item|
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.items.each do |item|
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 a checklist item as checked"
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 a checklist item as completed.
185
+ # Mark one or more checklist items as completed.
186
186
  #
187
- # @param item_id [String] the unique identifier of the item
187
+ # @param item_ids [Array<String>] the unique identifiers of the items
188
188
  # @return [void]
189
- def check(item_id)
189
+ def check(*item_ids)
190
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
- }
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 a checklist item as unchecked"
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 a checklist item as not completed.
206
+ # Mark one or more checklist items as not completed.
206
207
  #
207
- # @param item_id [String] the unique identifier of the item
208
+ # @param item_ids [Array<String>] the unique identifiers of the items
208
209
  # @return [void]
209
- def uncheck(item_id)
210
+ def uncheck(*item_ids)
210
211
  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
- }
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
- space_id: space_id,
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
@@ -34,7 +34,7 @@ module Superthread
34
34
  @connection.send(method) do |req|
35
35
  req.url(relative_path)
36
36
  req.params = params if params
37
- req.body = body.to_json if body
37
+ req.body = body.to_json.encode(Encoding::UTF_8) if body
38
38
  end
39
39
  end
40
40
 
@@ -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
- # Default grouped to false so results come in a flat array
26
- # Use || instead of fetch because CLI may pass grouped: nil explicitly
27
- grouped = params[:grouped].nil? ? false : params[:grouped]
28
- search_params = compact_params(
29
- query: query,
30
- project_id: params[:space_id],
31
- grouped: grouped,
32
- **params.except(:space_id, :grouped)
33
- )
34
- response = http_get("/#{ws}/search", params: search_params)
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
- # Unwrap results - each item is {"card": {...}} or {"board": {...}}, etc.
37
- results = (response[:results] || []).map do |item|
38
- result_type, data = item.first
39
- data.merge(result_type: result_type.to_s)
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
- Objects::Collection.from_response(results)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Superthread
4
4
  # Current version of the Superthread gem.
5
- VERSION = "0.7.4"
5
+ VERSION = "0.8.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superthread
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Clarke