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
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/command_spec'
4
+
5
+ module Aspera
6
+ module Cli
7
+ # Stores CommandSpec objects indexed by their full path (Array<Symbol>).
8
+ # Each plugin class gets its own instance (not shared across the inheritance chain).
9
+ #
10
+ # Public API:
11
+ # register(spec) - store a CommandSpec; raises on duplicate full_path
12
+ # register_option(spec) - store an OptionSpec by name
13
+ # option_specs - Hash{Symbol => OptionSpec} of all registered options
14
+ # [](path) - retrieve a CommandSpec by full path
15
+ # children_of(path) - Hash{Symbol => CommandSpec} of direct children (O(1))
16
+ # all_paths - Array of all registered full paths
17
+ # any? - true if at least one spec has been registered
18
+ # validate! - cross-spec consistency checks; raises on violation
19
+ class CommandRegistry
20
+ # @param path [Array<Symbol>] full path to look up
21
+ # @return [CommandSpec, nil]
22
+ def [](path)
23
+ @specs[Array(path)]
24
+ end
25
+
26
+ # Register a CommandSpec. Raises if the full_path is already registered.
27
+ # Also updates the children index so children_of remains O(1).
28
+ # @param spec [CommandSpec]
29
+ # @raise [ArgumentError] on duplicate full path
30
+ # @return [CommandSpec] the registered spec
31
+ def register(spec)
32
+ path = spec.full_path
33
+ raise ArgumentError, "Duplicate command path: #{path.inspect}" if @specs.key?(path)
34
+ @specs[path] = spec
35
+ # Index: parent_path -> { child_id -> spec }
36
+ parent = path[0..-2] # [] for root-level commands
37
+ (@children_index[parent] ||= {})[spec.id] = spec
38
+ spec
39
+ end
40
+
41
+ # Returns a Hash mapping each child id to its CommandSpec for all direct
42
+ # children of `path`. Empty hash if no children are registered.
43
+ # O(1) lookup via the children index built in register().
44
+ # @param path [Array<Symbol>] parent path ([] for root-level commands)
45
+ # @return [Hash{Symbol => CommandSpec}]
46
+ def children_of(path)
47
+ @children_index[Array(path)] || {}
48
+ end
49
+
50
+ # @return [Array<Array<Symbol>>] all registered full paths
51
+ def all_paths
52
+ @specs.keys
53
+ end
54
+
55
+ # Register an OptionSpec. Raises if the option name is already registered.
56
+ # @param spec [OptionSpec]
57
+ # @raise [ArgumentError] on duplicate option name
58
+ # @return [OptionSpec] the registered spec
59
+ def register_option(spec)
60
+ raise ArgumentError, "Duplicate option: #{spec.name.inspect}" if @option_specs.key?(spec.name)
61
+ @option_specs[spec.name] = spec
62
+ end
63
+
64
+ # @return [Hash{Symbol => OptionSpec}] all registered option specs
65
+ def option_specs
66
+ @option_specs.dup
67
+ end
68
+
69
+ # @return [Boolean] true if at least one spec is registered
70
+ def any?
71
+ !@specs.empty?
72
+ end
73
+
74
+ # @return [Boolean] true if no specs have been registered
75
+ def none?
76
+ @specs.empty?
77
+ end
78
+
79
+ # Cross-spec consistency checks.
80
+ # @param plugin_class [Class, nil] when given, also verify that implicit action methods exist
81
+ # @raise [ArgumentError] on any violation
82
+ # @return [self]
83
+ def validate!(plugin_class: nil)
84
+ # Rule: every non-root parent path that appears in the children index must have
85
+ # a registered CommandSpec. A missing parent means commands_under(:x) was used
86
+ # without a matching command :x declaration.
87
+ @children_index.each_key do |parent_path|
88
+ next if parent_path.empty? # root is never a CommandSpec
89
+ unless @specs.key?(parent_path)
90
+ raise ArgumentError,
91
+ "commands_under(#{parent_path.map(&:inspect).join(', ')}) used but #{parent_path.last.inspect} has no command declaration"
92
+ end
93
+ end
94
+
95
+ @specs.each_value do |spec|
96
+ path = spec.full_path
97
+
98
+ # Rule: delegates_to must point to a known path when present (non-empty array or symbol)
99
+ if spec.delegates_to
100
+ dt_path =
101
+ case spec.delegates_to
102
+ when Symbol then [spec.delegates_to]
103
+ when Array then spec.delegates_to
104
+ end
105
+ # An empty array [] means re-enter the root - always valid
106
+ unless dt_path.empty? || @specs.key?(dt_path)
107
+ raise ArgumentError,
108
+ "#{path.inspect}: delegates_to #{dt_path.inspect} points to unknown path"
109
+ end
110
+ end
111
+
112
+ # Rule: delegate_instance requires delegates_to
113
+ if spec.delegate_instance && spec.delegates_to.nil?
114
+ raise ArgumentError,
115
+ "#{path.inspect}: delegate_instance requires delegates_to to be set"
116
+ end
117
+
118
+ # Rule: leaf commands with no explicit action must have a matching instance method
119
+ next if spec.action # explicit action: skip
120
+ next if @children_index[path]&.any? # intermediate node: skip
121
+ next if spec.delegates_to || spec.entity_execute # delegated: skip
122
+ next unless plugin_class
123
+ implicit_method = CommandSpec.action_method(path)
124
+ unless plugin_class.method_defined?(implicit_method) || plugin_class.private_method_defined?(implicit_method)
125
+ raise ArgumentError,
126
+ "#{path.inspect}: no action: and no method #{implicit_method} on #{plugin_class}"
127
+ end
128
+ end
129
+ self
130
+ end
131
+
132
+ private
133
+
134
+ def initialize
135
+ # Keyed by Array<Symbol> full path
136
+ @specs = {}
137
+ # Keyed by Symbol option name
138
+ @option_specs = {}
139
+ # Children index: parent Array<Symbol> -> Hash{child_id Symbol => CommandSpec}
140
+ # Built incrementally in register(); enables O(1) children_of lookups.
141
+ @children_index = {}
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Cli
5
+ # Declares a positional argument consumed by a command.
6
+ # Declaration order in a command's `arguments:` array defines parsing order.
7
+ # Mandatory arguments must come before optional ones.
8
+ #
9
+ # @!attribute name [Symbol] Name used in help and error messages
10
+ # @!attribute description [String] User-facing description
11
+ # @!attribute type [Class, Array<Class>, :identifier] Validated type; :identifier triggers instance_identifier
12
+ # @!attribute mandatory [Boolean] Default true; optional args must come after all mandatory ones
13
+ # @!attribute multiple [Boolean, String] true: consume all remaining; String: consume until named marker
14
+ # @!attribute default [Object, nil] Default value when mandatory: false and no argument provided
15
+ # @!attribute schema [String, nil] JSON schema name for validation and --help introspection
16
+ # @!attribute bulk [Boolean] When true, wraps read+loop for bulk mode (Array if --bulk yes)
17
+ # @!attribute lookup [Symbol, nil] Instance method name for percent-selector resolution (only used when type: :identifier)
18
+ # @!attribute allowed [Array<Symbol>, nil] Allowed Symbol values; when set, type is forced to Symbol and accept_list is applied
19
+ # @!attribute interactive [Boolean] When true, sets ask_missing_mandatory before resolving so interactive prompting is triggered when no CLI args are provided
20
+ ArgumentSpec = Struct.new(
21
+ :name,
22
+ :description,
23
+ :type,
24
+ :mandatory,
25
+ :multiple,
26
+ :default,
27
+ :schema,
28
+ :bulk,
29
+ :lookup,
30
+ :allowed,
31
+ :interactive,
32
+ keyword_init: true
33
+ ) do
34
+ def initialize(**kwargs)
35
+ kwargs[:mandatory] = true if kwargs[:mandatory].nil?
36
+ kwargs[:multiple] = false if kwargs[:multiple].nil?
37
+ kwargs[:bulk] = false if kwargs[:bulk].nil?
38
+ kwargs[:interactive] = false if kwargs[:interactive].nil?
39
+ super
40
+ end
41
+ end
42
+
43
+ # Declares an option referenced by name from command declarations.
44
+ # Mirrors the existing `options.declare` call but associates the option with
45
+ # the command(s) that use it.
46
+ #
47
+ # @!attribute name [Symbol] Option name (same symbol used in options.declare)
48
+ # @!attribute description [String, nil] User-facing description; nil derives it from schema:
49
+ # @!attribute allowed [Array, nil] Allowed values (forwarded to options.declare)
50
+ # @!attribute default [Object, nil] Default value
51
+ # @!attribute short [String, nil] Single-character short form (e.g. 'x')
52
+ # @!attribute handler [Symbol, Hash, nil]
53
+ # - Symbol: resolved to {o: <plugin instance>, m: <symbol>} at runtime (Category B)
54
+ # - Hash: {o: <object>, m: <method>} used as-is (Category A: singletons / class constants)
55
+ # - nil: option stores its value locally (no delegation)
56
+ # @!attribute deprecation [String, nil] Forwarded to options.declare as deprecation:
57
+ # @!attribute schema [String, nil] JSON schema name; also derives description when nil
58
+ OptionSpec = Struct.new(
59
+ :name,
60
+ :description,
61
+ :allowed,
62
+ :default,
63
+ :short,
64
+ :handler, # kept as-is: this is the option accessor delegation, not a command action
65
+ :deprecation,
66
+ :schema,
67
+ keyword_init: true
68
+ )
69
+
70
+ # Declares a single command node in the flat registry.
71
+ #
72
+ # @!attribute id [Symbol] Unique identifier within its parent's namespace
73
+ # @!attribute parent [Symbol, Array<Symbol>, nil] Full path to parent; nil for root commands
74
+ # @!attribute description [String] User-facing help text
75
+ # @!attribute options [Array<Symbol>] Option names consumed by this command
76
+ # @!attribute arguments [Array<ArgumentSpec>] Positional arguments, in order.
77
+ # The first ArgumentSpec with type: :identifier is treated as the instance
78
+ # identifier for intermediate nodes (consumed in Phase A) and leaf nodes.
79
+ # @!attribute action [Symbol, Proc, nil] Instance method (Symbol) or inline block (Proc) called when this is a leaf command
80
+ # @!attribute setup [Symbol, nil] Instance method called before dispatching to children; returns Hash merged into ctx
81
+ # @!attribute delegates_to [Symbol, Array<Symbol>, nil] Re-enter the command tree at this path
82
+ # @!attribute delegate_instance [Symbol, nil] Instance method returning a different plugin object
83
+ # @!attribute aliases [Array<Symbol>, nil] Alternative names accepted for this command (each resolves to this command's id)
84
+ # @!attribute entity_execute [Hash, nil] Shorthand: expand to Base#entity_execute with these parameters
85
+ # @!attribute transfer_paths [:send, :receive, nil] File-list resolution delegated to TransferAgent; mutually exclusive with arguments
86
+ # @!attribute condition [Symbol, nil] Instance method returning Boolean; if false command is hidden from dispatch
87
+ # @!attribute query_schema [String, nil] Schema path for --query help; when set, the runner hints `--query=help`
88
+ CommandSpec = Struct.new(
89
+ :id,
90
+ :parent,
91
+ :description,
92
+ :options,
93
+ :arguments,
94
+ :action,
95
+ :setup,
96
+ :delegates_to,
97
+ :delegate_instance,
98
+ :aliases,
99
+ :entity_execute,
100
+ :transfer_paths,
101
+ :condition,
102
+ :query_schema,
103
+ keyword_init: true
104
+ ) do
105
+ def initialize(**kwargs)
106
+ # Coerce each element of arguments: from Hash to ArgumentSpec if needed
107
+ if kwargs[:arguments]
108
+ kwargs[:arguments] = kwargs[:arguments].map do |a|
109
+ a.is_a?(Hash) ? ArgumentSpec.new(**a) : a
110
+ end
111
+ end
112
+ super
113
+ end
114
+
115
+ class << self
116
+ # Derive the implicit action method name from a path array.
117
+ # e.g. [:admin, :user, :list] -> :action_admin_user_list
118
+ # @param path [Array<Symbol>]
119
+ # @return [Symbol]
120
+ def action_method(path)
121
+ :"action_#{path.join('_')}"
122
+ end
123
+ end
124
+
125
+ # Compute the full path as Array<Symbol> from parent + id.
126
+ # @return [Array<Symbol>]
127
+ def full_path
128
+ Array(parent) + [id]
129
+ end
130
+
131
+ # Derive the implicit action method name from the full path.
132
+ # @return [Symbol]
133
+ def action_method_name
134
+ self.class.action_method(full_path)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/assert'
4
+ require 'aspera/cli/mailer'
5
+ require 'aspera/cli/secret_finder'
4
6
 
