aspera-cli 4.27.0 → 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 (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +297 -313
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +16 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +12 -12
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -2,6 +2,7 @@
2
2
 
3
3
  # cspell:ignore jsonpp
4
4
  require 'aspera/schema/registry'
5
+ require 'aspera/cli/option_declarator'
5
6
  require 'aspera/cli/special_values'
6
7
  require 'aspera/cli/terminal_formatter'
7
8
  require 'aspera/preview/terminal'
@@ -22,6 +23,33 @@ module Aspera
22
23
  module Cli
23
24
  # Take care of CLI output on terminal
24
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
+
25
53
  class << self
26
54
  # Replace special values with a readable version on terminal
27
55
  def replace_specific_for_terminal(input_hash, string_list_separator)
@@ -58,7 +86,7 @@ module Aspera
58
86
  end
59
87
 
60
88
  def all_but(list)
61
- Array(list).map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
89
+ Array(list).map { |i| "#{FIELDS_LESS}#{i}" }.unshift(SpecialValues::ALL)
62
90
  end
63
91
  end
64
92
 
@@ -109,38 +137,20 @@ module Aspera
109
137
  end
110
138
  end
111
139
 
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
140
  # Bind all formatter options to this instance using set_handler.
132
141
  # Called from Runner after Formatter.new.
133
142
  # @param options [Aspera::Cli::Parser]
134
143
  # @return [nil]
135
144
  def bind_options(options)
136
- %i[display format output fields select table_style flat_hash multi_single show_secrets image].each do |opt|
145
+ @parser = options
146
+ %i[out display format output fields select table_style flat_hash multi_single show_secrets image].each do |opt|
137
147
  options.set_handler(opt, object: self, method: :option_handler)
138
148
  end
139
149
  nil
140
150
  end
141
151
 
142
152
  # 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)
153
+ # @param option_symbol [Symbol] Option name (one of :out, :format, :output, :display, :fields, :select, :table_style, :flat_hash, :multi_single, :show_secrets, :image)
144
154
  # @param operation [Symbol] :get or :set
145
155
  # @param value [Object, nil] Value to set (only used when operation is :set)
146
156
  # @return [Object, nil] Current option value when operation is :get; nil otherwise
@@ -148,6 +158,29 @@ module Aspera
148
158
  Aspera.assert_values(operation, %i[set get])
149
159
  case operation
150
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
151
184
  @options[option_symbol] = value
152
185
  # special handling of some options
153
186
  case option_symbol
@@ -161,12 +194,14 @@ module Aspera
161
194
  end
162
195
  when :image
163
196
  # get list if key arguments of method
164
- 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] }
165
198
  # check that only supported options are given
166
199
  unknown_options = value.keys.map(&:to_sym) - allowed_options
167
- Aspera.assert(unknown_options.empty?){"Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}"}
200
+ Aspera.assert(unknown_options.empty?) { "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" }
168
201
  end
169
- when :get then return @options[option_symbol]
202
+ when :get
203
+ return if option_symbol.eql?(:out)
204
+ return @options[option_symbol]
170
205
  else Aspera.error_unreachable_line
171
206
  end
172
207
  nil
@@ -212,9 +247,9 @@ module Aspera
212
247
  # @param result [Result] Result object to display
213
248
  def display_results(result)
214
249
  require 'aspera/cli/result'
215
- 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' }
216
251
 
217
- Log.log.debug{"display_results: result class=#{result.class.name}"}
252
+ Log.log.debug { "display_results: result class=#{result.class.name}" }
218
253
  Log.dump(:data, result.data, level: :trace1)
219
254
  Log.dump(:fields, result.fields, level: :trace1)
220
255
 
@@ -258,14 +293,14 @@ module Aspera
258
293
 
259
294
  # @return [Array<String>] all fields of all objects in list of objects
260
295
  def all_fields(data)
261
- 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
262
297
  end
263
298
 
264
299
  # @return [Array<String>] the list of fields to display
265
300
  # @param data [Array<Hash>] data to display
266
301
  # @param default [Array<String>, Proc] list of fields to display by default (may contain special values)
267
302
  def compute_fields(data, default)
268
- 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}" }
269
304
  Log.dump(:compute_fields_default, default, level: :trace1)
