Class: SmilyCli::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/smily_cli/context.rb

Overview

Resolves effective settings for a command from three layers, highest wins:

1. explicit flags (the Thor `options` hash)
2. environment variables (`SMILY_*`, with `BOOKINGSYNC_*` fallbacks)
3. the selected config profile

A Context is the single object a command needs: it exposes the resolved token/base_url/output settings and can build a Client. It never performs I/O beyond reading the config file.

Constant Summary collapse

DEFAULT_BASE_URL =
"https://www.bookingsync.com"
OUTPUT_FORMATS =
%w[table json yaml csv ndjson jsonl].freeze
DEFAULT_MAX_PAGES =
1000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options, config) ⇒ Context

Returns a new instance of Context.

Parameters:

  • options (Hash{String=>Object})
  • config (Config)


41
42
43
44
45
# File 'lib/smily_cli/context.rb', line 41

def initialize(options, config)
  @options = options
  @config = config
  apply_color_setting
end

Instance Attribute Details

#configConfig (readonly)

Returns:



21
22
23
# File 'lib/smily_cli/context.rb', line 21

def config
  @config
end

#optionsHash (readonly)

Returns the raw flags this context was built from.

Returns:

  • (Hash)

    the raw flags this context was built from



23
24
25
# File 'lib/smily_cli/context.rb', line 23

def options
  @options
end

Class Method Details

.from(options, config: nil) ⇒ Context

Build a Context from a Thor options hash.

Parameters:

  • options (Hash)

    parsed CLI options (string or symbol keys)

  • config (Config) (defaults to: nil)

    loaded configuration

Returns:



30
31
32
33
34
35
36
37
# File 'lib/smily_cli/context.rb', line 30

def self.from(options, config: nil)
  opts = options.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
  cfg = config || Config.load
  if (profile = opts["profile"] || ENV.fetch("SMILY_PROFILE", nil))
    cfg.current_profile_name = profile
  end
  new(opts, cfg)
end

Instance Method Details

#account_idString?

Account to scope requests to. Primarily for Client-Credentials-flow tokens that span multiple accounts; sent as the account_id query parameter (REST) or account_id tool argument (MCP).

Returns:

  • (String, nil)


82
83
84
# File 'lib/smily_cli/context.rb', line 82

def 
  resolve("account_id", env: %w[SMILY_ACCOUNT_ID BOOKINGSYNC_ACCOUNT_ID])
end

#base_urlString

API base URL (no trailing /api/v3; the client appends that).

Returns:

  • (String)


73
74
75
# File 'lib/smily_cli/context.rb', line 73

def base_url
  resolve("base_url", env: %w[SMILY_BASE_URL BOOKINGSYNC_URL]) || DEFAULT_BASE_URL
end

#clientClient

Returns:



190
191
192
193
194
195
196
197
198
199
200
# File 'lib/smily_cli/context.rb', line 190

def client
  warn_insecure_transport(base_url)
  Client.new(
    token: token!,
    base_url: base_url,
    account_id: ,
    max_pages: max_pages,
    retries: retries,
    verbose: verbose?
  )
end

#client_idString?

Values used to seed OAuth commands, pulled from flags then profile.

Returns:

  • (String, nil)


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

def client_id     = resolve("client_id", env: %w[SMILY_CLIENT_ID])
# @return [String, nil]

#client_secretString?

Returns:

  • (String, nil)


165
166
# File 'lib/smily_cli/context.rb', line 165

def client_secret = resolve("client_secret", env: %w[SMILY_CLIENT_SECRET])
# @return [String, nil]

#color?Boolean

Returns whether ANSI color is active for this run.

Returns:

  • (Boolean)

    whether ANSI color is active for this run



157
158
159
# File 'lib/smily_cli/context.rb', line 157

def color?
  Color.enabled
end

#fieldsArray<String>?

Field selection shared between sparse fieldsets (sent to the API) and column selection (used by table/csv). Accepts repeated flags and/or comma-separated values.

Returns:

  • (Array<String>, nil)


138
139
140
141
142
143
144
# File 'lib/smily_cli/context.rb', line 138

def fields
  raw = options["fields"]
  return nil if raw.nil?

  list = Array(raw).flat_map { |v| v.to_s.split(",") }.map(&:strip).reject(&:empty?)
  list.empty? ? nil : list
end

#max_pagesInteger

Build an API client from these settings.

Hard backstop for auto-pagination (--all / --limit), so a server that keeps advertising a next link can't loop forever.

Returns:

  • (Integer)


175
176
177
178
179
# File 'lib/smily_cli/context.rb', line 175

