aspera-cli 4.27.2 → 4.27.4

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 +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -9,6 +9,8 @@ require 'aspera/assert'
9
9
  require 'aspera/yaml'
10
10
  require 'digest'
11
11
  require 'yaml'
12
+ require 'aspera/rainbow'
13
+ using Rainbow
12
14
 
13
15
  module Aspera
14
16
  module Cli
@@ -57,30 +59,38 @@ module Aspera
57
59
 
58
60
  # Read and validate the YAML config file.
59
61
  # Sets @config_presets and @checksum_on_disk.
62
+ # A file that cannot be read (YAML error, duplicate keys, access) is left untouched: it shall be fixed.
63
+ # A file with an unexpected structure is renamed for manual conversion, and a new one is created next time.
60
64
  def read_config_file
61
65
  Log.log.debug { "config file is: #{@config_file}".red }
62
66
  if File.exist?(@config_file)
63
67
  Log.log.debug { "loading #{@config_file}" }
64
- @config_presets = Yaml.safe_load(File.read(@config_file))
68
+ begin
69
+ @config_presets = Yaml.safe_load(File.read(@config_file))
70
+ rescue Psych::SyntaxError
71
+ Log.log.error { "YAML error in config file: #{@config_file}" }
72
+ raise
73
+ rescue StandardError => e
74
+ raise Cli::Error, "Config file #{@config_file}: #{e.message}"
75
+ end
65
76
  @checksum_on_disk = checksum
66
77
  else
67
78
  Log.log.warn { "No config file found. New configuration file: #{@config_file}" }
68
79
  @config_presets = {Key::CONFIG => {Key::VERSION => 'new file'}}
69
80
  # @checksum_on_disk remains nil: will be saved on first write
70
81
  end
71
- validate_config_presets!
72
- rescue Psych::SyntaxError => e
73
- Log.log.error('YAML error in config file')
74
- raise e
75
- rescue StandardError => e
76
- Log.log.debug { "-> #{e.class.name} : #{e}" }
77
- if File.exist?(@config_file)
78
- new_name = "#{@config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
79
- File.rename(@config_file, new_name)
80
- Log.log.warn { "Renamed config file to #{new_name}." }
81
- Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
82
+ begin
83
+ validate_config_presets!
84
+ rescue StandardError => e
85
+ Log.log.debug { "-> #{e.class.name} : #{e}" }
86
+ if File.exist?(@config_file)
87
+ new_name = "#{@config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
88
+ File.rename(@config_file, new_name)
89
+ Log.log.warn { "Renamed config file to #{new_name}." }
90
+ Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
91
+ end
92
+ raise Cli::Error, e.to_s
82
93
  end
83
- raise Cli::Error, e.to_s
84
94
  end
85
95
 
86
96
  # Save to disk only if content changed since last load/save.
@@ -113,8 +123,8 @@ module Aspera
113
123
  Log.log.error do
114
124
  "Default config name [#{default_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
115
125
  "Please fix: either create preset:\n" \
116
- "#{Info::CMD_NAME} config id #{default_name} init @json:'{}'\n" \
117
- "or remove default:\n#{Info::CMD_NAME} config id default remove #{plugin_name_sym}"
126
+ "#{Info::CMD_NAME} config preset initialize #{default_name} @json:'{}'\n" \
127
+ "or remove default:\n#{Info::CMD_NAME} config preset unset #{Key::DEFAULTS} #{plugin_name_sym}"
118
128
  end
119
129
  raise Cli::Error, "No such preset: #{default_name}"
120
130
  end
@@ -130,7 +140,11 @@ module Aspera
130
140
  Aspera.assert(!include_path.include?(config_name), type: Cli::Error) { 'loop in include' }
131
141
  include_path = include_path.clone
132
142
  current = @config_presets