270
305
  # the requested list of fields, but if can contain special values
271
306
  request =
@@ -273,8 +308,8 @@ module Aspera
273
308
  # when NilClass then [SpecialValues::DEF]
274
309
  when String then @options[:fields].split(',')
275
310
  when Array then @options[:fields]
276
- when Regexp then return all_fields(data).select{ |i| i.match(@options[:fields])}
277
- 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) }
278
313
  else Aspera.error_unexpected_value(@options[:fields])
279
314
  end
280
315
  Aspera.assert_array_all(request, String)
@@ -291,12 +326,12 @@ module Aspera
291
326
  # get the list of all column names used in all lines, not just first one, as all lines may have different columns
292
327
  request.unshift(*all_fields(data))
293
328
  when SpecialValues::DEF
294
- 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)
295
330
  default = all_fields(data) if default.nil?
296
331
  request.unshift(*default)
297
332
  else
298
333
  if removal
299
- result = result.reject{ |i| i.eql?(item)}
334
+ result = result.reject { |i| i.eql?(item) }
300
335
  else
301
336
  result.push(item)
302
337
  end
@@ -312,12 +347,12 @@ module Aspera
312
347
  return data unless data.is_a?(Array)
313
348
  # by default, keep all data intact
314
349
  return data if @options[:fields].eql?(SpecialValues::DEF) && @options[:select].nil?
315
- Aspera.assert_array_all(data, Hash){'filter or select'}
350
+ Aspera.assert_array_all(data, Hash) { 'filter or select' }
316
351
  filter_columns_on_select(data)
317
352
  return data if @options[:fields].eql?(SpecialValues::DEF)
318
353
  selected_fields = compute_fields(data, @options[:fields])
319
- return data.map{ |i| i[selected_fields.first]} if selected_fields.length == 1
320
- 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) }
321
356
  end
322
357
 
323
358
  # filter the list of items on the select option
@@ -326,12 +361,12 @@ module Aspera
326
361
  case @options[:select]
327
362
  when Proc
328
363
  begin
329
- data.select!{ |i| @options[:select].call(i)}
364
+ data.select! { |i| @options[:select].call(i) }
330
365
  rescue StandardError => e
331
366
  raise Cli::BadArgument, "Error in user-provided ruby lambda code during select: #{e.message}"
332
367
  end
333
368
  when Hash
334
- @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) } }
335
370
  end
336
371
  end
337
372
 
@@ -351,7 +386,7 @@ module Aspera
351
386
  format_style = @options[:table_style].symbolize_keys
352
387
  string_list_separator = format_style.delete(:str_lst_sep) || STR_LST_SEP_VERT
353
388
  # convert data to string, and keep only display fields
354
- 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) }
355
390
  # if table has only one element, and only one field, display the value
356
391
  if object_array.length == 1 && fields.length == 1
357
392
  Log.log.debug("single element, field: #{fields.first}")
@@ -367,9 +402,9 @@ module Aspera
367
402
  Log.dump(:object_array, object_array)
368
403
  Log.dump(:fields, fields)
369
404
  # convert data to string, and keep only display fields
370
- 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 } }
371
406
  # remove empty rows
372
- final_table_rows.select!{ |i| !(i.is_a?(Hash) && i.empty?)}
407
+ final_table_rows.select! { |i| !(i.is_a?(Hash) && i.empty?) }
373
408
  # fields: list of column names to display
374
409
  case @options[:format]
375
410
  when :table
@@ -406,19 +441,6 @@ module Aspera
406
441
  end
407
442
  nil
408
443
  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
422
444
  end
423
445
  end
424
446
  end
@@ -51,7 +51,7 @@ module Aspera
51
51
  )
52
52
  current_date = Date.today
53
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}"}
54
+ Log.log.debug { "gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}" }
55
55
  return if !last_check_days.nil? && last_check_days < delay_days
56
56
  last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
57
57
  check_date_persist.save
@@ -142,10 +142,10 @@ module Aspera
142
142
  next unless message.eql?(m)
143
143
  when Regexp
144
144
  next unless message.match?(m)
145
- else Aspera.error_unexpected_value(m){'hint match'}
145
+ else Aspera.error_unexpected_value(m) { 'hint match' }
146
146
  end
