aspera-cli 4.26.2 → 4.27.1

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 (159) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +310 -21
  4. data/CONTRIBUTING.md +99 -14
  5. data/TODO.md +50 -0
  6. data/bin/ascli +3 -3
  7. data/docs/README.md +10977 -0
  8. data/docs/test-mcp-with-ai.md +202 -0
  9. data/lib/aspera/agent/base.rb +29 -7
  10. data/lib/aspera/agent/connect.rb +58 -21
  11. data/lib/aspera/agent/desktop.rb +53 -18
  12. data/lib/aspera/agent/direct.rb +71 -40
  13. data/lib/aspera/agent/factory.rb +3 -3
  14. data/lib/aspera/agent/httpgw.rb +55 -14
  15. data/lib/aspera/agent/node.rb +34 -2
  16. data/lib/aspera/agent/transferd.rb +48 -12
  17. data/lib/aspera/api/alee.rb +1 -1
  18. data/lib/aspera/api/aoc.rb +42 -44
  19. data/lib/aspera/api/cos_node.rb +10 -9
  20. data/lib/aspera/api/faspex.rb +16 -15
  21. data/lib/aspera/api/httpgw.rb +23 -23
  22. data/lib/aspera/api/node.rb +51 -70
  23. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  24. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  25. data/lib/aspera/ascmd.rb +32 -30
  26. data/lib/aspera/ascp/installation.rb +75 -69
  27. data/lib/aspera/ascp/management.rb +7 -8
  28. data/lib/aspera/assert.rb +12 -13
  29. data/lib/aspera/cli/ascp_actions.rb +155 -0
  30. data/lib/aspera/cli/async_transfer_store.rb +125 -0
  31. data/lib/aspera/cli/bootstrapper.rb +197 -0
  32. data/lib/aspera/cli/command_registry.rb +145 -0
  33. data/lib/aspera/cli/command_spec.rb +141 -0
  34. data/lib/aspera/cli/context.rb +19 -6
  35. data/lib/aspera/cli/error.rb +16 -0
  36. data/lib/aspera/cli/extended_value.rb +27 -26
  37. data/lib/aspera/cli/formatter.rb +95 -59
  38. data/lib/aspera/cli/gem_checker.rb +65 -0
  39. data/lib/aspera/cli/hints.rb +3 -3
  40. data/lib/aspera/cli/http.rb +74 -30
  41. data/lib/aspera/cli/info.rb +2 -0
  42. data/lib/aspera/cli/mailer.rb +97 -0
  43. data/lib/aspera/cli/mcp_tool.rb +225 -0
  44. data/lib/aspera/cli/option_declarator.rb +77 -0
  45. data/lib/aspera/cli/options.schema.yaml +634 -7
  46. data/lib/aspera/cli/parser.rb +1217 -0
  47. data/lib/aspera/cli/plugins/alee.rb +20 -22
  48. data/lib/aspera/cli/plugins/aoc.rb +1178 -881
  49. data/lib/aspera/cli/plugins/ats.rb +205 -162
  50. data/lib/aspera/cli/plugins/base.rb +703 -175
  51. data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
  52. data/lib/aspera/cli/plugins/config.rb +434 -757
  53. data/lib/aspera/cli/plugins/console.rb +107 -65
  54. data/lib/aspera/cli/plugins/cos.rb +46 -33
  55. data/lib/aspera/cli/plugins/factory.rb +6 -6
  56. data/lib/aspera/cli/plugins/faspex5.rb +627 -388
  57. data/lib/aspera/cli/plugins/faspio.rb +54 -51
  58. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  59. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  60. data/lib/aspera/cli/plugins/node.rb +988 -840
  61. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  62. data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
  63. data/lib/aspera/cli/plugins/preview.rb +195 -157
  64. data/lib/aspera/cli/plugins/server.rb +141 -91
  65. data/lib/aspera/cli/plugins/shares.rb +343 -110
  66. data/lib/aspera/cli/preset_actions.rb +159 -0
  67. data/lib/aspera/cli/preset_manager.rb +82 -39
  68. data/lib/aspera/cli/result.rb +78 -26
  69. data/lib/aspera/cli/runner.rb +323 -153
  70. data/lib/aspera/cli/secret_finder.rb +40 -0
  71. data/lib/aspera/cli/special_values.rb +1 -0
  72. data/lib/aspera/cli/sync_actions.rb +89 -64
  73. data/lib/aspera/cli/terminal_formatter.rb +3 -3
  74. data/lib/aspera/cli/transfer_actions.rb +90 -0
  75. data/lib/aspera/cli/transfer_agent.rb +129 -64
  76. data/lib/aspera/cli/transfer_progress.rb +9 -9
  77. data/lib/aspera/cli/vault_manager.rb +79 -0
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +28 -30
  80. data/lib/aspera/colors.rb +3 -3
  81. data/lib/aspera/command_line_builder.rb +34 -34
  82. data/lib/aspera/command_line_converter.rb +1 -1
  83. data/lib/aspera/coverage.rb +1 -2
  84. data/lib/aspera/data_repository.rb +2 -1
  85. data/lib/aspera/dot_container.rb +12 -12
  86. data/lib/aspera/environment.rb +30 -23
  87. data/lib/aspera/exec_spec.rb +13 -0
  88. data/lib/aspera/faspex_gw.rb +5 -5
  89. data/lib/aspera/faspex_postproc.rb +16 -10
  90. data/lib/aspera/graphql.rb +37 -0
  91. data/lib/aspera/hash_ext.rb +8 -2
  92. data/lib/aspera/json_rpc/client.rb +62 -0
  93. data/lib/aspera/json_rpc/version.rb +7 -0
  94. data/lib/aspera/keychain/base.rb +10 -3
  95. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  96. data/lib/aspera/keychain/factory.rb +20 -8
  97. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  98. data/lib/aspera/keychain/macos_security.rb +23 -25
  99. data/lib/aspera/keychain/one_password_api.rb +86 -0
  100. data/lib/aspera/keychain/one_password_base.rb +34 -0
  101. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  102. data/lib/aspera/link_header.rb +82 -0
  103. data/lib/aspera/log.rb +29 -15
  104. data/lib/aspera/markdown.rb +90 -7
  105. data/lib/aspera/nagios.rb +8 -8
  106. data/lib/aspera/node_simulator.rb +35 -27
  107. data/lib/aspera/oauth/base.rb +9 -10
  108. data/lib/aspera/oauth/boot.rb +5 -5
  109. data/lib/aspera/oauth/factory.rb +12 -10
  110. data/lib/aspera/oauth/jwt.rb +9 -11
  111. data/lib/aspera/oauth/web.rb +6 -6
  112. data/lib/aspera/persistency_action_once.rb +12 -12
  113. data/lib/aspera/persistency_folder.rb +18 -11
  114. data/lib/aspera/preview/file_types.rb +5 -5
  115. data/lib/aspera/preview/generator.rb +52 -49
  116. data/lib/aspera/preview/options.rb +3 -2
  117. data/lib/aspera/preview/terminal.rb +5 -5
  118. data/lib/aspera/preview/utils.rb +66 -19
  119. data/lib/aspera/products/connect.rb +2 -2
  120. data/lib/aspera/products/desktop.rb +1 -1
  121. data/lib/aspera/products/other.rb +3 -3
  122. data/lib/aspera/products/transferd.rb +4 -4
  123. data/lib/aspera/proxy_auto_config.rb +15 -14
  124. data/lib/aspera/rest.rb +159 -151
  125. data/lib/aspera/rest_error_analyzer.rb +6 -6
  126. data/lib/aspera/rest_errors_aspera.rb +0 -10
  127. data/lib/aspera/rest_list.rb +11 -10
  128. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  129. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  130. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  131. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  132. data/lib/aspera/schema/documentation.rb +65 -28
  133. data/lib/aspera/schema/reader.rb +136 -13
  134. data/lib/aspera/schema/registry.rb +60 -8
  135. data/lib/aspera/secret_hider.rb +12 -7
  136. data/lib/aspera/ssh.rb +64 -31
  137. data/lib/aspera/ssl.rb +5 -5
  138. data/lib/aspera/sync/conf.schema.yaml +2 -2
  139. data/lib/aspera/sync/database.rb +2 -2
  140. data/lib/aspera/sync/operations.rb +22 -25
  141. data/lib/aspera/temp_file_manager.rb +25 -5
  142. data/lib/aspera/timer_limiter.rb +1 -1
  143. data/lib/aspera/transfer/faux_file.rb +24 -11
  144. data/lib/aspera/transfer/parameters.rb +36 -34
  145. data/lib/aspera/transfer/result.rb +74 -0
  146. data/lib/aspera/transfer/resumer.rb +10 -10
  147. data/lib/aspera/transfer/spec.rb +18 -1
  148. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  149. data/lib/aspera/uri_reader.rb +52 -15
  150. data/lib/aspera/web_auth.rb +8 -7
  151. data/lib/aspera/web_server_simple.rb +15 -12
  152. data/lib/aspera/yaml.rb +5 -4
  153. data.tar.gz.sig +0 -0
  154. metadata +34 -6
  155. metadata.gz.sig +0 -0
  156. data/lib/aspera/cli/manager.rb +0 -766
  157. data/lib/aspera/cli/plugins/faspex.rb +0 -539
  158. data/lib/aspera/json_rpc.rb +0 -52
  159. data/lib/aspera/transfer/uri.rb +0 -56