133
- config_name.split(PRESET_DIG_SEPARATOR).each do |name|
143
+ # A preset name may contain a dot (e.g. a host name): the longest name of an existing preset is used
144
+ parts = config_name.split(PRESET_DIG_SEPARATOR)
145
+ count = parts.length.downto(1).find { |n| @config_presets.key?(parts.first(n).join(PRESET_DIG_SEPARATOR)) } || 1
146
+ names = [parts.first(count).join(PRESET_DIG_SEPARATOR)] + parts.drop(count)
147
+ names.each do |name|
134
148
  Aspera.assert_type(current, Hash, type: Cli::Error) { "sub key: #{include_path}" }
135
149
  include_path.push(name)
136
150
  current = current[name]
@@ -186,16 +200,27 @@ module Aspera
186
200
  nil
187
201
  end
188
202
 
189
- # @return [String] name of the global default preset, creating it if needed
190
- def global_default_preset
203
+ # @param create [Boolean] `true`: declare and create the global default preset if needed (to write in it)
204
+ # @return [String] name of the global default preset
205
+ def global_default_preset(create: true)
191
206
  result = plugin_default_name(CONF_GLOBAL_SYM)
192
207
  if result.nil?
193
208
  result = Key::GLOBAL
194
- set_key(Key::DEFAULTS, CONF_GLOBAL_SYM, result)
209
+ if create
210
+ set_key(Key::DEFAULTS, CONF_GLOBAL_SYM, result)
211
+ # A default declaration requires the preset to exist
212
+ @config_presets[result] ||= {}
213
+ end
195
214
  end
196
215
  result
197
216
  end
198
217
 
218
+ # @return [Boolean] true if `preset_name` is declared as the global default preset (does not create it)
219
+ def global_preset?(preset_name)
220
+ defaults = @config_presets[Key::DEFAULTS]
221
+ defaults.is_a?(Hash) && defaults[CONF_GLOBAL_SYM.to_s].eql?(preset_name)
222
+ end
223
+
199
224
  # Set param in the global defaults preset
200
225
  def set_global_default(key, value)
201
226
  set_key(global_default_preset, key, value)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ # Console input
9
+ module Prompt
10
+ # Prompt user for console input
11
+ # The prompt is displayed on `stderr`, like for sensitive input, so that `stdout` contains only results.
12
+ # @param prompt [String] prompt string to display
13
+ # @param sensitive [Boolean] whether to hide typed input
14
+ # @return [String] user input stripped of trailing newline
15
+ def prompt_user_input(prompt, sensitive: false)
16
+ return $stdin.getpass("#{prompt}> ") if sensitive
17
+ $stderr.print("#{prompt}> ")
18
+ line = $stdin.gets
19
+ Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
20
+ line.chomp
21
+ end
22
+
23
+ # Prompt user for input in a list of symbols
24
+ # @param prompt [String] prompt to display
25
+ # @param sym_list [Array] list of symbols to select from
26
+ # @return [Symbol] selected symbol
27
+ def prompt_user_input_in_list(prompt, sym_list)
28
+ loop do
29
+ input = prompt_user_input(prompt).to_sym
30
+ return input if sym_list.any? { |a| a.eql?(input) }
31
+ $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -20,9 +20,11 @@ module Aspera
20
20
  class Result
21
21
  attr_reader :data, :fields
22
22
 
23
- # @param data [Object,nil] The result data
24
- # @param fields [Object] Specification of fields to include
23
+ # @param data [nil, Object] The result data
24
+ # @param fields [nil, Array<String>, Proc] Specification of fields to include
25
25
  def initialize(data: nil, fields: nil)
26
+ Aspera.assert_type(fields, NilClass, Array, Proc)
27
+ Aspera.assert_array_all(fields, String) if fields.is_a?(Array)
26
28
  @data = data
27
29
  @fields = fields
28
30
  end
@@ -39,18 +41,9 @@ module Aspera
39
41
  # Apply field filtering once for formats that need it
40
42
  filtered_data = formatter.filter_list_on_fields(@data)
