Commit 5cc9a6b
committed
INF-1307 twoctl: spec-driven command-line for every Two API
A public Go CLI for the Two merchant APIs. Resource-verb command tree
(`twoctl <resource> <action>`) generated entirely from the published
OpenAPI specs in two-inc/docs, with auth, contexts, self-upgrade, and
agent-friendly UX out of the box.
API coverage
- Six merchant-facing APIs: checkout, billing-account, repay, recourse,
company, limits. Every operationId surfaces as `twoctl <resource>
<action>` with non-CRUD actions (cancel, refund, fulfill, search,
notify, ...) as first-class verbs alongside get/create/edit/delete.
- Spec-driven: cmd/twoctl/cli embeds the six processed OpenAPI specs
and walks them at startup. No per-operation Go code to maintain.
- internal/preprocess rewrites OAS 3.1 nullable patterns into a 3.0-
compatible shape so the openapi3 parser can consume the upstream
specs unchanged.
Contexts (kubectl-style)
- Named contexts each bundle a base URL and an OS-keychain entry.
`twoctl config set-context / use-context / get-contexts /
current-context / delete-context` manage them.
- Built-in env aliases: prod, sandbox, staging, cyber, perf, release.
Unknown names fall back to https://api.<name>.two.inc.
- Per-invocation overrides without switching: --env / --context / --url
/ --api-key. Key resolution order: --api-key, TWO_API_KEY, keychain.
- Config at $XDG_CONFIG_HOME/twoctl or ~/.config/twoctl (XDG, never the
macOS Library default).
- `twoctl auth login / logout / whoami` for keychain management.
- Keychain fallback detection: clear error when libsecret/DBus is
missing on headless Linux instead of an opaque failure.
Agent + human UX
- `twoctl api-resources [--resource X] [--action Y]` emits a JSON
catalog of every operation (command path, method, URL template,
flags with kind/type/required, body status) for agent planners.
- `twoctl explain <resource> <action>` prints the operation's
parameter, request body, and response schemas as JSON without a
network call.
- `--describe` / `--dry-run` on every leaf operation. Dry-run prints
the HTTP request that would be sent with sensitive headers redacted.
- `-o auto|table|json|yaml`: auto-detects TTY for default (table on
terminal, json when piped). Tables render arrays of objects with
union-of-keys columns; non-tabular shapes fall back to json.
- `--all` flag auto-registered on operations with `page_cursor`;
follows `next_page_cursor` until exhausted and concatenates pages.
- Structured stderr error envelope: {error, message, status, response,
request_id}. Captures upstream X-Request-Id / X-Trace-Id /
X-Correlation-Id / Request-Id headers for support tickets.
- Stable exit codes: 0 ok, 1 generic, 2 usage, 3 auth, 4 not-found,
5 rate-limited, 6 server, 7 network. Documented in README.
- Required parameters render as "(path, required)" in --help.
- Friendly HTTP error messages: 401 points at `twoctl auth login`,
429 mentions rate limiting, 5xx suggests retry.
HTTP transport
- Shared transport injects X-Api-Key, User-Agent (`twoctl/<ver>
(<os>/<arch>)`), and Accept on every call.
- Exponential backoff with jitter on 429 and 5xx, capped at 3 retries
by default. Honours `Retry-After` header.
- Idempotency-Key (`twoctl-<hex>`) auto-set on POST/PUT/PATCH/DELETE
so a retried mutation never duplicates.
- Request body buffered before send so retries can replay it.
- Response body capped at 4 MiB to prevent OOM on misbehaving servers.
- Context cancellation stops the retry loop cleanly.
Self-upgrade
- Once-per-day check on every invocation against the GitHub releases
API. Network failures during the check are silent so they never
block the actual command.
- Interactive prompt offers `y` install now / `n` not now / `s` skip
this version. Skip list persists; `twoctl upgrade --reset-skips`
clears it.
- Non-interactive shells (no TTY) get a single-line stderr nudge.
- `twoctl upgrade` runs the check + install on demand; `--check`,
`--disable-autocheck`, `--enable-autocheck` available.
- `TWOCTL_SKIP_UPGRADE_CHECK=1` env var and `--no-upgrade-check` flag
for scripts/CI.
- Binary swap via minio/selfupdate; supports tar.gz and zip archives.
- State persisted to ~/.config/twoctl/state.json.
Shell completion
- bash, zsh, fish, powershell via `twoctl completion <shell>`.
- Enum-valued OpenAPI parameters get value-completion registered
automatically (e.g. tab-complete --country with ISO codes).
Tooling + CI
- Makefile with `build / install / regen / test / test-cover / lint /
tidy / release-dry / clean`. `make help` self-documents.
- `make install` injects git describe --tags --always --dirty into the
binary via ldflags so dev builds carry a version.
- scripts/lint.sh runs every Go Report Card tool (gofmt -s, go vet,
ineffassign, misspell, gocyclo over 15, golint); exits non-zero on
any output.
- CI: build + `make test-cover` gates coverage at 80% (currently
81.3%) and the six-lint sweep runs on every PR.
- .github/workflows/regenerate.yml: repository_dispatch from
two-inc/docs on openapi/* changes pulls fresh specs, runs codegen,
opens a PR. Nightly cron as safety net; gracefully exits when
DOCS_READ_TOKEN is unset.
- .github/workflows/release.yml + .goreleaser.yaml: tag push builds
darwin/linux/windows binaries (amd64 + arm64) and updates the
two-inc/homebrew-tap formula.
- docs/notify-twoctl.yml.example: drop-in workflow for two-inc/docs
to fire the dispatch.
Telemetry stance
- No telemetry, no phone-home, no anonymous usage metrics. Outbound
network calls are limited to the configured API endpoint and the
GitHub releases API for upgrade checks. Documented in README.
Linear: https://linear.app/tillit/issue/INF-1307
Shared CLI patterns reference: https://linear.app/tillit/issue/INF-13140 parents commit 5cc9a6b
71 files changed
Lines changed: 65413 additions & 0 deletions
File tree
- .github/workflows
- cmd/twoctl
- cli
- specs
- docs
- internal
- config
- httpx
- output
- preprocess
- updater
- openapi
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
0 commit comments