147
147
  hint[:remediation].each do |r|
148
- Log.log.info{"#{'HINT:'.bg_green.gray.blink.freeze} #{r}"}
148
+ Log.log.info { "#{'HINT:'.bg_green.gray.blink.freeze} #{r}" }
149
149
  end
150
150
  end
151
151
  end
@@ -5,6 +5,7 @@ require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'aspera/line_logger'
7
7
  require 'aspera/schema/registry'
8
+ require 'aspera/cli/option_declarator'
8
9
  require 'aspera/rest'
9
10
  require 'aspera/oauth'
10
11
  require 'aspera/ssl'
@@ -16,6 +17,8 @@ module Aspera
16
17
  # Extracted from Plugins::Config so it can be referenced independently
17
18
  # via Context#http_config without coupling to the plugin machinery.
18
19
  class Http
20
+ extend OptionDeclarator
21
+
19
22
  # Certificate file extensions recognized when scanning a folder
20
23
  CERT_EXT = %w[crt cer pem der].freeze
21
24
  # OpenSSL constant that disables peer verification (VERIFY_NONE)
@@ -23,6 +26,13 @@ module Aspera
23
26
 
24
27
  private_constant :CERT_EXT, :SELF_SIGNED_CERT
25
28
 
29
+ option :insecure, description: 'HTTP/S: Do not validate any certificate', allowed: Type::BOOLEAN, default: false
30
+ option :ignore_certificate, description: 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass]
31
+ option :warn_insecure, description: 'HTTP/S: Issue a warning if certificate is ignored', allowed: Type::BOOLEAN, default: true
32
+ option :cert_stores, description: 'HTTP/S: List of folder with trusted certificates', allowed: Type::STRING_ARRAY
33
+ option :http_options, schema: Schema::Registry::HTTP_OPTIONS
34
+ option :http_proxy, description: 'HTTP/S: URL for proxy with optional credentials'
35
+
26
36
  def initialize
27
37
  @insecure = false
28
38
  @warn_insecure = true
@@ -36,23 +46,6 @@ module Aspera
36
46
  attr_accessor :insecure, :warn_insecure
37
47
  attr_reader :ignore_cert_host_port, :http_options
38
48
 
39
- class << self
40
- # Declare all HTTP/S CLI options (metadata only - no handler binding yet).
41
- # Called once from Config#initialize before this instance is available as a target.
42
- # Handlers are bound in a second pass via bind_options once the instance exists.
43
- # @param options [Aspera::Cli::Parser] CLI options manager to declare options into
44
- # @return [nil]
45
- def declare_options(options)
46
- options.declare(:insecure, description: 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, default: false)
47
- options.declare(:ignore_certificate, description: 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass])
48
- options.declare(:warn_insecure, description: 'HTTP/S: Issue a warning if certificate is ignored', allowed: Allowed::TYPES_BOOLEAN, default: true)
49
- options.declare(:cert_stores, description: 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY)
50
- options.declare(:http_options, schema: Schema::Registry::HTTP_OPTIONS)
51
- options.declare(:http_proxy, description: 'HTTP/S: URL for proxy with optional credentials')
52
- nil
53
- end
54
- end
55
-
56
49
  # Bind all HTTP options to this instance using set_handler.
57
50
  # Called from Config#initialize immediately after Http.new.
58
51
  # @param options [Aspera::Cli::Parser]
@@ -113,7 +106,7 @@ module Aspera
113
106
  def ignore_cert_host_port=(url_list)
114
107
  url_list.each do |url|
115
108
  uri = URI.parse(url)
116
- Aspera.assert(uri.scheme.eql?('https')){"Expecting https scheme: #{url}"}
109
+ Aspera.assert(uri.scheme.eql?('https')) { "Expecting https scheme: #{url}" }
117
110
  @ignore_cert_host_port.push([uri.host, uri.port].freeze)
118
111
  end
119
112
  end
@@ -126,11 +119,11 @@ module Aspera
126
119
  if ignore_cert && @warn_insecure
127
120
  base_url = "https://#{address}:#{port}"
128
121
  unless @ssl_warned_urls.include?(base_url)
