Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .circleci/config.yml

This file was deleted.

68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: CI

on:
push:
branches: [master, main]
pull_request:

permissions:
contents: read

jobs:
test:
name: Test (Ruby ${{ matrix.ruby }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ["3.2", "3.3", "3.4"]
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run tests
run: bundle exec rspec

rubocop:
name: RuboCop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop --format github

brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
# This is a plain Ruby gem (not a Rails app), so Brakeman needs --force.
# -z makes it exit non-zero if any security warning is found.
- name: Run Brakeman
run: bundle exec brakeman --force --no-progress --quiet -z

bundler-audit:
name: Bundler Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Audit dependencies for known CVEs
run: bundle exec bundler-audit check --update
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@

# rspec failure tracking
.rspec_status

# built gems
/*.gem

# local bundler config / lockfile for an application-less gem
/Gemfile.lock
70 changes: 70 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
AllCops:
TargetRubyVersion: 3.2
NewCops: enable
SuggestExtensions: false
Exclude:
- "bin/**/*"
- "vendor/**/*"
- "tmp/**/*"
- "pkg/**/*"

# We document with YARD; a class comment isn't always warranted.
Style/Documentation:
Enabled: false

# The CLI leans on descriptive multi-branch case/format methods; keep the
# metrics as guardrails rather than hard style rules for this codebase.
Metrics/AbcSize:
Max: 40
Metrics/MethodLength:
Max: 30
Metrics/ClassLength:
Max: 250
Metrics/ModuleLength:
Max: 250
Exclude:
- "spec/**/*"
Metrics/BlockLength:
# Thor groups its instance helpers in a `no_commands do ... end` block.
AllowedMethods:
- "no_commands"
Exclude:
- "spec/**/*"
- "*.gemspec"
Metrics/CyclomaticComplexity:
Max: 13
Metrics/PerceivedComplexity:
Max: 13
Metrics/ParameterLists:
Max: 6
# Keyword args are self-documenting at the call site and don't suffer the
# positional-argument ordering problem this cop guards against.
CountKeywordArgs: false

# Our error classes take keyword args (status:, body:), so `raise Error.new(...)`
# is intentional and clearer than the exploded form.
Style/RaiseArgs:
Enabled: false

# Endless methods and long single-purpose lines read fine here.
Layout/LineLength:
Max: 120
AllowedPatterns:
- '\A\s*#' # long comment/URLs

Style/EndlessMethod:
EnforcedStyle: allow_single_line

# Heredocs and message strings frequently mix quote styles intentionally.
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/WordArray:
Enabled: true

# Guard clauses that raise with a message are clearer un-negated at times.
Style/GuardClause:
Enabled: false

# Specs use `expect { }.to raise_error { |e| ... }` blocks.
Lint/AmbiguousBlockAssociation:
Enabled: false
64 changes: 62 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
## [Unreleased]

## [0.1.0] - 2026-07-06
- MCP `list` now speaks the API v3 filter DSL: `attribute[op]=value` applies an
operator (`eq`, `not_eq`, `gt`, `gteq`, `lt`, `lteq`, `in`, `matches`,
`does_not_match`) and repeating an attribute combines conditions with AND
(e.g. a numeric or date range). Values are coerced to numbers/booleans; ISO
8601 timestamps stay strings.
- Fixed the `booking_discount_codes` / `booking_discount_code_usages` resources
to use their flat top-level v3 paths (were pointing at a nested
`booking/discount_codes` path).
- Internal: the library now autoloads via Zeitwerk instead of hand-maintained
`require_relative` chains.
- Tests: added VCR-cassette acceptance tests exercising the REST and MCP flows
end-to-end (list, Link-header pagination, 404 handling, operator/range filter).
- Security: `auth client-credentials` / `refresh` / `exchange` no longer dump the
full token payload (including the refresh token) when their output is piped or
captured. The full payload is now opt-in via an explicit `-o json` /
`SMILY_OUTPUT=json`; the default stays access-token-only.
- Fixed a spurious "results may be truncated" warning from `--all` / `--limit`
when a collection finished on exactly the `--max-pages` page; the warning now
fires only when a further page is actually being left behind.
- Config: a structurally valid config file carrying a value `safe_load` refuses
(e.g. an unquoted date) now fails with a clean configuration error instead of
an uncaught YAML backtrace.
- Argv parsing: a forgotten value for a global flag (`smily --token rentals
list`) now fails with a clear "Missing value for --token" error instead of
swallowing the subcommand into the flag (which produced 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.

- Initial release
## [0.1.0]

Initial (unreleased) version: a command-line client for the BookingSync (Smily)
API v3.

- Per-resource commands (`list`, `get`, `create`, `update`, `delete`) for the
documented API v3 endpoints, generated from a resource registry.
- Raw `smily api <method> <path>` escape hatch for any endpoint/verb, with
optional `--paginate`.
- OAuth helpers: `auth client-credentials`, `auth refresh`, `auth exchange`,
`auth authorize-url`, `auth status`, `auth token`.
- Config profiles (`smily config …`) stored at `~/.config/smily/config.yml`
(0600), with flag > env > profile resolution.
- Account scoping via `--account-id` / `SMILY_ACCOUNT_ID` for
Client-Credentials-flow tokens.
- Output formats: table (default on a TTY), json, yaml, csv, ndjson; sparse
fieldsets, filtering, sideloading and pagination (`--all`, `--limit`,
`--page`, `--per-page`).
- MCP mode (`smily mcp`): talk to a BookingSync MCP server (Model Context
Protocol) over the Streamable-HTTP JSON-RPC 2.0 transport. Includes `info`,
`tools`, and a low-level `call`, plus `list`/`get`/`schema`/`resources`
convenience wrappers over the generic dispatcher tools, and `mcp login` for
its dedicated setup. Separate credential (`mcp_token`) and endpoint
(`mcp_url`, default `<base-url>/mcp`); `--account-id` scopes tool calls.
- `whoami`, `resources`, `completion` (bash/zsh/fish) and `version` commands.
- Built on the official `bookingsync-api` client.
- Security: `--verbose` redacts `Authorization`/`client_secret`/tokens; config
written `0600` in a `0700` directory; warns when credentials would be sent
over non-HTTPS.
- Reliability: `--max-pages` backstop on auto-pagination (default 1000);
`--limit` sizes the page request; `--retry N` retries 429s honoring
`Retry-After` / `X-RateLimit-Reset`.
- Ergonomics: single-value global flags may precede the subcommand;
`create --path` for nested creates.
- CI: `bundler-audit` dependency CVE scan.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ gem "irb"
gem "rake", "~> 13.0"

gem "rspec", "~> 3.0"
gem "vcr", "~> 6.0"
gem "webmock", "~> 3.0"

gem "brakeman", "~> 8.0", require: false
gem "bundler-audit", "~> 0.9", require: false
gem "rubocop", "~> 1.80", require: false
Loading
Loading