5
7
  module Aspera
6
8
  module Cli
7
9
  # Global objects shared with plugins
8
10
  class Context
9
11
  # @type [Array<Symbol>]
10
- MEMBERS = %i[options transfer config formatter persistency man_header presets http_config].freeze
12
+ # Members that must be non-nil after bootstrap (validated in #validate)
13
+ MEMBERS = %i[options transfer config formatter persistency man_header presets http_config main_folder mailer secret_finder].freeze
11
14
  # @!attribute [rw] options
12
- # @return [Manager] the command line options manager
15
+ # @return [Parser] the command line options manager
13
16
  # @!attribute [rw] transfer
14
17
  # @return [TransferAgent] the transfer agent, used by transfer plugins
15
18
  # @!attribute [rw] config
@@ -24,15 +27,25 @@ module Aspera
24
27
  # @return [PresetManager] manages the YAML config file and preset resolution
25
28
  # @!attribute [rw] http_config
26
29
  # @return [Http] manages HTTP/S and TLS runtime options
30
+ # @!attribute [rw] mailer
31
+ # @return [Mailer] sends emails (SMTP) via ERB templates
32
+ # @!attribute [rw] secret_finder
33
+ # @return [SecretFinder] resolves secrets for url+username pairs
27
34
  attr_accessor(*MEMBERS)
