Skip to content

Commit 390bb09

Browse files
committed
INF-1307 initial twoctl
Public Go CLI for the Two merchant APIs. Coverage - Every operation in checkout, billing-account, repay, recourse, company, and limits is surfaced as `twoctl <api> <op>` (72 operations total). - The CLI is spec-driven: cmd/twoctl/cli/operations.go embeds the six processed OpenAPI specs and walks them at startup, so the command tree tracks the upstream OpenAPI surface 1:1 with no per-operation Go code to maintain. Path, query, and header parameters surface as flags; the request body is passed via --file or --data. - internal/preprocess rewrites OAS 3.1 nullable patterns into a 3.0- compatible shape (anyOf:[T,null], type:[T,null], bare type:null, const-without-required) so the openapi3 parser can consume the upstream specs. Contexts (kubectl-style) - Named contexts each bundle a base URL and an OS-keychain entry holding the API key. `twoctl ctx set/use/list/current/delete` 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. Agent + human UX - `twoctl catalog [-a <api>]` emits a JSON catalog of every operation (command path, method, URL template, flags with kind/type/required, body status) for agent planners. - `twoctl <api> <op> --describe` prints the operation's parameter, request body, and response schemas as JSON without a network call. - `twoctl <api> <op> --dry-run` shows the HTTP request that would be sent (URL, redacted headers, body presence). No credentials needed. - Structured stderr error envelope on failure: {error, message, status, response}. Stable exit codes: 0 ok, 1 generic, 2 usage, 3 auth, 4 not-found, 5 rate-limited, 6 server, 7 network. - Required parameters render as "(path, required)" in --help. - Shell completion for bash/zsh/fish/powershell via cobra; enum-valued parameters get value completions registered automatically. Self-upgrade - Every invocation runs an at-most-daily check against the GitHub releases API. If a newer release is available and the user has not skipped it, a prompt offers [y] install / [n] not now / [s] skip. - `twoctl upgrade` runs the check + install on demand; --reset-skips clears the skip list (escape hatch); --disable-autocheck / --enable-autocheck toggle the daily check; --no-upgrade-check is a per-run script bypass. - State persisted to $XDG_CONFIG_HOME/twoctl/state.json. Network failures during the check are silent so they never block the command. - Binary swap via minio/selfupdate; supports tar.gz and zip archives. Tooling + CI - Makefile with build / install / test / test-cover / lint / regen / tidy / release-dry / clean targets. - scripts/lint.sh runs every Go Report Card check (gofmt -s, go vet, ineffassign, misspell, gocyclo > 15, golint) and exits non-zero on any output - lint job in CI enforces it. - Self-managed test coverage at 80.8% across the project. - .github/workflows/regenerate.yml: repository_dispatch from two-inc/docs on openapi/* changes pulls the fresh specs, runs codegen, opens a PR. Nightly cron as safety net. - .github/workflows/release.yml + .goreleaser.yaml: tag push builds darwin/linux/windows binaries and updates two-inc/homebrew-tap. - docs/notify-twoctl.yml.example: drop-in workflow for two-inc/docs to fire the dispatch. Linear: https://linear.app/tillit/issue/INF-1307
0 parents  commit 390bb09

54 files changed

Lines changed: 32777 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
- uses: actions/setup-go@v6
18+
with:
19+
go-version: '1.26'
20+
cache: true
21+
- run: go build ./...
22+
- run: go test ./...
23+
24+
lint:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v5
28+
- uses: actions/setup-go@v6
29+
with:
30+
go-version: '1.26'
31+
cache: true
32+
- name: Install lint tools
33+
run: |
34+
go install github.com/gordonklaus/ineffassign@latest
35+
go install github.com/client9/misspell/cmd/misspell@latest
36+
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
37+
go install golang.org/x/lint/golint@latest
38+
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
39+
- run: ./scripts/lint.sh

