aspera-cli 4.26.2 → 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 (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  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 +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -5,6 +5,7 @@ require 'aspera/cli/version'
5
5
  require 'aspera/cli/info'
6
6
  require 'aspera/log'
7
7
  require 'aspera/assert'
8
+ require 'aspera/yaml'
8
9
  require 'digest'
9
10
  require 'yaml'
10
11
 
@@ -39,7 +40,7 @@ module Aspera
39
40
  read_config_file
40
41
  end
41
42
 
42
- attr_reader :config_presets, :use_plugin_defaults
43
+ attr_reader :config_file, :config_presets, :use_plugin_defaults
43
44
  attr_writer :use_plugin_defaults
44
45
 
45
46
  # ------------------------------------------------------------------
@@ -57,12 +58,12 @@ module Aspera
57
58
  Log.log.debug{"config file is: #{@config_file}".red}
58
59
  if File.exist?(@config_file)
59
60
  Log.log.debug{"loading #{@config_file}"}
60
- @config_presets = YAML.load_file(@config_file)
61
+ @config_presets = Yaml.safe_load(File.read(@config_file))
61
62
  @checksum_on_disk = checksum
62
63
  else
63
64
  Log.log.warn{"No config file found. New configuration file: #{@config_file}"}
64
65
  @config_presets = {Key::CONFIG => {Key::VERSION => 'new file'}}
65
- # @checksum_on_disk remains nil will be saved on first write
66
+ # @checksum_on_disk remains nil: will be saved on first write
66
67
  end
67
68
  validate_config_presets!
68
69
  rescue Psych::SyntaxError => e
@@ -82,13 +83,13 @@ module Aspera
82
83
  # Save to disk only if content changed since last load/save.
83
84
  # @return [Boolean] true if actually written
84
85
  def save_if_needed
85
- raise Cli::Error, 'no configuration loaded' if @config_presets.nil?
86
+ Aspera.assert(!@config_presets.nil?, type: Cli::Error){'no configuration loaded'}
86
87
  current = checksum
87
88
  return false if @checksum_on_disk.eql?(current)
88
89
  FileUtils.mkdir_p(File.dirname(@config_file))
89
90
  Environment.restrict_file_access(File.dirname(@config_file))
90
91
  Log.log.info{"Saving config file: #{@config_file}"}
91
- Environment.write_file_restricted(@config_file, force: true){ @config_presets.to_yaml }
92
+ Environment.write_file_restricted(@config_file, force: true){@config_presets.to_yaml}
92
93
  @checksum_on_disk = current
93
94
  true
94
95
  end
@@ -100,10 +101,10 @@ module Aspera
100
101
  # @return [String, nil] name of the default preset for a plugin, or nil
101
102
  def plugin_default_name(plugin_name_sym)
102
103
  Aspera.assert(!@config_presets.nil?, 'config_presets shall be defined')
103
- return nil unless @use_plugin_defaults
104
- return nil unless @config_presets.key?(Key::DEFAULTS)
104
+ return unless @use_plugin_defaults
105
+ return unless @config_presets.key?(Key::DEFAULTS)
105
106
  Aspera.assert_type(@config_presets[Key::DEFAULTS], Hash){'default section'}
106
- return nil unless @config_presets[Key::DEFAULTS].key?(plugin_name_sym.to_s)
107
+ return unless @config_presets[Key::DEFAULTS].key?(plugin_name_sym.to_s)
107
108
  default_name = @config_presets[Key::DEFAULTS][plugin_name_sym.to_s]
108
109
  unless @config_presets.key?(default_name)
109
110
  Log.log.error do
@@ -123,14 +124,14 @@ module Aspera
123
124
  # @param config_name [String]
124
125
  # @param include_path [Array] guard against include loops
125
126
  def by_name(config_name, include_path = [])
126
- raise Cli::Error, 'loop in include' if include_path.include?(config_name)
127
+ Aspera.assert(!include_path.include?(config_name), type: Cli::Error){'loop in include'}
127
128
  include_path = include_path.clone
128
129
  current = @config_presets
129
130
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
130
131
  Aspera.assert_type(current, Hash, type: Cli::Error){"sub key: #{include_path}"}
131
132
  include_path.push(name)
132
133
  current = current[name]
133
- raise Cli::Error, "Unknown config preset: #{include_path}" if current.nil?
134
+ Aspera.assert(!current.nil?, type: Cli::Error){"Unknown config preset: #{include_path}"}
134
135
  end
135
136
  current = self.class.deep_clone(current) unless current.is_a?(String)
136
137
  ExtendedValue.instance.evaluate(current, context: 'preset')
@@ -212,7 +213,7 @@ module Aspera
212
213
 
213
214
  private
214
215
 
215
- # Normalise a URL for comparison
216
+ # Normalize a URL for comparison
216
217
  def canonical_url(url)
217
218
  url.chomp('/').sub(%r{^(https://[^/]+):443$}, '\1')
218
219
  end
@@ -223,7 +224,7 @@ module Aspera
223
224
  Aspera.assert_type(@config_presets, Hash){'config file YAML'}
224
225
  Aspera.assert(@config_presets.key?(Key::CONFIG)){"Cannot find key: #{Key::CONFIG}"}
225
226
  version = @config_presets[Key::CONFIG][Key::VERSION]
226
- raise Cli::Error, 'No version found in config section.' if version.nil?
227
+ Aspera.assert(!version.nil?, type: Cli::Error){'No version found in config section.'}
227
228
  Log.log.debug{"conf version: #{version}"}
228
229
  # Fix bug in 4.4 (creating key "true" in "default" preset)
229
230
  @config_presets[Key::DEFAULTS].delete(true) if @config_presets[Key::DEFAULTS].is_a?(Hash)
@@ -34,7 +34,7 @@ module Aspera
34
34
  # Base implementation handles common formats: text, nagios, ruby, json, jsonpp, yaml
35
35
  # Subclasses should call super first, then handle their specific formats
36
36
  # @param formatter [Formatter] The formatter to use
37
- # @return [void]
37
+ # @return [nil]
38
38
  def format(formatter)
39
39
  # Apply field filtering once for formats that need it
40
40
  filtered_data = formatter.filter_list_on_fields(@data)
@@ -72,14 +72,16 @@ module Aspera
72
72
  rescue URI::InvalidURIError
73
73
  @data
74
74
  end
75
- # try base64
76
- data = begin
77
- Base64.strict_decode64(data)
78
- rescue
79
- data
75
+ unless data.eql?(:done)
76
+ # try base64
77
+ data = begin
78
+ Base64.strict_decode64(data)
79
+ rescue ArgumentError
80
+ data
81
+ end
82
+ # here, data is the image blob
83
+ formatter.display_message(:data, Preview::Terminal.build(data, **formatter.image_options))
80
84
  end
81
- # here, data is the image blob
82
- formatter.display_message(:data, Preview::Terminal.build(data, **formatter.image_options)) unless data.eql?(:done)
83
85
  else
84
86
  Aspera.error_unexpected_value(formatter.format_type){'format'}
85
87
  end
@@ -189,7 +191,7 @@ module Aspera
189
191
 
190
192
  def format(formatter)
191
193
  # Force image format for Image results
192
- formatter.set_format_type(:image)
194
+ formatter.format_type = :image
193
195
  super
194
196
  end
195
197
  end
@@ -234,8 +236,7 @@ module Aspera
234
236
  # @param fields [Array<String>, Proc, nil] Fields to display in table/csv format
235
237
  # @param total [Integer, nil] Total number of items available (for pagination display)
236
238
  def initialize(data, fields: nil, total: nil)
237
- Aspera.assert_type(data, Array){'object list result data'}
238
- raise ArgumentError, 'Object list result requires Array of Hash' unless data.all?(Hash)
239
+ Aspera.assert_array_all(data, Hash, type: ArgumentError){'object list result data'}
239
240
  Aspera.assert_type(total, Integer, NilClass){'total'}
240
241
  super(data: data, fields: fields)
241
242
  @total = total
@@ -273,6 +274,16 @@ module Aspera
273
274
  case formatter.format_type
274
275
  when :table, :csv
275
276
  formatter.display_table(@data.map{ |i| {@name => i}}, [@name])
277
+ when :text
278
+ formatter.display_message(:data, @data.join("\n"))
279
+ when :ruby
280
+ formatter.display_message(:data, PP.pp(@data, +''))
281
+ when :json
282
+ formatter.display_message(:data, JSON.generate(@data))
283
+ when :jsonpp
284
+ formatter.display_message(:data, JSON.pretty_generate(@data))
285
+ when :yaml
286
+ formatter.display_message(:data, YAML.dump(@data))
276
287
  else
277
288
  super
278
289
  end
@@ -280,6 +291,45 @@ module Aspera
280
291
  end
281
292
 
282
293
  class << self
294
+ # Execute a bulk operation over a list of already-resolved items and return a CLI result.
295
+ # This is a pure helper: it reads nothing from the CLI.
296
+ # @param items [Array] List of items to process (one element when non-bulk)
297
+ # @param is_bulk [Boolean] Whether the caller is in bulk mode
298
+ # @param command [Symbol] Operation name (:create, :delete, …) used to build the past-tense status string
299
+ # @param id_result [String] Key in the result Hash used as item identifier
300
+ # @param fields [Object] Fields hint passed to the Result constructor (non-bulk only, when not :default)
301
+ # @param bfail [Boolean] When true, re-raise errors; when false, capture them as a status string
302
+ # @yieldparam item [Object] Each item in +items+
303
+ # @yieldreturn [Hash, Array, nil] REST response; a Hash replaces the default id-keyed result
304
+ # @return [Result::ObjectList, Result::SingleObject]
305
+ def bulk(items, is_bulk:, command:, id_result: 'id', fields: :default, bfail: true)
306
+ Aspera.assert(block_given?, 'missing block')
307
+ Log.log.warn('Empty list given for bulk operation') if items.empty?
308
+ Log.dump(:bulk_operation, items)
309
+ result_list = []
310
+ items.each do |item|
311
+ result = {id_result => item}
312
+ begin
313
+ res = yield(item)
314
+ result = res if res.is_a?(Hash)
315
+ # TODO: remove when faspio gw api fixes this
316
+ result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
317
+ result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
318
+ rescue StandardError => e
319
+ raise e if bfail
320
+ result['status'] = e.to_s
321
+ end
322
+ result_list.push(result)
323
+ end
324
+ display_fields = [id_result, 'status']
325
+ if is_bulk
326
+ return ObjectList.new(result_list, fields: display_fields)
327
+ else
328
+ display_fields = fields unless fields.eql?(:default)
329
+ return SingleObject.new(result_list.first, fields: display_fields)
330
+ end
331
+ end
332
+
283
333
  # Class method to automatically determine result type from data
284
334
  # @param data [Object] the data to analyze and format
285
335
  # @return [Result]