aspera-cli 4.26.2 → 4.27.1
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 +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- 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 +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -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 +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -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 +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- 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 +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/cli/http.rb
CHANGED
|
@@ -4,6 +4,11 @@ require 'aspera/cli/special_values'
|
|
|
4
4
|
require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
|
6
6
|
require 'aspera/line_logger'
|
|
7
|
+
require 'aspera/schema/registry'
|
|
8
|
+
require 'aspera/cli/option_declarator'
|
|
9
|
+
require 'aspera/rest'
|
|
10
|
+
require 'aspera/oauth'
|
|
11
|
+
require 'aspera/ssl'
|
|
7
12
|
require 'openssl'
|
|
8
13
|
|
|
9
14
|
module Aspera
|
|
@@ -12,6 +17,8 @@ module Aspera
|
|
|
12
17
|
# Extracted from Plugins::Config so it can be referenced independently
|
|
13
18
|
# via Context#http_config without coupling to the plugin machinery.
|
|
14
19
|
class Http
|
|
20
|
+
extend OptionDeclarator
|
|
21
|
+
|
|
15
22
|
# Certificate file extensions recognized when scanning a folder
|
|
16
23
|
CERT_EXT = %w[crt cer pem der].freeze
|
|
17
24
|
# OpenSSL constant that disables peer verification (VERIFY_NONE)
|
|
@@ -19,6 +26,13 @@ module Aspera
|
|
|
19
26
|
|
|
20
27
|
private_constant :CERT_EXT, :SELF_SIGNED_CERT
|
|
21
28
|
|
|
29
|
+
option :insecure, description: 'HTTP/S: Do not validate any certificate', allowed: Type::BOOLEAN, default: false
|
|
30
|
+
option :ignore_certificate, description: 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass]
|
|
31
|
+
option :warn_insecure, description: 'HTTP/S: Issue a warning if certificate is ignored', allowed: Type::BOOLEAN, default: true
|
|
32
|
+
option :cert_stores, description: 'HTTP/S: List of folder with trusted certificates', allowed: Type::STRING_ARRAY
|
|
33
|
+
option :http_options, schema: Schema::Registry::HTTP_OPTIONS
|
|
34
|
+
option :http_proxy, description: 'HTTP/S: URL for proxy with optional credentials'
|
|
35
|
+
|
|
22
36
|
def initialize
|
|
23
37
|
@insecure = false
|
|
24
38
|
@warn_insecure = true
|
|
@@ -29,19 +43,47 @@ module Aspera
|
|
|
29
43
|
@certificate_paths = nil
|
|
30
44
|
end
|
|
31
45
|
|
|
32
|
-
attr_accessor :insecure, :warn_insecure
|
|
33
|
-
attr_reader :ignore_cert_host_port
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
# Called
|
|
37
|
-
# @param options [Aspera::Cli::
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
options.
|
|
41
|
-
options.
|
|
42
|
-
options.
|
|
43
|
-
options.
|
|
44
|
-
options.
|
|
46
|
+
attr_accessor :insecure, :warn_insecure
|
|
47
|
+
attr_reader :ignore_cert_host_port, :http_options
|
|
48
|
+
|
|
49
|
+
# Bind all HTTP options to this instance using set_handler.
|
|
50
|
+
# Called from Config#initialize immediately after Http.new.
|
|
51
|
+
# @param options [Aspera::Cli::Parser]
|
|
52
|
+
# @return [nil]
|
|
53
|
+
def bind_options(options)
|
|
54
|
+
options.set_handler(:insecure, object: self, method: :insecure)
|
|
55
|
+
options.set_handler(:ignore_certificate, object: self, method: :ignore_cert_host_port)
|
|
56
|
+
options.set_handler(:warn_insecure, object: self, method: :warn_insecure)
|
|
57
|
+
options.set_handler(:cert_stores, object: self, method: :trusted_cert_locations)
|
|
58
|
+
options.set_handler(:http_options, object: self, method: :http_options)
|
|
59
|
+
options.set_handler(:http_proxy, object: self, method: :http_proxy)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Setter for http_options: dispatch each key to its target singleton immediately.
|
|
63
|
+
# Keys matching RestParameters setters go to RestParameters, 'ssl_options' goes to SSL,
|
|
64
|
+
# keys matching OAuth::Factory.instance.parameters go to OAuth, and the rest are kept
|
|
65
|
+
# in @http_options for Net::HTTP session configuration in update_session.
|
|
66
|
+
# This runs on every assignment (JSON hash, dotted notation, preset merge) so timing
|
|
67
|
+
# of option parsing never matters.
|
|
68
|
+
# @param new_options [Hash] merged http_options hash
|
|
69
|
+
# @return [nil]
|
|
70
|
+
def http_options=(new_options)
|
|
71
|
+
Aspera.assert_type(new_options, Hash)
|
|
72
|
+
kept = {}
|
|
73
|
+
new_options.each do |k, v|
|
|
74
|
+
method = "#{k}=".to_sym
|
|
75
|
+
if RestParameters.instance.respond_to?(method)
|
|
76
|
+
RestParameters.instance.send(method, v)
|
|
77
|
+
elsif k.to_s.eql?('ssl_options')
|
|
78
|
+
Aspera::SSL.option_list = v
|
|
79
|
+
elsif OAuth::Factory.instance.parameters.key?(k.to_sym)
|
|
80
|
+
OAuth::Factory.instance.parameters[k.to_sym] = v
|
|
81
|
+
else
|
|
82
|
+
kept[k] = v
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
@http_options = kept
|
|
86
|
+
nil
|
|
45
87
|
end
|
|
46
88
|
|
|
47
89
|
# ------------------------------------------------------------------
|
|
@@ -64,7 +106,7 @@ module Aspera
|
|
|
64
106
|
def ignore_cert_host_port=(url_list)
|
|
65
107
|
url_list.each do |url|
|
|
66
108
|
uri = URI.parse(url)
|
|
67
|
-
|
|
109
|
+
Aspera.assert(uri.scheme.eql?('https')) { "Expecting https scheme: #{url}" }
|
|
68
110
|
@ignore_cert_host_port.push([uri.host, uri.port].freeze)
|
|
69
111
|
end
|
|
70
112
|
end
|
|
@@ -77,11 +119,11 @@ module Aspera
|
|
|
77
119
|
if ignore_cert && @warn_insecure
|
|
78
120
|
base_url = "https://#{address}:#{port}"
|
|
79
121
|
unless @ssl_warned_urls.include?(base_url)
|
|
80
|
-
Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
|
|
122
|
+
Log.log.warn { "Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production." }
|
|
81
123
|
@ssl_warned_urls.push(base_url)
|
|
82
124
|
end
|
|
83
125
|
end
|
|
84
|
-
Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
|
|
126
|
+
Log.log.debug { "ignore cert? #{endpoint} -> #{ignore_cert}" }
|
|
85
127
|
ignore_cert
|
|
86
128
|
end
|
|
87
129
|
|
|
@@ -90,23 +132,24 @@ module Aspera
|
|
|
90
132
|
# ------------------------------------------------------------------
|
|
91
133
|
|
|
92
134
|
# Add files, folders or the default OS locations to the cert store.
|
|
93
|
-
# @param path_list [Array<String>]
|
|
135
|
+
# @param path_list [Array<String>] list of file/folder paths to add to the certificate store
|
|
136
|
+
# @return [nil]
|
|
94
137
|
def trusted_cert_locations=(path_list)
|
|
95
|
-
Aspera.assert_type(path_list, Array){'cert locations'}
|
|
138
|
+
Aspera.assert_type(path_list, Array) { 'cert locations' }
|
|
96
139
|
if @certificate_store.nil?
|
|
97
140
|
Log.log.debug('Creating SSL Cert store')
|
|
98
141
|
@certificate_store = OpenSSL::X509::Store.new
|
|
99
142
|
@certificate_paths = []
|
|
100
143
|
end
|
|
101
144
|
path_list.each do |path|
|
|
102
|
-
Aspera.assert_type(path, String){'Expecting a String for certificate location'}
|
|
145
|
+
Aspera.assert_type(path, String) { 'Expecting a String for certificate location' }
|
|
103
146
|
paths_to_add = [path]
|
|
104
|
-
Log.log.debug{"Adding cert location: #{path}"}
|
|
147
|
+
Log.log.debug { "Adding cert location: #{path}" }
|
|
105
148
|
if path.eql?(SpecialValues::DEF)
|
|
106
149
|
@certificate_store.set_default_paths
|
|
107
150
|
paths_to_add = [OpenSSL::X509::DEFAULT_CERT_DIR]
|
|
108
151
|
paths_to_add.push(OpenSSL::X509::DEFAULT_CERT_FILE) unless defined?(JRUBY_VERSION)
|
|
109
|
-
paths_to_add.select!{ |f| File.exist?(f)}
|
|
152
|
+
paths_to_add.select! { |f| File.exist?(f) }
|
|
110
153
|
elsif File.file?(path)
|
|
111
154
|
@certificate_store.add_file(path)
|
|
112
155
|
elsif File.directory?(path)
|
|
@@ -118,14 +161,15 @@ module Aspera
|
|
|
118
161
|
pp = [File.realpath(p)]
|
|
119
162
|
if File.directory?(p)
|
|
120
163
|
pp = Dir.entries(p)
|
|
121
|
-
.map{ |e| File.realpath(File.join(p, e))}
|
|
122
|
-
.select{ |entry| File.file?(entry)}
|
|
123
|
-
.select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
|
|
164
|
+
.map { |e| File.realpath(File.join(p, e)) }
|
|
165
|
+
.select { |entry| File.file?(entry) }
|
|
166
|
+
.select { |entry| CERT_EXT.any? { |ext| entry.end_with?(ext) } }
|
|
124
167
|
end
|
|
125
168
|
@certificate_paths.concat(pp)
|
|
126
169
|
end
|
|
127
170
|
end
|
|
128
171
|
@certificate_paths.uniq!
|
|
172
|
+
nil
|
|
129
173
|
end
|
|
130
174
|
|
|
131
175
|
# Return cert file paths (computes OS defaults lazily if never set).
|
|
@@ -145,22 +189,22 @@ module Aspera
|
|
|
145
189
|
# Called every time a new Net::HTTP session is opened.
|
|
146
190
|
# ------------------------------------------------------------------
|
|
147
191
|
|
|
148
|
-
# @param http_session [Net::HTTP]
|
|
192
|
+
# @param http_session [Net::HTTP] HTTP session to configure
|
|
193
|
+
# @return [nil]
|
|
149
194
|
def update_session(http_session)
|
|
150
195
|
http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
|
|
151
|
-
if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
|
|
152
|
-
http_session.verify_mode = SELF_SIGNED_CERT
|
|
153
|
-
end
|
|
196
|
+
http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
|
|
154
197
|
http_session.cert_store = @certificate_store if @certificate_store
|
|
155
|
-
Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
|
|
198
|
+
Log.log.debug { "Using cert store #{http_session.cert_store} (#{@certificate_store})" } unless http_session.cert_store.nil?
|
|
156
199
|
@http_options.each do |k, v|
|
|
157
200
|
method = "#{k}=".to_sym
|
|
158
201
|
if http_session.respond_to?(method)
|
|
159
202
|
http_session.send(method, v)
|
|
160
203
|
else
|
|
161
|
-
Log.log.error{"Unknown HTTP session attribute: #{k}"}
|
|
204
|
+
Log.log.error { "Unknown HTTP session attribute: #{k}" }
|
|
162
205
|
end
|
|
163
206
|
end
|
|
207
|
+
nil
|
|
164
208
|
end
|
|
165
209
|
end
|
|
166
210
|
end
|
data/lib/aspera/cli/info.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Aspera
|
|
|
15
15
|
# Set this to warn in advance when minimum required ruby version will increase
|
|
16
16
|
# See also required_ruby_version in gemspec file
|
|
17
17
|
RUBY_FUTURE_MINIMUM_VERSION = '3.2'
|
|
18
|
+
# Version with which this version of CLI was tested
|
|
19
|
+
SDK_VERSION = '1.1.9'
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/environment'
|
|
4
|
+
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
6
|
+
require 'erb'
|
|
7
|
+
|
|
8
|
+
module Aspera
|
|
9
|
+
module Cli
|
|
10
|
+
# Email service injected into Context as :mailer so that any component
|
|
11
|
+
# (TransferAgent, plugins, ...) can send emails via SMTP and ERB templates.
|
|
12
|
+
# Provides :smtp, :notify_to, :notify_template options via the Options manager.
|
|
13
|
+
class Mailer
|
|
14
|
+
SMTP_CONF_PARAMS = %i[server tls ssl port domain username password from_name from_email].freeze
|
|
15
|
+
SMTP_BOOL_PARAMS = %i[tls ssl].freeze
|
|
16
|
+
SMTP_INT_PARAMS = %i[port].freeze
|
|
17
|
+
SMTP_STR_PARAMS = %i[server domain username password from_email from_name].freeze
|
|
18
|
+
|
|
19
|
+
# @param options [Parser] CLI options manager (provides :smtp, :notify_to, :notify_template)
|
|
20
|
+
# @param main_folder [String] application main folder (unused directly but kept for symmetry)
|
|
21
|
+
def initialize(options, main_folder)
|
|
22
|
+
@options = options
|
|
23
|
+
@main_folder = main_folder
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Parser]
|
|
27
|
+
attr_reader :options
|
|
28
|
+
|
|
29
|
+
# @return [Hash] email server settings with defaults applied
|
|
30
|
+
def email_settings
|
|
31
|
+
smtp = options.get_option(:smtp, mandatory: true)
|
|
32
|
+
# Change keys from string into symbol
|
|
33
|
+
smtp = smtp.symbolize_keys
|
|
34
|
+
unsupported = smtp.keys - SMTP_CONF_PARAMS
|
|
35
|
+
Aspera.assert(unsupported.empty?, type: Cli::Error) { "Unsupported SMTP parameter: #{unsupported.join(', ')}, use: #{SMTP_CONF_PARAMS.join(', ')}" }
|
|
36
|
+
# Boolean fields must be actual booleans, not strings like "false"
|
|
37
|
+
SMTP_BOOL_PARAMS.each do |k|
|
|
38
|
+
Aspera.assert_values(smtp[k], [true, false], type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
|
|
39
|
+
end
|
|
40
|
+
SMTP_INT_PARAMS.each do |k|
|
|
41
|
+
Aspera.assert_type(smtp[k], Integer, type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
|
|
42
|
+
end
|
|
43
|
+
SMTP_STR_PARAMS.each do |k|
|
|
44
|
+
Aspera.assert_type(smtp[k], String, type: Cli::Error) { "smtp.#{k}" } if smtp.key?(k)
|
|
45
|
+
end
|
|
46
|
+
# smtp[:ssl] = nil (false)
|
|
47
|
+
smtp[:tls] = !smtp[:ssl] unless smtp.key?(:tls)
|
|
48
|
+
smtp[:port] ||= if smtp[:tls]
|
|
49
|
+
587
|
|
50
|
+
elsif smtp[:ssl]
|
|
51
|
+
465
|
|
52
|
+
else
|
|
53
|
+
25
|
|
54
|
+
end
|
|
55
|
+
smtp[:from_email] ||= smtp[:username] if smtp.key?(:username)
|
|
56
|
+
smtp[:from_name] ||= smtp[:from_email].sub(/@.*$/, '').gsub(/[^a-zA-Z]/, ' ').capitalize if smtp.key?(:username)
|
|
57
|
+
smtp[:domain] ||= smtp[:from_email].sub(/^.*@/, '') if smtp.key?(:from_email)
|
|
58
|
+
%i[server port domain].each do |n|
|
|
59
|
+
Aspera.assert(smtp.key?(n)) { "Missing mandatory smtp parameter: #{n}" }
|
|
60
|
+
end
|
|
61
|
+
Log.dump(:smtp, smtp.reject { |k, _| k == :password })
|
|
62
|
+
return smtp
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Send email using ERB template
|
|
66
|
+
# @param email_template_default [String] default template, can be overridden by option
|
|
67
|
+
# @param values [Hash] values to be used in template, keys with default: to, from_name, from_email
|
|
68
|
+
def send_email_template(email_template_default: nil, values: {})
|
|
69
|
+
values[:to] ||= options.get_option(:notify_to, mandatory: true)
|
|
70
|
+
notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
|
|
71
|
+
mail_conf = email_settings
|
|
72
|
+
values[:from_name] ||= mail_conf[:from_name]
|
|
73
|
+
values[:from_email] ||= mail_conf[:from_email]
|
|
74
|
+
%i[to from_email].each do |n|
|
|
75
|
+
Aspera.assert_type(values[n], String) { "Missing email parameter: #{n} in config" }
|
|
76
|
+
end
|
|
77
|
+
start_options = [mail_conf[:domain]]
|
|
78
|
+
start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
|
|
79
|
+
template_binding = Environment.empty_binding
|
|
80
|
+
values.each do |k, v|
|
|
81
|
+
Aspera.assert_type(k, Symbol)
|
|
82
|
+
template_binding.local_variable_set(k, v)
|
|
83
|
+
end
|
|
84
|
+
msg_with_headers = ERB.new(notify_template).result(template_binding)
|
|
85
|
+
Log.dump(:msg_with_headers, msg_with_headers)
|
|
86
|
+
require 'net/smtp'
|
|
87
|
+
smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
|
|
88
|
+
smtp.enable_starttls if mail_conf[:tls]
|
|
89
|
+
smtp.enable_tls if mail_conf[:ssl]
|
|
90
|
+
smtp.start(*start_options) do |smtp_session|
|
|
91
|
+
smtp_session.send_message(msg_with_headers, values[:from_email], values[:to])
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# cspell:ignore ascli
|
|
4
|
+
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'aspera/log'
|
|
7
|
+
require 'aspera/cli/runner'
|
|
8
|
+
require 'aspera/cli/error'
|
|
9
|
+
require 'aspera/schema/reader'
|
|
10
|
+
require 'aspera/schema/registry'
|
|
11
|
+
unless defined?(MCP::Tool)
|
|
12
|
+
begin
|
|
13
|
+
require 'mcp'
|
|
14
|
+
rescue LoadError
|
|
15
|
+
raise Cli::Error, "The 'mcp' and 'rack' gems are required. Install them with: gem install mcp rack"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Aspera
|
|
20
|
+
module Cli
|
|
21
|
+
# MCP Tool: executes an ascli command in-process.
|
|
22
|
+
class McpTool < MCP::Tool
|
|
23
|
+
# Default maximum byte size of the JSON text content returned for list results.
|
|
24
|
+
# Items are appended whole until the limit is reached; the full list is always
|
|
25
|
+
# available in structuredContent.
|
|
26
|
+
DEFAULT_MAX_TEXT_BYTES = 100_000
|
|
27
|
+
# Default extra arguments automatically prepended to every ascli call.
|
|
28
|
+
# Keeps the AI from having to remember mandatory flags on every invocation.
|
|
29
|
+
DEFAULT_EXTRA_ARGS = ['--interactive=no', '--transfer.asynchronous=true'].freeze
|
|
30
|
+
|
|
31
|
+
tool_name 'execute_ascli_command'
|
|
32
|
+
|
|
33
|
+
description <<~DESC.strip
|
|
34
|
+
Execute any ascli (Aspera CLI) command in-process and return its result.
|
|
35
|
+
|
|
36
|
+
SYNTAX
|
|
37
|
+
args is a JSON array of strings mirroring the CLI command line.
|
|
38
|
+
Element 0 : plugin name (aoc, faspex5, node, server, config, …).
|
|
39
|
+
Elements 1+: sub-commands, then --option=value flags in any order.
|
|
40
|
+
Passing structured values: use an extended-value prefix on the relevant element:
|
|
41
|
+
"@json:{...}" — inline JSON object or array (preferred for LLMs: no shell quoting, natural JSON)
|
|
42
|
+
"@preset:name" — expand a saved credential preset
|
|
43
|
+
"@env:VAR" — read value from environment variable
|
|
44
|
+
"@file:/path" — read value from a file
|
|
45
|
+
Note: the dot-path form (@: key=value ...) is designed for humans typing in a terminal shell.
|
|
46
|
+
Prefer @json: when building args programmatically or as an LLM.
|
|
47
|
+
|
|
48
|
+
AUTOMATIC FLAGS
|
|
49
|
+
The server automatically prepends extra_args to every call (default:
|
|
50
|
+
#{DEFAULT_EXTRA_ARGS.join(' ')}).
|
|
51
|
+
Do NOT repeat these flags in your args array — they are already injected.
|
|
52
|
+
If you explicitly need to override one (e.g. --interactive=yes), include it
|
|
53
|
+
in your args; your value will take precedence because it appears after the
|
|
54
|
+
injected ones.
|
|
55
|
+
If credentials are missing or incomplete, the command will return an error;
|
|
56
|
+
report it and stop, never wait for input.
|
|
57
|
+
|
|
58
|
+
DISCOVERY — recommended sequence
|
|
59
|
+
Step 1 — enumerate all commands:
|
|
60
|
+
["config", "commands"]
|
|
61
|
+
Returns { syntax, description } for every leaf command of every plugin.
|
|
62
|
+
Syntax notation: <arg> mandatory, [<arg>] optional, <a|b> enum, <arg...> variadic.
|
|
63
|
+
This single call covers all 800+ commands — no other discovery step is needed
|
|
64
|
+
unless you want details about a specific command or its Hash arguments.
|
|
65
|
+
IMPORTANT: never guess command names from training data. If you are unsure of
|
|
66
|
+
the exact subcommand name (e.g. shared_folders vs shared_inboxes), always call
|
|
67
|
+
["config", "commands"] first to find the correct name.
|
|
68
|
+
|
|
69
|
+
Step 2 — inspect a Hash argument schema BEFORE calling any command with <data>:
|
|
70
|
+
MANDATORY: whenever the command syntax shows a <data> argument, you MUST call
|
|
71
|
+
"help" offline first. Never infer field names from server error messages.
|
|
72
|
+
["<plugin>", "<cmd>", ..., "help"]
|
|
73
|
+
Replace the Hash positional argument with the literal string "help".
|
|
74
|
+
Returns a table of field names, types, and descriptions for that argument.
|
|
75
|
+
Example: ["aoc", "admin", "user", "create", "help"]
|
|
76
|
+
Note: only works for Hash-typed arguments, not for plain String arguments.
|
|
77
|
+
|
|
78
|
+
Step 2b — discover available query/filter parameters for list commands:
|
|
79
|
+
["<plugin>", "<cmd>", ..., "--query=help"]
|
|
80
|
+
Add --query=help to any list command to see all supported filter parameters
|
|
81
|
+
with their types and descriptions.
|
|
82
|
+
Example: ["aoc", "admin", "user", "list", "--query=help"]
|
|
83
|
+
Example: ["faspex5", "admin", "packages", "list", "--query=help"]
|
|
84
|
+
Note: only works on commands that support --query filtering (list/delete).
|
|
85
|
+
|
|
86
|
+
Step 3 — list all options for a plugin as structured data:
|
|
87
|
+
["config", "options", "<plugin>"]
|
|
88
|
+
Returns { option, description, allowed, deprecated } for every --flag
|
|
89
|
+
accepted by that plugin (global + plugin-specific, ~80 entries).
|
|
90
|
+
Use when you need to know the exact allowed values or find a specific flag.
|
|
91
|
+
|
|
92
|
+
Full documentation:
|
|
93
|
+
["config", "documentation", "toc"]
|
|
94
|
+
Returns the table of contents: { level, title, anchor } for every heading.
|
|
95
|
+
["config", "documentation", "local", "<anchor>"]
|
|
96
|
+
Returns only the section matching that anchor (same slugs as GitHub).
|
|
97
|
+
["config", "documentation", "local", "--ui=text"]
|
|
98
|
+
Returns the complete README (~300 KB). Use only when a specific section
|
|
99
|
+
is insufficient and you need broader narrative context.
|
|
100
|
+
|
|
101
|
+
RESULT FORMAT
|
|
102
|
+
Structured data is always in structuredContent (a JSON object).
|
|
103
|
+
For list results, text content is limited to #{DEFAULT_MAX_TEXT_BYTES} bytes (whole items only).
|
|
104
|
+
When truncated, a WARNING block is appended: "WARNING: result truncated to N of TOTAL items."
|
|
105
|
+
You MUST read structuredContent.items to obtain the full dataset — never report
|
|
106
|
+
counts, totals, or search results from a truncated text block.
|
|
107
|
+
|
|
108
|
+
FILE LIST FOR TRANSFERS
|
|
109
|
+
For all transfers (upload, download, package send, …), append source file paths
|
|
110
|
+
at the end of the args array — no --sources flag needed.
|
|
111
|
+
["server", "upload", "--to-folder=/dst", "/local/file1", "/local/file2"]
|
|
112
|
+
["aoc", "packages", "send", "@:", "name=pkg", "recipients.0=user@example.com", "END",
|
|
113
|
+
"/local/file1", "/local/file2"]
|
|
114
|
+
|
|
115
|
+
EXAMPLES
|
|
116
|
+
["config", "commands"] ← Step 1: full capability map
|
|
117
|
+
["aoc", "admin", "user", "create", "help"] ← Step 2: schema of <data> Hash
|
|
118
|
+
["config", "options", "aoc"] ← Step 3: all --flags for aoc plugin
|
|
119
|
+
["config", "documentation", "toc"] ← TOC of local README
|
|
120
|
+
["config", "documentation", "local",
|
|
121
|
+
"leveraging-ai-assistance"] ← single README section by anchor
|
|
122
|
+
["aoc", "admin", "user", "create",
|
|
123
|
+
'@json:{"email":"a@b.com","name":"Alice"}',
|
|
124
|
+
"--url=https://org.ibmaspera.com", "--username=admin@org.com",
|
|
125
|
+
"--password=secret"]
|
|
126
|
+
["server", "browse", "/",
|
|
127
|
+
"--url=https://host", "--username=user", "--password=secret"]
|
|
128
|
+
["server", "upload", "--url=https://host", "--username=user", "--password=secret",
|
|
129
|
+
"--to-folder=/uploads", "/local/file1.txt", "/local/file2.txt"]
|
|
130
|
+
["aoc", "packages", "list", "--workspace=MyWorkspace"]
|
|
131
|
+
["node", "info", "--url=https://node-host",
|
|
132
|
+
"--username=user", "--password=pass"]
|
|
133
|
+
DESC
|
|
134
|
+
|
|
135
|
+
input_schema(
|
|
136
|
+
properties: {
|
|
137
|
+
args: {
|
|
138
|
+
type: 'array',
|
|
139
|
+
items: {type: 'string'},
|
|
140
|
+
minItems: 1,
|
|
141
|
+
description: 'ascli arguments: first element is the plugin name, followed by action and --option=value flags'
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: ['args']
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
class << self
|
|
148
|
+
attr_accessor :max_text_bytes, :extra_args
|
|
149
|
+
|
|
150
|
+
def call(args:, server_context: nil)
|
|
151
|
+
effective_args = Array(extra_args || DEFAULT_EXTRA_ARGS) + args
|
|
152
|
+
Log.dump(:mcp_execute, effective_args)
|
|
153
|
+
runner = Runner.new(effective_args)
|
|
154
|
+
result = runner.run_with_result
|
|
155
|
+
case result
|
|
156
|
+
when Result::Nothing, Result::Empty, NilClass
|
|
157
|
+
MCP::Tool::Response.new([{type: 'text', text: ''}])
|
|
158
|
+
when Result::SingleObject, Result::ObjectList, Result::ValueList
|
|
159
|
+
# Apply --select filter in place (affects both text and structuredContent).
|
|
160
|
+
runner.context.formatter.filter_columns_on_select(result.data) if result.data.is_a?(Array)
|
|
161
|
+
# MCP spec requires structuredContent to be a JSON object (not an array).
|
|
162
|
+
structured = result.data.is_a?(Array) ? {items: result.data} : result.data
|
|
163
|
+
content = if result.data.is_a?(Array)
|
|
164
|
+
text_limit = max_text_bytes || DEFAULT_MAX_TEXT_BYTES
|
|
165
|
+
truncated_items = truncate_items_by_bytes(result.data, text_limit)
|
|
166
|
+
total = result.data.size
|
|
167
|
+
if truncated_items.size < total
|
|
168
|
+
[
|
|
169
|
+
{type: 'text', text: JSON.generate(truncated_items)},
|
|
170
|
+
{type: 'text', text: "WARNING: result truncated to #{truncated_items.size} of #{total} items. Full dataset available in structuredContent."}
|
|
171
|
+
]
|
|
172
|
+
else
|
|
173
|
+
[{type: 'text', text: JSON.generate(result.data)}]
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
[{type: 'text', text: JSON.generate(result.data)}]
|
|
177
|
+
end
|
|
178
|
+
MCP::Tool::Response.new(content, structured_content: structured)
|
|
179
|
+
else
|
|
180
|
+
MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}])
|
|
181
|
+
end
|
|
182
|
+
rescue Cli::SchemaRequest => e
|
|
183
|
+
schema_path = e.path
|
|
184
|
+
if schema_path.nil?
|
|
185
|
+
MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{e.message} (no schema available)"}], error: true)
|
|
186
|
+
else
|
|
187
|
+
rows = schema_to_rows(Schema::Registry.instance.reader(schema_path))
|
|
188
|
+
structured = {items: rows}
|
|
189
|
+
MCP::Tool::Response.new([{type: 'text', text: JSON.generate(rows)}], structured_content: structured)
|
|
190
|
+
end
|
|
191
|
+
rescue SystemExit => e
|
|
192
|
+
MCP::Tool::Response.new([{type: 'text', text: "exited with status #{e.status}"}], error: !e.status.zero?)
|
|
193
|
+
rescue => e
|
|
194
|
+
MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{dedupe_lines(e.message)}"}], error: true)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Delegate to Schema::Reader#to_rows — semantic fields, no ANSI, MCP/JSON-ready.
|
|
198
|
+
def schema_to_rows(reader)
|
|
199
|
+
reader.to_rows
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Collapse consecutive duplicate lines in a multi-line message.
|
|
203
|
+
# Each run of identical lines is replaced by one line + "(×N)" suffix when N > 1.
|
|
204
|
+
def dedupe_lines(msg)
|
|
205
|
+
return msg unless msg.include?("\n")
|
|
206
|
+
msg.split("\n").chunk_while { |a, b| a == b }.map do |group|
|
|
207
|
+
group.size > 1 ? "#{group.first} (x#{group.size})" : group.first
|
|
208
|
+
end.join("\n")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns the largest prefix of +items+ whose JSON serialization fits within +max_bytes+.
|
|
212
|
+
# Items are appended whole — no item is ever split mid-JSON.
|
|
213
|
+
def truncate_items_by_bytes(items, max_bytes)
|
|
214
|
+
buf = +''
|
|
215
|
+
items.each_with_index do |item, i|
|
|
216
|
+
fragment = (i.zero? ? '[' : ',') + JSON.generate(item)
|
|
217
|
+
break if buf.bytesize + fragment.bytesize + 1 > max_bytes # +1 for closing ']'
|
|
218
|
+
buf << fragment
|
|
219
|
+
end
|
|
220
|
+
buf.empty? ? [] : JSON.parse("#{buf}]")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/parser'
|
|
4
|
+
require 'aspera/cli/command_spec'
|
|
5
|
+
|
|
6
|
+
module Aspera
|
|
7
|
+
module Cli
|
|
8
|
+
# Mixin providing declarative CLI option definitions (`option :name, ...`)
|
|
9
|
+
# and batch declaration onto a Parser instance (`declare_options(parser)`).
|
|
10
|
+
module OptionDeclarator
|
|
11
|
+
# Registry of OptionSpec objects defined on this class/module.
|
|
12
|
+
# @return [Hash{Symbol => OptionSpec}]
|
|
13
|
+
def option_specs
|
|
14
|
+
@option_specs ||= {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Declare an option in this class's registry.
|
|
18
|
+
#
|
|
19
|
+
# @param name [Symbol] Option name
|
|
20
|
+
# @param description [String, nil] User-facing description
|
|
21
|
+
# @param short [String, nil] Single-character short form
|
|
22
|
+
# @param allowed [Object, nil] Allowed values
|
|
23
|
+
# @param default [Object, nil] Default value
|
|
24
|
+
# @param handler [Symbol, Hash, nil] Handler (Symbol or Hash)
|
|
25
|
+
# @param deprecation [String, nil] Deprecation message
|
|
26
|
+
# @param schema [String, nil] Schema reference
|
|
27
|
+
def option(name, description: nil,
|
|
28
|
+
short: nil, allowed: nil, default: nil,
|
|
29
|
+
handler: nil, deprecation: nil, schema: nil)
|
|
30
|
+
raise ArgumentError, "Duplicate option: #{name.inspect}" if option_specs.key?(name)
|
|
31
|
+
option_specs[name] = OptionSpec.new(
|
|
32
|
+
name: name,
|
|
33
|
+
description: description,
|
|
34
|
+
short: short,
|
|
35
|
+
allowed: allowed,
|
|
36
|
+
default: default,
|
|
37
|
+
handler: handler,
|
|
38
|
+
deprecation: deprecation,
|
|
39
|
+
schema: schema
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Declare all options registered on this class onto a Parser instance.
|
|
44
|
+
# Skips options already declared on the parser.
|
|
45
|
+
#
|
|
46
|
+
# @param parser [Aspera::Cli::Parser]
|
|
47
|
+
# @param target [Object, nil] default target object for Symbol handlers (defaults to self)
|
|
48
|
+
# @return [void]
|
|
49
|
+
def declare_options(parser, target: self)
|
|
50
|
+
option_specs.each_value do |spec|
|
|
51
|
+
next if parser.option_declared?(spec.name)
|
|
52
|
+
resolved_handler =
|
|
53
|
+
case spec.handler
|
|
54
|
+
when Symbol then {o: target, m: spec.handler}
|
|
55
|
+
when Hash then spec.handler
|
|
56
|
+
end
|
|
57
|
+
parser.declare(
|
|
58
|
+
spec.name,
|
|
59
|
+
description: spec.description,
|
|
60
|
+
short: spec.short,
|
|
61
|
+
allowed: spec.allowed,
|
|
62
|
+
default: spec.default,
|
|
63
|
+
handler: resolved_handler,
|
|
64
|
+
deprecation: spec.deprecation,
|
|
65
|
+
schema: schema_for_spec(spec)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def schema_for_spec(spec)
|
|
73
|
+
spec.schema
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|