Class: Rubino::CLI::ConfigCommand

Inherits:
Thor
  • Object
show all
Defined in:
lib/rubino/cli/config_command.rb

Overview

Subcommands for managing configuration

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (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”.

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
# File 'lib/rubino/cli/config_command.rb', line 75

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.



131
132
133
134
135
136
137
138
139
# File 'lib/rubino/cli/config_command.rb', line 131

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.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/rubino/cli/config_command.rb', line 51

def self.render_get(key, ui:)
  path = key.split(".")
  value =
    begin
      Rubino.configuration.dig(*path)
    rescue TypeError
      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).



121
122
123
# File 'lib/rubino/cli/config_command.rb', line 121

def self.render_show(ui:)
  ui.info(redact(Rubino.configuration.raw).to_yaml)
end

Instance Method Details

#get(key) ⇒ Object

Raises:

  • (Thor::Error)


25
26
27
28
29
30
31
32
33
# File 'lib/rubino/cli/config_command.rb', line 25

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

#pathObject



142
143
144
# File 'lib/rubino/cli/config_command.rb', line 142

def path
  Rubino.ui.info(config_path)
end

#set(key, value) ⇒ Object



87
88
89
90
91
92
93
94
95
96
# File 'lib/rubino/cli/config_command.rb', line 87

def set(key, value)
  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
  Rubino.ui.error(e.message)
  exit(1)
end

#showObject



114
115
116
# File 'lib/rubino/cli/config_command.rb', line 114

def show
  self.class.render_show(ui: Rubino.ui)
end

#unset(key) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/rubino/cli/config_command.rb', line 99

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.message)
  exit(1)
end