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,355 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+
6
+ module Superthread
7
+ module Cli
8
+ # Base class for all CLI commands.
9
+ # Provides common options, client access, and output formatting.
10
+ class Base < Thor
11
+ # Indicates Thor should exit with failure code on errors.
12
+ #
13
+ # @return [Boolean] always returns true
14
+ def self.exit_on_failure?
15
+ true
16
+ end
17
+
18
+ class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
19
+ class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
20
+ class_option :workspace, type: :string, aliases: "-w", desc: "Workspace (ID or name)"
21
+ class_option :json, type: :boolean, desc: "Output as JSON"
22
+ class_option :account, type: :string, aliases: "-a", desc: "Use specific account"
23
+ class_option :skip_confirm, type: :boolean, aliases: ["-y", "--yes"], desc: "Auto-confirm prompts"
24
+ class_option :limit, type: :numeric, desc: "Max items to show (default: 50)"
25
+
26
+ include Concerns::WorkspaceResolvable
27
+ include Concerns::SpaceResolvable
28
+ include Concerns::BoardResolvable
29
+ include Concerns::SprintResolvable
30
+ include Concerns::UserResolvable
31
+ include Concerns::TagResolvable
32
+ include Concerns::ListResolvable
33
+
34
+ private
35
+
36
+ # Get the Superthread API client, creating if needed.
37
+ #
38
+ # @return [Superthread::Client] the configured API client
39
+ def client
40
+ @client ||= if options[:account]
41
+ # Create client with specific account's API key
42
+ cfg = config_for_account(options[:account])
43
+ Superthread::Client.new(api_key: cfg.api_key)
44
+ else
45
+ Superthread::Client.new
46
+ end
47
+ end
48
+
49
+ # Get configuration for a specific account (for --account flag).
50
+ #
51
+ # @param account_name [String] the account name from config file
52
+ # @return [Superthread::Configuration] configured for the specified account
53
+ # @raise [Thor::Error] if the account is not found
54
+ def config_for_account(account_name)
55
+ cfg = Superthread::Configuration.new
56
+ account_data = cfg.accounts[account_name]
57
+
58
+ unless account_data
59
+ raise Thor::Error, "Account '#{account_name}' not found. " \
60
+ "Use 'suth account list' to see configured accounts."
61
+ end
62
+
63
+ # Create a new config with this account as current
64
+ cfg.current_account = account_name
65
+ cfg
66
+ end
67
+
68
+ # Check if a value looks like a resource ID (alphanumeric, reasonable length).
69
+ #
70
+ # @param value [String] the value to check
71
+ # @return [Boolean] true if it matches ID pattern
72
+ def looks_like_id?(value)
73
+ # IDs are short alphanumeric strings or full UUIDs (36 chars with hyphens)
74
+ value.match?(/\A[a-zA-Z0-9_-]+\z/) && value.length <= 36
75
+ end
76
+
77
+ # Validate that --space is provided when --sprint is used.
78
+ #
79
+ # @return [void]
80
+ # @raise [Thor::Error] if --sprint is set without --space
81
+ def require_space_for_sprint!
82
+ return unless options[:sprint] && !options[:space]
83
+
84
+ raise Thor::Error, "--space is required when using --sprint"
85
+ end
86
+
87
+ # Check if color output is enabled based on TTY and quiet mode.
88
+ #
89
+ # @return [Boolean] true if colors should be applied to output
90
+ def color_enabled?
91
+ $stdout.tty? && !options[:quiet]
92
+ end
93
+
94
+ # Check if JSON output is enabled via --json flag or config setting.
95
+ #
96
+ # @return [Boolean] true if output should be JSON formatted
97
+ def json_output?
98
+ options[:json] || app_config.format == "json"
99
+ end
100
+
101
+ # Get the application configuration object.
102
+ #
103
+ # Named app_config to avoid collision with Thor's subcommand delegation
104
+ # (Thor generates a `config` method for the `config` subcommand).
105
+ #
106
+ # @return [Superthread::Configuration] the configuration instance
107
+ def app_config
108
+ @app_config ||= Superthread::Configuration.new
109
+ end
110
+
111
+ # Output a single item as detail view or JSON.
112
+ #
113
+ # In JSON mode, outputs as JSON object. Otherwise, outputs as key-value pairs.
114
+ #
115
+ # @param item [Object] the item to output (model object or hash)
116
+ # @param fields [Array<Symbol>] the fields to display in table mode
117
+ # @param labels [Hash{Symbol => String}] custom labels for field names
118
+ # @return [void]
119
+ def output_item(item, fields: nil, labels: {})
120
+ if json_output?
121
+ puts Formatter.json(item)
122
+ else
123
+ fields ||= default_detail_fields(item)
124
+ puts Formatter.detail(item, fields: fields, labels: labels, color_enabled: color_enabled?)
125
+ end
126
+ end
127
+
128
+ # Output a collection as table or JSON array.
129
+ #
130
+ # Applies --limit truncation (default: 50). When truncated, shows a count
131
+ # footer in table mode or wraps with metadata in JSON mode.
132
+ #
133
+ # @param items [Array, Collection] the items to output
134
+ # @param columns [Array<Symbol>] the columns to display in table mode
135
+ # @param headers [Hash{Symbol => String}] custom labels for column headers
136
+ # @return [void]
137
+ def output_list(items, columns: nil, headers: {})
138
+ all_items = items.respond_to?(:items) ? items.items : Array(items)
139
+ limit = effective_limit
140
+ truncated = all_items.length > limit
141
+ visible = truncated ? all_items.first(limit) : all_items
142
+
143
+ if json_output?
144
+ if truncated
145
+ puts JSON.pretty_generate(
146
+ items: visible.map { |i| i.respond_to?(:to_h) ? i.to_h : i },
147
+ total: all_items.length,
148
+ truncated: true,
149
+ limit: limit
150
+ )
151
+ else
152
+ puts Formatter.json(visible)
153
+ end
154
+ else
155
+ columns ||= default_list_columns(visible)
156
+ result = Formatter.table(visible, columns: columns, headers: headers, color_enabled: color_enabled?)
157
+ if result.empty?
158
+ say "No items found.", :yellow unless options[:quiet]
159
+ else
160
+ puts result
161
+ if truncated
162
+ say "Showing #{limit} of #{all_items.length}. Use --limit to adjust.", :yellow
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ # Output raw data with automatic format detection.
169
+ #
170
+ # Legacy support for commands not yet updated. In JSON mode or when data
171
+ # is not a recognized object type, outputs as JSON.
172
+ #
173
+ # @param data [Object] the data to output (auto-detects format)
174
+ # @return [void]
175
+ def output(data)
176
+ if json_output?
177
+ puts Formatter.json(data)
178
+ elsif data.respond_to?(:items)
179
+ output_list(data)
180
+ elsif data.is_a?(Superthread::Object)
181
+ output_item(data)
182
+ else
183
+ puts Formatter.json(data)
184
+ end
185
+ end
186
+
187
+ # Output a success message in green or as JSON.
188
+ #
189
+ # @param message [String] the success message to display
190
+ # @return [void]
191
+ def output_success(message)
192
+ if json_output?
193
+ puts Formatter.json({success: true, message: message})
194
+ else
195
+ say message, :green unless options[:quiet]
196
+ end
197
+ end
198
+
199
+ # Get default detail fields based on the item's model type.
200
+ #
201
+ # @param item [Object] the item to determine fields for
202
+ # @return [Array<Symbol>] the default fields for this item type
203
+ def default_detail_fields(item)
204
+ fields = item.class.detail_fields if item.class.respond_to?(:detail_fields)
205
+ return fields if fields&.any?
206
+
207
+ item.respond_to?(:keys) ? item.keys.take(10) : []
208
+ end
209
+
210
+ # Get default list columns based on the first item's model type.
211
+ #
212
+ # @param items [Array, Collection] the items to determine columns for
213
+ # @return [Array<Symbol>] the default columns for this collection type
214
+ def default_list_columns(items)
215
+ first = items.respond_to?(:first) ? items.first : nil
216
+ return [] if first.nil?
217
+
218
+ columns = first.class.list_columns if first.class.respond_to?(:list_columns)
219
+ return columns if columns&.any?
220
+
221
+ first.respond_to?(:keys) ? first.keys.take(5) : []
222
+ end
223
+
224
+ # Extract specified options as a hash with symbol keys for API calls.
225
+ #
226
+ # @param keys [Array<Symbol>] the option keys to extract
227
+ # @return [Hash{Symbol => Object}] hash of non-nil option values
228
+ def symbolized_options(*keys)
229
+ keys.each_with_object({}) do |key, hash|
230
+ value = options[key.to_s]
231
+ hash[key] = value unless value.nil?
232
+ end
233
+ end
234
+
235
+ # Display an informational message in cyan unless quiet mode.
236
+ #
237
+ # @param message [String] the info message to display
238
+ # @return [void]
239
+ def say_info(message)
240
+ say message, :cyan unless options[:quiet]
241
+ end
242
+
243
+ # Display a warning message in yellow (always shown).
244
+ #
245
+ # @param message [String] the warning message to display
246
+ # @return [void]
247
+ def say_warning(message)
248
+ say message, :yellow
249
+ end
250
+
251
+ # Get the effective limit for list output.
252
+ #
253
+ # @return [Integer] the limit from --limit option or default of 50
254
+ def effective_limit
255
+ limit = options[:limit]
256
+ (limit.is_a?(Integer) && limit > 0) ? limit : 50
257
+ end
258
+
259
+ # Output an error as JSON or human-readable text.
260
+ #
261
+ # In JSON mode, outputs a structured error object to stdout.
262
+ # In text mode, uses Ui.error and optionally Ui.muted for fix hints.
263
+ #
264
+ # @param type [String] machine-readable error type (e.g., "not_found")
265
+ # @param message [String] human-readable error message
266
+ # @param fix [String, nil] optional hint for how to resolve the error
267
+ # @return [void]
268
+ def output_error(type:, message:, fix: nil)
269
+ if json_output?
270
+ error_hash = {ok: false, error: {type: type, message: message}}
271
+ error_hash[:fix] = fix if fix
272
+ puts JSON.pretty_generate(error_hash)
273
+ else
274
+ Ui.error(message)
275
+ Ui.muted(fix) if fix
276
+ end
277
+ end
278
+
279
+ # Execute a block, converting not-found errors to a user-friendly Thor error.
280
+ #
281
+ # Wraps an API call so that ForbiddenError and NotFoundError are caught
282
+ # and re-raised as Thor::Error with a resource-specific message.
283
+ #
284
+ # @param message [String] the error message to display if the resource is not found
285
+ # @yield the block containing the API call
286
+ # @return [Object] the return value of the block
287
+ # @raise [Thor::Error] if the API call raises NotFoundError or ForbiddenError
288
+ def with_not_found(message)
289
+ yield
290
+ rescue Superthread::ForbiddenError, Superthread::NotFoundError
291
+ raise Thor::Error, message
292
+ end
293
+
294
+ # Wrap a block with consistent error handling for API operations.
295
+ #
296
+ # Catches API and configuration errors and displays user-friendly messages
297
+ # (or structured JSON in --json mode) before exiting with appropriate
298
+ # status codes.
299
+ #
300
+ # @yield the block to execute with error handling
301
+ # @return [Object] the return value of the block
302
+ def handle_error
303
+ yield
304
+ rescue Superthread::NotFoundError => e
305
+ output_error(type: "not_found", message: "Not found: #{e.message}")
306
+ exit 1
307
+ rescue Superthread::AuthenticationError => e
308
+ output_error(
309
+ type: "authentication_error",
310
+ message: "Authentication failed: #{e.message}",
311
+ fix: "Check your API key with: suth config show"
312
+ )
313
+ exit 1
314
+ rescue Superthread::ForbiddenError => e
315
+ output_error(type: "forbidden", message: "Access denied: #{e.message}")
316
+ exit 1
317
+ rescue Superthread::RateLimitError => e
318
+ output_error(
319
+ type: "rate_limited",
320
+ message: "Rate limited: #{e.message}",
321
+ fix: "Try again in #{e.retry_after || 60} seconds"
322
+ )
323
+ exit 1
324
+ rescue Superthread::ValidationError => e
325
+ output_error(type: "validation_error", message: "Validation error: #{e.message}")
326
+ exit 1
327
+ rescue Superthread::ApiError => e
328
+ output_error(type: "api_error", message: "API error: #{e.message}")
329
+ exit 1
330
+ rescue Superthread::ConfigurationError => e
331
+ output_error(type: "configuration_error", message: "Configuration error: #{e.message}")
332
+ exit 1
333
+ end
334
+
335
+ # Wrap a block with an optional confirmation prompt.
336
+ #
337
+ # Skips confirmation if --skip-confirm/-y flag is set, otherwise prompts
338
+ # the user and only executes the block if they confirm.
339
+ #
340
+ # @param question [String] the confirmation question to display
341
+ # @yield the block to execute if confirmed
342
+ # @return [Object, nil] the block's return value, or nil if aborted
343
+ def confirming(question)
344
+ return yield if options[:skip_confirm]
345
+
346
+ if Ui.confirm(question, default: false)
347
+ yield
348
+ else
349
+ Ui.muted("Aborted")
350
+ nil
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Cli
5
+ # CLI commands for managing Superthread boards.
6
+ #
7
+ # Provides subcommands for listing, creating, updating, and deleting boards,
8
+ # as well as managing board lists (columns).
9
+ class Boards < Base
10
+ desc "list", "List all boards in a space"
11
+ option :space, type: :string, required: true, aliases: "-s", desc: "Space to list boards from (ID or name)"
12
+ option :bookmarked, type: :boolean, desc: "Filter by bookmarked boards"
13
+ option :include_archived, type: :boolean, desc: "Include archived boards"
14
+ # List all boards within a specified space.
15
+ #
16
+ # @return [void]
17
+ def list
18
+ handle_error do
19
+ opts = symbolized_options(:bookmarked)
20
+ opts[:archived] = options[:include_archived] if options[:include_archived]
21
+ boards = client.boards.list(workspace_id, space_id: space_id, **opts)
22
+ output_list boards, columns: %i[id title], headers: {id: "BOARD_ID"}
23
+ end
24
+ end
25
+
26
+ desc "get BOARD", "Get board details"
27
+ option :space, type: :string, aliases: "-s", desc: "Space (helps resolve board name)"
28
+ # Display detailed information about a specific board.
29
+ #
30
+ # @param board_ref [String] the board ID or name to retrieve
31
+ # @return [void]
32
+ def get(board_ref)
33
+ handle_error do
34
+ resolved_board_id = resolve_board(board_ref)
35
+ board = with_not_found("Board not found: '#{board_ref}'. Use 'suth boards list -s SPACE' to see available boards.") do
36
+ client.boards.find(workspace_id, resolved_board_id)
37
+ end
38
+ output_item board, fields: %i[id title time_created time_updated], labels: {id: "Board ID"}
39
+ end
40
+ end
41
+
42
+ # Available layout options for boards.
43
+ LAYOUTS = %w[board list timeline calendar].freeze
44
+
45
+ # Available color options for boards and lists.
46
+ COLORS = %w[fog slate grey charcoal black red orange yellow green ocean blue purple pink].freeze
47
+
48
+ desc "create", "Create a new board"
49
+ option :space, type: :string, required: true, aliases: "-s", desc: "Space to create board in (ID or name)"
50
+ option :title, type: :string, required: true, desc: "Board title"
51
+ option :description, type: :string, desc: "Board description"
52
+ option :layout, type: :string, enum: LAYOUTS, desc: "Layout: #{LAYOUTS.join(", ")}"
53
+ option :icon, type: :string, desc: "Icon name (e.g., shield, rocket)"
54
+ option :color, type: :string, desc: "Color: #{COLORS.join(", ")}"
55
+ # Create a new board in a space.
56
+ #
57
+ # @return [void]
58
+ def create
59
+ handle_error do
60
+ opts = symbolized_options(:title, :icon, :color, :layout)
61
+ opts[:content] = options[:description] if options[:description]
62
+ board = client.boards.create(workspace_id, space_id: space_id, **opts)
63
+ output_item board, fields: %i[id title time_created], labels: {id: "Board ID"}
64
+ end
65
+ end
66
+
67
+ desc "update BOARD", "Update a board"
68
+ option :space, type: :string, aliases: "-s", desc: "Space (helps resolve board name)"
69
+ option :title, type: :string, desc: "New title"
70
+ option :description, type: :string, desc: "New description"
71
+ option :layout, type: :string, enum: LAYOUTS, desc: "Layout: #{LAYOUTS.join(", ")}"
72
+ option :icon, type: :string, desc: "Icon name (e.g., shield, rocket)"
73
+ option :color, type: :string, desc: "Color: #{COLORS.join(", ")}"
74
+ option :archived, type: :boolean, desc: "Archive/unarchive"
75
+ # Update an existing board's properties.
76
+ #
77
+ # @param board_ref [String] the board ID or name to update
78
+ # @return [void]
79
+ def update(board_ref)
80
+ handle_error do
81
+ resolved_board_id = resolve_board(board_ref)
82
+ opts = symbolized_options(:title, :icon, :color, :layout, :archived)
83
+ opts[:content] = options[:description] if options[:description]
84
+ board = with_not_found("Board not found: '#{board_ref}'. Use 'suth boards list -s SPACE' to see available boards.") do
85
+ client.boards.update(workspace_id, resolved_board_id, **opts)
86
+ end
87
+ output_item board, fields: %i[id title time_created time_updated], labels: {id: "Board ID"}
88
+ end
89
+ end
90
+
91
+ desc "duplicate BOARD", "Duplicate a board"
92
+ option :space, type: :string, required: true, aliases: "-s", desc: "Destination space (ID or name)"
93
+ option :title, type: :string, desc: "Title for the duplicated board"
94
+ option :copy_cards, type: :boolean, default: false, desc: "Copy cards from source board"
95
+ option :create_missing_tags, type: :boolean, default: false, desc: "Create missing tags in target space"
96
+ # Create a copy of an existing board in a specified space.
97
+ #
98
+ # @param board_ref [String] the board ID or name to duplicate
99
+ # @return [void]
100
+ def duplicate(board_ref)
101
+ handle_error do
102
+ resolved_board_id = resolve_board(board_ref)
103
+ opts = symbolized_options(:title, :copy_cards, :create_missing_tags)
104
+ board = with_not_found("Board not found: '#{board_ref}'. Use 'suth boards list -s SPACE' to see available boards.") do
105
+ client.boards.duplicate(workspace_id, resolved_board_id, space_id: space_id, **opts)
106
+ end
107
+ output_item board, fields: %i[id title time_created], labels: {id: "Board ID"}
108
+ end
109
+ end
110
+
111
+ desc "delete BOARD", "Delete a board"
112
+ option :space, type: :string, aliases: "-s", desc: "Space (helps resolve board name)"
113
+ # Permanently delete a board after confirmation.
114
+ #
115
+ # @param board_ref [String] the board ID or name to delete
116
+ # @return [void]
117
+ def delete(board_ref)
118
+ handle_error do
119
+ resolved_board_id = resolve_board(board_ref)
120
+ board = with_not_found("Board not found: '#{board_ref}'. Use 'suth boards list -s SPACE' to see available boards.") do
121
+ client.boards.find(workspace_id, resolved_board_id)
122
+ end
123
+ confirming("Delete board '#{board.title}' (#{board.id})?") do
124
+ client.boards.destroy(workspace_id, board.id)
125
+ output_success "Board '#{board.title}' deleted"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end