@@ -4,9 +4,25 @@ module Aspera
4
4
  module Cli
5
5
  # CLI base exception
6
6
  class Error < StandardError; end
7
+
8
+ # Raised by dispatch_from_registry when --help is encountered mid-dispatch.
9
+ # Carries the plugin instance (with #help_path set) so the runner can display
10
+ # context-sensitive help without unwinding through a full rescue chain.
11
+ class HelpRequest < StandardError
12
+ # @return [Plugins::Base]
13
+ attr_reader :plugin
14
+
15
+ def initialize(plugin)
16
+ super('help requested')
17
+ @plugin = plugin
18
+ end
19
+ end
20
+
7
21
  # Raised when an unexpected argument is provided.
8
22
  class BadArgument < Error; end
23
+
9
24
  class MissingArgument < Error; end
25
+
10
26
  class NoSuchElement < Error; end
11
27
 
12
28
  # Raised when a lookup for a specific entity fails to return exactly one result.
@@ -3,6 +3,7 @@
3
3
  # cspell:ignore csvt jsonpp stdbin
4
4
  require 'aspera/uri_reader'
5
5
  require 'aspera/environment'
6
+ require 'aspera/yaml'
6
7
  require 'aspera/log'
7
8
  require 'aspera/assert'
8
9
  require 'aspera/cli/error'