35
+ # Optional: nil when progress bar is disabled
36
+ attr_accessor :progress_bar
37
+ # Optional: nil when no PAC script is configured
38
+ attr_accessor :pac_executor
28
39
 
29
40
  # Initialize all members to nil, so that they are defined and can be validated later
30
41
  # @return [nil]
31
42
  def initialize
32
43
  MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
44
+ @progress_bar = nil
45
+ @pac_executor = nil
33
46
  end
34
47
 
35
- # Validate that all members are set, raise exception if not
48
+ # Validate that all mandatory members are non-nil (detect bootstrap bugs)
36
49
  # @raise [Aspera::AssertionError] if any member is not set
37
50
  # @return [nil]
38
51
  def validate
@@ -4,9 +4,25 @@ module Aspera
4
4
  module Cli
5
5
  # CLI base exception
6
6
  class Error < StandardError; end
7
+
8
+ # Raised by dispatch_from_registry when --help is encountered mid-dispatch.
9
+ # Carries the plugin instance (with #help_path set) so the runner can display
10
+ # context-sensitive help without unwinding through a full rescue chain.
11
+ class HelpRequest < StandardError
12
+ # @return [Plugins::Base]
13
+ attr_reader :plugin
14
+
15
+ def initialize(plugin)
16
+ super('help requested')
17
+ @plugin = plugin
18
+ end
19
+ end
20
+
7
21
  # Raised when an unexpected argument is provided.
