aspera-cli 4.26.1 → 4.27.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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +300 -22
  4. data/CONTRIBUTING.md +100 -12
  5. data/bin/ascli +5 -5
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +27 -25
  12. data/lib/aspera/agent/factory.rb +4 -0
  13. data/lib/aspera/agent/httpgw.rb +2 -2
  14. data/lib/aspera/agent/node.rb +34 -2
  15. data/lib/aspera/agent/transferd.rb +39 -3
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +56 -51
  18. data/lib/aspera/api/cos_node.rb +2 -2
  19. data/lib/aspera/api/faspex.rb +22 -20
  20. data/lib/aspera/api/httpgw.rb +4 -4
  21. data/lib/aspera/api/node.rb +33 -30
  22. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  23. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  24. data/lib/aspera/ascmd.rb +25 -23
  25. data/lib/aspera/ascp/installation.rb +43 -40
  26. data/lib/aspera/ascp/management.rb +2 -3
  27. data/lib/aspera/cli/ascp_actions.rb +155 -0
  28. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  29. data/lib/aspera/cli/bootstrapper.rb +197 -0
  30. data/lib/aspera/cli/command_registry.rb +145 -0
  31. data/lib/aspera/cli/command_spec.rb +138 -0
  32. data/lib/aspera/cli/context.rb +71 -0
  33. data/lib/aspera/cli/error.rb +16 -0
  34. data/lib/aspera/cli/extended_value.rb +5 -3
  35. data/lib/aspera/cli/formatter.rb +122 -209
  36. data/lib/aspera/cli/gem_checker.rb +65 -0
  37. data/lib/aspera/cli/hints.rb +2 -2
  38. data/lib/aspera/cli/http.rb +218 -0
  39. data/lib/aspera/cli/info.rb +2 -0
  40. data/lib/aspera/cli/mailer.rb +97 -0
  41. data/lib/aspera/cli/mcp_tool.rb +198 -0
  42. data/lib/aspera/cli/options.schema.yaml +364 -7
  43. data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
  44. data/lib/aspera/cli/plugins/alee.rb +20 -22
  45. data/lib/aspera/cli/plugins/aoc.rb +1173 -849
  46. data/lib/aspera/cli/plugins/ats.rb +200 -161
  47. data/lib/aspera/cli/plugins/base.rb +470 -107
  48. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  49. data/lib/aspera/cli/plugins/config.rb +434 -1032
  50. data/lib/aspera/cli/plugins/console.rb +106 -64
  51. data/lib/aspera/cli/plugins/cos.rb +44 -32
  52. data/lib/aspera/cli/plugins/factory.rb +7 -4
  53. data/lib/aspera/cli/plugins/faspex.rb +296 -259
  54. data/lib/aspera/cli/plugins/faspex5.rb +592 -344
  55. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  56. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  57. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  58. data/lib/aspera/cli/plugins/node.rb +1001 -797
  59. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  60. data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
  61. data/lib/aspera/cli/plugins/preview.rb +183 -142
  62. data/lib/aspera/cli/plugins/server.rb +125 -80
  63. data/lib/aspera/cli/plugins/shares.rb +301 -107
  64. data/lib/aspera/cli/preset_actions.rb +139 -0
  65. data/lib/aspera/cli/preset_manager.rb +236 -0
  66. data/lib/aspera/cli/result.rb +360 -0
  67. data/lib/aspera/cli/runner.rb +473 -0
  68. data/lib/aspera/cli/secret_finder.rb +40 -0
  69. data/lib/aspera/cli/special_values.rb +1 -0
  70. data/lib/aspera/cli/sync_actions.rb +84 -56
  71. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  72. data/lib/aspera/cli/transfer_actions.rb +83 -0
  73. data/lib/aspera/cli/transfer_agent.rb +116 -51
  74. data/lib/aspera/cli/transfer_progress.rb +9 -9
  75. data/lib/aspera/cli/vault_manager.rb +57 -0
  76. data/lib/aspera/cli/version.rb +1 -1
  77. data/lib/aspera/cli/wizard.rb +26 -26
  78. data/lib/aspera/command_line_builder.rb +23 -23
  79. data/lib/aspera/data_repository.rb +6 -1
  80. data/lib/aspera/dot_container.rb +5 -5
  81. data/lib/aspera/environment.rb +39 -13
  82. data/lib/aspera/exec_spec.rb +13 -0
  83. data/lib/aspera/faspex_gw.rb +2 -2
  84. data/lib/aspera/faspex_postproc.rb +4 -3
  85. data/lib/aspera/graphql.rb +35 -0
  86. data/lib/aspera/hash_ext.rb +6 -0
  87. data/lib/aspera/json_rpc/client.rb +62 -0
  88. data/lib/aspera/json_rpc/version.rb +7 -0
  89. data/lib/aspera/keychain/base.rb +1 -1
  90. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  91. data/lib/aspera/keychain/factory.rb +4 -4
  92. data/lib/aspera/keychain/macos_security.rb +4 -4
  93. data/lib/aspera/link_header.rb +82 -0
  94. data/lib/aspera/log.rb +23 -5
  95. data/lib/aspera/markdown.rb +85 -2
  96. data/lib/aspera/nagios.rb +2 -2
  97. data/lib/aspera/node_simulator.rb +3 -1
  98. data/lib/aspera/oauth/base.rb +10 -10
  99. data/lib/aspera/oauth/boot.rb +43 -0
  100. data/lib/aspera/oauth/factory.rb +41 -15
  101. data/lib/aspera/oauth/jwt.rb +4 -4
  102. data/lib/aspera/oauth/web.rb +4 -4
  103. data/lib/aspera/oauth.rb +1 -0
  104. data/lib/aspera/persistency_action_once.rb +10 -10
  105. data/lib/aspera/persistency_folder.rb +10 -3
  106. data/lib/aspera/preview/file_types.rb +7 -3
  107. data/lib/aspera/preview/generator.rb +3 -3
  108. data/lib/aspera/preview/utils.rb +1 -1
  109. data/lib/aspera/products/connect.rb +4 -1
  110. data/lib/aspera/products/other.rb +1 -1
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -9
  113. data/lib/aspera/rest.rb +18 -40
  114. data/lib/aspera/rest_error_analyzer.rb +7 -3
  115. data/lib/aspera/rest_errors_aspera.rb +0 -10
  116. data/lib/aspera/rest_list.rb +13 -6
  117. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
  119. data/lib/aspera/schema/async_tables.yaml +361 -0
  120. data/lib/aspera/schema/documentation.rb +57 -26
  121. data/lib/aspera/schema/reader.rb +60 -11
  122. data/lib/aspera/schema/registry.rb +50 -10
  123. data/lib/aspera/secret_hider.rb +11 -2
  124. data/lib/aspera/ssl.rb +4 -4
  125. data/lib/aspera/sync/conf.schema.yaml +2 -2
  126. data/lib/aspera/sync/database.rb +14 -10
  127. data/lib/aspera/sync/operations.rb +15 -18
  128. data/lib/aspera/temp_file_manager.rb +5 -1
  129. data/lib/aspera/timer_limiter.rb +1 -1
  130. data/lib/aspera/transfer/faux_file.rb +24 -11
  131. data/lib/aspera/transfer/parameters.rb +27 -25
  132. data/lib/aspera/transfer/result.rb +74 -0
  133. data/lib/aspera/transfer/resumer.rb +7 -7
  134. data/lib/aspera/transfer/spec.rb +18 -1
  135. data/lib/aspera/transfer/spec.schema.yaml +26 -8
  136. data/lib/aspera/transfer/uri.rb +1 -1
  137. data/lib/aspera/uri_reader.rb +2 -2
  138. data/lib/aspera/web_auth.rb +2 -2
  139. data/lib/aspera/web_server_simple.rb +8 -5
  140. data/lib/aspera/yaml.rb +2 -1
  141. data.tar.gz.sig +0 -0
  142. metadata +45 -16
  143. metadata.gz.sig +0 -0
  144. data/lib/aspera/cli/main.rb +0 -480
  145. data/lib/aspera/json_rpc.rb +0 -51
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore jsonpp
4
+ require 'aspera/schema/registry'
4
5
  require 'aspera/cli/special_values'
