smily_cli 0.1.0
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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +68 -0
- data/.rubocop.yml +70 -0
- data/CHANGELOG.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +387 -0
- data/Rakefile +17 -0
- data/exe/smily +17 -0
- data/lib/smily_cli/base_command.rb +134 -0
- data/lib/smily_cli/cli.rb +205 -0
- data/lib/smily_cli/cli_options.rb +70 -0
- data/lib/smily_cli/client.rb +291 -0
- data/lib/smily_cli/color.rb +53 -0
- data/lib/smily_cli/commands/auth_command.rb +171 -0
- data/lib/smily_cli/commands/config_command.rb +159 -0
- data/lib/smily_cli/commands/mcp_command.rb +195 -0
- data/lib/smily_cli/completion.rb +85 -0
- data/lib/smily_cli/config.rb +192 -0
- data/lib/smily_cli/context.rb +278 -0
- data/lib/smily_cli/errors.rb +143 -0
- data/lib/smily_cli/formatters.rb +222 -0
- data/lib/smily_cli/mcp/client.rb +200 -0
- data/lib/smily_cli/oauth.rb +141 -0
- data/lib/smily_cli/query_options.rb +164 -0
- data/lib/smily_cli/redaction.rb +74 -0
- data/lib/smily_cli/registry.rb +175 -0
- data/lib/smily_cli/resource.rb +42 -0
- data/lib/smily_cli/resource_command.rb +140 -0
- data/lib/smily_cli/result.rb +149 -0
- data/lib/smily_cli/version.rb +5 -0
- data/lib/smily_cli.rb +37 -0
- data/sig/smily_cli.rbs +21 -0
- metadata +157 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SmilyCli
|
|
6
|
+
# A normalized view over one or more API v3 responses. It pulls the record
|
|
7
|
+
# array out of the JSON:API envelope (the single top-level key that isn't
|
|
8
|
+
# `links`/`linked`/`meta`), while keeping the raw envelope, `meta`, sideloaded
|
|
9
|
+
# `linked` records, pagination relations and rate-limit headers available for
|
|
10
|
+
# formatters and verbose output.
|
|
11
|
+
class Result
|
|
12
|
+
SPECIAL_KEYS = %w[links linked meta].freeze
|
|
13
|
+
|
|
14
|
+
# @return [Array<Hash>] the resource records (always an array)
|
|
15
|
+
attr_reader :records
|
|
16
|
+
# @return [Hash, nil] the raw parsed envelope of the last response
|
|
17
|
+
attr_reader :envelope
|
|
18
|
+
# @return [Hash, nil] JSON:API `meta`
|
|
19
|
+
attr_reader :meta
|
|
20
|
+
# @return [Hash, nil] sideloaded associations (`linked`)
|
|
21
|
+
attr_reader :linked
|
|
22
|
+
# @return [Integer, nil] HTTP status of the last response
|
|
23
|
+
attr_reader :status
|
|
24
|
+
# @return [Hash] response headers of the last response
|
|
25
|
+
attr_reader :headers
|
|
26
|
+
# @return [String, nil] resolved JSON:API key the records live under
|
|
27
|
+
attr_reader :key
|
|
28
|
+
|
|
29
|
+
# @param records [Array<Hash>]
|
|
30
|
+
# @param single [Boolean] whether this represents one member (get/create)
|
|
31
|
+
# @param envelope [Hash, nil]
|
|
32
|
+
# @param key [String, nil]
|
|
33
|
+
# @param status [Integer, nil]
|
|
34
|
+
# @param headers [Hash]
|
|
35
|
+
def initialize(records:, single: false, envelope: nil, key: nil, status: nil, headers: {})
|
|
36
|
+
@records = records
|
|
37
|
+
@single = single
|
|
38
|
+
@envelope = envelope
|
|
39
|
+
@key = key
|
|
40
|
+
@meta = envelope.is_a?(Hash) ? envelope["meta"] : nil
|
|
41
|
+
@linked = envelope.is_a?(Hash) ? envelope["linked"] : nil
|
|
42
|
+
@status = status
|
|
43
|
+
@headers = headers || {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build a Result from a single {BookingSync::API::Response}.
|
|
47
|
+
#
|
|
48
|
+
# @param response [BookingSync::API::Response, nil] nil for 204 No Content
|
|
49
|
+
# @param single [Boolean]
|
|
50
|
+
# @return [Result]
|
|
51
|
+
def self.from_response(response, single: false)
|
|
52
|
+
return empty(single: single) if response.nil?
|
|
53
|
+
|
|
54
|
+
body = parse(response.body)
|
|
55
|
+
key = resource_key(body)
|
|
56
|
+
records = extract_records(body, key)
|
|
57
|
+
new(
|
|
58
|
+
records: records,
|
|
59
|
+
single: single,
|
|
60
|
+
envelope: body,
|
|
61
|
+
key: key,
|
|
62
|
+
status: response.status,
|
|
63
|
+
headers: response.headers
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# An empty result (e.g. a 204 delete). `records` is empty.
|
|
68
|
+
#
|
|
69
|
+
# @return [Result]
|
|
70
|
+
def self.empty(single: false, status: nil, headers: {})
|
|
71
|
+
new(records: [], single: single, envelope: nil, key: nil, status: status, headers: headers)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Build a Result from an already-parsed JSON:API envelope (e.g. the payload
|
|
75
|
+
# an MCP tool returns), rather than an HTTP response.
|
|
76
|
+
#
|
|
77
|
+
# @param body [Hash]
|
|
78
|
+
# @param single [Boolean]
|
|
79
|
+
# @return [Result]
|
|
80
|
+
def self.from_body(body, single: false)
|
|
81
|
+
key = resource_key(body)
|
|
82
|
+
new(records: extract_records(body, key), single: single, envelope: body, key: key)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Boolean] whether this result represents a single member
|
|
86
|
+
def single?
|
|
87
|
+
@single
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Hash, nil] the sole record, when {#single?}
|
|
91
|
+
def record
|
|
92
|
+
records.first
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def empty?
|
|
97
|
+
records.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Integer]
|
|
101
|
+
def count
|
|
102
|
+
records.length
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Remaining requests in the current rate-limit window, if reported.
|
|
106
|
+
#
|
|
107
|
+
# @return [Integer, nil]
|
|
108
|
+
def rate_limit_remaining
|
|
109
|
+
value = headers["X-RateLimit-Remaining"] || headers["x-ratelimit-remaining"]
|
|
110
|
+
value&.to_i
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Total number of pages reported by the API, if any.
|
|
114
|
+
#
|
|
115
|
+
# @return [Integer, nil]
|
|
116
|
+
def total_pages
|
|
117
|
+
value = headers["X-Total-Pages"] || headers["x-total-pages"]
|
|
118
|
+
value&.to_i
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @api private
|
|
122
|
+
def self.parse(raw)
|
|
123
|
+
return {} if raw.nil? || raw.to_s.empty?
|
|
124
|
+
|
|
125
|
+
JSON.parse(raw)
|
|
126
|
+
rescue JSON::ParserError
|
|
127
|
+
{}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @api private
|
|
131
|
+
def self.resource_key(body)
|
|
132
|
+
return nil unless body.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
(body.keys.map(&:to_s) - SPECIAL_KEYS).first
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @api private
|
|
138
|
+
def self.extract_records(body, key)
|
|
139
|
+
return [] unless body.is_a?(Hash) && key
|
|
140
|
+
|
|
141
|
+
# v3 always wraps records in an array, but tolerate a bare object too.
|
|
142
|
+
case (value = body[key])
|
|
143
|
+
when Array then value
|
|
144
|
+
when nil then []
|
|
145
|
+
else [value]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/smily_cli.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
|
|
5
|
+
require_relative "smily_cli/version"
|
|
6
|
+
require_relative "smily_cli/errors"
|
|
7
|
+
|
|
8
|
+
# SmilyCli is a command-line client for the BookingSync (Smily) API v3.
|
|
9
|
+
#
|
|
10
|
+
# The library layer (everything under {SmilyCli}) is deliberately usable on its
|
|
11
|
+
# own, without Thor: build a {SmilyCli::Context}, hand it to a
|
|
12
|
+
# {SmilyCli::Client}, and render results with {SmilyCli::Formatters}. The Thor
|
|
13
|
+
# command layer ({SmilyCli::CLI} and friends) autoloads lazily, so requiring the
|
|
14
|
+
# library does not pull in Thor until the command classes are actually touched.
|
|
15
|
+
module SmilyCli
|
|
16
|
+
# The Thor-powered command-line interface. Referencing the constant autoloads
|
|
17
|
+
# (and thereby `require`s Thor); kept behind a method so the entry point reads
|
|
18
|
+
# as `SmilyCli.cli.start`.
|
|
19
|
+
#
|
|
20
|
+
# @return [Class] the {SmilyCli::CLI} Thor class
|
|
21
|
+
def self.cli
|
|
22
|
+
CLI
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Autoload everything under lib/smily_cli/**. version.rb and errors.rb hold a
|
|
27
|
+
# bare constant and a multi-class hierarchy respectively (neither maps 1:1 to a
|
|
28
|
+
# file name), so they are required eagerly above and ignored by the loader.
|
|
29
|
+
loader = Zeitwerk::Loader.for_gem
|
|
30
|
+
loader.inflector.inflect(
|
|
31
|
+
"cli" => "CLI",
|
|
32
|
+
"cli_options" => "CLIOptions",
|
|
33
|
+
"oauth" => "OAuth"
|
|
34
|
+
)
|
|
35
|
+
loader.ignore("#{__dir__}/smily_cli/version.rb")
|
|
36
|
+
loader.ignore("#{__dir__}/smily_cli/errors.rb")
|
|
37
|
+
loader.setup
|
data/sig/smily_cli.rbs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SmilyCli
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
def self.cli: () -> untyped
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
def exit_status: () -> Integer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class ConfigurationError < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class UsageError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ApiError < Error
|
|
17
|
+
attr_reader status: Integer?
|
|
18
|
+
attr_reader body: untyped
|
|
19
|
+
attr_reader headers: Hash[String, untyped]
|
|
20
|
+
end
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: smily_cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Karol Galanciak
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bookingsync-api
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thor
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: zeitwerk
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.6'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.6'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: base64
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
type: :runtime
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: csv
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
description: |
|
|
89
|
+
smily is an ergonomic, comprehensive command-line client for the BookingSync
|
|
90
|
+
(Smily) API v3. It offers per-resource commands (list/get/create/update/delete)
|
|
91
|
+
for the documented endpoints, a raw `api` escape hatch for anything else,
|
|
92
|
+
OAuth token helpers, config profiles, pagination, and table/json/yaml/csv/ndjson
|
|
93
|
+
output. Built on the official bookingsync-api client.
|
|
94
|
+
email:
|
|
95
|
+
- karol.galanciak@gmail.com
|
|
96
|
+
executables:
|
|
97
|
+
- smily
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- ".github/workflows/ci.yml"
|
|
102
|
+
- ".rubocop.yml"
|
|
103
|
+
- CHANGELOG.md
|
|
104
|
+
- LICENSE.txt
|
|
105
|
+
- README.md
|
|
106
|
+
- Rakefile
|
|
107
|
+
- exe/smily
|
|
108
|
+
- lib/smily_cli.rb
|
|
109
|
+
- lib/smily_cli/base_command.rb
|
|
110
|
+
- lib/smily_cli/cli.rb
|
|
111
|
+
- lib/smily_cli/cli_options.rb
|
|
112
|
+
- lib/smily_cli/client.rb
|
|
113
|
+
- lib/smily_cli/color.rb
|
|
114
|
+
- lib/smily_cli/commands/auth_command.rb
|
|
115
|
+
- lib/smily_cli/commands/config_command.rb
|
|
116
|
+
- lib/smily_cli/commands/mcp_command.rb
|
|
117
|
+
- lib/smily_cli/completion.rb
|
|
118
|
+
- lib/smily_cli/config.rb
|
|
119
|
+
- lib/smily_cli/context.rb
|
|
120
|
+
- lib/smily_cli/errors.rb
|
|
121
|
+
- lib/smily_cli/formatters.rb
|
|
122
|
+
- lib/smily_cli/mcp/client.rb
|
|
123
|
+
- lib/smily_cli/oauth.rb
|
|
124
|
+
- lib/smily_cli/query_options.rb
|
|
125
|
+
- lib/smily_cli/redaction.rb
|
|
126
|
+
- lib/smily_cli/registry.rb
|
|
127
|
+
- lib/smily_cli/resource.rb
|
|
128
|
+
- lib/smily_cli/resource_command.rb
|
|
129
|
+
- lib/smily_cli/result.rb
|
|
130
|
+
- lib/smily_cli/version.rb
|
|
131
|
+
- sig/smily_cli.rbs
|
|
132
|
+
homepage: https://github.com/BookingSync/smily_cli
|
|
133
|
+
licenses:
|
|
134
|
+
- MIT
|
|
135
|
+
metadata:
|
|
136
|
+
homepage_uri: https://github.com/BookingSync/smily_cli
|
|
137
|
+
source_code_uri: https://github.com/BookingSync/smily_cli
|
|
138
|
+
changelog_uri: https://github.com/BookingSync/smily_cli/blob/master/CHANGELOG.md
|
|
139
|
+
rubygems_mfa_required: 'true'
|
|
140
|
+
rdoc_options: []
|
|
141
|
+
require_paths:
|
|
142
|
+
- lib
|
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: 3.2.0
|
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
requirements: []
|
|
154
|
+
rubygems_version: 3.6.9
|
|
155
|
+
specification_version: 4
|
|
156
|
+
summary: Command-line client for the BookingSync (Smily) API v3
|
|
157
|
+
test_files: []
|