@@ -67,7 +68,7 @@ module Aspera
67
68
  # @param value [String] The value as parameter
68
69
  # @param ext_type [Symbol] The method of extended value
69
70
  def assert_no_value(value, ext_type)
70
- Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
71
+ Aspera.assert(value.empty?, type: BadArgument) { "no value allowed for extended value type: #{ext_type}" }
71
72
  end
72
73
 
73
74
  def read_stdin(mode)
@@ -88,25 +89,25 @@ module Aspera
88
89
  # e.g. `preset` is reader in config plugin
89
90
  # @type [Hash{Symbol => Proc}]
90
91
  @handlers = {
91
- val: lambda{ |i| i},
92
- base64: lambda{ |i| Base64.decode64(i)},
93
- csvt: lambda{ |i| ExtendedValue.decode_csvt(i)},
94
- env: lambda{ |i| ENV.fetch(i, nil)},
95
- file: lambda{ |i| File.read(File.expand_path(i))},
96
- uri: lambda{ |i| UriReader.read(i)},
97
- json: lambda{ |i| ExtendedValue.JSON_parse(i)},
98
- lines: lambda{ |i| i.split("\n")},
99
- list: lambda{ |i| i[1..].split(i[0])},
100
- none: lambda{ |i| ExtendedValue.assert_no_value(i, :none); nil}, # rubocop:disable Style/Semicolon
101
- path: lambda{ |i| File.expand_path(i)},
102
- re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
103
- ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
92
+ val: lambda { |i| i },
93
+ base64: lambda { |i| Base64.decode64(i) },
94
+ csvt: lambda { |i| ExtendedValue.decode_csvt(i) },
95
+ env: lambda { |i| ENV.fetch(i, nil) },
96
+ file: lambda { |i| File.read(File.expand_path(i)) },
97
+ uri: lambda { |i| UriReader.read(i) },
98
+ json: lambda { |i| ExtendedValue.JSON_parse(i) },
99
+ lines: lambda { |i| i.split("\n") },
100
+ list: lambda { |i| i[1..].split(i[0]) },
101
+ none: lambda { |i| ExtendedValue.assert_no_value(i, :none); nil }, # rubocop:disable Style/Semicolon
102
+ path: lambda { |i| File.expand_path(i) },
103
+ re: lambda { |i| Regexp.new(i, Regexp::MULTILINE) },
104
+ ruby: lambda { |i| Environment.secure_eval(i, __FILE__, __LINE__) },
104
105
  s: lambda(&:to_s),
105
- secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
106
- stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
107
- yaml: lambda{ |i| YAML.load(i)},
108
- zlib: lambda{ |i| Zlib::Inflate.inflate(i)},
109
- extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
106
+ secret: lambda { |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ") }, # rubocop:disable Style/Semicolon
107
+ stdin: lambda { |i| ExtendedValue.read_stdin(i) },
108
+ yaml: lambda { |i| YAML.safe_load(i) },
109
+ zlib: lambda { |i| Zlib::Inflate.inflate(i) },
110
+ extend: lambda { |i| ExtendedValue.instance.evaluate_extend(i) }
110
111
  }
111
112
  @regex_single = nil
112
113
  @regex_extend = nil
@@ -126,7 +127,7 @@ module Aspera
126
127
  attr_reader :default_decoder
127
128
 
128
129
  def default_decoder=(value)
129
- Log.log.debug{"Setting default decoder to (#{value.class}) #{value}"}
130
+ Log.log.debug { "Setting default decoder to (#{value.class}) #{value}" }
130
131
  Aspera.assert_values(value, DEFAULT_DECODERS)
131
132
  value = nil if value.eql?(:none)
132
133
  @default_decoder = value
@@ -137,9 +138,9 @@ module Aspera
137
138
 
138
139
  # Add a new handler
139
140
  def on(name, &block)
140
- Aspera.assert_type(name, Symbol){'name'}
141
- Aspera.assert(block){"block required when registering handler for #{name}"}
142
- Log.log.debug{"Setting handler for #{name}"}
141
+ Aspera.assert_type(name, Symbol) { 'name' }
142
+ Aspera.assert(block) { "block required when registering handler for #{name}" }
143
+ Log.log.debug { "Setting handler for #{name}" }
143
144
  @handlers[name] = block
144
145
  update_regex
145
146
  end
@@ -155,7 +156,7 @@ module Aspera
155
156
  return value unless value.is_a?(String)
156
157
  Aspera.assert_array_all(allowed, Class) unless allowed.nil?
157
158
  # use default decoder if not an extended value and expect complex types
158
- using_default_decoder = allowed&.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)} && !@regex_single.match?(value) && !@default_decoder.nil?
159
+ using_default_decoder = allowed&.all? { |t| DEFAULT_PARSER_TYPES.include?(t) } && !@regex_single.match?(value) && !@default_decoder.nil?
159
160
  value = [MARKER_START, @default_decoder, MARKER_END, value].join if using_default_decoder