6
+ require 'aspera/cli/terminal_formatter'
5
7
  require 'aspera/preview/terminal'
6
8
  require 'aspera/secret_hider'
7
9
  require 'aspera/environment'
@@ -9,7 +11,6 @@ require 'aspera/log'
9
11
  require 'aspera/assert'
10
12
  require 'aspera/markdown'
11
13
  require 'aspera/dot_container'
12
- require 'aspera/formatter_interface'
13
14
  require 'terminal-table'
14
15
  require 'tty-spinner'
15
16
  require 'yaml'
@@ -19,75 +20,8 @@ require 'word_wrap'
19
20
 
20
21
  module Aspera
21
22
  module Cli
22
- # Terminal formatter with ANSI colors and Unicode support
23
- # @see FormatterInterface
24
- # @see MarkdownFormatter (in build/lib/doc_helper.rb)
25
- module TerminalFormatter
26
- include FormatterInterface
27
-
28
- # Format boolean with colored symbol (✓/✗ or Y/ )
29
- def tick(yes)
30
- result =
31
- if Environment.terminal_supports_unicode?
32
- yes ? "\u2713" : "\u2717"
33
- else
34
- yes ? 'Y' : ' '
35
- end
36
- return result.green if yes
37
- return result.red
38
- end
39
-
40
- # Format special values with colors (dim for empty, reverse for others)
41
- def special_format(what)
42
- result = "<#{what}>"
43
- return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
44
- end
45
-
46
- # Prepare table row for terminal display (word wrap arrays)
47
- def check_row(row)
48
- row.each_key do |k|
49
- row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
50
- end
51
- end
52
-
53
- # Convert Markdown to terminal format (**bold** -> blue, `code` -> bold)
54
- # @param match [MatchData, String]
55
- def markdown_text(match)
56
- if match.is_a?(String)
57
- match = Markdown::FORMATS.match(match)
58
- Aspera.assert(match)
59
- end
60
- Aspera.assert_type(match, MatchData)
61
- if match[:entity]
62
- Aspera.assert_values(match[:entity], %w[bsol])
63
- '\\'
64
- elsif match[:bold]
65
- match[:bold].to_s.blue
66
- elsif match[:code]
67
- match[:code].to_s.bold
68
- else
69
- Aspera.error_unexpected_value(match.to_s)
70
- end
71
- end
72
-
73
- module_function :tick, :special_format, :check_row, :markdown_text
74
- end
75
-
76
23
  # Take care of CLI output on terminal
