Class: Rubino::CLI::ConfigCommand
- Inherits:
-
Thor
- Object
- Thor
- Rubino::CLI::ConfigCommand
- Defined in:
- lib/rubino/cli/config_command.rb
Overview
Subcommands for managing configuration
Constant Summary collapse
- ALIASES =
Discoverability aliases (#36-follow-up): /status advertises settings by the short label a user then types into ‘/config <key>`, but the real config key is nested — so `/config reasoning` reported “not found” even though /status shows “reasoning:” and `/reasoning` is a command. Map the advertised short names (and the names of the commands that WRITE them) to their dotted config paths so get/set resolve the same key /status shows. Dotted keys are unaffected (a user can still pass `display.reasoning`).
{ "reasoning" => "display.reasoning", "effort" => "thinking.effort", "think" => "thinking.effort" }.freeze
- GET_FALLBACKS =
GET-only fallbacks (#66): a resolved key whose value, when unset, still lives under a legacy sibling. ‘/reasoning` and `/status` use display.reasoning (the SET target stays canonical), but a config carrying only the documented legacy display.show_reasoning boolean would otherwise report “not found” on `/config reasoning`. Read through to the legacy key so a value set either way resolves; set never touches the legacy key.
{ "display.reasoning" => "display.show_reasoning" }.freeze
Class Method Summary collapse
-
.dig_config(path) ⇒ Object
Effective-config read for a dotted
path. - .exit_on_failure? ⇒ Boolean
-
.from_defaults?(path) ⇒ Boolean
True when
pathhas no value in the user’s config.yml as written on disk (so the merged value is coming from the built-in defaults). -
.redact(value, key: nil) ⇒ Object
Deep DISPLAY masking for config values (#187): a secret-named key’s value renders as *** (Util::SecretsMask — the same heuristic approval prompts use), hashes/arrays are walked, and plain strings are scanned for inline ‘Bearer …`-style credentials.
-
.render_get(key, ui:) ⇒ Object
ONE get rendering for both surfaces (#187): this CLI verb and the in-chat ‘/config get` (Commands::Executor).
-
.render_show(ui:) ⇒ Object
ONE full-config rendering for both surfaces (#187): this CLI verb and the in-chat ‘/config show` — with secret-named keys masked, which the clear-text dump never did (api_key landed verbatim in the scrollback).
Instance Method Summary collapse
Class Method Details
.dig_config(path) ⇒ Object
Effective-config read for a dotted path. A scalar intermediate node (descending into a String) has no #dig; treat such a path as unset rather than crashing.
98 99 100 101 102 |
# File 'lib/rubino/cli/config_command.rb', line 98 def self.dig_config(path) Rubino.configuration.dig(*path) rescue TypeError nil end |
.exit_on_failure? ⇒ Boolean
13 14 15 |
# File 'lib/rubino/cli/config_command.rb', line 13 def self.exit_on_failure? true end |
.from_defaults?(path) ⇒ Boolean
True when path has no value in the user’s config.yml as written on disk (so the merged value is coming from the built-in defaults). Best-effort: any read hiccup reports false (no annotation) rather than a false “(default)”. A nil at the path in the raw file counts as “not set”.
108 109 110 111 112 113 114 115 116 |
# File 'lib/rubino/cli/config_command.rb', line 108 def self.from_defaults?(path) raw = begin Config::Loader.new.raw_config rescue StandardError {} end raw.is_a?(Hash) && raw.dig(*path).nil? end |
.redact(value, key: nil) ⇒ Object
Deep DISPLAY masking for config values (#187): a secret-named key’s value renders as *** (Util::SecretsMask — the same heuristic approval prompts use), hashes/arrays are walked, and plain strings are scanned for inline ‘Bearer …`-style credentials. Display-only — the file and the live configuration keep the real values. Empty/nil values pass through unmasked so a *** never fakes a value that isn’t set.
171 172 173 174 175 176 177 178 179 |
# File 'lib/rubino/cli/config_command.rb', line 171 def self.redact(value, key: nil) case value when Hash then value.to_h { |k, v| [k, redact(v, key: k)] } when Array then value.map { |v| redact(v, key: key) } when String value.empty? ? value : Util::SecretsMask.mask_value(value, key: key) else value end end |
.render_get(key, ui:) ⇒ Object
ONE get rendering for both surfaces (#187): this CLI verb and the in-chat ‘/config get` (Commands::Executor). Resolves against the effective config (file merged over defaults), the same source `show` and the running agent use, so default-valued keys are returned instead of falsely reported “not found” (issue #36). A scalar intermediate node (e.g. descending into a String) has no #dig; treat such a path as “not found” rather than crashing. Secret-named keys render masked.
Returns true when the key resolved, false when not found, so the CLI verb can exit non-zero on a miss (P2-H1) while the REPL surface ignores the return. The not-found NOTICE is left to each caller: the CLI verb raises a Thor::Error (stderr + non-zero), the in-chat handler shows the stdout warning below — so a miss never double-prints. rubocop:disable Naming/PredicateMethod – it RENDERS (a side effect) and returns found?; it isn’t a pure predicate, and the name is the documented shared-renderer seam (#187) referenced by the in-chat handler.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/rubino/cli/config_command.rb', line 74 def self.render_get(key, ui:) key = ALIASES.fetch(key, key) path = key.split(".") value = dig_config(path) if value.nil? && (legacy = GET_FALLBACKS[key]) path = legacy.split(".") value = dig_config(path) key = legacy unless value.nil? end return false if value.nil? # F4: annotate a value that comes from the built-in DEFAULTS rather than # the user's config.yml, so "I unset it but `config get` still shows a # value" reads correctly — the default is what's in effect, not a stale # setting. A key whose resolved value is NOT present in the raw (un-merged) # user file is default-sourced. suffix = from_defaults?(path) ? " (default)" : "" ui.info("#{key} = #{redact(value, key: path.last)}#{suffix}") true end |
.render_show(ui:) ⇒ Object
ONE full-config rendering for both surfaces (#187): this CLI verb and the in-chat ‘/config show` — with secret-named keys masked, which the clear-text dump never did (api_key landed verbatim in the scrollback).
161 162 163 |
# File 'lib/rubino/cli/config_command.rb', line 161 def self.render_show(ui:) ui.info(redact(Rubino.configuration.raw).to_yaml) end |
Instance Method Details
#get(key) ⇒ Object
48 49 50 51 52 53 54 55 56 |
# File 'lib/rubino/cli/config_command.rb', line 48 def get(key) # A missing key is a FAILURE on the automation surface (P2-H1/H2): when # render_get reports not-found, raise Thor::Error so exit_on_failure? # exits non-zero with the message on stderr (the shared renderer's # ui.warning went to stdout and returned 0). The in-chat `/config get` # surface ignores the return value, so its REPL-friendly warning stays. found = self.class.render_get(key, ui: Rubino.ui) raise Thor::Error, "config key not found: #{key}" unless found end |
#path ⇒ Object
182 183 184 |
# File 'lib/rubino/cli/config_command.rb', line 182 def path Rubino.ui.info(config_path) end |
#set(key, value) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/rubino/cli/config_command.rb', line 120 def set(key, value) key = ALIASES.fetch(key, key) writer = Config::Writer.new(config_path: config_path) writer.set(key, value) # Mask a secret-named value the SAME way `config get`/`show` do (#187): # a successful SET must not echo a raw api_key/token into the scrollback. Rubino.ui.success("#{key} = #{self.class.redact(value, key: key.split(".").last)}") rescue ConfigurationError => e # A validation failure is a FAILURE on the automation surface: route the # ✗ line to STDERR (it used to print on stdout), keeping exit 1. For an # array-typed key, append the accepted syntax — Writer.coerce_array only # accepts an explicit JSON array literal — so the user isn't left # guessing how to pass multiple values (e.g. `"git log"` was rejected). warn "✗ #{e.}" warn array_syntax_hint(key) if e..include?("expected array") exit(1) end |
#show ⇒ Object
154 155 156 |
# File 'lib/rubino/cli/config_command.rb', line 154 def show self.class.render_show(ui: Rubino.ui) end |
#unset(key) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/rubino/cli/config_command.rb', line 139 def unset(key) writer = Config::Writer.new(config_path: config_path) if writer.unset(key) Rubino.ui.success("unset #{key}") else # Not present is a no-op, not a failure: exit 0 with a clear notice so # `config unset` is idempotent (re-running it never errors). Rubino.ui.info("#{key} was not set (nothing to remove)") end rescue ConfigurationError => e Rubino.ui.error(e.) exit(1) end |