160
161
  # First determine decoders, in reversed order
161
162
  handlers_reversed = []
@@ -165,7 +166,7 @@ module Aspera
165
166
  value = m[2]
166
167
  break if SPECIAL_HANDLERS.include?(handler)
167
168
  end
168
- Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
169
+ Log.log.trace1 { "evaluating: #{handlers_reversed}, value: #{value.class}:#{value}" }
169
170
  handlers_reversed.each do |handler|
170
171
  value = @handlers[handler].call(value)
171
172
  rescue => e
@@ -179,7 +180,7 @@ module Aspera
179
180
  def evaluate_extend(value)
180
181
  while (m = value.match(@regex_extend))
181
182
  sub_value = "@#{m[2]}:#{m[3]}"
182
- Log.log.debug{"evaluating #{sub_value}"}
183
+ Log.log.debug { "evaluating #{sub_value}" }
183
184
  value = "#{m[1]}#{evaluate(sub_value, context: 'composite extended value')}#{m[4]}"
184
185
  end
185
186
  return value
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore jsonpp
4
+ require 'aspera/schema/registry'
5
+ require 'aspera/cli/option_declarator'
4
6
  require 'aspera/cli/special_values'
5
7
  require 'aspera/cli/terminal_formatter'
6
8
  require 'aspera/preview/terminal'
