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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f157eb28c398c7038493c5c558f5faec807d8e2b89761046ba137f086049fb6c
|
|
4
|
+
data.tar.gz: ff3e7be3ae92f10ddbd32cb86764ca84b59b3b76ca92fc4fb07e3634890115c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0b165be215a87ccba5e4f9411bc94683d3894cf6d96da72646e0589d622f06e3088f43013dbb1a47d7b4802be8b2a188e47c550ff632e2678868a7867c36043e
|
|
7
|
+
data.tar.gz: 4cf8ffdcac8182837f1311949aa2f94902a6661f00467e546c28cd662977815f77e6f4c3801a736e3d610fb72d72df735b843e44d62db821aa5ba43ac7e3fe35
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: Test (Ruby ${{ matrix.ruby }})
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
ruby: ["3.2", "3.3", "3.4"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby }}
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rspec
|
|
28
|
+
|
|
29
|
+
rubocop:
|
|
30
|
+
name: RuboCop
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- name: Set up Ruby
|
|
35
|
+
uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: "3.4"
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
- name: Run RuboCop
|
|
40
|
+
run: bundle exec rubocop --format github
|
|
41
|
+
|
|
42
|
+
brakeman:
|
|
43
|
+
name: Brakeman
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- name: Set up Ruby
|
|
48
|
+
uses: ruby/setup-ruby@v1
|
|
49
|
+
with:
|
|
50
|
+
ruby-version: "3.4"
|
|
51
|
+
bundler-cache: true
|
|
52
|
+
# This is a plain Ruby gem (not a Rails app), so Brakeman needs --force.
|
|
53
|
+
# -z makes it exit non-zero if any security warning is found.
|
|
54
|
+
- name: Run Brakeman
|
|
55
|
+
run: bundle exec brakeman --force --no-progress --quiet -z
|
|
56
|
+
|
|
57
|
+
bundler-audit:
|
|
58
|
+
name: Bundler Audit
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
- name: Set up Ruby
|
|
63
|
+
uses: ruby/setup-ruby@v1
|
|
64
|
+
with:
|
|
65
|
+
ruby-version: "3.4"
|
|
66
|
+
bundler-cache: true
|
|
67
|
+
- name: Audit dependencies for known CVEs
|
|
68
|
+
run: bundle exec bundler-audit check --update
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.2
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- "bin/**/*"
|
|
7
|
+
- "vendor/**/*"
|
|
8
|
+
- "tmp/**/*"
|
|
9
|
+
- "pkg/**/*"
|
|
10
|
+
|
|
11
|
+
# We document with YARD; a class comment isn't always warranted.
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
# The CLI leans on descriptive multi-branch case/format methods; keep the
|
|
16
|
+
# metrics as guardrails rather than hard style rules for this codebase.
|
|
17
|
+
Metrics/AbcSize:
|
|
18
|
+
Max: 40
|
|
19
|
+
Metrics/MethodLength:
|
|
20
|
+
Max: 30
|
|
21
|
+
Metrics/ClassLength:
|
|
22
|
+
Max: 250
|
|
23
|
+
Metrics/ModuleLength:
|
|
24
|
+
Max: 250
|
|
25
|
+
Exclude:
|
|
26
|
+
- "spec/**/*"
|
|
27
|
+
Metrics/BlockLength:
|
|
28
|
+
# Thor groups its instance helpers in a `no_commands do ... end` block.
|
|
29
|
+
AllowedMethods:
|
|
30
|
+
- "no_commands"
|
|
31
|
+
Exclude:
|
|
32
|
+
- "spec/**/*"
|
|
33
|
+
- "*.gemspec"
|
|
34
|
+
Metrics/CyclomaticComplexity:
|
|
35
|
+
Max: 13
|
|
36
|
+
Metrics/PerceivedComplexity:
|
|
37
|
+
Max: 13
|
|
38
|
+
Metrics/ParameterLists:
|
|
39
|
+
Max: 6
|
|
40
|
+
# Keyword args are self-documenting at the call site and don't suffer the
|
|
41
|
+
# positional-argument ordering problem this cop guards against.
|
|
42
|
+
CountKeywordArgs: false
|
|
43
|
+
|
|
44
|
+
# Our error classes take keyword args (status:, body:), so `raise Error.new(...)`
|
|
45
|
+
# is intentional and clearer than the exploded form.
|
|
46
|
+
Style/RaiseArgs:
|
|
47
|
+
Enabled: false
|
|
48
|
+
|
|
49
|
+
# Endless methods and long single-purpose lines read fine here.
|
|
50
|
+
Layout/LineLength:
|
|
51
|
+
Max: 120
|
|
52
|
+
AllowedPatterns:
|
|
53
|
+
- '\A\s*#' # long comment/URLs
|
|
54
|
+
|
|
55
|
+
Style/EndlessMethod:
|
|
56
|
+
EnforcedStyle: allow_single_line
|
|
57
|
+
|
|
58
|
+
# Heredocs and message strings frequently mix quote styles intentionally.
|
|
59
|
+
Style/StringLiterals:
|
|
60
|
+
EnforcedStyle: double_quotes
|
|
61
|
+
Style/WordArray:
|
|
62
|
+
Enabled: true
|
|
63
|
+
|
|
64
|
+
# Guard clauses that raise with a message are clearer un-negated at times.
|
|
65
|
+
Style/GuardClause:
|
|
66
|
+
Enabled: false
|
|
67
|
+
|
|
68
|
+
# Specs use `expect { }.to raise_error { |e| ... }` blocks.
|
|
69
|
+
Lint/AmbiguousBlockAssociation:
|
|
70
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- MCP `list` now speaks the API v3 filter DSL: `attribute[op]=value` applies an
|
|
4
|
+
operator (`eq`, `not_eq`, `gt`, `gteq`, `lt`, `lteq`, `in`, `matches`,
|
|
5
|
+
`does_not_match`) and repeating an attribute combines conditions with AND
|
|
6
|
+
(e.g. a numeric or date range). Values are coerced to numbers/booleans; ISO
|
|
7
|
+
8601 timestamps stay strings.
|
|
8
|
+
- Fixed the `booking_discount_codes` / `booking_discount_code_usages` resources
|
|
9
|
+
to use their flat top-level v3 paths (were pointing at a nested
|
|
10
|
+
`booking/discount_codes` path).
|
|
11
|
+
- Internal: the library now autoloads via Zeitwerk instead of hand-maintained
|
|
12
|
+
`require_relative` chains.
|
|
13
|
+
- Tests: added VCR-cassette acceptance tests exercising the REST and MCP flows
|
|
14
|
+
end-to-end (list, Link-header pagination, 404 handling, operator/range filter).
|
|
15
|
+
- Security: `auth client-credentials` / `refresh` / `exchange` no longer dump the
|
|
16
|
+
full token payload (including the refresh token) when their output is piped or
|
|
17
|
+
captured. The full payload is now opt-in via an explicit `-o json` /
|
|
18
|
+
`SMILY_OUTPUT=json`; the default stays access-token-only.
|
|
19
|
+
- Fixed a spurious "results may be truncated" warning from `--all` / `--limit`
|
|
20
|
+
when a collection finished on exactly the `--max-pages` page; the warning now
|
|
21
|
+
fires only when a further page is actually being left behind.
|
|
22
|
+
- Config: a structurally valid config file carrying a value `safe_load` refuses
|
|
23
|
+
(e.g. an unquoted date) now fails with a clean configuration error instead of
|
|
24
|
+
an uncaught YAML backtrace.
|
|
25
|
+
- Argv parsing: a forgotten value for a global flag (`smily --token rentals
|
|
26
|
+
list`) now fails with a clear "Missing value for --token" error instead of
|
|
27
|
+
swallowing the subcommand into the flag (which produced a baffling "command
|
|
28
|
+
not found" or a request fired with a bogus value). Use `--flag=value` when a
|
|
29
|
+
value legitimately collides with a command name.
|
|
30
|
+
|
|
31
|
+
## [0.1.0]
|
|
32
|
+
|
|
33
|
+
Initial (unreleased) version: a command-line client for the BookingSync (Smily)
|
|
34
|
+
API v3.
|
|
35
|
+
|
|
36
|
+
- Per-resource commands (`list`, `get`, `create`, `update`, `delete`) for the
|
|
37
|
+
documented API v3 endpoints, generated from a resource registry.
|
|
38
|
+
- Raw `smily api <method> <path>` escape hatch for any endpoint/verb, with
|
|
39
|
+
optional `--paginate`.
|
|
40
|
+
- OAuth helpers: `auth client-credentials`, `auth refresh`, `auth exchange`,
|
|
41
|
+
`auth authorize-url`, `auth status`, `auth token`.
|
|
42
|
+
- Config profiles (`smily config …`) stored at `~/.config/smily/config.yml`
|
|
43
|
+
(0600), with flag > env > profile resolution.
|
|
44
|
+
- Account scoping via `--account-id` / `SMILY_ACCOUNT_ID` for
|
|
45
|
+
Client-Credentials-flow tokens.
|
|
46
|
+
- Output formats: table (default on a TTY), json, yaml, csv, ndjson; sparse
|
|
47
|
+
fieldsets, filtering, sideloading and pagination (`--all`, `--limit`,
|
|
48
|
+
`--page`, `--per-page`).
|
|
49
|
+
- MCP mode (`smily mcp`): talk to a BookingSync MCP server (Model Context
|
|
50
|
+
Protocol) over the Streamable-HTTP JSON-RPC 2.0 transport. Includes `info`,
|
|
51
|
+
`tools`, and a low-level `call`, plus `list`/`get`/`schema`/`resources`
|
|
52
|
+
convenience wrappers over the generic dispatcher tools, and `mcp login` for
|
|
53
|
+
its dedicated setup. Separate credential (`mcp_token`) and endpoint
|
|
54
|
+
(`mcp_url`, default `<base-url>/mcp`); `--account-id` scopes tool calls.
|
|
55
|
+
- `whoami`, `resources`, `completion` (bash/zsh/fish) and `version` commands.
|
|
56
|
+
- Built on the official `bookingsync-api` client.
|
|
57
|
+
- Security: `--verbose` redacts `Authorization`/`client_secret`/tokens; config
|
|
58
|
+
written `0600` in a `0700` directory; warns when credentials would be sent
|
|
59
|
+
over non-HTTPS.
|
|
60
|
+
- Reliability: `--max-pages` backstop on auto-pagination (default 1000);
|
|
61
|
+
`--limit` sizes the page request; `--retry N` retries 429s honoring
|
|
62
|
+
`Retry-After` / `X-RateLimit-Reset`.
|
|
63
|
+
- Ergonomics: single-value global flags may precede the subcommand;
|
|
64
|
+
`create --path` for nested creates.
|
|
65
|
+
- CI: `bundler-audit` dependency CVE scan.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karol Galanciak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# smily — BookingSync (Smily) API v3 CLI
|
|
2
|
+
|
|
3
|
+
`smily` is an ergonomic, comprehensive command-line client for the
|
|
4
|
+
[BookingSync (Smily) API v3](https://developers.bookingsync.com/reference).
|
|
5
|
+
|
|
6
|
+
It aims to be *the* tool you reach for when you want to talk to API v3 from a
|
|
7
|
+
terminal or a script:
|
|
8
|
+
|
|
9
|
+
- **Per-resource commands** — `smily rentals list`, `smily bookings get 42`,
|
|
10
|
+
`smily clients create …` — for every documented endpoint.
|
|
11
|
+
- **A raw escape hatch** — `smily api get <path>` reaches *anything* the API
|
|
12
|
+
exposes, with pagination and wrapping out of your way.
|
|
13
|
+
- **OAuth built in** — client-credentials, refresh, and authorization-code
|
|
14
|
+
helpers; no need to hand-roll token requests.
|
|
15
|
+
- **Config profiles** — keep tokens for multiple accounts/environments and
|
|
16
|
+
switch with `--profile`.
|
|
17
|
+
- **Output that fits your workflow** — a readable table on a TTY, clean JSON
|
|
18
|
+
when piped, plus YAML, CSV and NDJSON.
|
|
19
|
+
- **Pagination, filtering, sparse fieldsets, sideloading** — first-class flags.
|
|
20
|
+
|
|
21
|
+
It is built on top of the official
|
|
22
|
+
[`bookingsync-api`](https://github.com/BookingSync/bookingsync-api) gem, so the
|
|
23
|
+
HTTP, JSON:API and error-handling behavior matches the reference client.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem install smily_cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or with Bundler:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem "smily_cli"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This installs the `smily` executable.
|
|
38
|
+
|
|
39
|
+
## Setup (first run)
|
|
40
|
+
|
|
41
|
+
Once the gem is installed, get from zero to your first authenticated call in
|
|
42
|
+
four steps:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. Confirm the executable is on your PATH
|
|
46
|
+
smily version
|
|
47
|
+
|
|
48
|
+
# 2. Get an API token. For scripts/automation the Client Credentials flow is
|
|
49
|
+
# simplest: you need your application's Client ID and Secret from the
|
|
50
|
+
# BookingSync developer portal (https://www.bookingsync.com/oauth/applications).
|
|
51
|
+
# --save writes the token to the default profile at
|
|
52
|
+
# ~/.config/smily/config.yml (created 0600, since it holds secrets).
|
|
53
|
+
smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
|
|
54
|
+
|
|
55
|
+
# 3. Verify the token actually works (calls /me)
|
|
56
|
+
smily auth status --check
|
|
57
|
+
|
|
58
|
+
# 4. Make your first real call
|
|
59
|
+
smily rentals list --limit 5
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A Client-Credentials token can read across every account that authorized your
|
|
63
|
+
app; pin one account with `--account-id <ID>` per call, or store it once:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
smily config set account_id <ID>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Don't want to store anything? Every command also reads `--token` / `SMILY_TOKEN`
|
|
70
|
+
directly, so `SMILY_TOKEN=… smily rentals list` works with no config file at
|
|
71
|
+
all. See [Authentication](#authentication) for the Authorization-Code (per-user)
|
|
72
|
+
flow and [Configuration & profiles](#configuration--profiles) for multiple
|
|
73
|
+
environments.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 1. Get a token (Client Credentials flow) and save it to the default profile
|
|
79
|
+
smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
|
|
80
|
+
|
|
81
|
+
# 2. Explore
|
|
82
|
+
smily resources # what can I talk to?
|
|
83
|
+
smily rentals list --limit 5 # a readable table
|
|
84
|
+
smily bookings list --filter status=booked from=2026-01-01
|
|
85
|
+
|
|
86
|
+
# 3. Fetch one, as JSON
|
|
87
|
+
smily rentals get 42 -o json
|
|
88
|
+
|
|
89
|
+
# 4. Anything the resource commands don't cover
|
|
90
|
+
smily api get rentals/42/bookings --paginate -o ndjson
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Authentication
|
|
94
|
+
|
|
95
|
+
API v3 uses OAuth 2.0 Bearer tokens. A token can be supplied (in order of
|
|
96
|
+
precedence) by:
|
|
97
|
+
|
|
98
|
+
1. `--token` flag
|
|
99
|
+
2. `SMILY_TOKEN` (or `BOOKINGSYNC_TOKEN`) environment variable
|
|
100
|
+
3. the active config profile
|
|
101
|
+
|
|
102
|
+
### Getting a token
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Client Credentials — app-level, public scope, reads across all authorized
|
|
106
|
+
# accounts. Great for scripts. (Tokens last ~2h.)
|
|
107
|
+
smily auth client-credentials --client-id ID --client-secret SECRET --save
|
|
108
|
+
|
|
109
|
+
# Authorization Code — per-account access.
|
|
110
|
+
smily auth authorize-url --client-id ID --redirect-uri https://app.example.com/cb --scope "rentals_read bookings_read"
|
|
111
|
+
# (open the URL, approve, copy the ?code=… back)
|
|
112
|
+
smily auth exchange --client-id ID --client-secret SECRET --code CODE --redirect-uri https://app.example.com/cb --save
|
|
113
|
+
|
|
114
|
+
# Refresh an expiring token
|
|
115
|
+
smily auth refresh --client-id ID --client-secret SECRET --refresh-token RT --save
|
|
116
|
+
|
|
117
|
+
# Check what's configured (optionally validate against /me)
|
|
118
|
+
smily auth status --check
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--save` stores the token (and any refresh token / client credentials) in the
|
|
122
|
+
active profile so subsequent commands just work.
|
|
123
|
+
|
|
124
|
+
### Scoping a Client-Credentials token to one account
|
|
125
|
+
|
|
126
|
+
A Client-Credentials token spans every account that authorized your app. Pin a
|
|
127
|
+
single account with `--account-id` (sent as the `account_id` query parameter),
|
|
128
|
+
via `SMILY_ACCOUNT_ID`, or by storing it in a profile:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
smily rentals list --account-id 12345
|
|
132
|
+
smily config set account_id 12345 --profile acme
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For Authorization-Code tokens the account is implicit, so this is a no-op.
|
|
136
|
+
|
|
137
|
+
## Configuration & profiles
|
|
138
|
+
|
|
139
|
+
Configuration lives in `~/.config/smily/config.yml` (override with
|
|
140
|
+
`SMILY_CONFIG`; honors `XDG_CONFIG_HOME`). The file is written with `0600`
|
|
141
|
+
permissions because it holds secrets.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
smily config init # interactive: store a token + base URL
|
|
145
|
+
smily config set token TOKEN --profile prod
|
|
146
|
+
smily config set account_id 123 --profile prod
|
|
147
|
+
smily config use prod # make "prod" the default profile
|
|
148
|
+
smily config list # profiles + settings (secrets masked)
|
|
149
|
+
smily config path # where's the file?
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Use a specific profile per command with `--profile NAME`, or set
|
|
153
|
+
`SMILY_PROFILE`.
|
|
154
|
+
|
|
155
|
+
## Working with resources
|
|
156
|
+
|
|
157
|
+
Every registered resource supports the same verbs:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
smily <resource> list [options]
|
|
161
|
+
smily <resource> get ID [options]
|
|
162
|
+
smily <resource> create --data <json> # not on read-only resources
|
|
163
|
+
smily <resource> update ID --data <json>
|
|
164
|
+
smily <resource> delete ID [--yes]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Run `smily resources` for the full catalog, or `smily <resource> help`.
|
|
168
|
+
|
|
169
|
+
### Listing, filtering, pagination
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
smily bookings list --filter status=booked --filter from=2026-01-01
|
|
173
|
+
smily rentals list --fields id name --limit 10
|
|
174
|
+
smily rentals list --all -o ndjson # every page, one JSON object per line
|
|
175
|
+
smily rentals list --page 2 --per-page 50 # a specific page
|
|
176
|
+
smily rentals list --include photos # sideload associations
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `--filter k=v` / `--query k=v` — API query parameters (repeat or space-separate).
|
|
180
|
+
These are sent verbatim, so a resource's documented filters work directly
|
|
181
|
+
(`--filter status=booked from=2026-01-01`), and operator query params are
|
|
182
|
+
forwarded as-is where the endpoint supports them
|
|
183
|
+
(`--filter 'final_price[gteq]=600'`). For the *validated* operator/range DSL,
|
|
184
|
+
see [MCP mode](#advanced-filtering-operators--ranges).
|
|
185
|
+
- `--fields a b c` — sparse fieldset (also limits table/CSV columns).
|
|
186
|
+
- `--include a b` — sideload associations (returned under `linked`).
|
|
187
|
+
- `--limit N` — stop after N records (follows pages as needed; also sizes the
|
|
188
|
+
page request to `min(N, 100)`).
|
|
189
|
+
- `--all` — fetch every page.
|
|
190
|
+
- `--page` / `--per-page` — fetch one specific page.
|
|
191
|
+
- `--max-pages N` — hard backstop on auto-pagination (default 1000); the CLI
|
|
192
|
+
warns if it stops early.
|
|
193
|
+
- `--retry N` — retry `429 Too Many Requests` up to N times, waiting for the
|
|
194
|
+
rate-limit window (`Retry-After` / `X-RateLimit-Reset`). Off by default.
|
|
195
|
+
|
|
196
|
+
### Creating & updating
|
|
197
|
+
|
|
198
|
+
Provide attributes as a JSON object; the `{ "<resource>": [ … ] }` envelope
|
|
199
|
+
that v3 expects is added for you:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
smily rentals create --data '{"name":"Villa Sunset","sleeps":6}'
|
|
203
|
+
smily clients create --data @client.json # from a file
|
|
204
|
+
echo '{"sleeps":8}' | smily rentals update 42 --data - # from stdin
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For nested creates (e.g. a booking under a rental), point `--path` at the
|
|
208
|
+
parent collection (the envelope key stays the resource's):
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
smily bookings create --path rentals/42/bookings --data @booking.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## The raw `api` command
|
|
215
|
+
|
|
216
|
+
`smily api <method> <path>` is the universal escape hatch — it can reach any
|
|
217
|
+
endpoint, with any verb, and sends your body verbatim:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
smily api get me
|
|
221
|
+
smily api get rentals --query per_page=5
|
|
222
|
+
smily api get bookings/123
|
|
223
|
+
smily api post rentals --data '{"rentals":[{"name":"X"}]}'
|
|
224
|
+
smily api get rentals --paginate -o json # combine all pages into one array
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
By default it prints the raw JSON response envelope. With `--paginate` it
|
|
228
|
+
follows pagination and prints the combined records.
|
|
229
|
+
|
|
230
|
+
## MCP mode (`smily mcp`)
|
|
231
|
+
|
|
232
|
+
Besides the REST API, `smily` can talk to a **BookingSync MCP server** (Model
|
|
233
|
+
Context Protocol) — the same servers an AI agent connects to. This is a separate
|
|
234
|
+
mode with its own credential (`mcp_token`) and endpoint (`mcp_url`, defaulting to
|
|
235
|
+
`<base-url>/mcp`).
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# One-time setup: store the MCP token (and optionally a non-default endpoint)
|
|
239
|
+
smily mcp login "$MCP_TOKEN"
|
|
240
|
+
smily mcp login "$MCP_TOKEN" --url https://www.bookingsync.com/mcp --profile prod
|
|
241
|
+
|
|
242
|
+
# Handshake + capabilities, and discover the tools the server exposes
|
|
243
|
+
smily mcp info
|
|
244
|
+
smily mcp tools
|
|
245
|
+
|
|
246
|
+
# Convenience wrappers that mirror the REST commands, over the generic
|
|
247
|
+
# list / get / resources / resource_schema tools:
|
|
248
|
+
smily mcp resources
|
|
249
|
+
smily mcp list rentals --limit 5
|
|
250
|
+
smily mcp list bookings --filter status=booked --account-id 42
|
|
251
|
+
smily mcp get rentals 42
|
|
252
|
+
smily mcp schema bookings
|
|
253
|
+
|
|
254
|
+
# Low-level escape hatch — call any tool with a raw JSON argument object:
|
|
255
|
+
smily mcp call list --args '{"resource":"rentals","limit":5}'
|
|
256
|
+
smily mcp call resource_schema --args @args.json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The convenience wrappers resolve the generic tool names for you — they work
|
|
260
|
+
against a server that exposes `list`/`get`/… as well as one that prefixes them
|
|
261
|
+
(`api_v3_list`, `api_v3_get`, …).
|
|
262
|
+
|
|
263
|
+
### Advanced filtering (operators & ranges)
|
|
264
|
+
|
|
265
|
+
`smily mcp list` speaks the API v3 filter DSL. A plain `attribute=value` is an
|
|
266
|
+
exact match; `attribute[op]=value` applies an operator; repeating an attribute
|
|
267
|
+
combines the conditions with AND (e.g. a range). Operators: `eq`, `not_eq`,
|
|
268
|
+
`gt`, `gteq`, `lt`, `lteq`, `in`, `matches`, `does_not_match` (exactly the set a
|
|
269
|
+
resource reports via `smily mcp schema <resource>`).
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# final_price > 1000
|
|
273
|
+
smily mcp list bookings --filter 'final_price[gt]=1000'
|
|
274
|
+
|
|
275
|
+
# 600 <= final_price < 800 (compound AND range)
|
|
276
|
+
smily mcp list bookings --filter 'final_price[gteq]=600' --filter 'final_price[lt]=800'
|
|
277
|
+
|
|
278
|
+
# starts on/after a date, and one of several references
|
|
279
|
+
smily mcp list bookings --filter 'start_at[gteq]=2026-01-01T00:00:00Z' --filter 'reference[in]=A123,B456'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Numeric and boolean values are coerced automatically; ISO 8601 timestamps are
|
|
283
|
+
kept as strings. Use `smily mcp schema <resource>` to see which attributes are
|
|
284
|
+
filterable and the operators each accepts.
|
|
285
|
+
|
|
286
|
+
Credentials/endpoint resolve like everything else — `--mcp-token` /
|
|
287
|
+
`SMILY_MCP_TOKEN` / profile `mcp_token`, and `--mcp-url` / `SMILY_MCP_URL` /
|
|
288
|
+
profile `mcp_url`. `--account-id` scopes tool calls to a single account (as the
|
|
289
|
+
`account_id` tool argument), which is how a multi-account MCP token is pinned.
|
|
290
|
+
Output honors `-o` just like the REST commands.
|
|
291
|
+
|
|
292
|
+
Transport details: `smily` speaks the Streamable-HTTP JSON-RPC 2.0 transport,
|
|
293
|
+
performing the `initialize` handshake (capturing the `Mcp-Session-Id`) and
|
|
294
|
+
accepting either `application/json` or SSE-framed responses.
|
|
295
|
+
|
|
296
|
+
## Output formats
|
|
297
|
+
|
|
298
|
+
`-o, --output` selects the format. The default is a human-readable **table** on
|
|
299
|
+
an interactive terminal, and **json** when the output is piped — so scripts get
|
|
300
|
+
machine-readable data automatically.
|
|
301
|
+
|
|
302
|
+
| Format | Notes |
|
|
303
|
+
|----------|---------------------------------------------------|
|
|
304
|
+
| `table` | Aligned grid (collections) or key/value (single). |
|
|
305
|
+
| `json` | Pretty JSON — array for lists, object for `get`. |
|
|
306
|
+
| `yaml` | YAML. |
|
|
307
|
+
| `csv` | Header + rows; nested values become JSON. |
|
|
308
|
+
| `ndjson` | One compact JSON object per line (a.k.a. `jsonl`).|
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
smily rentals list -o csv --fields id name status > rentals.csv
|
|
312
|
+
smily bookings list --all -o ndjson | jq 'select(.status=="booked")'
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Global options
|
|
316
|
+
|
|
317
|
+
Single-value global flags work before or after the subcommand
|
|
318
|
+
(`smily -o json rentals list` and `smily rentals list -o json` are equivalent).
|
|
319
|
+
Array-valued flags (`--fields`, `--filter`) must follow the subcommand.
|
|
320
|
+
|
|
321
|
+
| Flag | Env | Meaning |
|
|
322
|
+
|------|-----|---------|
|
|
323
|
+
| `--profile NAME` | `SMILY_PROFILE` | Config profile to use |
|
|
324
|
+
| `--token TOKEN` | `SMILY_TOKEN` | OAuth access token (REST) |
|
|
325
|
+
| `--mcp-token TOKEN` | `SMILY_MCP_TOKEN` | MCP access token (`smily mcp`) |
|
|
326
|
+
| `--mcp-url URL` | `SMILY_MCP_URL` | MCP endpoint (default `<base-url>/mcp`) |
|
|
327
|
+
| `--base-url URL` | `SMILY_BASE_URL` | API base (default `https://www.bookingsync.com`) |
|
|
328
|
+
| `--account-id ID` | `SMILY_ACCOUNT_ID` | Scope to an account (client-credentials tokens) |
|
|
329
|
+
| `--max-pages N` | `SMILY_MAX_PAGES` | Auto-pagination backstop (default 1000) |
|
|
330
|
+
| `--retry N` | `SMILY_RETRY` | Retry 429s N times, honoring the rate-limit reset |
|
|
331
|
+
| `-o, --output` | `SMILY_OUTPUT` | Output format |
|
|
332
|
+
| `--fields a b c` | | Field/column selection |
|
|
333
|
+
| `--no-color` | `NO_COLOR` | Disable ANSI color |
|
|
334
|
+
| `-q, --quiet` | | Suppress status messages |
|
|
335
|
+
| `-v, --verbose` | `SMILY_VERBOSE` | Log HTTP traffic to stderr (secrets redacted) |
|
|
336
|
+
|
|
337
|
+
### Security notes
|
|
338
|
+
|
|
339
|
+
- Config files are written `0600` in a `0700` directory; secrets are masked in
|
|
340
|
+
`smily config list` / `smily auth status`.
|
|
341
|
+
- `--verbose` scrubs `Authorization` / `client_secret` / tokens from the logged
|
|
342
|
+
traffic, so verbose output is safe to share.
|
|
343
|
+
- The CLI warns (like the GitHub CLI) if a credential would be sent over a
|
|
344
|
+
non-HTTPS endpoint.
|
|
345
|
+
|
|
346
|
+
## Shell completion
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
smily completion bash > /usr/local/etc/bash_completion.d/smily
|
|
350
|
+
smily completion zsh > "${fpath[1]}/_smily"
|
|
351
|
+
smily completion fish > ~/.config/fish/completions/smily.fish
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Exit codes
|
|
355
|
+
|
|
356
|
+
`0` success · `1` generic/API error · `2` usage error · `3` auth error
|
|
357
|
+
(401/403) · `4` not found (404) · `5` rate limited (429).
|
|
358
|
+
|
|
359
|
+
## Using it as a library
|
|
360
|
+
|
|
361
|
+
The library layer is usable without Thor:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
require "smily_cli"
|
|
365
|
+
|
|
366
|
+
client = SmilyCli::Client.new(token: ENV["SMILY_TOKEN"])
|
|
367
|
+
result = client.list("rentals", limit: 10)
|
|
368
|
+
result.records # => [ { "id" => 1, ... }, ... ]
|
|
369
|
+
|
|
370
|
+
puts SmilyCli::Formatters.render(result, format: "json")
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Development
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
bin/setup # install dependencies
|
|
377
|
+
bundle exec rake # run the specs and RuboCop
|
|
378
|
+
bundle exec rspec # just the specs
|
|
379
|
+
bin/console # an IRB session with the gem loaded
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
CI (GitHub Actions) runs the specs across Ruby 3.2–3.4, RuboCop, and Brakeman.
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
Available as open source under the terms of the
|
|
387
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
# RuboCop is a development-only dependency; only define the task when present so
|
|
9
|
+
# the gem still loads its Rakefile without it (e.g. in a production install).
|
|
10
|
+
begin
|
|
11
|
+
require "rubocop/rake_task"
|
|
12
|
+
RuboCop::RakeTask.new
|
|
13
|
+
rescue LoadError
|
|
14
|
+
task(:rubocop) { warn "rubocop is not available" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task default: %i[spec rubocop]
|
data/exe/smily
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "smily_cli"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
SmilyCli.cli.start(ARGV)
|
|
8
|
+
rescue SmilyCli::Error => e
|
|
9
|
+
warn SmilyCli::Color.red("Error: #{e.message}")
|
|
10
|
+
exit e.exit_status
|
|
11
|
+
rescue Interrupt
|
|
12
|
+
warn "\nAborted."
|
|
13
|
+
exit 130
|
|
14
|
+
rescue Errno::EPIPE
|
|
15
|
+
# Downstream pipe closed (e.g. `smily ... | head`); exit quietly.
|
|
16
|
+
exit 0
|
|
17
|
+
end
|