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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +281 -21
- data/CONTRIBUTING.md +98 -7
- data/bin/ascli +3 -3
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +26 -24
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +32 -0
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -16
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +15 -14
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +26 -23
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +21 -19
- data/lib/aspera/ascp/installation.rb +37 -38
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +16 -3
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +2 -1
- data/lib/aspera/cli/formatter.rb +41 -27
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +70 -19
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1166 -853
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +457 -100
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +428 -754
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +294 -257
- data/lib/aspera/cli/plugins/faspex5.rb +585 -357
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +998 -794
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
- data/lib/aspera/cli/plugins/preview.rb +180 -139
- data/lib/aspera/cli/plugins/server.rb +124 -79
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +13 -12
- data/lib/aspera/cli/result.rb +61 -11
- data/lib/aspera/cli/runner.rb +275 -141
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +83 -59
- data/lib/aspera/cli/terminal_formatter.rb +1 -1
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +3 -3
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +21 -20
- data/lib/aspera/command_line_builder.rb +22 -22
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +4 -4
- data/lib/aspera/environment.rb +18 -11
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +3 -3
- data/lib/aspera/keychain/macos_security.rb +3 -3
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +17 -3
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +2 -2
- data/lib/aspera/oauth/factory.rb +7 -5
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_action_once.rb +8 -8
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +3 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +7 -6
- data/lib/aspera/rest.rb +10 -36
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +3 -3
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +59 -10
- data/lib/aspera/schema/registry.rb +39 -6
- data/lib/aspera/secret_hider.rb +7 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +10 -13
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +23 -21
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +6 -6
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +25 -3
- metadata.gz.sig +0 -0
- 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
|
data/lib/aspera/cli/context.rb
CHANGED
|
@@ -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
|
-
|
|
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 [
|
|
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
|
|
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
|
data/lib/aspera/cli/error.rb
CHANGED
|
@@ -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.
|
|
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
|
}
|
data/lib/aspera/cli/formatter.rb
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
options
|
|
116
|
-
:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
#
|
|
132
|
-
#
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
data/lib/aspera/cli/hints.rb
CHANGED
|
@@ -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
|