41
43
  case formatter.format_type
42
- when :text
43
- formatter.display_message(:data, @data.to_s)
44
- when :nagios
45
- Nagios.process(@data)
46
- when :ruby
47
- formatter.display_message(:data, PP.pp(filtered_data, +''))
48
- when :json
49
- formatter.display_message(:data, JSON.generate(filtered_data))
50
- when :jsonpp
51
- formatter.display_message(:data, JSON.pretty_generate(filtered_data))
52
- when :yaml
53
- formatter.display_message(:data, YAML.dump(filtered_data))
44
+ when :nagios then Nagios.process(@data)
45
+ when :text then formatter.display_message(:data, @data.to_s)
46
+ when *STRUCTURED_FORMATS then display_structured(formatter, filtered_data)
54
47
  when :image
55
48
  if @data.nil?
56
49
  formatter.display_message(:data, formatter.special_format('null (no image)'))
@@ -82,10 +75,30 @@ module Aspera
82
75
  # here, data is the image blob
83
76
  formatter.display_message(:data, Preview::Terminal.build(data, **formatter.image_options))
84
77
  end
85
- else
86
- Aspera.error_unexpected_value(formatter.format_type) { 'format' }
78
+ else Aspera.error_unexpected_value(formatter.format_type) { 'format' }
87
79
  end
88
80
  end
81
+
82
+ # Formats rendering the data structure itself
83
+ STRUCTURED_FORMATS = %i[ruby json jsonpp yaml].freeze
84
+
85
+ private
86
+
87
+ # Display a value in one of STRUCTURED_FORMATS
88
+ # @param formatter [Formatter] The formatter to use
89
+ # @param value [Object] Value to render
90
+ # @return [nil]
91
+ def display_structured(formatter, value)
92
+ text =
93
+ case formatter.format_type
94
+ when :ruby then PP.pp(value, +'')
95
+ when :json then JSON.generate(value)
96
+ when :jsonpp then JSON.pretty_generate(value)
97
+ when :yaml then YAML.dump(value)
98
+ else Aspera.error_unexpected_value(formatter.format_type) { 'structured format' }
99
+ end
100
+ formatter.display_message(:data, text)
101
+ end
89
102
  end
90
103
 
91
104
  # Special result types - each has its own class
@@ -101,8 +114,11 @@ module Aspera
101
114
 
102
115
  def format(formatter)
103
116
  case formatter.format_type
104
- when :text, :nagios, :ruby, :json, :jsonpp, :yaml
117
+ when :text, :nagios
105
118
  formatter.display_message(:data, @data.to_s)
119
+ when *STRUCTURED_FORMATS
120
+ # An empty list, or no value
121
+ display_structured(formatter, @data.eql?(:empty) ? [] : nil)
106
122
  when :table, :csv
107
123
  if @data.eql?(:nothing)
108
124
  Log.log.debug('no result expected')
@@ -137,8 +153,10 @@ module Aspera
137
153
  def format(formatter)
138
154
  data = formatter.hide_secrets? ? formatter.hide_secrets_in_string(@data) : @data
139
155
  case formatter.format_type
140
- when :text, :nagios, :ruby, :json, :jsonpp, :yaml
156
+ when :text, :nagios
141
157
  formatter.display_message(:data, data)
158
+ when *STRUCTURED_FORMATS
159
+ display_structured(formatter, data)
142
160
  when :table, :csv
143
161
  formatter.display_message(:info, data)
144
162
  else
@@ -166,14 +184,8 @@ module Aspera
166
184
  case formatter.format_type
167
185
  when :text, :nagios
168
186
  formatter.display_message(:data, data.to_s)
169
- when :ruby
170
- formatter.display_message(:data, PP.pp(data, +''))
171
- when :json
172
- formatter.display_message(:data, JSON.generate(data))
173
- when :jsonpp
174
- formatter.display_message(:data, JSON.pretty_generate(data))
175
- when :yaml
176
- formatter.display_message(:data, YAML.dump(data))
187
+ when *STRUCTURED_FORMATS
188
+ display_structured(formatter, data)
177
189
  when :table, :csv
