Class: SmilyCli::CLI

Inherits:
BaseCommand show all
Defined in:
lib/smily_cli/cli.rb

Overview

The top-level smily command. It wires the reusable resource CRUD commands (one registered subcommand per Registry entry), the auth/config subcommand groups, and a handful of core commands (api, resources, whoami, completion, version). Business logic lives in the library classes; this class only parses input and dispatches.

Constant Summary collapse

HOISTABLE_VALUE_FLAGS =

Single-value global flags that are safe to move past the subcommand.

%w[
  --profile --token --base-url --account-id --max-pages --retry
  -o --output --mcp-token --mcp-url
].freeze
HOISTABLE_BOOL_FLAGS =

Value-less global flags.

%w[-v --verbose -q --quiet --no-color].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseCommand

exit_on_failure?

Class Method Details

.command_namesArray<String>

Every top-level command word, so normalization can tell a real flag value from a subcommand the user meant to run.

Returns:

  • (Array<String>)


31
32
33
34
35
# File 'lib/smily_cli/cli.rb', line 31

def self.command_names
  @command_names ||= (
    Registry.commands + %w[api auth config mcp resources whoami completion version help]
  ).freeze
end

.normalize_argv(argv) ⇒ Array<String>

Thor's registered subcommands can't see class options that appear before the subcommand word, so smily -o json rentals list would drop -o. We normalize by moving any leading global flags to the end of the args, where every command re-declares them. (Array-valued flags like --fields are left alone and must follow the subcommand.)

A leading value flag with no attached =value whose next token is missing or is itself a command word is a forgotten value (smily --token rentals list). We fail with a clear UsageError rather than swallow the subcommand into the flag — which otherwise yields a baffling "command not found" or a request fired with a bogus value. Use --flag=value when a value legitimately collides with a command name (e.g. a profile named like a resource).

Parameters:

  • argv (Array<String>)

Returns:

  • (Array<String>)

Raises:

  • (UsageError)

    when a leading value flag is missing its value



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/smily_cli/cli.rb', line 54

def self.normalize_argv(argv)
  leading = []
  rest = argv.dup
  until rest.empty?
    token = rest.first
    break if token == "--"

    name = token.split("=", 2).first
    if HOISTABLE_VALUE_FLAGS.include?(name)
      leading << rest.shift
      next if token.include?("=") # value already attached

      value = rest.first
      unless takes_value?(value)
        raise UsageError,
              "Missing value for #{name}. Pass it as `#{name} <VALUE>`, or as " \
              "`#{name}=<VALUE>` if the value is also a command name."
      end
      leading << rest.shift
    elsif HOISTABLE_BOOL_FLAGS.include?(token)
      leading << rest.shift
    else
      break
    end
  end
  leading.empty? || rest.empty? ? argv : rest + leading
end

.start(given_args = ARGV, config = {}) ⇒ Object



91
92
93
# File 'lib/smily_cli/cli.rb', line 91

def self.start(given_args = ARGV, config = {})
  super(normalize_argv(Array(given_args)), config)
end

.takes_value?(token) ⇒ Boolean

A token is a flag's value only when it's present and not a command word the user more likely meant to run (a forgotten flag value).

Parameters:

  • token (String, nil)

Returns:

  • (Boolean)


87
88
89
# File 'lib/smily_cli/cli.rb', line 87

def self.takes_value?(token)
  !token.nil? && !command_names.include?(token)
end

Instance Method Details

#api(method, path) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/smily_cli/cli.rb', line 129

def api(method, path)
  verb = method.to_s.downcase
  unless %w[get post put patch delete head].include?(verb)
    raise UsageError, "Unknown HTTP method #{method.inspect}. Use get, post, put, patch or delete."
  end

  query = QueryOptions.build(fields: context.fields, query: options["query"] || [])

  if options["paginate"]
    result = client.list(path, query: query, all: true)
    render_api(result, records: true)
  else
    body = options["data"] ? parse_data(options["data"]) : nil
    result = client.request(verb, path, query: query, body: body)
    render_api(result, records: false)
  end
end

#completion(shell) ⇒ Object



168
169
170
# File 'lib/smily_cli/cli.rb', line 168

def completion(shell)
  emit(Completion.script(shell))
end

#resources(filter = nil) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/smily_cli/cli.rb', line 148

def resources(filter = nil)
  selected = Registry.all.select { |r| filter.nil? || r.command.include?(filter) }

  if context.output_format == "table"
    render_resources_table(selected)
  else
    payload = selected.map do |r|
      { "command" => r.command, "path" => r.path, "group" => r.group,
        "description" => r.description, "readonly" => r.readonly }
    end
    render_result(Result.new(records: payload, single: false))
  end
end

#versionObject



173
174
175
# File 'lib/smily_cli/cli.rb', line 173

def version
  emit("smily #{SmilyCli::VERSION} (ruby #{RUBY_VERSION}, bookingsync-api #{BookingSync::API::VERSION})")
end

#whoamiObject



163
164
165
# File 'lib/smily_cli/cli.rb', line 163

def whoami
  render_result(client.me)
end