@@ -21,6 +23,33 @@ module Aspera
21
23
  module Cli
22
24
  # Take care of CLI output on terminal
23
25
  class Formatter
26
+ extend OptionDeclarator
27
+
28
+ # remove a fields from the list
29
+ FIELDS_LESS = '-'
30
+ # supported output formats
31
+ DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
32
+ # user output levels
33
+ DISPLAY_LEVELS = %i[info data error].freeze
34
+ # column names for single object display in table
35
+ SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
36
+ # Terminal: vertical separator for list of strings.
37
+ STR_LST_SEP_VERT = "\n"
38
+
39
+ private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
40
+
41
+ option :out, description: 'Output rendering options (dot-notation: format, level, file, fields, select, table[.pivot], flat, secrets, img)', schema: Schema::Registry::OUT_OPTIONS
42
+ option :display, description: 'Output only some information', allowed: DISPLAY_LEVELS, default: :data, deprecation: 'use --out.level'
43
+ option :format, description: 'Output format (also: --out.format)', allowed: DISPLAY_FORMATS, default: :table
44
+ option :output, description: 'Destination for results', deprecation: 'use --out.file'
45
+ option :fields, description: "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF} (also: --out.fields)", allowed: [String, Array, Regexp, Proc], default: SpecialValues::DEF
46
+ option :select, description: 'Select only some items in lists: column, value (also: --out.select)', allowed: [Hash, Proc]
47
+ option :table_style, description: '(Table) Display style', allowed: [Hash], deprecation: 'use --out.table'
48
+ option :flat_hash, description: '(Table) Display deep values as additional keys', allowed: Type::BOOLEAN, default: true, deprecation: 'use --out.flat'
49
+ option :multi_single, description: '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single], default: :no, deprecation: 'use --out.table.pivot'
50
+ option :show_secrets, description: 'Show secrets on command output', allowed: Type::BOOLEAN, default: false, deprecation: 'use --out.secrets'
51
+ option :image, schema: Schema::Registry::IMAGE_OPTIONS, deprecation: 'use --out.img'
52
+
24
53
  class << self
25
54
  # Replace special values with a readable version on terminal
26
55
  def replace_specific_for_terminal(input_hash, string_list_separator)
@@ -57,7 +86,7 @@ module Aspera
57
86
  end
58
87
 
59
88
  def all_but(list)
60
- Array(list).map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
89
+ Array(list).map { |i| "#{FIELDS_LESS}#{i}" }.unshift(SpecialValues::ALL)
61
90
  end
62
91
  end
63
92
 
@@ -108,32 +137,50 @@ module Aspera
108
137
  end
109
138
  end
110
139
 