178
190
  formatter.display_message(:data, data)
179
191
  else
@@ -228,7 +240,7 @@ module Aspera
228
240
  end
229
241
 
230
242
  # Object list result (Array of Hash)
231
- # @note The +total+ parameter is used to display pagination information (e.g., "Items: 10/100")
243
+ # @note The `total` parameter is used to display pagination information (e.g., "Items: 10/100")
232
244
  class ObjectList < Result
233
245
  attr_reader :total
234
246
 
@@ -276,14 +288,8 @@ module Aspera
276
288
  formatter.display_table(@data.map { |i| {@name => i} }, [@name])
277
289
  when :text
278
290
  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))
291
+ when *STRUCTURED_FORMATS
292
+ display_structured(formatter, @data)
287
293
  else
288
294
  super
289
295
  end
@@ -299,7 +305,7 @@ module Aspera
299
305
  # @param id_result [String] Key in the result Hash used as item identifier
300
306
  # @param fields [Object] Fields hint passed to the Result constructor (non-bulk only, when not :default)
301
307
  # @param bfail [Boolean] When true, re-raise errors; when false, capture them as a status string
302
- # @yieldparam item [Object] Each item in +items+
308
+ # @yieldparam item [Object] Each item in `items`
303
309
  # @yieldreturn [Hash, Array, nil] REST response; a Hash replaces the default id-keyed result
304
310
  # @return [Result::ObjectList, Result::SingleObject]
305
311
  def bulk(items, is_bulk:, command:, id_result: 'id', fields: :default, bfail: true)
@@ -166,7 +166,7 @@ module Aspera
166
166
  rescue Cli::SchemaRequest => e; exception_info = {e: e, t: 'Schema'}
167
167
  rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
168
168
  rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
169
- rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
169
+ rescue Rest::CallError => e; exception_info = {e: e, t: 'Rest'}
170
170
  rescue SocketError => e; exception_info = {e: e, t: 'Network'}
171
171
  rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
172
172
  rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
@@ -228,29 +228,19 @@ module Aspera
228
228
  Result::Text.new(usage_text(plugin: plugin))
229
229
  end
230
230
 
231
- # Composite option handler for the `log` option (dot-notation sub-properties).
232
- # Supported sub-properties: +level+, +type+, +format+
233
- # @param _option_sym [Symbol] Option name (unused, always :log)
234
- # @param operation [Symbol] +:set+ or +:get+
235
- # @param value [Hash,nil] Hash of sub-properties to set (only for +:set+)
236
- def option_log(_option_sym, operation, value = nil)
237
- Aspera.assert_values(operation, %i[set get])
238
- case operation
239
- when :set
240
- Aspera.assert_type(value, Hash)
241
- value.each do |k, v|
242
- case k.to_sym
243
- when :level then Log.instance.level = v.to_sym
244
- when :type then Log.instance.logger_type = v.to_sym
245
- when :format then Log.instance.formatter = v
246
- when :secrets then SecretHider.instance.log_secrets = BoolValue.true?(v)
247
- else Aspera.error_unexpected_value(k) { 'log sub-option (level, type, format, secrets)' }
248
- end
231
+ # `on_set` callback of the `log` option (dot-notation sub-properties): `level`, `type`, `format`, `secrets`
232
+ # @param value [Hash] sub-properties to set
233
+ def option_log=(value)
234
+ Aspera.assert_type(value, Hash)
235
+ value.each do |k, v|
236
+ case k.to_sym
237
+ when :level then Log.instance.level = v.to_sym
238
+ when :type then Log.instance.logger_type = v.to_sym
239
+ when :format then Log.instance.formatter = v
240
+ when :secrets then SecretHider.instance.log_secrets = Parser.get_from_list(v.to_s, 'log secrets', BoolValue::ALL)
241
+ else Aspera.error_unexpected_value(k) { 'log sub-option (level, type, format, secrets)' }
249
242
  end