77
24
  class Formatter
78
- # remove a fields from the list
79
- FIELDS_LESS = '-'
80
- # supported output formats
81
- DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
82
- # user output levels
83
- DISPLAY_LEVELS = %i[info data error].freeze
84
- # column names for single object display in table
85
- SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
86
- # Terminal: vertical separator for list of strings.
87
- STR_LST_SEP_VERT = "\n"
88
-
89
- private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
90
-
91
25
  class << self
92
26
  # Replace special values with a readable version on terminal
93
27
  def replace_specific_for_terminal(input_hash, string_list_separator)
@@ -134,6 +68,17 @@ module Aspera
134
68
  @spinner = nil
135
69
  end
136
70
 
71
+ # Display item count if total is provided
72
+ def display_item_count(count, total)
73
+ return if total.nil?
74
+ count = count.to_i
75
+ total = total.to_i
76
+ return if total.eql?(0) && count.eql?(0)
77
+ count_msg = "Items: #{count}/#{total}"
78
+ count_msg = count_msg.bg_red unless count.eql?(total)
79
+ display_status(count_msg)
80
+ end
81
+
137
82
  def long_operation(title = nil, action: :spin)
138
83
  return unless Environment.terminal?
139
84
  return if %i[error data].include?(@options[:display])
@@ -164,28 +109,41 @@ module Aspera
164
109
  end
165
110
  end
166
111
 