111
- def declare_options(options)
112
- options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
113
- options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
114
- options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
115
- options.declare(
116
- :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
117
- allowed: [String, Array, Regexp, Proc],
118
- default: SpecialValues::DEF
119
- )
120
- options.declare(:select, 'Select only some items in lists: column, value', allowed: [Hash, Proc], handler: {o: self, m: :option_handler})
121
- options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: {})
122
- options.declare(:flat_hash, '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: true)
123
- options.declare(
124
- :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single],
125
- handler: {o: self, m: :option_handler}, default: :no
126
- )
127
- options.declare(:show_secrets, 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: false)
128
- options.declare(:image, 'Options for image display', allowed: Hash, handler: {o: self, m: :option_handler}, default: {})
140
+ # Bind all formatter options to this instance using set_handler.
141
+ # Called from Runner after Formatter.new.
142
+ # @param options [Aspera::Cli::Parser]
143
+ # @return [nil]
144
+ def bind_options(options)
145
+ @parser = options
146
+ %i[out display format output fields select table_style flat_hash multi_single show_secrets image].each do |opt|
147
+ options.set_handler(opt, object: self, method: :option_handler)
148
+ end
149
+ nil
129
150
  end
130
151
 
131
- # method accessed by option manager
132
- # options are: format, output, display, fields, select, table_style, flat_hash, multi_single
152
+ # Getter/setter handler called by the option manager for all formatter options
153
+ # @param option_symbol [Symbol] Option name (one of :out, :format, :output, :display, :fields, :select, :table_style, :flat_hash, :multi_single, :show_secrets, :image)
154
+ # @param operation [Symbol] :get or :set
155
+ # @param value [Object, nil] Value to set (only used when operation is :set)
156
+ # @return [Object, nil] Current option value when operation is :get; nil otherwise
133
157
  def option_handler(option_symbol, operation, value = nil)
134
158
  Aspera.assert_values(operation, %i[set get])
135
159
  case operation
136
160
  when :set
161
+ # --out is a composite hash: dispatch each sub-key through the parser (validation included)
162
+ if option_symbol.eql?(:out)
163
+ Aspera.assert_type(value, Hash)
164
+ value.each do |k, v|
165
+ case k.to_sym
166
+ when :format then @parser.set_option(:format, v, warn_deprecation: false)
167
+ when :level then @parser.set_option(:display, v, warn_deprecation: false)
168
+ when :file then @parser.set_option(:output, v, warn_deprecation: false)
169
+ when :fields then @parser.set_option(:fields, v, warn_deprecation: false)
170
+ when :select then @parser.set_option(:select, v, warn_deprecation: false)
171
+ when :table
172
+ Aspera.assert_type(v, Hash)
173
+ table_hash = v.transform_keys(&:to_sym)
174
+ @parser.set_option(:multi_single, table_hash.delete(:pivot).to_s, warn_deprecation: false) if table_hash.key?(:pivot)
175
+ @parser.set_option(:table_style, table_hash, warn_deprecation: false) unless table_hash.empty?
176
+ when :flat then @parser.set_option(:flat_hash, v, warn_deprecation: false)
177
+ when :secrets then @parser.set_option(:show_secrets, v, warn_deprecation: false)
178
+ when :img then @parser.set_option(:image, v, warn_deprecation: false)
179
+ else Aspera.error_unexpected_value(k) { 'out sub-option (format, level, file, fields, select, table[.pivot], flat, secrets, img)' }
180
+ end
181
+ end
182
+ return
183
+ end
137
184
  @options[option_symbol] = value
138
185
  # special handling of some options
139
186
  case option_symbol
@@ -147,12 +194,14 @@ module Aspera
147
194
  end
148
195
  when :image
149
196
  # get list if key arguments of method
150
- allowed_options = Preview::Terminal.method(:build).parameters.select{ |i| i[0].eql?(:key)}.map{ |i| i[1]}
197
+ allowed_options = Preview::Terminal.method(:build).parameters.select { |i| i[0].eql?(:key) }.map { |i| i[1] }
151
198
  # check that only supported options are given
152
199
  unknown_options = value.keys.map(&:to_sym) - allowed_options
153
- raise "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" unless unknown_options.empty?
200
+ Aspera.assert(unknown_options.empty?) { "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" }
154
201
  end
155
- when :get then return @options[option_symbol]
202
+ when :get
203
+ return if option_symbol.eql?(:out)
204
+ return @options[option_symbol]
156
205
  else Aspera.error_unreachable_line