250
- when :get
251
- return {level: Log.instance.level, type: Log.instance.logger_type, format: Log.instance.formatter, secrets: SecretHider.instance.log_secrets}
252
243
  end
253
- nil
254
244
  end
255
245
 
256
246
  private
@@ -279,25 +269,11 @@ module Aspera
279
269
  registry = plugin.class.command_registry
280
270
  cmds = registry.children_of(path)
281
271
  label = plugin.class.name.split('::').last.downcase
282
- # Build label with positional argument slots inserted after each intermediate node
272
+ # Build label with positional argument slots inserted after each command
283
273
  # e.g. [:access_keys, :do, :download] -> "node access_keys do <access_key_id> download"
284
- path.each_with_index do |seg, i|
285
- label += " #{seg}"
286
- seg_path = path[0..i]
287
- seg_spec = registry[seg_path]
288
- if seg_spec && registry.children_of(seg_path).any? && seg_spec.arguments
289
- seg_spec.arguments.each do |arg_spec|
290
- label += " <#{arg_spec.name}>"
291
- end
292
- end
293
- end
294
- if cmds.none?
295
- leaf_spec = registry[path]
296
- leaf_spec&.arguments&.each do |arg_spec|
297
- argument = arg_spec.mandatory ? "<#{arg_spec.name}>" : "[#{arg_spec.name}]"
298
- argument += '...' if arg_spec.multiple
299
- label += " #{argument}"
300
- end
274
+ path.each_index do |i|
275
+ label += " #{path[i]}"
276
+ registry.arguments_at(path[0..i]).each { |arg_spec| label += " #{arg_spec.syntax}" }
301
277
  end
302
278
  if cmds.any?
303
279
  # Intermediate node: show current command + description, then list subcommands
@@ -314,7 +290,7 @@ module Aspera
314
290
  spec = registry[path]
315
291
  lines << "\nCOMMAND: #{label}"
316
292
  lines << " #{spec.description}" if spec&.description
317
- display_args = spec&.arguments || []
293
+ display_args = registry.arguments_at(path)
318
294
  # transfer_paths commands use --sources for the file list; default is positional args (@args)
319
295
  if spec&.transfer_paths
320
296
  file_desc = if spec.transfer_paths == :receive
@@ -372,9 +348,7 @@ module Aspera
372
348
  # Create command line manager with arguments
373
349
  @context.options = Parser.new(Info::CMD_NAME, @argv)
374
350
  ExtendedValue.instance.on(EXTEND_ARGS) { |v| @context.options.args_as_extended(v) }
375
- # Formatter: declare metadata (class method), then bind to the instance
376
- Formatter.declare_options(@context.options)
377
- @context.formatter.bind_options(@context.options)
351
+ @context.formatter.declare_options(@context.options)
378
352
  # Compare $0 with expected name
379
353
  current_prog_name = File.basename($PROGRAM_NAME)
380
354
  Aspera.assert(current_prog_name.eql?(Info::CMD_NAME), type: :warn) { "Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'" }
@@ -395,15 +369,13 @@ module Aspera
395
369
  @context.config = Plugins::Config.new(context: @context)
396
370
  @context.man_header = true
397
371
  # Sync cache_tokens from Config into the OAuth persist_mgr (now that option is parsed)
398
- OAuth::Factory.instance.persist_mgr = @context.persistency if @context.config.option_cache_tokens
372
+ OAuth::Factory.instance.persist_mgr = @context.persistency if @context.options.get_option(:cache_tokens)
399
373
  # Email service: depends on options declared by Config
400
374
  @context.mailer = Mailer.new(@context.options, @context.main_folder)
401
375
  # Secret finder: depends on options (:secret) and presets, both set by Bootstrapper