129
- Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
122
+ Log.log.warn { "Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production." }
130
123
  @ssl_warned_urls.push(base_url)
131
124
  end
132
125
  end
133
- Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
126
+ Log.log.debug { "ignore cert? #{endpoint} -> #{ignore_cert}" }
134
127
  ignore_cert
135
128
  end
136
129
 
@@ -142,21 +135,21 @@ module Aspera
142
135
  # @param path_list [Array<String>] list of file/folder paths to add to the certificate store
143
136
  # @return [nil]
144
137
  def trusted_cert_locations=(path_list)
145
- Aspera.assert_type(path_list, Array){'cert locations'}
138
+ Aspera.assert_type(path_list, Array) { 'cert locations' }
146
139
  if @certificate_store.nil?
147
140
  Log.log.debug('Creating SSL Cert store')
148
141
  @certificate_store = OpenSSL::X509::Store.new
149
142
  @certificate_paths = []
150
143
  end
151
144
  path_list.each do |path|
152
- Aspera.assert_type(path, String){'Expecting a String for certificate location'}
145
+ Aspera.assert_type(path, String) { 'Expecting a String for certificate location' }
153
146
  paths_to_add = [path]
154
- Log.log.debug{"Adding cert location: #{path}"}
147
+ Log.log.debug { "Adding cert location: #{path}" }
155
148
  if path.eql?(SpecialValues::DEF)
156
149
  @certificate_store.set_default_paths
157
150
  paths_to_add = [OpenSSL::X509::DEFAULT_CERT_DIR]
158
151
  paths_to_add.push(OpenSSL::X509::DEFAULT_CERT_FILE) unless defined?(JRUBY_VERSION)
159
- paths_to_add.select!{ |f| File.exist?(f)}
152
+ paths_to_add.select! { |f| File.exist?(f) }
160
153
  elsif File.file?(path)
161
154
  @certificate_store.add_file(path)
162
155
  elsif File.directory?(path)
@@ -168,9 +161,9 @@ module Aspera
168
161
  pp = [File.realpath(p)]
169
162
  if File.directory?(p)
170
163
  pp = Dir.entries(p)
171
- .map{ |e| File.realpath(File.join(p, e))}
172
- .select{ |entry| File.file?(entry)}
173
- .select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
164
+ .map { |e| File.realpath(File.join(p, e)) }
165
+ .select { |entry| File.file?(entry) }
166
+ .select { |entry| CERT_EXT.any? { |ext| entry.end_with?(ext) } }
174
167
  end
175
168
  @certificate_paths.concat(pp)
176
169
  end
@@ -202,13 +195,13 @@ module Aspera
202
195
  http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
203
196
  http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
204
197
  http_session.cert_store = @certificate_store if @certificate_store
205
- Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
198
+ Log.log.debug { "Using cert store #{http_session.cert_store} (#{@certificate_store})" } unless http_session.cert_store.nil?
206
199
  @http_options.each do |k, v|
207
200
  method = "#{k}=".to_sym
208
201
  if http_session.respond_to?(method)
209
202
  http_session.send(method, v)
210
203
  else
211
- Log.log.error{"Unknown HTTP session attribute: #{k}"}
204
+ Log.log.error { "Unknown HTTP session attribute: #{k}" }
212
205
  end
213
206
  end
214
207
  nil
@@ -32,16 +32,16 @@ module Aspera
32
32
  # Change keys from string into symbol
33
33
  smtp = smtp.symbolize_keys
34
34
  unsupported = smtp.keys - SMTP_CONF_PARAMS
35
- Aspera.assert(unsupported.empty?, type: Cli::Error){"Unsupported SMTP parameter: #{unsupported.join(', ')}, use: #{SMTP_CONF_PARAMS.join(', ')}"}
35
+ Aspera.assert(unsupported.empty?, type: Cli::Error) { "Unsupported SMTP parameter: #{unsupported.join(', ')}, use: #{SMTP_CONF_PARAMS.join(', ')}" }
36
36
  # Boolean fields must be actual booleans, not strings like "false"
37
37
  SMTP_BOOL_PARAMS.each do |k|