157
206
  end
158
207
  nil
@@ -163,7 +212,7 @@ module Aspera
163
212
  # @param message_level [Symbol] The level of the message - must be one of: :data, :info, :error
164
213
  # @param message [String] The message to display
165
214
  # @param hide_secrets [Boolean] Whether to hide secrets in the message (default: true)
166
- # @return [void]
215
+ # @return [nil]
167
216
  # @note Message display behavior depends on the message_level:
168
217
  # - +:data+ messages are displayed unless display level is +:error+
169
218
  # - +:info+ messages are only displayed when display level is +:info+
@@ -198,9 +247,9 @@ module Aspera
198
247
  # @param result [Result] Result object to display
199
248
  def display_results(result)
200
249
  require 'aspera/cli/result'
201
- Aspera.assert_type(result, Cli::Result){'result must be a Result object'}
250
+ Aspera.assert_type(result, Cli::Result) { 'result must be a Result object' }
202
251
 
203
- Log.log.debug{"display_results: result class=#{result.class.name}"}
252
+ Log.log.debug { "display_results: result class=#{result.class.name}" }
204
253
  Log.dump(:data, result.data, level: :trace1)
205
254
  Log.dump(:fields, result.fields, level: :trace1)
206
255
 
@@ -218,7 +267,7 @@ module Aspera
218
267
  end
219
268
 
220
269
  # Set the format type (used by Image result)
221
- def set_format_type(format)
270
+ def format_type=(format)
222
271
  @options[:format] = format
223
272
  end
224
273
 
@@ -242,16 +291,16 @@ module Aspera
242
291
  @options[:image].symbolize_keys
243
292
  end
244
293
 
245
- # @return all fields of all objects in list of objects
294
+ # @return [Array<String>] all fields of all objects in list of objects
246
295
  def all_fields(data)
247
- data.each_with_object({}){ |v, m| v.each_key{ |c| m[c] = true}}.keys
296
+ data.each_with_object({}) { |v, m| v.each_key { |c| m[c] = true } }.keys
248
297
  end
249
298
 
250
- # @return the list of fields to display
299
+ # @return [Array<String>] the list of fields to display
251
300
  # @param data [Array<Hash>] data to display
252
301
  # @param default [Array<String>, Proc] list of fields to display by default (may contain special values)
253
302
  def compute_fields(data, default)
254
- Log.log.debug{"compute_fields: data:#{data.class} default:#{default.class} #{default}"}
303
+ Log.log.debug { "compute_fields: data:#{data.class} default:#{default.class} #{default}" }
255
304
  Log.dump(:compute_fields_default, default, level: :trace1)
256
305
  # the requested list of fields, but if can contain special values
257
306
  request =
@@ -259,8 +308,8 @@ module Aspera
259
308
  # when NilClass then [SpecialValues::DEF]
260
309
  when String then @options[:fields].split(',')
261
310
  when Array then @options[:fields]
262
- when Regexp then return all_fields(data).select{ |i| i.match(@options[:fields])}
263
- when Proc then return all_fields(data).select{ |i| @options[:fields].call(i)}
311
+ when Regexp then return all_fields(data).select { |i| i.match(@options[:fields]) }
312
+ when Proc then return all_fields(data).select { |i| @options[:fields].call(i) }
264
313
  else Aspera.error_unexpected_value(@options[:fields])
265
314
  end
266
315
  Aspera.assert_array_all(request, String)
@@ -277,12 +326,12 @@ module Aspera
277
326
  # get the list of all column names used in all lines, not just first one, as all lines may have different columns
278
327
  request.unshift(*all_fields(data))
279
328
  when SpecialValues::DEF
280
- default = all_fields(data).select{ |i| default.call(i)} if default.is_a?(Proc)
329
+ default = all_fields(data).select { |i| default.call(i) } if default.is_a?(Proc)
281
330
  default = all_fields(data) if default.nil?
282
331
  request.unshift(*default)
283
332
  else