402
376
  @context.secret_finder = SecretFinder.new(@context.options, @context.presets)
403
377
  # The TransferAgent plugin may use the @preset parser
404
378
  @context.transfer = TransferAgent.new(@context)
405
- # Add commands for config plugin after all options have been added
406
- @context.config.add_manual_header(false)
407
379
  @context.validate
408
380
  # Set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
409
381
  end
@@ -450,32 +422,33 @@ module Aspera
450
422
  Log.log.debug('declare_global_options')
451
423
  @context.options.declare(:help, description: 'Show this message', allowed: Type::NONE, short: 'h') do
452
424
  @option_help = true
453
- @context.options.help_requested = true
425
+ @context.help_requested = true
454
426
  end
455
427
  @context.options.declare(:show_config, description: 'Display parameters used for the provided action', allowed: Type::NONE) { @option_show_config = true }
456
428
  @context.options.declare(:version, description: 'Display version', allowed: Type::NONE, short: 'v') { @context.formatter.display_message(:data, Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon
457
429
  @context.options.declare(
458
430
  :ui, description: 'Method to start browser',
459
- allowed: USER_INTERFACES,
460
- handler: {o: Environment.instance, m: :url_method}
431
+ allowed: USER_INTERFACES, default: Environment.instance.url_method,
432
+ on_set: Environment.instance.method(:url_method=)
461
433
  )
462
434
  @context.options.declare(
463
435
  :invalid_characters, description: 'Replacement character and invalid filename characters',
464
- handler: {o: Environment.instance, m: :file_illegal_characters}
436
+ default: Environment.instance.file_illegal_characters,
437
+ on_set: Environment.instance.method(:file_illegal_characters=)
465
438
  )
466
- @context.options.declare(:log_level, description: 'Log level', allowed: Log::LEVELS, handler: {o: Log.instance, m: :level})
467
- @context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
468
- @context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
469
- @context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', handler: {o: self, m: :option_log}, schema: Schema::Registry::LOG_OPTIONS)
439
+ @context.options.declare(:log_level, description: 'Log level', allowed: Log::LEVELS, default: Log.instance.level, on_set: Log.instance.method(:level=))
440
+ @context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], on_set: Log.instance.method(:formatter=))
441
+ @context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, on_set: Log.instance.method(:logger_type=))
442
+ @context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', on_set: method(:option_log=), schema: Schema::Registry::LOG_OPTIONS)
470
443
  @context.options.declare(:lock_port, description: 'Prevent dual execution of a command, e.g. in cron', allowed: Type::INTEGER)
471
444
  @context.options.declare(:once_only, description: 'Process only new items (some commands)', allowed: Type::BOOLEAN, default: false)
472
- @context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Type::BOOLEAN, handler: {o: SecretHider.instance, m: :log_secrets})
473
- @context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Type::BOOLEAN, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
474
- @context.options.declare(:temp_folder, description: 'Temporary folder', handler: {o: TempFileManager.instance, m: :global_temp})
445
+ @context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Type::BOOLEAN, default: SecretHider.instance.log_secrets, on_set: SecretHider.instance.method(:log_secrets=))
446
+ @context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Type::BOOLEAN, default: TempFileManager.instance.cleanup_on_exit, on_set: TempFileManager.instance.method(:cleanup_on_exit=))
447
+ @context.options.declare(:temp_folder, description: 'Temporary folder', default: TempFileManager.instance.global_temp, on_set: TempFileManager.instance.method(:global_temp=))
475
448
  @context.options.declare(:pid_file, description: 'Write process identifier to file, delete on exit')
476
449
  @context.options.declare(
477
450
  :parser, description: 'Default parser for structured parameters and options',
478
- handler: {o: ExtendedValue.instance, m: :default_decoder},
451
+ on_set: ExtendedValue.instance.method(:default_decoder=),
479
452
  allowed: ExtendedValue::DEFAULT_DECODERS,
480
453
  default: ExtendedValue::DEFAULT_DECODERS.first
481
454
  )
