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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/cli/extended_value'
|
|
4
|
+
require 'aspera/cli/parser'
|
|
4
5
|
require 'aspera/assert'
|
|
5
6
|
require 'aspera/cli/result'
|
|
7
|
+
require 'aspera/cli/command_registry'
|
|
8
|
+
require 'aspera/schema/registry'
|
|
6
9
|
|
|
7
10
|
module Aspera
|
|
8
11
|
module Cli
|
|
@@ -20,25 +23,168 @@ module Aspera
|
|
|
20
23
|
ALL = (GLOBAL + INSTANCE).freeze
|
|
21
24
|
end
|
|
22
25
|
class << self
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
# Per-class DSL registry (not inherited: each subclass gets its own instance).
|
|
27
|
+
# @return [CommandRegistry]
|
|
28
|
+
def command_registry
|
|
29
|
+
@command_registry ||= CommandRegistry.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# DSL class method: register a command in this plugin's registry.
|
|
33
|
+
# Inherits parent from the enclosing commands_under block when parent: is omitted.
|
|
34
|
+
# @param id [Symbol]
|
|
35
|
+
# @param kwargs [Hash] forwarded to CommandSpec
|
|
36
|
+
def command(id, **kwargs)
|
|
37
|
+
kwargs[:parent] = @current_parent if kwargs[:parent].nil? && @current_parent
|
|
38
|
+
command_registry.register(CommandSpec.new(id: id, **kwargs))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# DSL class method: shorthand for a command whose sole action is Base#entity_execute.
|
|
42
|
+
# The api: value is a Symbol resolved at runtime: if it starts with '@' it is treated
|
|
43
|
+
# as an instance variable name; otherwise it is sent as a method call.
|
|
44
|
+
# description: defaults to "Manage <last segment of entity path>" when omitted.
|
|
45
|
+
# @param id [Symbol] Command identifier
|
|
46
|
+
# @param api [Symbol] Method name or :@ivar returning the REST API at runtime
|
|
47
|
+
# @param entity [String] API sub-path (e.g. 'admin/dropboxes')
|
|
48
|
+
# @param description [String, nil] User-facing help text; derived from entity when nil
|
|
49
|
+
# @param kwargs [Hash] Any other entity_execute params (display_fields:, command:, is_singleton:, etc.)
|
|
50
|
+
def entity_command(id, api:, entity:, description: nil, **kwargs)
|
|
51
|
+
description ||= "Manage #{entity.split('/').last}"
|
|
52
|
+
command(id, description: description, entity_execute: {api: api, entity: entity, **kwargs})
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# DSL class method: define an instance method whose name is derived from a path array.
|
|
56
|
+
# Equivalent to: define_method(CommandSpec.action_method(path), &block)
|
|
57
|
+
# @param path [Array<Symbol>] command path segments, e.g. [:admin, :user, :list]
|
|
58
|
+
# @yieldparam [Hash] keyword context forwarded from dispatch
|
|
59
|
+
def define_action_method(path, &block)
|
|
60
|
+
define_method(CommandSpec.action_method(path), &block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# DSL class method: scope block that sets a default parent for nested command() calls.
|
|
64
|
+
# Fully re-entrant: blocks may be nested for multi-level parent paths.
|
|
65
|
+
# @param parent [Symbol, Array<Symbol>] parent path applied to every command() inside
|
|
66
|
+
# @yieldreturn [void]
|
|
67
|
+
def commands_under(parent)
|
|
68
|
+
previous = @current_parent
|
|
69
|
+
@current_parent = parent
|
|
70
|
+
yield
|
|
71
|
+
ensure
|
|
72
|
+
@current_parent = previous
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# DSL class method: declare an option in this plugin's registry.
|
|
76
|
+
# Metadata is stored as an OptionSpec at class-load time; the actual
|
|
77
|
+
# options.declare call happens in Base#initialize once the instance exists.
|
|
78
|
+
#
|
|
79
|
+
# Raises ArgumentError at class-load time if the same option name is already
|
|
80
|
+
# declared by any ancestor class, preventing silent shadowing.
|
|
81
|
+
#
|
|
82
|
+
# @param name [Symbol] Option name
|
|
83
|
+
# @param description [String, nil] User-facing description; if nil, derived from schema: title/description
|
|
84
|
+
# @param short [String, nil] Single-character short form (without leading '-')
|
|
85
|
+
# @param allowed [Object, nil] Allowed values (see OptionValue)
|
|
86
|
+
# @param default [Object, nil] Default value
|
|
87
|
+
# @param handler [Symbol, Hash, nil]
|
|
88
|
+
# - Symbol: resolved to {o: <plugin instance>, m: <symbol>} at runtime (Category B)
|
|
89
|
+
# - Hash: {o: <object>, m: <method>} used as-is (Category A: singletons / constants)
|
|
90
|
+
# - nil: option stores its value locally (no delegation)
|
|
91
|
+
# @param deprecation [String, nil] Deprecation message forwarded to options.declare
|
|
92
|
+
# @param schema [String, nil] Schema reference (e.g. "opts:components.schemas.Foo");
|
|
93
|
+
# when description: is nil, the schema title or first description line is used
|
|
94
|
+
def option(name, description: nil,
|
|
95
|
+
short: nil, allowed: nil, default: nil,
|
|
96
|
+
handler: nil, deprecation: nil, schema: nil)
|
|
97
|
+
ancestor_owner = ancestors.drop(1).find do |klass|
|
|
98
|
+
klass.is_a?(Class) && klass <= Base &&
|
|
99
|
+
klass.instance_variable_defined?(:@command_registry) &&
|
|
100
|
+
klass.command_registry.option_specs.key?(name)
|
|
101
|
+
end
|
|
102
|
+
raise ArgumentError, "#{self}: option :#{name} already declared in ancestor #{ancestor_owner}" if ancestor_owner
|
|
103
|
+
command_registry.register_option(
|
|
104
|
+
OptionSpec.new(
|
|
105
|
+
name: name,
|
|
106
|
+
description: description,
|
|
107
|
+
short: short,
|
|
108
|
+
allowed: allowed,
|
|
109
|
+
default: default,
|
|
110
|
+
handler: handler,
|
|
111
|
+
deprecation: deprecation,
|
|
112
|
+
schema: schema
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# DSL class method: declare a setup method to run once before root dispatch.
|
|
118
|
+
# The method is called before any command is consumed, and its return value
|
|
119
|
+
# (a Hash) is merged into the initial ctx. This is useful when conditions
|
|
120
|
+
# on root commands depend on state built during setup (e.g. @connection_type).
|
|
121
|
+
# @param method_name [Symbol]
|
|
122
|
+
def root_setup(method_name)
|
|
123
|
+
@root_setup_method = method_name
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Symbol, nil]
|
|
127
|
+
attr_reader :root_setup_method
|
|
128
|
+
|
|
129
|
+
# DSL class method: declare the human-readable application name shown in wizards.
|
|
130
|
+
# When called with an argument, sets the name. When called with no argument, returns it.
|
|
131
|
+
# Falls back to the last component of the class name if never set.
|
|
132
|
+
# @param name [String, nil]
|
|
133
|
+
# @return [String]
|
|
134
|
+
def application_name(name = nil)
|
|
135
|
+
@application_name = name unless name.nil?
|
|
136
|
+
@application_name || self.name.split('::').last
|
|
27
137
|
end
|
|
28
138
|
end
|
|
29
139
|
|
|
140
|
+
option :query, description: 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass]
|
|
141
|
+
option :bulk, description: 'Bulk operation (only some)', allowed: Allowed::TYPES_BOOLEAN, default: false
|
|
142
|
+
option :bfail, description: 'Bulk operation error handling', allowed: Allowed::TYPES_BOOLEAN, default: true
|
|
143
|
+
|
|
30
144
|
def initialize(context:)
|
|
31
|
-
|
|
32
|
-
Aspera.
|
|
33
|
-
Aspera.assert(self.class.const_defined?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
|
|
145
|
+
Aspera.assert_type(context, Context){'context'}
|
|
146
|
+
Aspera.assert_type(context.man_header, TrueClass, FalseClass){'context.man_header'}
|
|
34
147
|
@context = context
|
|
148
|
+
# Auto-declare all options registered via the DSL `option` class method.
|
|
149
|
+
# Walk the ancestor chain so that options declared on parent plugin classes
|
|
150
|
+
# (e.g. Oauth, BasicAuth) are also registered for sub-classes (e.g. Aoc).
|
|
151
|
+
# The options object is shared across all plugins in a run; skip options already
|
|
152
|
+
# declared by an earlier plugin (Base.option prevents duplicates within one hierarchy).
|
|
153
|
+
# Each OptionSpec is translated to an options.declare call, resolving the
|
|
154
|
+
# handler: shorthand:
|
|
155
|
+
# Symbol handler: {o: self, m: <symbol>} (Category B - plugin instance methods)
|
|
156
|
+
# Hash handler: used as-is (Category A - singletons / class constants)
|
|
157
|
+
self.class.ancestors.each do |klass|
|
|
158
|
+
next unless klass.is_a?(Class) && klass <= Base && klass.instance_variable_defined?(:@command_registry)
|
|
159
|
+
klass.command_registry.option_specs.each_value do |spec|
|
|
160
|
+
next if options.option_declared?(spec.name)
|
|
161
|
+
resolved_handler =
|
|
162
|
+
case spec.handler
|
|
163
|
+
when Symbol then {o: self, m: spec.handler}
|
|
164
|
+
when Hash then spec.handler
|
|
165
|
+
end
|
|
166
|
+
options.declare(
|
|
167
|
+
spec.name,
|
|
168
|
+
description: spec.description,
|
|
169
|
+
short: spec.short,
|
|
170
|
+
allowed: spec.allowed,
|
|
171
|
+
default: spec.default,
|
|
172
|
+
handler: resolved_handler,
|
|
173
|
+
deprecation: spec.deprecation,
|
|
174
|
+
schema: spec.schema
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
35
178
|
add_manual_header if @context.man_header
|
|
36
179
|
end
|
|
37
180
|
|
|
38
181
|
# Global objects
|
|
39
182
|
attr_reader :context
|
|
183
|
+
# Path reached in the command tree at the moment --help was intercepted.
|
|
184
|
+
# Nil until set by dispatch_from_registry.
|
|
185
|
+
attr_reader :help_path
|
|
40
186
|
|
|
41
|
-
# @return [Aspera::Cli::
|
|
187
|
+
# @return [Aspera::Cli::Parser]
|
|
42
188
|
def options; @context.options; end
|
|
43
189
|
# @return [Aspera::Cli::TransferAgent]
|
|
44
190
|
def transfer; @context.transfer; end
|
|
@@ -52,63 +198,270 @@ module Aspera
|
|
|
52
198
|
def presets; @context.presets; end
|
|
53
199
|
# @return [Aspera::Cli::Http]
|
|
54
200
|
def http_config; @context.http_config; end
|
|
201
|
+
# @return [Aspera::Cli::TransferProgress, nil]
|
|
202
|
+
def progress_bar; @context.progress_bar; end
|
|
55
203
|
|
|
56
|
-
def add_manual_header(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Create -> created
|
|
99
|
-
result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
raise e if options.get_option(:bfail)
|
|
102
|
-
result['status'] = e.to_s
|
|
204
|
+
def add_manual_header(_has_options = true)
|
|
205
|
+
options.rename_current_group(self.class.name.split('::').last.downcase)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Entry point for all DSL-based plugins.
|
|
209
|
+
def execute_action
|
|
210
|
+
@help_path = nil
|
|
211
|
+
# Validate the registry once per class (memoised by the ivar check).
|
|
212
|
+
# Passes the plugin class so implicit handler methods can be verified.
|
|
213
|
+
unless self.class.instance_variable_defined?(:@registry_validated)
|
|
214
|
+
self.class.command_registry.validate!(plugin_class: self.class)
|
|
215
|
+
self.class.instance_variable_set(:@registry_validated, true)
|
|
216
|
+
end
|
|
217
|
+
# Run the root setup (if declared) before consuming any argument.
|
|
218
|
+
# This ensures condition methods on root commands can read instance variables
|
|
219
|
+
# populated by the setup (e.g. @connection_type in server.rb).
|
|
220
|
+
init_ctx = {}
|
|
221
|
+
if (rsm = self.class.root_setup_method)
|
|
222
|
+
init_ctx = send(rsm) || {}
|
|
223
|
+
end
|
|
224
|
+
dispatch_from_registry([], init_ctx)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Two-phase dispatcher: run setup on the current node (Phase A), then either
|
|
228
|
+
# execute a leaf directly or consume the next argument and recurse (Phase B).
|
|
229
|
+
# @param current_path [Array<Symbol>] path of the node currently being dispatched
|
|
230
|
+
# @param ctx [Hash] accumulated context passed down from parent nodes
|
|
231
|
+
# @param skip_setup [Boolean] when true, skip Phase A (setup already done by caller)
|
|
232
|
+
# @return [Object] result suitable for CLI output
|
|
233
|
+
def dispatch_from_registry(current_path, ctx = {}, skip_setup: false)
|
|
234
|
+
registry = self.class.command_registry
|
|
235
|
+
spec = registry[current_path]
|
|
236
|
+
is_leaf = spec && registry.children_of(current_path).empty?
|
|
237
|
+
|
|
238
|
+
if options.help_requested || skip_setup
|
|
239
|
+
# help_requested on an intermediate node: drain positional args without validation
|
|
240
|
+
# so that dispatch_child can still consume the correct sub-command token
|
|
241
|
+
if !is_leaf && !skip_setup && spec&.arguments
|
|
242
|
+
spec.arguments.each do |arg_spec|
|
|
243
|
+
next if ctx.key?(arg_spec.name)
|
|
244
|
+
options.get_next_argument(arg_spec.name.to_s, mandatory: false)
|
|
245
|
+
end
|
|
103
246
|
end
|
|
104
|
-
|
|
247
|
+
else
|
|
248
|
+
# Phase A - for intermediate nodes only: resolve all ArgumentSpec declared on this node
|
|
249
|
+
# before dispatching to children (leaf nodes resolve their arguments inside execute_leaf).
|
|
250
|
+
if !is_leaf
|
|
251
|
+
(spec&.arguments || []).each do |arg_spec|
|
|
252
|
+
next if ctx.key?(arg_spec.name)
|
|
253
|
+
if arg_spec.type.eql?(:identifier)
|
|
254
|
+
lookup_method = arg_spec.lookup
|
|
255
|
+
res_id = if lookup_method
|
|
256
|
+
options.instance_identifier(description: arg_spec.name.to_s){ |f, v| send(lookup_method, f, v, **ctx)}
|
|
257
|
+
else
|
|
258
|
+
options.instance_identifier(description: arg_spec.name.to_s)
|
|
259
|
+
end
|
|
260
|
+
ctx = ctx.merge(arg_spec.name => res_id)
|
|
261
|
+
else
|
|
262
|
+
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
ctx = ctx.merge(send(spec.setup, **ctx)) if spec&.setup
|
|
105
267
|
end
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
268
|
+
|
|
269
|
+
# Phase B - leaf fast-path or child dispatch
|
|
270
|
+
if is_leaf
|
|
271
|
+
dispatch_leaf(current_path, spec, ctx)
|
|
109
272
|
else
|
|
110
|
-
|
|
111
|
-
|
|
273
|
+
dispatch_child(current_path, registry, ctx)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Phase B, leaf branch: execute a spec that is already a leaf (no children).
|
|
278
|
+
# Intercepts --help before calling execute_leaf.
|
|
279
|
+
# @param current_path [Array<Symbol>]
|
|
280
|
+
# @param spec [CommandSpec]
|
|
281
|
+
# @param ctx [Hash]
|
|
282
|
+
# @return [Object]
|
|
283
|
+
def dispatch_leaf(current_path, spec, ctx)
|
|
284
|
+
if options.help_requested
|
|
285
|
+
@help_path = current_path
|
|
286
|
+
raise Cli::HelpRequest, self
|
|
287
|
+
end
|
|
288
|
+
execute_leaf(spec, ctx)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Phase B, child branch: consume the next command argument, resolve the matching
|
|
292
|
+
# child spec, handle delegation / entity_execute shorthands, and recurse or execute.
|
|
293
|
+
# @param current_path [Array<Symbol>]
|
|
294
|
+
# @param registry [CommandRegistry]
|
|
295
|
+
# @param ctx [Hash]
|
|
296
|
+
# @return [Object]
|
|
297
|
+
def dispatch_child(current_path, registry, ctx)
|
|
298
|
+
children = registry.children_of(current_path)
|
|
299
|
+
available = children.reject{ |_, c| c.condition && !send(c.condition)}
|
|
300
|
+
aliases = children.values.each_with_object({}) do |c, h|
|
|
301
|
+
Array(c.aliases).each{ |a| h[a] = c.id} if c.aliases
|
|
302
|
+
end
|
|
303
|
+
command = options.get_next_command(available.keys, aliases: aliases.empty? ? nil : aliases)
|
|
304
|
+
child = available[command]
|
|
305
|
+
|
|
306
|
+
# Intercept --help when no more positional args remain after consuming this command.
|
|
307
|
+
# (e.g. `aoc files -h` or `aoc files find -h`). When args remain, keep recursing.
|
|
308
|
+
if options.help_requested && options.command_or_arg_empty?
|
|
309
|
+
@help_path = current_path + [command]
|
|
310
|
+
raise Cli::HelpRequest, self
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Instance delegation: hand off to a different plugin object
|
|
314
|
+
if child.delegate_instance
|
|
315
|
+
target = send(child.delegate_instance)
|
|
316
|
+
return target.dispatch_from_registry(Array(child.delegates_to), {})
|
|
317
|
+
end
|
|
318
|
+
return dispatch_from_registry(Array(child.delegates_to), ctx) if child.delegates_to
|
|
319
|
+
|
|
320
|
+
# entity_execute shorthand
|
|
321
|
+
return run_entity_execute(child, ctx) if child.entity_execute
|
|
322
|
+
|
|
323
|
+
# Both intermediate and leaf: instance_arg + setup are handled by Phase A of the next call
|
|
324
|
+
dispatch_from_registry(current_path + [command], ctx)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Resolve the action for a leaf CommandSpec.
|
|
328
|
+
# Returns spec.action (Symbol or Proc) if explicitly set; otherwise derives a Symbol
|
|
329
|
+
# from the full path as :action_<path_segment_1>_<path_segment_2>_...
|
|
330
|
+
# (e.g. [:access_key, :list] -> :action_access_key_list).
|
|
331
|
+
# @param spec [CommandSpec]
|
|
332
|
+
# @return [Symbol, Proc]
|
|
333
|
+
def action_for(spec)
|
|
334
|
+
spec.action || spec.action_method_name
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Invoke an action (Symbol method or Proc block) with the given positional
|
|
338
|
+
# arguments and keyword context.
|
|
339
|
+
# Procs are executed via instance_exec so they share the plugin's `self`.
|
|
340
|
+
# @param action [Symbol, Proc]
|
|
341
|
+
# @param args [Array] positional arguments
|
|
342
|
+
# @param ctx [Hash] keyword context
|
|
343
|
+
# @return [Object]
|
|
344
|
+
def invoke_action(action, args, ctx)
|
|
345
|
+
if action.is_a?(Proc)
|
|
346
|
+
instance_exec(*args, **ctx, &action)
|
|
347
|
+
else
|
|
348
|
+
send(action, *args, **ctx)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Execute a leaf CommandSpec: resolve arguments (or skip for transfer_paths) and call action.
|
|
353
|
+
# instance_arg (if any) is resolved here as an ArgumentSpec(type: :identifier) and merged
|
|
354
|
+
# into ctx, exactly like any other keyword argument received by the action.
|
|
355
|
+
# @param spec [CommandSpec] a leaf node (no children)
|
|
356
|
+
# @param ctx [Hash] accumulated context
|
|
357
|
+
# @return [Object]
|
|
358
|
+
def execute_leaf(spec, ctx)
|
|
359
|
+
a = action_for(spec)
|
|
360
|
+
# Always resolve declared arguments (even when transfer_paths is set — those arguments
|
|
361
|
+
# are consumed first; ts_source_paths then reads whatever remains in the queue).
|
|
362
|
+
(spec.arguments || []).each do |arg_spec|
|
|
363
|
+
if arg_spec.type.eql?(:identifier)
|
|
364
|
+
unless ctx.key?(arg_spec.name)
|
|
365
|
+
lookup_method = arg_spec.lookup
|
|
366
|
+
block = lookup_method ? ->(f, v){send(lookup_method, f, v, **ctx)} : nil
|
|
367
|
+
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec, &block))
|
|
368
|
+
end
|
|
369
|
+
else
|
|
370
|
+
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec))
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
invoke_action(a, [], ctx)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Expand an entity_execute shorthand from a CommandSpec.
|
|
377
|
+
# Calls Base#entity_execute with the parameters from spec.entity_execute merged
|
|
378
|
+
# with the context hash (context entries are low-priority: spec params win).
|
|
379
|
+
# @param spec [CommandSpec] the command spec carrying entity_execute: Hash
|
|
380
|
+
# @param ctx [Hash] accumulated context (e.g. api:, lookup block)
|
|
381
|
+
# @return [Object]
|
|
382
|
+
def run_entity_execute(spec, ctx)
|
|
383
|
+
ee_params = spec.entity_execute.dup
|
|
384
|
+
# Resolve api: Symbol at runtime: :@ivar -> instance_variable_get, :method -> send
|
|
385
|
+
if (api_ref = ee_params[:api]).is_a?(Symbol)
|
|
386
|
+
ee_params[:api] = if api_ref.to_s.start_with?('@')
|
|
387
|
+
instance_variable_get(api_ref)
|
|
388
|
+
else
|
|
389
|
+
send(api_ref)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
# Merge context into params (spec wins on key collision)
|
|
393
|
+
merged = ctx.merge(ee_params)
|
|
394
|
+
# Extract lookup_block before passing to entity_execute (it is not a kwarg of entity_execute)
|
|
395
|
+
block = merged.delete(:lookup_block)
|
|
396
|
+
if block
|
|
397
|
+
entity_execute(**merged, &block)
|
|
398
|
+
else
|
|
399
|
+
entity_execute(**merged)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Resolve a single positional argument from the CLI argument stream.
|
|
404
|
+
# When arg_spec.bulk is true, always returns an Array (normalized to [value] when non-bulk).
|
|
405
|
+
# For type: :identifier, an optional block provides the percent-selector lookup.
|
|
406
|
+
# @param arg_spec [ArgumentSpec]
|
|
407
|
+
# @yieldparam field [String] field name from a percent-selector (%field:value)
|
|
408
|
+
# @yieldparam value [String] value from a percent-selector
|
|
409
|
+
# @yieldreturn [String] resolved identifier
|
|
410
|
+
# @return [Object] the resolved value, or Array when arg_spec.bulk is true
|
|
411
|
+
def resolve_argument(arg_spec, &block)
|
|
412
|
+
if arg_spec.bulk
|
|
413
|
+
is_bulk = options.get_option(:bulk)
|
|
414
|
+
if arg_spec.type.eql?(:identifier)
|
|
415
|
+
val = options.instance_identifier(description: arg_spec.name.to_s, &block)
|
|
416
|
+
else
|
|
417
|
+
val = options.get_next_argument(
|
|
418
|
+
arg_spec.name.to_s,
|
|
419
|
+
mandatory: arg_spec.mandatory,
|
|
420
|
+
validation: is_bulk ? Array : arg_spec.type,
|
|
421
|
+
default: arg_spec.default,
|
|
422
|
+
schema: arg_spec.schema
|
|
423
|
+
)
|
|
424
|
+
if is_bulk
|
|
425
|
+
Aspera.assert_array_all(val, arg_spec.type, type: Cli::BadArgument){'type'} unless arg_spec.type.nil?
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
# Always return an Array when bulk: true
|
|
429
|
+
is_bulk ? val : [val]
|
|
430
|
+
else
|
|
431
|
+
case arg_spec.type
|
|
432
|
+
when :identifier
|
|
433
|
+
options.instance_identifier(description: arg_spec.name.to_s, &block)
|
|
434
|
+
else
|
|
435
|
+
# Class or Array<Class> -> pass as validation type
|
|
436
|
+
# When interactive: true, set ask_missing_mandatory so that get_interactive is triggered
|
|
437
|
+
# when no CLI arguments are provided (mandatory is forced to true for the same reason:
|
|
438
|
+
# a non-nil default would short-circuit get_interactive before it is ever called).
|
|
439
|
+
options.ask_missing_mandatory = true if arg_spec.interactive
|
|
440
|
+
options.get_next_argument(
|
|
441
|
+
arg_spec.name.to_s,
|
|
442
|
+
mandatory: arg_spec.interactive ? true : arg_spec.mandatory,
|
|
443
|
+
multiple: arg_spec.multiple || false,
|
|
444
|
+
validation: arg_spec.type,
|
|
445
|
+
accept_list: arg_spec.allowed,
|
|
446
|
+
default: arg_spec.interactive ? nil : arg_spec.default,
|
|
447
|
+
schema: arg_spec.schema
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Build a nested Hash tree of the registered command tree for help display.
|
|
454
|
+
# Conditional commands are included with a '[condition_name]' annotation.
|
|
455
|
+
# @param path [Array<Symbol>] starting path ([] for the full tree)
|
|
456
|
+
# @return [Hash] { command_id => { description:, condition:, children: } }
|
|
457
|
+
def generate_help(path = [])
|
|
458
|
+
self.class.command_registry.children_of(path).transform_values do |child_spec|
|
|
459
|
+
annotation = child_spec.condition ? " [#{child_spec.condition}]" : ''
|
|
460
|
+
{
|
|
461
|
+
description: "#{child_spec.description}#{annotation}",
|
|
462
|
+
condition: child_spec.condition,
|
|
463
|
+
children: generate_help(child_spec.full_path)
|
|
464
|
+
}
|
|
112
465
|
end
|
|
113
466
|
end
|
|
114
467
|
|
|
@@ -116,13 +469,24 @@ module Aspera
|
|
|
116
469
|
# @param api [Aspera::Rest] API to use
|
|
117
470
|
# @param entity [String] Sub path in URL to resource relative to base url
|
|
118
471
|
# @param command [Symbol, nil] Command to execute: :create, :show, :list, :modify, :delete
|
|
472
|
+
# When nil, reads the next command token from the CLI (fallback, deprecated).
|
|
119
473
|
# @param display_fields [Array, nil] Fields to display by default
|
|
120
474
|
# @param items_key [String, nil] Result is in a sub key of the JSON
|
|
121
475
|
# @param delete_style [String, nil] If set, the delete operation by array in payload
|
|
122
476
|
# @param id_as_arg [Boolean, String] If set, the id is provided as url argument ?<id_as_arg>=<id>
|
|
123
477
|
# @param is_singleton [Boolean] If `true`, entity is the full path to the resource
|
|
124
478
|
# @param list_query [Hash, nil] Query parameters for list operation
|
|
125
|
-
# @
|
|
479
|
+
# @param schema [String, nil] JSON schema name for create/modify validation
|
|
480
|
+
# @param query_component [String, nil] Registry component key (e.g. 'faspex', 'aoc') used to derive
|
|
481
|
+
# the query parameters schema path from the entity name. When set, `--query=help` on list/delete
|
|
482
|
+
# commands displays the available filter parameters from the OpenAPI spec.
|
|
483
|
+
# @param input_data [Array, Hash, nil] Pre-resolved data for :create (Array) or :modify (Hash).
|
|
484
|
+
# When nil, data is read from the CLI (fallback, deprecated).
|
|
485
|
+
# @param res_id [String, Array, nil] Pre-resolved resource identifier(s) for instance commands.
|
|
486
|
+
# When nil, identifier is read from the CLI (fallback, deprecated).
|
|
487
|
+
# @param is_bulk [Boolean] Whether operation runs in bulk mode (used when data: is provided)
|
|
488
|
+
# @param bfail [Boolean] When false, errors are captured as status strings instead of re-raised
|
|
489
|
+
# @yieldparam value [String] Value to search for identifier (lookup block)
|
|
126
490
|
# @yieldreturn [String] The identifier
|
|
127
491
|
# @return [Hash] Result suitable for CLI result
|
|
128
492
|
def entity_execute(
|
|
@@ -136,25 +500,43 @@ module Aspera
|
|
|
136
500
|
is_singleton: false,
|
|
137
501
|
list_query: nil,
|
|
138
502
|
schema: nil,
|
|
503
|
+
query_component: nil,
|
|
504
|
+
input_data: nil,
|
|
505
|
+
res_id: nil,
|
|
506
|
+
is_bulk: false,
|
|
507
|
+
bfail: true,
|
|
139
508
|
&block
|
|
140
509
|
)
|
|
510
|
+
# Fallback: read command from CLI when not provided (entity_command shorthand without command:)
|
|
141
511
|
command = options.get_next_command(Operations::ALL) if command.nil?
|
|
512
|
+
# Derive query schema path from component + entity when component is given
|
|
513
|
+
qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
|
|
514
|
+
|
|
142
515
|
if is_singleton
|
|
143
516
|
one_res_path = entity
|
|
144
517
|
elsif Operations::INSTANCE.include?(command)
|
|
145
|
-
|
|
518
|
+
# Use pre-resolved identifier if provided; otherwise fall back to CLI read (entity_command shorthand)
|
|
519
|
+
one_res_id = res_id || options.instance_identifier(&block)
|
|
146
520
|
one_res_path = "#{entity}/#{one_res_id}"
|
|
147
521
|
one_res_path = "#{entity}?#{id_as_arg}=#{one_res_id}" if id_as_arg
|
|
148
522
|
end
|
|
149
523
|
|
|
150
524
|
case command
|
|
151
525
|
when :create
|
|
152
|
-
|
|
153
|
-
|
|
526
|
+
Aspera.assert(!is_singleton, type: BadArgument){'cannot create singleton'}
|
|
527
|
+
unless input_data
|
|
528
|
+
# No pre-resolved data: read from CLI
|
|
529
|
+
cli_is_bulk = options.get_option(:bulk)
|
|
530
|
+
raw = options.get_next_argument('data', validation: cli_is_bulk ? Array : Hash, schema: schema)
|
|
531
|
+
input_data = cli_is_bulk ? raw : [raw]
|
|
532
|
+
is_bulk = cli_is_bulk
|
|
533
|
+
bfail = options.get_option(:bfail)
|
|
534
|
+
end
|
|
535
|
+
return Result.bulk(input_data, is_bulk: is_bulk, command: command, fields: display_fields, bfail: bfail) do |params|
|
|
154
536
|
api.create(entity, params)
|
|
155
537
|
end
|
|
156
538
|
when :delete
|
|
157
|
-
|
|
539
|
+
Aspera.assert(!is_singleton, type: BadArgument){'cannot delete singleton'}
|
|
158
540
|
if !delete_style.nil?
|
|
159
541
|
one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
|
|
160
542
|
Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
|
|
@@ -166,14 +548,15 @@ module Aspera
|
|
|
166
548
|
)
|
|
167
549
|
return Result::Status.new('deleted')
|
|
168
550
|
end
|
|
169
|
-
|
|
170
|
-
|
|
551
|
+
items = one_res_id.is_a?(Array) ? one_res_id : [one_res_id]
|
|
552
|
+
return Result.bulk(items, is_bulk: is_bulk, command: command, bfail: bfail) do |one_id|
|
|
553
|
+
api.delete("#{entity}/#{one_id}", query_read_delete(schema: qs_path))
|
|
171
554
|
{'id' => one_id}
|
|
172
555
|
end
|
|
173
556
|
when :show
|
|
174
557
|
return Result::SingleObject.new(api.read(one_res_path), fields: display_fields)
|
|
175
558
|
when :list
|
|
176
|
-
data, http = api.read(entity, query_read_delete, ret: :both)
|
|
559
|
+
data, http = api.read(entity, query_read_delete(default: list_query, schema: qs_path), ret: :both)
|
|
177
560
|
return Result::Empty.new if http.code == '204'
|
|
178
561
|
# TODO: not generic : which application is this for ?
|
|
179
562
|
if http['Content-Type'].start_with?('application/vnd.api+json')
|
|
@@ -191,7 +574,8 @@ module Aspera
|
|
|
191
574
|
Aspera.error_unexpected_value(data.class.name){'list type'}
|
|
192
575
|
end
|
|
193
576
|
when :modify
|
|
194
|
-
|
|
577
|
+
# Use pre-resolved data if provided; otherwise read from CLI
|
|
578
|
+
parameters = input_data || options.get_next_argument('data', validation: Hash, schema: schema)
|
|
195
579
|
api.update(one_res_path, parameters)
|
|
196
580
|
return Result::Status.new('modified')
|
|
197
581
|
else
|
|
@@ -201,10 +585,11 @@ module Aspera
|
|
|
201
585
|
|
|
202
586
|
# Query parameters in URL suitable for REST: list/`GET` and delete/`DELETE`
|
|
203
587
|
# @param default [Hash, nil] Default query parameters
|
|
588
|
+
# @param schema [String, nil] Contextual schema path for --query help display
|
|
204
589
|
# @return [Hash, nil] Query parameters
|
|
205
|
-
def query_read_delete(default: nil)
|
|
590
|
+
def query_read_delete(default: nil, schema: nil)
|
|
206
591
|
# Dup default, as it could be frozen
|
|
207
|
-
query = options.get_option(:query) || default
|
|
592
|
+
query = options.get_option(:query, schema: schema) || default&.dup
|
|
208
593
|
Log.log.debug{"query_read_delete=#{query}".bg_red}
|
|
209
594
|
begin
|
|
210
595
|
# Check it is suitable
|
|
@@ -214,34 +599,6 @@ module Aspera
|
|
|
214
599
|
end
|
|
215
600
|
return query
|
|
216
601
|
end
|
|
217
|
-
|
|
218
|
-
# Retrieves an extended value from command line.
|
|
219
|
-
# Used for creation or modification of entities.
|
|
220
|
-
# @param command [Symbol] Command name for error message
|
|
221
|
-
# @param description [String, nil] Description of the value
|
|
222
|
-
# @param type [Class, nil] Expected type of value
|
|
223
|
-
# @param bulk [Boolean] If `true`, value must be an Array of `type`
|
|
224
|
-
# @param default [Object, nil] Default value if not provided
|
|
225
|
-
# @param schema [Hash, nil] JSON schema for validation
|
|
226
|
-
# @return [Hash, Array<Hash>] The value(s) to create object(s)
|
|
227
|
-
def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil, schema: nil)
|
|
228
|
-
value = options.get_next_argument(
|
|
229
|
-
"parameters for #{command}#{" (#{description})" unless description.nil?}",
|
|
230
|
-
mandatory: default.nil?,
|
|
231
|
-
validation: bulk ? Array : type,
|
|
232
|
-
schema: schema
|
|
233
|
-
)
|
|
234
|
-
value = default if value.nil?
|
|
235
|
-
unless type.nil?
|
|
236
|
-
Aspera.assert_type(type, Class){'type'}
|
|
237
|
-
if bulk
|
|
238
|
-
Aspera.assert_array_all(value, type, type: Cli::BadArgument){'type'}
|
|
239
|
-
else
|
|
240
|
-
Aspera.assert_type(value, type, type: Cli::BadArgument){'type'}
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
return value
|
|
244
|
-
end
|
|
245
602
|
end
|
|
246
603
|
end
|
|
247
604
|
end
|