284
333
  if removal
285
- result = result.reject{ |i| i.eql?(item)}
334
+ result = result.reject { |i| i.eql?(item) }
286
335
  else
287
336
  result.push(item)
288
337
  end
@@ -298,12 +347,12 @@ module Aspera
298
347
  return data unless data.is_a?(Array)
299
348
  # by default, keep all data intact
300
349
  return data if @options[:fields].eql?(SpecialValues::DEF) && @options[:select].nil?
301
- Aspera.assert_array_all(data, Hash){'filter or select'}
350
+ Aspera.assert_array_all(data, Hash) { 'filter or select' }
302
351
  filter_columns_on_select(data)
303
352
  return data if @options[:fields].eql?(SpecialValues::DEF)
304
353
  selected_fields = compute_fields(data, @options[:fields])
305
- return data.map{ |i| i[selected_fields.first]} if selected_fields.length == 1
306
- return data.map{ |i| i.slice(*selected_fields)}
354
+ return data.map { |i| i[selected_fields.first] } if selected_fields.length == 1
355
+ return data.map { |i| i.slice(*selected_fields) }
307
356
  end
308
357
 
309
358
  # filter the list of items on the select option
@@ -312,12 +361,12 @@ module Aspera
312
361
  case @options[:select]
313
362
  when Proc
314
363
  begin
315
- data.select!{ |i| @options[:select].call(i)}
316
- rescue Exception => e # rubocop:disable Lint/RescueException
364
+ data.select! { |i| @options[:select].call(i) }
365
+ rescue StandardError => e
317
366
  raise Cli::BadArgument, "Error in user-provided ruby lambda code during select: #{e.message}"
318
367
  end
319
368
  when Hash
320
- @options[:select].each{ |k, v| data.select!{ |i| i[k].eql?(v)}}
369
+ @options[:select].each { |k, v| data.select! { |i| i[k].eql?(v) } }
321
370
  end
322
371
  end
323
372
 
@@ -337,7 +386,7 @@ module Aspera
337
386
  format_style = @options[:table_style].symbolize_keys
338
387
  string_list_separator = format_style.delete(:str_lst_sep) || STR_LST_SEP_VERT
339
388
  # convert data to string, and keep only display fields
340
- object_array.each{ |i| self.class.replace_specific_for_terminal(i, string_list_separator)}
389
+ object_array.each { |i| self.class.replace_specific_for_terminal(i, string_list_separator) }
341
390
  # if table has only one element, and only one field, display the value
342
391
  if object_array.length == 1 && fields.length == 1
343
392
  Log.log.debug("single element, field: #{fields.first}")
@@ -353,10 +402,10 @@ module Aspera
353
402
  Log.dump(:object_array, object_array)
354
403
  Log.dump(:fields, fields)
355
404
  # convert data to string, and keep only display fields
356
- final_table_rows = object_array.map{ |r| fields.map{ |c| r[c].to_s}}
405
+ final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
357
406
  # remove empty rows
358
- final_table_rows.select!{ |i| !(i.is_a?(Hash) && i.empty?)}
359
- # here : fields : list of column names
407
+ final_table_rows.select! { |i| !(i.is_a?(Hash) && i.empty?) }
408
+ # fields: list of column names to display
360
409
  case @options[:format]
361
410
  when :table
362
411
  format_style[:border] = :unicode_round if Environment.terminal_supports_unicode?
@@ -392,19 +441,6 @@ module Aspera
392
441
  end
393
442
  nil
394
443
  end
395
-
396
- # remove a fields from the list
397
- FIELDS_LESS = '-'
398
- # supported output formats
399
- DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
400
- # user output levels
401
- DISPLAY_LEVELS = %i[info data error].freeze
402
- # column names for single object display in table
403
- SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
404
- # Terminal: vertical separator for list of strings.
405
- STR_LST_SEP_VERT = "\n"
406
-
407
- private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
408
444
  end
409
445
  end
410
446
  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
- Log.log.info{"#{'HINT:'.bg_green.gray.blink.freeze} #{r}"}
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