@@ -5,10 +5,15 @@ module Aspera
5
5
  # base class for plugins modules
6
6
  module SpecialValues
7
7
  # special values
8
+ # `INIT` to unitialize with all current IDs
8
9
  INIT = 'INIT'
10
+ # `ALL` means "all ids"
9
11
  ALL = 'ALL'
12
+ # `DEF` for the default list
10
13
  DEF = 'DEF'
14
+ # `END` of arguments
11
15
  EOA = 'END'
16
+ # `LATEST`
12
17
  LATEST = 'LATEST'
13
18
  end
14
19
  end
@@ -16,29 +16,63 @@ module Aspera
16
16
  %w[Syncd Error Confl Pconf] +
17
17
  (23..24).map { |i| "P(#{i})" }).freeze
18
18
  # Positional arguments shared by sync transfer commands (push/pull/bidi) and sync admin commands.
19
- PATH_AND_INFO_ARGS = [{name: :path, type: String}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
20
- # Names of the leaf commands registered under any `sync admin` node.
21
- ADMIN_COMMANDS = %i[find status meta counters file_info overview query].freeze
19
+ PATH_AND_INFO_ARGS = [{name: :path}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
22
20
  # When a plugin class includes SyncActions, register the :sql option
23
- # in that class's DSL registry so Base#initialize auto-declares it.
21
+ # in that class's DSL registry so Base#initialize auto-declares it,
22
+ # and add the class-level DSL methods of ClassMethods.
24
23
  class << self
25
24
  def included(base)
26
25
  base.option(:sql, description: 'SQL suffix appended to sqlite3 queries for admin subcommands (e.g. WHERE clause)')
26
+ base.extend(ClassMethods)
27
27
  end
28
+ end
28
29
 
29
- # DSL helper: register the 7 `sync admin` leaf commands under the given parent path.
30
- # Called at class-load time from any plugin that includes SyncActions.
31
- # @param base [Class] the plugin class (receiver of DSL methods)
32
- # @param admin_path [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
33
- def register_sync_admin_commands(base, admin_path)
34
- base.commands_under(admin_path) do
35
- base.command(:find, description: 'Find sync database files', arguments: [{name: :path, type: String}], action: :action_sync_admin_find)
36
- base.command(:status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_status)
37
- base.command(:meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_meta)
38
- base.command(:counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_counters)
39
- base.command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
40
- base.command(:overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_overview)
41
- base.command(:query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_query)
30
+ # Class-level DSL methods added to the plugin class that includes SyncActions.
31
+ module ClassMethods
32
+ # DSL class method: declare the `sync admin` node and its 7 leaf commands.
33
+ # @param parent [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
34
+ def sync_admin_commands(parent)
35
+ commands_under(parent, description: 'Manage sync database (admin operations)') do
36
+ command(
37
+ :find, description: 'Find sync database files', arguments: [{name: :path}],
38
+ action: lambda do |path:, **|
39
+ dbs = Sync::Operations.list_db_files(path)
40
+ Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
41
+ end
42
+ )
43
+ command(
44
+ :status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS,
45
+ action: ->(path:, sync_info: {}, **) { Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info))) }
46
+ )
47
+ command(
48
+ :meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS,
49
+ action: lambda do |path:, sync_info: {}, **|
50
+ require 'aspera/sync/database'
51
+ Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
52
+ end
53
+ )
54
+ command(
55
+ :counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS,
56
+ action: lambda do |path:, sync_info: {}, **|
57
+ require 'aspera/sync/database'
58
+ Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
59
+ end
60
+ )
61
+ command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
62
+ command(
63
+ :overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS,
64
+ action: lambda do |path:, sync_info: {}, **|
65
+ require 'aspera/sync/database'
66
+ Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
67
+ end
68
+ )
69
+ command(
70
+ :query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS,
71
+ action: lambda do |path:, sync_info: {}, **|
72
+ require 'aspera/sync/database'
73
+ Result.auto(db_from_args(path: path, sync_info: sync_info).execute(options.get_option(:sql, mandatory: true)))
74
+ end
75
+ )
42
76
  end