8
22
  class BadArgument < Error; end
23
+
9
24
  class MissingArgument < Error; end
25
+
10
26
  class NoSuchElement < Error; end
11
27
 
12
28
  # Raised when a lookup for a specific entity fails to return exactly one result.
@@ -3,6 +3,7 @@
3
3
  # cspell:ignore csvt jsonpp stdbin
4
4
  require 'aspera/uri_reader'
5
5
  require 'aspera/environment'
6
+ require 'aspera/yaml'
6
7
  require 'aspera/log'
7
8
  require 'aspera/assert'
8
9
  require 'aspera/cli/error'
@@ -104,7 +105,7 @@ module Aspera
104
105
  s: lambda(&:to_s),
105
106
  secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
106
107
  stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
107
- yaml: lambda{ |i| YAML.load(i)},
108
+ yaml: lambda{ |i| YAML.safe_load(i)},
108
109
  zlib: lambda{ |i| Zlib::Inflate.inflate(i)},
109
110
  extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
110
111
  }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore jsonpp
4
+ require 'aspera/schema/registry'
4
5
  require 'aspera/cli/special_values'
5
6
  require 'aspera/cli/terminal_formatter'
6
7
  require 'aspera/preview/terminal'
@@ -108,28 +109,41 @@ module Aspera
108
109
  end
109
110
  end
110
111
 