.github/workflows/regenerate.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: regenerate
2+
3+
# Pulls the latest OpenAPI specs from two-inc/docs and regenerates the
4+
# vendored copies + clients. Opens a PR if anything changed.
5+
#
6+
# Triggered by:
7+
# - manual dispatch
8+
# - repository_dispatch (event_type: openapi-updated), fired by a workflow
9+
# in two-inc/docs whenever openapi/* files change on main.
10+
# - nightly cron (safety net in case the dispatch is dropped)
11+
12+
on:
13+
workflow_dispatch: {}
14+
repository_dispatch:
15+
types: [openapi-updated]
16+
schedule:
17+
- cron: '0 3 * * *'
18+
19+
permissions:
20+
contents: write
21+
pull-requests: write
22+
23+
jobs:
24+
regen:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v5
28+
- uses: actions/setup-go@v6
29+
with:
30+
go-version: '1.26'
31+
cache: true
32+
33+
- name: Fetch latest specs from two-inc/docs
34+
env:
35+
GH_TOKEN: ${{ secrets.DOCS_READ_TOKEN || github.token }}
36+
run: |
37+
set -euo pipefail
38+
for api in checkout billing-account repay recourse company limits; do
39+
gh api repos/two-inc/docs/contents/openapi/${api}-api.yaml \
40+
--jq '.content' \
41+
-H "Accept: application/vnd.github.v3+json" \
42+
| base64 -d > openapi/${api}-api.yaml
43+
done
44+
45+
- run: ./scripts/codegen.sh
46+
47+
- run: go build ./... && go vet ./...
48+
49+
- name: Open PR if changed
50+
uses: peter-evans/create-pull-request@v7
51+
with:
52+
commit-message: 'chore: regenerate clients from latest openapi specs'
53+
title: 'chore: regenerate clients from latest openapi specs'
54+
branch: regen/openapi
55+
delete-branch: true
56+
body: |
57+
Regenerated from the latest openapi specs in [two-inc/docs](https://github.com/two-inc/docs/tree/main/openapi).
58+
59+
This PR was opened automatically by the `regenerate` workflow.

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: release
2+
3+
on:
4+
push:
5+
tags: ['v*']
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
with:
16+
fetch-depth: 0
17+
- uses: actions/setup-go@v6
18+
with:
19+
go-version: '1.26'
20+
cache: true
21+
- uses: goreleaser/goreleaser-action@v6
22+
with:
23+
version: latest
24+
args: release --clean
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/dist/
2+
/bin/
3+
/twoctl
4+
*.test
5+
*.out
6+
.DS_Store
7+
.idea/
8+
.vscode/
9+
.worktrees/
10+
coverage.txt
11+
.build/
12+
/preprocess

.goreleaser.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
version: 2
2+
3+
project_name: twoctl
4+
5+
before:
6+
hooks:
7+
- go mod tidy
8+
9+
builds:
10+
- id: twoctl
11+
main: ./cmd/twoctl
12+
binary: twoctl
13+
env:
14+
- CGO_ENABLED=0
15+
goos: [linux, darwin, windows]
16+
goarch: [amd64, arm64]
17+
ignore:
18+
- goos: windows
19+
goarch: arm64
20+
ldflags:
21+
- -s -w
22+
- -X github.com/two-inc/twoctl-cli/internal/httpx.Version={{.Version}}
23+
24+
archives:
25+
- id: twoctl
26+
name_template: >-
27+
{{ .ProjectName }}_
28+
{{- .Version }}_
29+
{{- .Os }}_
30+
{{- if eq .Arch "amd64" }}x86_64
31+
{{- else if eq .Arch "386" }}i386
32+
{{- else }}{{ .Arch }}{{ end }}
33+
format_overrides:
34+
- goos: windows
35+
format: zip
36+
37+
checksum:
38+
name_template: 'checksums.txt'
39+
40+
snapshot:
41+
version_template: '{{ incpatch .Version }}-next'
42+
43+
changelog:
44+
sort: asc
45+
filters:
46+
exclude:
47+
- '^docs:'
48+
- '^test:'
49+
- '^chore: regenerate'
50+
51+
brews:
52+
- name: twoctl
53+
repository:
54+
owner: two-inc
55+
name: homebrew-tap
56+
token: '{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}'
57+
homepage: 'https://github.com/two-inc/twoctl'
58+
description: 'Command-line interface for the Two merchant APIs'
59+
license: MIT
60+
install: |
61+
bin.install "twoctl"
62+
test: |
63+
system "#{bin}/twoctl --version"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Two AS
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 all
13+
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 THE
21+
SOFTWARE.

Makefile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.PHONY: build install regen test test-cover lint tidy clean release-dry help
2+
3+
BINARY := twoctl
4+
PKG := github.com/two-inc/twoctl-cli
5+
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
6+
LDFLAGS := -X $(PKG)/internal/httpx.Version=$(VERSION) -s -w
7+
8+
help: ## Show this help.
9+
@awk 'BEGIN {FS = ":.*##"; printf "Usage: make <target>\n\nTargets:\n"} /^[a-zA-Z_-]+:.*##/ { printf " \033[36m%-14s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
10+
11+
build: ## Build the twoctl binary into ./twoctl.
12+
go build -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/twoctl
13+
14+
install: ## Install twoctl into $$GOBIN (or $$GOPATH/bin).
15+
go install -ldflags "$(LDFLAGS)" ./cmd/twoctl
16+
17+
regen: ## Refresh embedded OpenAPI specs from openapi/<api>-api.yaml.
18+
./scripts/codegen.sh
19+
20+
test: ## Run all unit tests.
21+
go test ./...
22+
23+
test-cover: ## Run tests with coverage and print per-package + total.
24+
go test -coverprofile=coverage.out -covermode=atomic ./...
25+
go tool cover -func=coverage.out | tail -1
26+
@echo "html report: open coverage.html"
27+
go tool cover -html=coverage.out -o coverage.html
28+
29+
lint: ## Run the same lints that goreportcard.com runs.
30+
./scripts/lint.sh
31+
32+
tidy: ## Tidy go.mod / go.sum.
33+
go mod tidy
34+
35+
release-dry: ## Dry-run a goreleaser build for the current OS.
36+
goreleaser build --snapshot --clean --single-target
37+
38+
clean: ## Remove build artefacts.
39+
rm -f $(BINARY) coverage.out coverage.html
40+
rm -rf dist/ .build/

0 commit comments

Comments
 (0)