def max_pages
  raw = resolve("max_pages", env: %w[SMILY_MAX_PAGES])
  value = raw.to_i
  value.positive? ? value : DEFAULT_MAX_PAGES
end

#mcp_clientMcp::Client

An MCP client for the configured MCP server and token.

Returns:



213
214
215
216
# File 'lib/smily_cli/context.rb', line 213

def mcp_client
  warn_insecure_transport(mcp_url)
  Mcp::Client.new(token: mcp_token!, url: mcp_url, verbose: verbose?)
end

#mcp_tokenString?

The MCP-mode access token (distinct from the REST API token).

Returns:

  • (String, nil)


89
90
91
# File 'lib/smily_cli/context.rb', line 89

def mcp_token
  resolve("mcp_token", env: %w[SMILY_MCP_TOKEN])
end

#mcp_token!String

Returns:

  • (String)


94
95
96
97
98
99
# File 'lib/smily_cli/context.rb', line 94

def mcp_token!
  mcp_token || raise(ConfigurationError, <<~MSG.strip)
    No MCP token configured. Provide one with --mcp-token, set SMILY_MCP_TOKEN, or run:
      smily mcp login --token <MCP_TOKEN>
  MSG
end

#mcp_urlString

The MCP server endpoint. Defaults to <base_url>/mcp, which is where a BookingSync app mounts its MCP server; override for other servers.

Returns:

  • (String)


105
106
107
108
109
110
# File 'lib/smily_cli/context.rb', line 105

def mcp_url
  explicit = resolve("mcp_url", env: %w[SMILY_MCP_URL])
  return explicit if explicit && !explicit.empty?

  URI.join("#{base_url.chomp('/')}/", "mcp").to_s
end

#oauthOAuth

An OAuth helper bound to this context's base URL.

Returns:



205
206
207
208
# File 'lib/smily_cli/context.rb', line 205

def oauth
  warn_insecure_transport(base_url)
  OAuth.new(base_url: base_url, verbose: verbose?)
end

#output_formatString

Chosen output format. Defaults to table on a TTY, json otherwise, so piping produces machine-readable output without a flag.

Returns:

  • (String)


129
130
131
# File 'lib/smily_cli/context.rb', line 129

def output_format
  requested_output_format || default_output_format
end

#profile_nameString

Returns the profile these settings resolve against.

Returns:

  • (String)

    the profile these settings resolve against



48
49
50
# File 'lib/smily_cli/context.rb', line 48

def profile_name
  config.current_profile_name
end

#quiet?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'lib/smily_cli/context.rb', line 152

def quiet?
  truthy(options["quiet"])
end

#refresh_tokenString?

Returns:

  • (String, nil)


167
# File 'lib/smily_cli/context.rb', line 167

def refresh_token = resolve("refresh_token", env: %w[SMILY_REFRESH_TOKEN])

#requested_output_formatString?

The output format explicitly requested via --output or SMILY_OUTPUT, validated, or nil when none was given. Commands whose default isn't the usual table/json (e.g. api, which defaults to the raw envelope) use this so they still honor an explicit flag/env choice.

Returns:

  • (String, nil)


118
119
120
121
122
123
# File 'lib/smily_cli/context.rb', line 118

def requested_output_format
  raw = (options["output"] || ENV.fetch("SMILY_OUTPUT", nil))&.to_s
  return nil if raw.nil? || raw.empty?

  validate_output_format!(raw)
end

#retriesInteger

Number of times to retry a 429 Too Many Requests (waiting for the rate-limit window each time). Off by default, like the GitHub CLI.

Returns:

  • (Integer)


185
186
187
# File 'lib/smily_cli/context.rb', line 185

def retries
  resolve("retry", env: %w[SMILY_RETRY]).to_i.clamp(0, 10)
end

#tokenString?

The OAuth access token. Resolution order applies.

Returns:

  • (String, nil)


55
56
57
# File 'lib/smily_cli/context.rb', line 55

def token
  resolve("token", env: %w[SMILY_TOKEN BOOKINGSYNC_TOKEN SMILY_ACCESS_TOKEN])
end

#token!String

Like #token but raises when nothing is configured.

Returns:

  • (String)


62
63
64
65
66
67
68
# File 'lib/smily_cli/context.rb', line 62

def token!
  token || raise(ConfigurationError, <<~MSG.strip)
    No API token configured. Provide one with --token, set SMILY_TOKEN, or run:
      smily config set token <ACCESS_TOKEN>
    You can obtain a token with `smily auth client-credentials` or `smily auth exchange`.
  MSG
end

#verbose?Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/smily_cli/context.rb', line 147

def verbose?
  truthy(options["verbose"]) || truthy(ENV.fetch("SMILY_VERBOSE", nil))
end