111
- def declare_options(options)
112
- options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
113
- options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
114
- options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
115
- options.declare(
116
- :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
117
- allowed: [String, Array, Regexp, Proc],
118
- default: SpecialValues::DEF
119
- )
120
- options.declare(:select, 'Select only some items in lists: column, value', allowed: [Hash, Proc], handler: {o: self, m: :option_handler})
121
- options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: {})
122
- options.declare(:flat_hash, '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: true)
123
- options.declare(
124
- :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single],
125
- handler: {o: self, m: :option_handler}, default: :no
126
- )
127
- options.declare(:show_secrets, 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: false)
128
- options.declare(:image, 'Options for image display', allowed: Hash, handler: {o: self, m: :option_handler}, default: {})
112
+ class << self
113
+ # Declare all formatter CLI options (metadata only - no handler binding yet).
114
+ # @param options [Aspera::Cli::Parser]
115
+ # @return [nil]
116
+ def declare_options(options)
117
+ options.declare(:display, description: 'Output only some information', allowed: DISPLAY_LEVELS, default: :data)
118
+ options.declare(:format, description: 'Output format', allowed: DISPLAY_FORMATS, default: :table)
119
+ options.declare(:output, description: 'Destination for results')
120
+ options.declare(:fields, description: "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", allowed: [String, Array, Regexp, Proc], default: SpecialValues::DEF)
121
+ options.declare(:select, description: 'Select only some items in lists: column, value', allowed: [Hash, Proc])
122
+ options.declare(:table_style, description: '(Table) Display style', allowed: [Hash])
123
+ options.declare(:flat_hash, description: '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, default: true)
124
+ options.declare(:multi_single, description: '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single], default: :no)
125
+ options.declare(:show_secrets, description: 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, default: false)
126
+ options.declare(:image, schema: Schema::Registry::IMAGE_OPTIONS)
127
+ nil
128
+ end
129
+ end
130
+
131
+ # Bind all formatter options to this instance using set_handler.
132
+ # Called from Runner after Formatter.new.
133
+ # @param options [Aspera::Cli::Parser]
134
+ # @return [nil]
135
+ def bind_options(options)
136
+ %i[display format output fields select table_style flat_hash multi_single show_secrets image].each do |opt|
137
+ options.set_handler(opt, object: self, method: :option_handler)
138
+ end
139
+ nil
129
140
  end
130
141
 
131
- # method accessed by option manager
132
- # options are: format, output, display, fields, select, table_style, flat_hash, multi_single
142
+ # Getter/setter handler called by the option manager for all formatter options
143
+ # @param option_symbol [Symbol] Option name (one of :format, :output, :display, :fields, :select, :table_style, :flat_hash, :multi_single, :show_secrets, :image)
144
+ # @param operation [Symbol] :get or :set
145
+ # @param value [Object, nil] Value to set (only used when operation is :set)
146
+ # @return [Object, nil] Current option value when operation is :get; nil otherwise
133
147
  def option_handler(option_symbol, operation, value = nil)
134
148
  Aspera.assert_values(operation, %i[set get])
135
149
  case operation
@@ -150,7 +164,7 @@ module Aspera
150
164
  allowed_options = Preview::Terminal.method(:build).parameters.select{ |i| i[0].eql?(:key)}.map{ |i| i[1]}
151
165
  # check that only supported options are given
152
166
  unknown_options = value.keys.map(&:to_sym) - allowed_options
153
- raise "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" unless unknown_options.empty?
167
+ Aspera.assert(unknown_options.empty?){"Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}"}
154
168
  end
155
169
  when :get then return @options[option_symbol]
156
170
  else Aspera.error_unreachable_line
@@ -163,7 +177,7 @@ module Aspera
163
177
  # @param message_level [Symbol] The level of the message - must be one of: :data, :info, :error
164
178
  # @param message [String] The message to display
165
179
  # @param hide_secrets [Boolean] Whether to hide secrets in the message (default: true)
166
- # @return [void]
180
+ # @return [nil]
167
181
  # @note Message display behavior depends on the message_level:
168
182
  # - +:data+ messages are displayed unless display level is +:error+
169
183
  # - +:info+ messages are only displayed when display level is +:info+
@@ -218,7 +232,7 @@ module Aspera
218
232
  end
219
233
 
220
234
  # Set the format type (used by Image result)
221
- def set_format_type(format)
235
+ def format_type=(format)
222
236
  @options[:format] = format
223
237
  end
224
238
 
@@ -242,12 +256,12 @@ module Aspera
242
256
  @options[:image].symbolize_keys
243
257
  end
244
258
 
245
- # @return all fields of all objects in list of objects
259
+ # @return [Array<String>] all fields of all objects in list of objects
246
260
  def all_fields(data)
247
261
  data.each_with_object({}){ |v, m| v.each_key{ |c| m[c] = true}}.keys
248
262
  end
249
263
 
250
- # @return the list of fields to display
264
+ # @return [Array<String>] the list of fields to display
251
265
  # @param data [Array<Hash>] data to display
252
266
  # @param default [Array<String>, Proc] list of fields to display by default (may contain special values)
253
267
  def compute_fields(data, default)
@@ -313,7 +327,7 @@ module Aspera
313
327
  when Proc
314
328
  begin
315
329
  data.select!{ |i| @options[:select].call(i)}
316
- rescue Exception => e # rubocop:disable Lint/RescueException
330
+ rescue StandardError => e
317
331
  raise Cli::BadArgument, "Error in user-provided ruby lambda code during select: #{e.message}"
318
332
  end
319
333
  when Hash
@@ -356,7 +370,7 @@ module Aspera
356
370
  final_table_rows = object_array.map{ |r| fields.map{ |c| r[c].to_s}}
357
371
  # remove empty rows
358
372
  final_table_rows.select!{ |i| !(i.is_a?(Hash) && i.empty?)}
359
- # here : fields : list of column names
373
+ # fields: list of column names to display
360
374
  case @options[:format]
361
375
  when :table
362
376
  format_style[:border] = :unicode_round if Environment.terminal_supports_unicode?
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/version'
4
+ require 'aspera/cli/info'
5
+ require 'aspera/environment'
6
+ require 'aspera/persistency_action_once'
7
+ require 'aspera/rest'
8
+ require 'aspera/log'
9
+ require 'date'
10
+
11
+ module Aspera
12
+ module Cli
13
+ # Mixin providing gem version-check functionality to Plugin::Config.
14
+ # Depends on `options` and `persistency` being available in the including class.
15
+ module GemChecker
16
+ GEM_CHECK_DATE_FMT = '%Y/%m/%d'
17
+
18
+ # @return [Hash] current vs latest gem version info
19
+ def check_gem_version
20
+ latest_version =
21
+ begin
22
+ Rest.new(base_url: 'https://rubygems.org/api/v1').read("versions/#{Info::GEM_NAME}/latest.json")['version']
23
+ rescue StandardError
24
+ Log.log.warn('Could not retrieve latest gem version on rubygems.')
25
+ '0'
26
+ end
27
+ if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(Info::RUBY_FUTURE_MINIMUM_VERSION)
28
+ Log.log.warn do
29
+ "Note that a future version will require Ruby version #{Info::RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
30
+ "you are using #{Environment.ruby_version}"
31
+ end
32
+ end
33
+ return {
34
+ name: Info::GEM_NAME,
35
+ current: Cli::VERSION,
36
+ latest: latest_version,
37
+ need_update: Gem::Version.new(Cli::VERSION) < Gem::Version.new(latest_version)
38
+ }
39
+ end
40
+
41
+ # Check periodically if a newer gem version is available; log a warning if so.
42
+ # Called once per run by Runner, before command execution.
43
+ def periodic_check_newer_gem_version
44
+ delay_days = options.get_option(:version_check_days, mandatory: true).to_i
45
+ return if delay_days.eql?(0)
46
+ last_check_array = []
47
+ check_date_persist = PersistencyActionOnce.new(
48
+ manager: persistency,
49
+ data: last_check_array,
50
+ id: 'version_last_check'
51
+ )
52
+ current_date = Date.today
53
+ last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
54
+ Log.log.debug{"gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}"}
55
+ return if !last_check_days.nil? && last_check_days < delay_days
56
+ last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
57
+ check_date_persist.save
58
+ check_data = check_gem_version
59
+ Log.log.warn do
60
+ "A new version is available: #{check_data[:latest]}. You have #{check_data[:current]}. Upgrade with: gem update #{check_data[:name]}"
61
+ end if check_data[:need_update]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -129,6 +129,7 @@ module Aspera
129
129
  class << self
130
130
  # @param error [Exception] exception object
131
131
  def hint_for(error, formatter)
132
+ # Loop on known error hints
132
133
  ERROR_HINTS.each do |hint|
133
134
  next unless error.is_a?(hint[:exception])
134
135
  message = error.message
@@ -141,12 +142,11 @@ module Aspera
141
142
  next unless message.eql?(m)
142
143
  when Regexp
143
144
  next unless message.match?(m)
144
- else Aspera.error_unexpected_value(m)
145
+ else Aspera.error_unexpected_value(m){'hint match'}
145
146
  end
146
147
  hint[:remediation].each do |r|
147
148
  Log.log.info{"#{'HINT:'.bg_green.gray.blink.freeze} #{r}"}
148
149
  end
149
- break
150
150
  end
151
151
  end
152
152
  end