43
77
  end
44
78
  end
@@ -119,25 +153,6 @@ module Aspera
119
153
  Sync::Database.new(Sync::Operations.session_db_file(sync_info))
120
154
  end
121
155
 
122
- def action_sync_admin_status(path:, sync_info: {}, **)
123
- Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info)))
124
- end
125
-
126
- def action_sync_admin_find(path:, **)
127
- dbs = Sync::Operations.list_db_files(path)
128
- Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
129
- end
130
-
131
- def action_sync_admin_meta(path:, sync_info: {}, **)
132
- require 'aspera/sync/database'
133
- Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
134
- end
135
-
136
- def action_sync_admin_counters(path:, sync_info: {}, **)
137
- require 'aspera/sync/database'
138
- Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
139
- end
140
-
141
156
  def action_sync_admin_file_info(path:, sync_info: {}, **)
142
157
  require 'aspera/sync/database'
143
158
  result = db_from_args(path: path, sync_info: sync_info).file_info(options.get_option(:sql))
@@ -147,16 +162,6 @@ module Aspera
147
162
  Result::ObjectList.new(result, fields: %w[sstate record_id f_meta_path message])
148
163
  end
149
164
 
150
- def action_sync_admin_overview(path:, sync_info: {}, **)
151
- require 'aspera/sync/database'
152
- Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
153
- end
154
-
155
- def action_sync_admin_query(path:, sync_info: {}, **)
156
- require 'aspera/sync/database'
157
- Result.auto(db_from_args(path: path, sync_info: sync_info).execute(options.get_option(:sql, mandatory: true)))
158
- end
159
-
160
165
  # Execute a sync transfer for a given direction.
161
166
  # @param direction [Symbol] one of Sync::Operations::DIRECTIONS (:push, :pull, :bidi)
162
167
  # @param path [String] local or remote path (depends on direction)
@@ -4,6 +4,8 @@ require 'aspera/environment'
4
4
  require 'aspera/formatter_interface'
5
5
  require 'aspera/markdown'
6
6
  require 'aspera/assert'
7
+ require 'aspera/rainbow'
8
+ using Rainbow
7
9
 
8
10
  module Aspera
9
11
  module Cli
@@ -11,9 +13,13 @@ module Aspera
11
13
  # @see FormatterInterface
12
14
  # @see MarkdownFormatter (in build/lib/doc_helper.rb)
13
15
  module TerminalFormatter
14
- HINT = 'HINT:'.bg_green.gray.blink.freeze
15
16
  include FormatterInterface
16
17
 
18
+ # Prefix for hint messages (evaluated on each call, as colors can be enabled or disabled by option)
19
+ def hint
20
+ 'HINT:'.bg(:green).white.blink
21
+ end
22
+
17
23
  # Format boolean with colored symbol (+/- or Y/ )
18
24
  def tick(yes)
19
25
  result =
@@ -29,7 +35,7 @@ module Aspera
29
35
  # Format special values with colors (dim for empty, reverse for others)
30
36
  def special_format(what)
31
37
  result = "<#{what}>"
32
- return %w[null empty].any? { |s| what.include?(s) } ? result.dim : result.reverse_color
38
+ return %w[null empty].any? { |s| what.include?(s) } ? result.faint : result.inverse
33
39
  end
34
40
 
35
41
  # Prepare table row for terminal display (word wrap arrays)
@@ -59,7 +65,7 @@ module Aspera
59
65
  end
60
66
  end
61
67
 
62
- module_function :tick, :special_format, :check_row, :markdown_text
68
+ module_function :hint, :tick, :special_format, :check_row, :markdown_text
63
69
  end
64
70
  end
65
71
  end