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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +492 -0
  5. data/exe/suth +19 -0
  6. data/lib/superthread/cli/accounts.rb +240 -0
  7. data/lib/superthread/cli/activity.rb +210 -0
  8. data/lib/superthread/cli/base.rb +355 -0
  9. data/lib/superthread/cli/boards.rb +131 -0
  10. data/lib/superthread/cli/cards.rb +530 -0
  11. data/lib/superthread/cli/checklists.rb +223 -0
  12. data/lib/superthread/cli/comments.rb +86 -0
  13. data/lib/superthread/cli/completion.rb +306 -0
  14. data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
  15. data/lib/superthread/cli/concerns/confirmable.rb +55 -0
  16. data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
  17. data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
  18. data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
  19. data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
  20. data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
  21. data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
  22. data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
  23. data/lib/superthread/cli/config.rb +129 -0
  24. data/lib/superthread/cli/formatter.rb +388 -0
  25. data/lib/superthread/cli/lists.rb +85 -0
  26. data/lib/superthread/cli/main.rb +121 -0
  27. data/lib/superthread/cli/members.rb +19 -0
  28. data/lib/superthread/cli/notes.rb +64 -0
  29. data/lib/superthread/cli/pages.rb +128 -0
  30. data/lib/superthread/cli/projects.rb +124 -0
  31. data/lib/superthread/cli/replies.rb +94 -0
  32. data/lib/superthread/cli/search.rb +34 -0
  33. data/lib/superthread/cli/setup.rb +253 -0
  34. data/lib/superthread/cli/spaces.rb +141 -0
  35. data/lib/superthread/cli/sprints.rb +32 -0
  36. data/lib/superthread/cli/tags.rb +86 -0
  37. data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
  38. data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
  39. data/lib/superthread/cli/ui.rb +263 -0
  40. data/lib/superthread/cli/workspaces.rb +105 -0
  41. data/lib/superthread/cli.rb +12 -0
  42. data/lib/superthread/client.rb +207 -0
  43. data/lib/superthread/configuration.rb +354 -0
  44. data/lib/superthread/connection.rb +57 -0
  45. data/lib/superthread/error.rb +164 -0
  46. data/lib/superthread/mention_formatter.rb +96 -0
  47. data/lib/superthread/model.rb +178 -0
  48. data/lib/superthread/models/board.rb +59 -0
  49. data/lib/superthread/models/card.rb +321 -0
  50. data/lib/superthread/models/checklist.rb +91 -0
  51. data/lib/superthread/models/checklist_item.rb +69 -0
  52. data/lib/superthread/models/comment.rb +71 -0
  53. data/lib/superthread/models/concerns/archivable.rb +32 -0
  54. data/lib/superthread/models/concerns/presentable.rb +113 -0
  55. data/lib/superthread/models/concerns/timestampable.rb +91 -0
  56. data/lib/superthread/models/list.rb +67 -0
  57. data/lib/superthread/models/member.rb +40 -0
  58. data/lib/superthread/models/note.rb +56 -0
  59. data/lib/superthread/models/page.rb +70 -0
  60. data/lib/superthread/models/project.rb +83 -0
  61. data/lib/superthread/models/space.rb +71 -0
  62. data/lib/superthread/models/sprint.rb +53 -0
  63. data/lib/superthread/models/tag.rb +52 -0
  64. data/lib/superthread/models/team.rb +68 -0
  65. data/lib/superthread/models/user.rb +76 -0
  66. data/lib/superthread/models.rb +12 -0
  67. data/lib/superthread/object.rb +285 -0
  68. data/lib/superthread/objects/collection.rb +179 -0
  69. data/lib/superthread/resources/base.rb +204 -0
  70. data/lib/superthread/resources/boards.rb +150 -0
  71. data/lib/superthread/resources/cards.rb +363 -0
  72. data/lib/superthread/resources/comments.rb +163 -0
  73. data/lib/superthread/resources/notes.rb +61 -0
  74. data/lib/superthread/resources/pages.rb +110 -0
  75. data/lib/superthread/resources/projects.rb +117 -0
  76. data/lib/superthread/resources/search.rb +46 -0
  77. data/lib/superthread/resources/spaces.rb +104 -0
  78. data/lib/superthread/resources/sprints.rb +37 -0
  79. data/lib/superthread/resources/tags.rb +52 -0
  80. data/lib/superthread/resources/users.rb +29 -0
  81. data/lib/superthread/version.rb +6 -0
  82. data/lib/superthread/version_checker.rb +174 -0
  83. data/lib/superthread.rb +30 -0
  84. 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