167
- def declare_options(options)
168
- options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
169
- options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
170
- options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
171
- options.declare(
172
- :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
173
- allowed: [String, Array, Regexp, Proc],
174
- default: SpecialValues::DEF
175
- )
176
- options.declare(:select, 'Select only some items in lists: column, value', allowed: [Hash, Proc], handler: {o: self, m: :option_handler})
177
- options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: {})
178
- options.declare(:flat_hash, '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: true)
179
- options.declare(
180
- :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single],
181
- handler: {o: self, m: :option_handler}, default: :no
182
- )
183
- options.declare(:show_secrets, 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: false)
184
- options.declare(:image, 'Options for image display', allowed: Hash, handler: {o: self, m: :option_handler}, default: {})
112
+ class << self
113
+ # Declare all formatter CLI options (metadata only - no handler binding yet).
114
+ # @param options [Aspera::Cli::Parser]
115
+ # @return [nil]
116
+ def declare_options(options)
117
+ options.declare(:display, description: 'Output only some information', allowed: DISPLAY_LEVELS, default: :data)
118
+ options.declare(:format, description: 'Output format', allowed: DISPLAY_FORMATS, default: :table)
119
+ options.declare(:output, description: 'Destination for results')
120
+ options.declare(:fields, description: "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", allowed: [String, Array, Regexp, Proc], default: SpecialValues::DEF)
121
+ options.declare(:select, description: 'Select only some items in lists: column, value', allowed: [Hash, Proc])
122
+ options.declare(:table_style, description: '(Table) Display style', allowed: [Hash])
123
+ options.declare(:flat_hash, description: '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, default: true)
124
+ options.declare(:multi_single, description: '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single], default: :no)
125
+ options.declare(:show_secrets, description: 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, default: false)
126
+ options.declare(:image, schema: Schema::Registry::IMAGE_OPTIONS)
127
+ nil
128
+ end
129
+ end
130
+
131
+ # Bind all formatter options to this instance using set_handler.
132
+ # Called from Runner after Formatter.new.
133
+ # @param options [Aspera::Cli::Parser]
134
+ # @return [nil]
135
+ def bind_options(options)
136
+ %i[display format output fields select table_style flat_hash multi_single show_secrets image].each do |opt|
137
+ options.set_handler(opt, object: self, method: :option_handler)
138
+ end
139
+ nil
185
140
  end
186
141
 
187
- # method accessed by option manager
188
- # options are: format, output, display, fields, select, table_style, flat_hash, multi_single
142
+ # Getter/setter handler called by the option manager for all formatter options
143
+ # @param option_symbol [Symbol] Option name (one of :format, :output, :display, :fields, :select, :table_style, :flat_hash, :multi_single, :show_secrets, :image)
144
+ # @param operation [Symbol] :get or :set
145
+ # @param value [Object, nil] Value to set (only used when operation is :set)
146
+ # @return [Object, nil] Current option value when operation is :get; nil otherwise
189
147
  def option_handler(option_symbol, operation, value = nil)
190
148
  Aspera.assert_values(operation, %i[set get])
191
149
  case operation
@@ -206,7 +164,7 @@ module Aspera
206
164
  allowed_options = Preview::Terminal.method(:build).parameters.select{ |i| i[0].eql?(:key)}.map{ |i| i[1]}
207
165
  # check that only supported options are given
208
166
  unknown_options = value.keys.map(&:to_sym) - allowed_options
209
- raise "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" unless unknown_options.empty?
167
+ Aspera.assert(unknown_options.empty?){"Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}"}
210
168
  end
211
169
  when :get then return @options[option_symbol]
212
170
  else Aspera.error_unreachable_line
@@ -214,10 +172,16 @@ module Aspera
214
172
  nil
215
173
  end
216
174
 
217
- # Main output method
218
- # data: for requested data, not displayed if level==error
219
- # info: additional info, displayed if level==info
220
- # error: always displayed on stderr
175
+ # Main output method for displaying messages to the user
176
+ # This method is used by Result classes to output formatted data
177
+ # @param message_level [Symbol] The level of the message - must be one of: :data, :info, :error
178
+ # @param message [String] The message to display
179
+ # @param hide_secrets [Boolean] Whether to hide secrets in the message (default: true)
180
+ # @return [nil]
181
+ # @note Message display behavior depends on the message_level:
182
+ # - +:data+ messages are displayed unless display level is +:error+
183
+ # - +:info+ messages are only displayed when display level is +:info+
184
+ # - +:error+ messages are always displayed on stderr
221
185
  def display_message(message_level, message, hide_secrets: true)
222
186
  message = SecretHider.instance.hide_secrets_in_string(message) if hide_secrets && message.is_a?(String) && hide_secrets?
223
187
  case message_level
@@ -232,15 +196,7 @@ module Aspera
232
196
  display_message(:info, status, **kwargs)
233
197
  end
234
198
 
235
- def display_item_count(number, total)
236
- number = number.to_i
237
- total = total.to_i
238
- return if total.eql?(0) && number.eql?(0)
239
- count_msg = "Items: #{number}/#{total}"
240
- count_msg = count_msg.bg_red unless number.eql?(total)
241
- display_status(count_msg)
242
- end
243
-
199
+ # Check if secrets should be hidden
244
200
  def hide_secrets?
245
201
  !@options[:show_secrets] && !@options[:display].eql?(:data)
246
202
  end
@@ -250,118 +206,62 @@ module Aspera
250
206
  SecretHider.instance.deep_remove_secret(data) if hide_secrets?
251
207
  end
252
208
 
253
- # Display results, especially the table format
254
- # @param type [Symbol] Type of data
255
- # @param data [Object] Data to display
256
- # @param fields [Array<String>] List of fields to display
257
- # @param total [Integer] Total number of items
258
- # @param name [String] Name of the column to display
259
- def display_results(type:, data: nil, fields: nil, total: nil, name: nil)
260
- Log.log.debug{"display_results: type=#{type}"}
261
- Log.dump(:data, data, level: :trace1)
262
- Log.dump(:fields, fields, level: :trace1)
263
- Aspera.assert_type(type, Symbol){'result must have type'}
264
- Aspera.assert(!data.nil? || %i[empty nothing].include?(type)){'result must have data'}
265
- display_item_count(data.length, total) unless total.nil?
266
- hide_secrets(data)
267
- data = SecretHider.instance.hide_secrets_in_string(data) if data.is_a?(String) && hide_secrets?
268
- @options[:format] = :image if type.eql?(:image)
269
- case @options[:format]
270
- when :text
271
- display_message(:data, data.to_s)
272
- when :nagios
273
- Nagios.process(data)
274
- when :ruby
275
- display_message(:data, PP.pp(filter_list_on_fields(data), +''))
276
- when :json
277
- display_message(:data, JSON.generate(filter_list_on_fields(data)))
278
- when :jsonpp
279
- display_message(:data, JSON.pretty_generate(filter_list_on_fields(data)))
280
- when :yaml
281
- display_message(:data, YAML.dump(filter_list_on_fields(data)))
282
- when :image
283
- # if object or list, then must be a single
284
- case type
285
- when :single_object, :object_list
286
- data = [data] if type.eql?(:single_object)
287
- raise BadArgument, 'image display requires a single result' unless data.length == 1
288
- fields = compute_fields(data, fields)
289
- raise BadArgument, 'select a single field to display' unless fields.length == 1
290
- data = data.first
291
- raise BadArgument, 'no such field' unless data.key?(fields.first)
292
- data = data[fields.first]
293
- end
294
- Aspera.assert_type(data, String){'URL or blob for image'}
295
- # Check if URL
296
- data =
297
- begin
298
- # just validate
299
- URI.parse(data)
300
- if Environment.instance.url_method.eql?(:text)
301
- UriReader.read(data)
302
- else
303
- Environment.instance.open_uri(data)
304
- display_message(:info, "Opened URL in browser: #{data}")
305
- :done
306
- end
307
- rescue URI::InvalidURIError
308
- data
309
- end
310
- # try base64
311
- data = begin
312
- Base64.strict_decode64(data)
313
- rescue
314
- data
315
- end
316
- # here, data is the image blob
317
- display_message(:data, Preview::Terminal.build(data, **@options[:image].symbolize_keys)) unless data.eql?(:done)
318
- when :table, :csv
319
- case type
320
- when :single_object
321
- # :single_object is a Hash, where key=column name
322
- Aspera.assert_type(data, Hash){'result'}
323
- if data.empty?
324
- display_message(:data, TerminalFormatter.special_format('empty dict'))
325
- else
326
- data = DotContainer.new(data).to_dotted if @options[:flat_hash]
327
- display_table([data], compute_fields([data], fields), single: true)
328
- end
329
- when :object_list
330
- # :object_list is an Array of Hash, where key=column name
331
- Aspera.assert_array_all(data, Hash){'result'}
332
- data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
333
- display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
334
- when :value_list
335
- # :value_list is a simple array of values, name of column provided in `name`
336
- display_table(data.map{ |i| {name => i}}, [name])
337
- when :special # no table
338
- if data.eql?(:nothing)
339
- Log.log.debug('no result expected')
340
- return
341
- end
342
- display_message(:info, TerminalFormatter.special_format(data.to_s))
343
- return
344
- when :status # no table
345
- # :status displays a simple message
346
- display_message(:info, data)
347
- when :text # no table
348
- # :status displays a simple message
349
- display_message(:data, data)
350
- else Aspera.error_unexpected_value(type){'data type'}
351
- end
352
- else Aspera.error_unexpected_value(@options[:format]){'format'}
353
- end
209
+ # Display results using the Visitor pattern
210
+ # Each Result subclass knows how to format itself by calling the appropriate
211
+ # formatter method, eliminating the need for type checking in the formatter.
212
+ # @param result [Result] Result object to display
213
+ def display_results(result)
214
+ require 'aspera/cli/result'
215
+ Aspera.assert_type(result, Cli::Result){'result must be a Result object'}
216
+
217
+ Log.log.debug{"display_results: result class=#{result.class.name}"}
218
+ Log.dump(:data, result.data, level: :trace1)
219
+ Log.dump(:fields, result.fields, level: :trace1)
220
+
221
+ # Hide secrets in data
222
+ hide_secrets(result.data)
223
+
224
+ # Use the Visitor pattern: delegate formatting to the result object
225
+ result.format(self)
354
226
  end
355
- #==========================================================================================
227
+ # Public methods exposed to Result classes for formatting
356
228
 
357
- private
229
+ # Get the current format type
230
+ def format_type
231
+ @options[:format]
232
+ end
358
233
 
359
- # @return all fields of all objects in list of objects
234
+ # Set the format type (used by Image result)
235
+ def format_type=(format)
236
+ @options[:format] = format
237
+ end
238
+
239
+ # Hide secrets in a string
240
+ def hide_secrets_in_string(string)
241
+ SecretHider.instance.hide_secrets_in_string(string)
242
+ end
243
+
244
+ # Get special format string
245
+ def special_format(text)
246
+ TerminalFormatter.special_format(text)
247
+ end
248
+
249
+ # Check if flat_hash option is enabled
250
+ def flat_hash?
251
+ @options[:flat_hash]
252
+ end
253
+
254
+ # Get image options
255
+ def image_options
256
+ @options[:image].symbolize_keys
257
+ end
258
+
259
+ # @return [Array<String>] all fields of all objects in list of objects
360
260
  def all_fields(data)
361
261
  data.each_with_object({}){ |v, m| v.each_key{ |c| m[c] = true}}.keys
362
262
  end
363
263
 
364
- # @return the list of fields to display
264
+ # @return [Array<String>] the list of fields to display
365
265
  # @param data [Array<Hash>] data to display
366
266
  # @param default [Array<String>, Proc] list of fields to display by default (may contain special values)
367
267
  def compute_fields(data, default)
@@ -427,7 +327,7 @@ module Aspera
427
327
  when Proc
428
328
  begin
429
329
  data.select!{ |i| @options[:select].call(i)}
430
- rescue Exception => e # rubocop:disable Lint/RescueException
330
+ rescue StandardError => e
431
331
  raise Cli::BadArgument, "Error in user-provided ruby lambda code during select: #{e.message}"
432
332
  end
433
333
  when Hash
@@ -470,7 +370,7 @@ module Aspera
470
370
  final_table_rows = object_array.map{ |r| fields.map{ |c| r[c].to_s}}
471
371
  # remove empty rows
472
372
  final_table_rows.select!{ |i| !(i.is_a?(Hash) && i.empty?)}
473
- # here : fields : list of column names
373
+ # fields: list of column names to display
474
374
  case @options[:format]
475
375
  when :table
476
376
  format_style[:border] = :unicode_round if Environment.terminal_supports_unicode?
@@ -506,6 +406,19 @@ module Aspera
506
406
  end
507
407
  nil
508
408
  end
409
+
410
+ # remove a fields from the list
411
+ FIELDS_LESS = '-'
412
+ # supported output formats
413
+ DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
414
+ # user output levels
415
+ DISPLAY_LEVELS = %i[info data error].freeze
416
+ # column names for single object display in table
417
+ SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
418
+ # Terminal: vertical separator for list of strings.
419
+ STR_LST_SEP_VERT = "\n"
420
+
421
+ private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
509
422
  end
510
423
  end
511
424
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/version'
4
+ require 'aspera/cli/info'
5
+ require 'aspera/environment'
6
+ require 'aspera/persistency_action_once'
7
+ require 'aspera/rest'
8
+ require 'aspera/log'
9
+ require 'date'
10
+
11
+ module Aspera
12
+ module Cli
13
+ # Mixin providing gem version-check functionality to Plugin::Config.
14
+ # Depends on `options` and `persistency` being available in the including class.
15
+ module GemChecker
16
+ GEM_CHECK_DATE_FMT = '%Y/%m/%d'
17
+
18
+ # @return [Hash] current vs latest gem version info
19
+ def check_gem_version
20
+ latest_version =
21
+ begin
22
+ Rest.new(base_url: 'https://rubygems.org/api/v1').read("versions/#{Info::GEM_NAME}/latest.json")['version']
23
+ rescue StandardError
24
+ Log.log.warn('Could not retrieve latest gem version on rubygems.')
25
+ '0'
26
+ end
27
+ if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(Info::RUBY_FUTURE_MINIMUM_VERSION)
28
+ Log.log.warn do
29
+ "Note that a future version will require Ruby version #{Info::RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
30
+ "you are using #{Environment.ruby_version}"
31
+ end
32
+ end
33
+ return {
34
+ name: Info::GEM_NAME,
35
+ current: Cli::VERSION,
36
+ latest: latest_version,
37
+ need_update: Gem::Version.new(Cli::VERSION) < Gem::Version.new(latest_version)
38
+ }
39
+ end
40
+
41
+ # Check periodically if a newer gem version is available; log a warning if so.
42
+ # Called once per run by Runner, before command execution.
43
+ def periodic_check_newer_gem_version
44
+ delay_days = options.get_option(:version_check_days, mandatory: true).to_i
45
+ return if delay_days.eql?(0)
46
+ last_check_array = []
47
+ check_date_persist = PersistencyActionOnce.new(
48
+ manager: persistency,
49
+ data: last_check_array,
50
+ id: 'version_last_check'
51
+ )
52
+ current_date = Date.today
53
+ last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
54
+ Log.log.debug{"gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}"}
55
+ return if !last_check_days.nil? && last_check_days < delay_days
56
+ last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
57
+ check_date_persist.save
58
+ check_data = check_gem_version
59
+ Log.log.warn do
60
+ "A new version is available: #{check_data[:latest]}. You have #{check_data[:current]}. Upgrade with: gem update #{check_data[:name]}"
61
+ end if check_data[:need_update]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -129,6 +129,7 @@ module Aspera
129
129
  class << self
130
130
  # @param error [Exception] exception object
131
131
  def hint_for(error, formatter)
132
+ # Loop on known error hints
132
133
  ERROR_HINTS.each do |hint|
133
134
  next unless error.is_a?(hint[:exception])
134
135
  message = error.message
@@ -141,12 +142,11 @@ module Aspera
141
142
  next unless message.eql?(m)
142
143
  when Regexp
143
144
  next unless message.match?(m)
144
- else Aspera.error_unexpected_value(m)
145
+ else Aspera.error_unexpected_value(m){'hint match'}
145
146
  end
146
147
  hint[:remediation].each do |r|
147
148
  Log.log.info{"#{'HINT:'.bg_green.gray.blink.freeze} #{r}"}
148
149
  end
149
- break
150
150
  end
151
151
  end
152
152
  end