38
- Aspera.assert_values(smtp[k], [true, false], type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
38
+ Aspera.assert_values(smtp[k], [true, false], type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
39
39
  end
40
40
  SMTP_INT_PARAMS.each do |k|
41
- Aspera.assert_type(smtp[k], Integer, type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
41
+ Aspera.assert_type(smtp[k], Integer, type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
42
42
  end
43
43
  SMTP_STR_PARAMS.each do |k|
44
- Aspera.assert_type(smtp[k], String, type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
44
+ Aspera.assert_type(smtp[k], String, type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
45
45
  end
46
46
  # smtp[:ssl] = nil (false)
47
47
  smtp[:tls] = !smtp[:ssl] unless smtp.key?(:tls)
@@ -56,9 +56,9 @@ module Aspera
56
56
  smtp[:from_name] ||= smtp[:from_email].sub(/@.*$/, '').gsub(/[^a-zA-Z]/, ' ').capitalize if smtp.key?(:username)
57
57
  smtp[:domain] ||= smtp[:from_email].sub(/^.*@/, '') if smtp.key?(:from_email)
58
58
  %i[server port domain].each do |n|
59
- Aspera.assert(smtp.key?(n)){"Missing mandatory smtp parameter: #{n}"}
59
+ Aspera.assert(smtp.key?(n)) { "Missing mandatory smtp parameter: #{n}" }
60
60
  end
61
- Log.log.debug{"smtp=#{smtp}"}
61
+ Log.dump(:smtp, smtp.reject { |k, _| k == :password })
62
62
  return smtp
63
63
  end
64
64
 
@@ -72,7 +72,7 @@ module Aspera
72
72
  values[:from_name] ||= mail_conf[:from_name]
73
73
  values[:from_email] ||= mail_conf[:from_email]
74
74
  %i[to from_email].each do |n|
75
- Aspera.assert_type(values[n], String){"Missing email parameter: #{n} in config"}
75
+ Aspera.assert_type(values[n], String) { "Missing email parameter: #{n} in config" }
76
76
  end
77
77
  start_options = [mail_conf[:domain]]
78
78
  start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
@@ -6,11 +6,13 @@ require 'json'
6
6
  require 'aspera/log'
7
7
  require 'aspera/cli/runner'
8
8
  require 'aspera/cli/error'
9
+ require 'aspera/schema/reader'
10
+ require 'aspera/schema/registry'
9
11
  unless defined?(MCP::Tool)
10
12
  begin
11
13
  require 'mcp'
12
14
  rescue LoadError
13
- raise Cli::Error, "The 'mcp' gem is required. Install it with: gem install mcp"
15
+ raise Cli::Error, "The 'mcp' and 'rack' gems are required. Install them with: gem install mcp rack"
14
16
  end
15
17
  end
16
18
 
@@ -36,14 +38,16 @@ module Aspera
36
38
  Element 0 : plugin name (aoc, faspex5, node, server, config, …).
37
39
  Elements 1+: sub-commands, then --option=value flags in any order.
38
40
  Passing structured values: use an extended-value prefix on the relevant element:
39
- "@json:{...}" — inline JSON object or array (use for Hash/Array arguments)
41
+ "@json:{...}" — inline JSON object or array (preferred for LLMs: no shell quoting, natural JSON)
40
42
  "@preset:name" — expand a saved credential preset
41
43
  "@env:VAR" — read value from environment variable
42
44
  "@file:/path" — read value from a file
45
+ Note: the dot-path form (@: key=value ...) is designed for humans typing in a terminal shell.
46
+ Prefer @json: when building args programmatically or as an LLM.
43
47
 
44
48
  AUTOMATIC FLAGS
45
49
  The server automatically prepends extra_args to every call (default:
46
- --interactive=no and --transfer.asynchronous=true).
50
+ #{DEFAULT_EXTRA_ARGS.join(' ')}).
47
51
  Do NOT repeat these flags in your args array — they are already injected.
48
52
  If you explicitly need to override one (e.g. --interactive=yes), include it
49
53
  in your args; your value will take precedence because it appears after the
@@ -175,10 +179,33 @@ module Aspera
175
179
  else
176
180
  MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}])
177
181
  end
182
+ rescue Cli::SchemaRequest => e
183
+ schema_path = e.path
184
+ if schema_path.nil?
185
+ MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{e.message} (no schema available)"}], error: true)
186
+ else
187
+ rows = schema_to_rows(Schema::Registry.instance.reader(schema_path))
188
+ structured = {items: rows}
189
+ MCP::Tool::Response.new([{type: 'text', text: JSON.generate(rows)}], structured_content: structured)
190
+ end
178
191
  rescue SystemExit => e
179
192
  MCP::Tool::Response.new([{type: 'text', text: "exited with status #{e.status}"}], error: !e.status.zero?)
180
193
  rescue => e
181
- MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{e.message}"}], error: true)
194
+ MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{dedupe_lines(e.message)}"}], error: true)
195
+ end
196
+
197
+ # Delegate to Schema::Reader#to_rows — semantic fields, no ANSI, MCP/JSON-ready.
198
+ def schema_to_rows(reader)
199
+ reader.to_rows
200
+ end
201
+
202
+ # Collapse consecutive duplicate lines in a multi-line message.
203
+ # Each run of identical lines is replaced by one line + "(×N)" suffix when N > 1.
204
+ def dedupe_lines(msg)
205
+ return msg unless msg.include?("\n")
206
+ msg.split("\n").chunk_while { |a, b| a == b }.map do |group|
207
+ group.size > 1 ? "#{group.first} (x#{group.size})" : group.first
208
+ end.join("\n")
182
209
  end
183
210
 
184
211
  # Returns the largest prefix of +items+ whose JSON serialization fits within +max_bytes+.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/parser'
4
+ require 'aspera/cli/command_spec'
5
+
6
+ module Aspera
7
+ module Cli
8
+ # Mixin providing declarative CLI option definitions (`option :name, ...`)
9
+ # and batch declaration onto a Parser instance (`declare_options(parser)`).
10
+ module OptionDeclarator
11
+ # Registry of OptionSpec objects defined on this class/module.
12
+ # @return [Hash{Symbol => OptionSpec}]
13
+ def option_specs
14
+ @option_specs ||= {}
15
+ end
16
+
17
+ # Declare an option in this class's registry.
18
+ #
19
+ # @param name [Symbol] Option name
20
+ # @param description [String, nil] User-facing description
21
+ # @param short [String, nil] Single-character short form
22
+ # @param allowed [Object, nil] Allowed values
23
+ # @param default [Object, nil] Default value
24
+ # @param handler [Symbol, Hash, nil] Handler (Symbol or Hash)
25
+ # @param deprecation [String, nil] Deprecation message
26
+ # @param schema [String, nil] Schema reference
27
+ def option(name, description: nil,
28
+ short: nil, allowed: nil, default: nil,
29
+ handler: nil, deprecation: nil, schema: nil)
30
+ raise ArgumentError, "Duplicate option: #{name.inspect}" if option_specs.key?(name)
31
+ option_specs[name] = OptionSpec.new(
32
+ name: name,
33
+ description: description,
34
+ short: short,
35
+ allowed: allowed,
36
+ default: default,
37
+ handler: handler,
38
+ deprecation: deprecation,
39
+ schema: schema
40
+ )
41
+ end
42
+
43
+ # Declare all options registered on this class onto a Parser instance.
44
+ # Skips options already declared on the parser.
45
+ #
46
+ # @param parser [Aspera::Cli::Parser]
47
+ # @param target [Object, nil] default target object for Symbol handlers (defaults to self)
48
+ # @return [void]
49
+ def declare_options(parser, target: self)
50
+ option_specs.each_value do |spec|
51
+ next if parser.option_declared?(spec.name)
52
+ resolved_handler =
53
+ case spec.handler
54
+ when Symbol then {o: target, m: spec.handler}
55
+ when Hash then spec.handler
56
+ end
57
+ parser.declare(
58
+ spec.name,
59
+ description: spec.description,
60
+ short: spec.short,
61
+ allowed: spec.allowed,
62
+ default: spec.default,
63
+ handler: resolved_handler,
64
+ deprecation: spec.deprecation,
65
+ schema: schema_for_spec(spec)
66
+ )
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def schema_for_spec(spec)
73
+ spec.schema
74
+ end
75
+ end
76
+ end
77
+ end