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,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
|