aspera-cli 4.27.2 → 4.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'json'
6
6
  require 'aspera/log'
7
+ require 'aspera/secret_hider'
7
8
  require 'aspera/cli/runner'
8
9
  require 'aspera/cli/error'
9
10
  require 'aspera/schema/reader'
@@ -36,100 +37,61 @@ module Aspera
36
37
  SYNTAX
37
38
  args is a JSON array of strings mirroring the CLI command line.
38
39
  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
40
+ Elements 1+: sub-commands, positional arguments, and --option=value flags in any order.
41
+ Structured values use an extended-value prefix on the relevant element:
42
+ "@json:{...}" — inline JSON object or array (preferred: no shell quoting)
43
+ "@preset:name" — expand a saved preset
43
44
  "@env:VAR" — read value from environment variable
44
45
  "@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
46
 
48
47
  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.
48
+ The server prepends extra_args to every call (default: #{DEFAULT_EXTRA_ARGS.join(' ')}).
49
+ Do NOT repeat them. To override one, include it in args: later values take precedence.
50
+ Commands never prompt: if credentials are missing, an error is returned; report it and stop.
51
+
52
+ DISCOVERY — never guess command or field names from training data
53
+ Step 1 — commands of a plugin, with syntax:
54
+ ["config", "commands", "<plugin>"]
55
+ Returns { syntax, description } for every command of that plugin.
56
+ Notation: <arg> mandatory, [<arg>] optional, <a|b> enum, <arg...> variadic, <arg:Hash> typed.
57
+ A line ending with <command...> and "(see: <plugin> <path>)" provides the commands of that path:
58
+ list them with ["config", "commands", "<plugin>", "<command>", ...] (command words only).
59
+ Omit <plugin> to list the commands of all plugins (large).
60
+ Step 2 — schema of a Hash argument (shown <name:Hash>), MANDATORY before calling such a command:
72
61
  ["<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:
62
+ Put the literal "help" in place of the Hash argument. Returns field names, types,
63
+ required flags and descriptions. Never infer fields from server error messages.
64
+ Step 2b — filter parameters of a list command:
79
65
  ["<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:
66
+ Step 3 — all --flags of a plugin, with allowed values:
87
67
  ["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.
68
+ Documentation:
69
+ ["config", "documentation", "toc"] → { level, title, anchor } per heading
70
+ ["config", "documentation", "local", "<anchor>"] → one README section
71
+ Presets (saved credentials):
72
+ ["config", "preset", "overview"]
100
73
 
101
74
  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.
75
+ Structured data is always in structuredContent (a JSON object; lists are under "items").
76
+ For lists, text content is limited to #{DEFAULT_MAX_TEXT_BYTES} bytes (whole items only);
77
+ when truncated, a WARNING line gives the real total. Read structuredContent.items for
78
+ the full dataset — never report counts or search results from a truncated text block.
107
79
 
108
80
  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"]
81
+ For all transfers (upload, download, package send, …), append source paths at the
82
+ end of args — no --sources flag needed.
114
83
 
115
84
  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"]
85
+ ["config", "commands", "aoc"]
86
+ ["aoc", "admin", "user", "create", "help"]
87
+ ["config", "options", "aoc"]
88
+ ["config", "documentation", "local", "leveraging-ai-assistance"]
89
+ ["aoc", "--preset=myaoc", "packages", "list"]
90
+ ["aoc", "--preset=myaoc", "admin", "user", "create", '@json:{"email":"a@b.com","name":"Alice"}']
91
+ ["aoc", "--preset=myaoc", "packages", "send",
92
+ '@json:{"name":"pkg","recipients":["user@example.com"]}', "/local/a.txt", "/local/b.txt"]
93
+ ["server", "--preset=myserver", "upload", "--to-folder=/uploads", "/local/a.txt"]
94
+ ["server", "browse", "/", "--url=ssh://host:33001", "--username=user", "--password=secret"]
133
95
  DESC
134
96
 
135
97
  input_schema(
@@ -156,6 +118,8 @@ module Aspera
156
118
  when Result::Nothing, Result::Empty, NilClass
157
119
  MCP::Tool::Response.new([{type: 'text', text: ''}])
158
120
  when Result::SingleObject, Result::ObjectList, Result::ValueList
121
+ # Secrets never reach the AI client, whatever the output format requested in args.
122
+ SecretHider.instance.deep_remove_secret(result.data)
159
123
  # Apply --select filter in place (affects both text and structuredContent).
160
124
  runner.context.formatter.filter_columns_on_select(result.data) if result.data.is_a?(Array)
161
125
  # MCP spec requires structuredContent to be a JSON object (not an array).
@@ -177,7 +141,7 @@ module Aspera
177
141
  end
178
142
  MCP::Tool::Response.new(content, structured_content: structured)
179
143
  else
180
- MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}])
144
+ MCP::Tool::Response.new([{type: 'text', text: SecretHider.instance.hide_secrets_in_string(result.data.to_s, all: true)}])
181
145
  end
182
146
  rescue Cli::SchemaRequest => e
183
147
  schema_path = e.path
@@ -208,7 +172,7 @@ module Aspera
208
172
  end.join("\n")
209
173
  end
210
174
 
211
- # Returns the largest prefix of +items+ whose JSON serialization fits within +max_bytes+.
175
+ # Returns the largest prefix of `items` whose JSON serialization fits within `max_bytes`.
212
176
  # Items are appended whole — no item is ever split mid-JSON.
213
177
  def truncate_items_by_bytes(items, max_bytes)
214
178
  buf = +''
@@ -16,62 +16,53 @@ module Aspera
16
16
 
17
17
  # Declare an option in this class's registry.
18
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
19
+ # @param name [Symbol] Option name
20
+ # @param description [String, nil] User-facing description; if nil, derived from schema: title/description
21
+ # @param short [String, nil] Single-character short form (without leading '-')
22
+ # @param allowed [Object, nil] Allowed values (see OptionValue)
23
+ # @param default [Object, nil] Default value
24
+ # @param on_set [Symbol, Proc, #call, nil] `on_set` callback (see OptionSpec)
25
+ # @param shorthand [String, nil] For a `Hash` option: a `String` value is stored as `{shorthand => value}`
26
+ # @param deprecation [Hash, nil] Deprecation `{last:, message:}` forwarded to options.declare (see `Deprecation`)
27
+ # @param schema [String, nil] Schema reference (e.g. "opts:components.schemas.Foo");
28
+ # when description: is nil, the schema title or first description line is used
27
29
  def option(name, description: nil,
28
30
  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
31
+ on_set: nil, shorthand: nil, deprecation: nil, schema: nil)
32
+ register_option_spec(
33
+ OptionSpec.new(
34
+ name: name,
35
+ description: description,
36
+ short: short,
37
+ allowed: allowed,
38
+ default: default,
39
+ on_set: on_set,
40
+ shorthand: shorthand,
41
+ deprecation: deprecation,
42
+ schema: schema
43
+ )
40
44
  )
41
45
  end
42
46
 
47
+ # Store an OptionSpec in this class's registry.
48
+ # @param spec [OptionSpec]
49
+ # @raise [ArgumentError] on duplicate option name
50
+ def register_option_spec(spec)
51
+ raise ArgumentError, "Duplicate option: #{spec.name.inspect}" if option_specs.key?(spec.name)
52
+ option_specs[spec.name] = spec
53
+ end
54
+
43
55
  # Declare all options registered on this class onto a Parser instance.
44
56
  # Skips options already declared on the parser.
45
57
  #
46
58
  # @param parser [Aspera::Cli::Parser]
47
- # @param target [Object, nil] default target object for Symbol handlers (defaults to self)
59
+ # @param target [Object, nil] default target object for Symbol and Proc `on_set` callbacks (defaults to self)
48
60
  # @return [void]
49
61
  def declare_options(parser, target: self)
50
62
  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
- )
63
+ spec.declare_on(parser, target: target) unless parser.option_declared?(spec.name)
67
64
  end
68
65
  end
69
-
70
- private
71
-
72
- def schema_for_spec(spec)
73
- spec.schema
74
- end
75
66
  end
76
67
  end
77
68
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/option_value'
4
+ require 'aspera/cli/error'
5
+ require 'aspera/assert'
6
+
7
+ module Aspera
8
+ module Cli
9
+ # Declared options, and their lookup by name: long, short, or unique abbreviation.
10
+ class OptionRegistry
11
+ # @return [Hash{Symbol => OptionValue}] declared options, in declaration order
12
+ attr_reader :options
13
+ # @return [String] help section of options declared next
14
+ attr_accessor :group
15
+
16
+ def initialize
17
+ @options = {}
18
+ # Short option char -> option, e.g. {'h' => help option}
19
+ @short_options = {}
20
+ @group = 'global'
21
+ end
22
+
23
+ # Register a new option
24
+ # @param option [OptionValue] option to register
25
+ # @param short [String, nil] short option char
26
+ # @return [OptionValue] the option
27
+ def add(option, short: nil)
28
+ Aspera.assert(!@options.key?(option.option)) { "#{option.option} already declared" }
29
+ option.group = @group
30
+ option.short = short
31
+ @options[option.option] = option
32
+ @short_options[short] = option unless short.nil?
33
+ option
34
+ end
35
+
36
+ # @param option_symbol [Symbol] option name
37
+ # @return [Boolean] `true` if option is declared
38
+ def declared?(option_symbol) = @options.key?(option_symbol)
39
+
40
+ # @param option_symbol [Symbol] option name
41
+ # @return [OptionValue] declared option
42
+ # @raise [BadArgument] if option is not declared
43
+ def fetch(option_symbol)
44
+ Aspera.assert(@options.key?(option_symbol), type: BadArgument) { "Unknown option: #{option_symbol}" }
45
+ @options[option_symbol]
46
+ end
47
+
48
+ # @param char [String] short option char
49
+ # @return [OptionValue, nil] declared option, or `nil`
50
+ def by_short(char) = @short_options[char]
51
+
52
+ # Find long option by exact name, or by unique abbreviation.
53
+ # @param name [String] option name with underscores
54
+ # @param allow_abbreviation [Boolean] accept a unique prefix of a declared option
55
+ # @return [OptionValue, nil] declared option, or `nil` if none matches (yet)
56
+ # @raise [BadArgument] if abbreviation is ambiguous
57
+ def by_long(name, allow_abbreviation: true)
58
+ option = @options[name.to_sym]
59
+ return option if !option.nil? || !allow_abbreviation
60
+ candidates = @options.keys.select { |k| k.to_s.start_with?(name) }
61
+ return if candidates.empty?
62
+ Aspera.assert(candidates.length.eql?(1), type: BadArgument) do
63
+ Parser.multi_choice_assert_msg("Ambiguous option: #{Parser.option_name_to_line(name)}", candidates.map { |c| Parser.option_name_to_line(c) })
64
+ end
65
+ @options[candidates.first]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/error'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ # Exception raised when schema is asked (`help`)
9
+ class SchemaRequest < Error
10
+ # Value of option or argument that requests its schema
11
+ KEYWORD = 'help'
12
+
13
+ # @return [String, nil] path to schema file
14
+ attr_reader :path
15
+
16
+ # @param type [Symbol] :argument or :option
17
+ # @param name [String] name of the option/argument
18
+ # @param schema_path [String, nil] path to schema file, or `nil` if not available
19
+ def initialize(type, name, schema_path)
20
+ super("#{type}: #{name}")
21
+ @path = schema_path
22
+ end
23
+ end
24
+
25
+ # Values accepted for boolean options: `true`, `false`, `yes`, `no`
26
+ module BoolValue
27
+ # Symbol for `true`
28
+ YES_SYM = :yes
29
+ # Symbol for `false`
30
+ NO_SYM = :no
31
+ # Values meaning `false`
32
+ FALSE_VALUES = [NO_SYM, false].freeze
33
+ # Values meaning `true`
34
+ TRUE_VALUES = [YES_SYM, true].freeze
35
+ private_constant :FALSE_VALUES, :TRUE_VALUES
36
+ # Boolean values
37
+ # @return [Array<true, false, :yes, :no>]
38
+ ALL = (TRUE_VALUES + FALSE_VALUES).freeze
39
+ # `false` and `true`
40
+ TYPES = [FalseClass, TrueClass].freeze
41
+ # `:no` and `:yes`
42
+ SYMBOLS = [NO_SYM, YES_SYM].freeze
43
+ # @return [Boolean] `true` if value is a value for `true` in `ALL`
44
+ def true?(enum)
45
+ Aspera.assert_values(enum, ALL) { 'boolean' }
46
+ TRUE_VALUES.include?(enum)
47
+ end
48
+
49
+ # @return [Boolean] `true` if value is a value for `true` or `false` in ALL
50
+ def symbol?(sym)
51
+ ALL.include?(sym)
52
+ end
53
+ module_function :true?, :symbol?
54
+ end
55
+
56
+ # Type specifiers for the `allowed:` parameter of option declarations.
57
+ # Public API: STRING_ARRAY, SYMBOL_ARRAY, INTEGER, FLOAT, BOOLEAN, NONE.
58
+ # Internal (do not pass as `allowed:`):
59
+ # ENUM - derived internally when `allowed:` is an Array<Symbol> (enum list)
60
+ # STRING - the implicit default; equivalent to omitting `allowed:` entirely
61
+ module Type
62
+ # Option value is a String or Array of Strings (cumulative)
63
+ STRING_ARRAY = [Array, String].freeze
64
+ # Option value is a Symbol from a constrained list; use as prefix: SYMBOL_ARRAY + [:val1, :val2]
65
+ SYMBOL_ARRAY = [Array, Symbol].freeze
66
+ # Option value is coerced to Integer
67
+ INTEGER = [Integer].freeze
68
+ # Option value is coerced to Float
69
+ FLOAT = [Float].freeze
70
+ # Option value is a Boolean
71
+ BOOLEAN = BoolValue::TYPES
72
+ # Option has no value — it is a flag switch (e.g. `-N`, `--help`)
73
+ NONE = [].freeze
74
+ # Internal: derived when allowed: is an Array<Symbol>; do not pass directly
75
+ ENUM = [Symbol].freeze
76
+ # Internal: implicit default (String); equivalent to omitting allowed: entirely
77
+ STRING = [String].freeze
78
+ end
79
+
80
+ # Sources of option values.
81
+ # A value is ignored if the option was already set from a source with higher priority.
82
+ module OptionSource
83
+ # Source -> priority (higher wins)
84
+ PRIORITY = {
85
+ default: 0,
86
+ # Default preset for plugin (added with `override: false`)
87
+ plugin_preset: 1,
88
+ preset: 2,
89
+ env: 3,
90
+ cmdline: 4,
91
+ # Set by code, or asked to user: same as command line
92
+ code: 4,
93
+ interactive: 4
94
+ }.freeze
95
+
96
+ class << self
97
+ # @param source [Symbol] one of `PRIORITY` keys
98
+ # @return [Integer] priority of source
99
+ def priority(source)
100
+ PRIORITY.fetch(source) { Aspera.error_unexpected_value(source) { 'option source' } }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end