Skip to content

Commit 8e6378e

Browse files
terraboopsclaude
andcommitted
initial: pin-cli v0.1.0
Standalone CLI for pin. Mirror of bitcomplete/kploy-cli's release setup: goreleaser builds darwin/linux/windows × amd64/arm64 binaries on tag push, attaches them to a GitHub release, and auto-commits a Formula/pin.rb into bitcomplete/homebrew-tap so `brew install bitcomplete/tap/pin` works. Source extracted from the (private) bitcomplete/pin repo's cmd/pin/. Audited: no secrets, no internal hostnames, no email addresses, no emit-anything-internal — the only URL is the public pin.bitcomplete.dev default host. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 parents  commit 8e6378e

10 files changed

Lines changed: 829 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-go@v5
17+
with:
18+
go-version-file: go.mod
19+
cache: true
20+
- run: go mod verify
21+
- run: go vet ./...
22+
- run: go build ./...

.github/workflows/release.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: release
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
goreleaser:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
- name: Set up Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
cache-dependency-path: go.sum
23+
# Org-scoped GitHub App that's allowed to push to homebrew-tap.
24+
# Same app + secret as kploy-cli; see bc-prod org settings.
25+
- name: Mint Homebrew tap installation token
26+
id: tap_token
27+
uses: actions/create-github-app-token@v1
28+
with:
29+
app-id: "182548"
30+
private-key: ${{ secrets.HOMEBREW_RELEASER_PRIVATE_KEY }}
31+
owner: bitcomplete
32+
repositories: homebrew-tap
33+
- name: Run GoReleaser
34+
uses: goreleaser/goreleaser-action@v6
35+
with:
36+
version: "~> v2"
37+
args: release --clean
38+
env:
39+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.tap_token.outputs.token }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist
2+
*.test
3+
*.out
4+
.DS_Store

.goreleaser.yaml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
version: 2
2+
project_name: pin
3+
4+
before:
5+
hooks:
6+
- go mod tidy
7+
8+
builds:
9+
- id: pin
10+
main: .
11+
binary: pin
12+
env:
13+
- CGO_ENABLED=0
14+
goos:
15+
- darwin
16+
- linux
17+
- windows
18+
goarch:
19+
- amd64
20+
- arm64
21+
ignore:
22+
- goos: windows
23+
goarch: arm64
24+
ldflags:
25+
- -s -w -X main.Version={{.Version}}
26+
27+
archives:
28+
- id: pin
29+
formats: [tar.gz]
30+
name_template: >-
31+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
32+
format_overrides:
33+
- goos: windows
34+
formats: [zip]
35+
files:
36+
- README.md
37+
- LICENSE*
38+
39+
checksum:
40+
name_template: checksums.txt
41+
42+
snapshot:
43+
version_template: "{{ incpatch .Version }}-next"
44+
45+
changelog:
46+
use: github
47+
sort: asc
48+
filters:
49+
exclude:
50+
- "^docs:"
51+
- "^test:"
52+
- "^chore:"
53+
54+
release:
55+
github:
56+
owner: bitcomplete
57+
name: pin-cli
58+
prerelease: auto
59+
60+
brews:
61+
- name: pin
62+
repository:
63+
owner: bitcomplete
64+
name: homebrew-tap
65+
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
66+
homepage: https://github.com/bitcomplete/pin-cli
67+
description: Command-line client for pin (HTML sharing service)
68+
license: MIT
69+
commit_author:
70+
name: pin-release-bot
71+
email: noreply@bitcomplete.io
72+
directory: Formula
73+
install: |
74+
bin.install "pin"
75+
test: |
76+
assert_match "pin", shell_output("#{bin}/pin --help")

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 Bit Complete
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.

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# pin-cli
2+
3+
Command-line client for pin — a tiny tool for sharing HTML files behind Google SSO.
4+
5+
## Install
6+
7+
```sh
8+
brew install bitcomplete/tap/pin
9+
```
10+
11+
Or `go install github.com/bitcomplete/pin-cli@latest` if you'd rather build from source.
12+
13+
## Use
14+
15+
```sh
16+
pin login # one-time, opens browser → Google SSO
17+
pin share /tmp/report.html # → https://pin.bitcomplete.dev/p/01HX...
18+
```
19+
20+
Subcommands:
21+
22+
| Command | What |
23+
|---|---|
24+
| `pin login [--device]` | Sign in. `--device` for SSH / headless boxes. |
25+
| `pin share <file>` | Upload an HTML file. Prints the share URL. |
26+
| `pin whoami` | Show the currently-logged-in email. |
27+
| `pin logout` | Revoke the refresh token + clear local creds. |
28+
| `pin version` | Print the CLI version. |
29+
30+
Environment variables:
31+
32+
| Var | Default | What |
33+
|---|---|---|
34+
| `PIN_HOST` | `https://pin.bitcomplete.dev` | Override the pin instance. |
35+
| `PIN_AGENT` | `pin-cli@<hostname>` | Label sent with auth + upload requests for audit. |
36+
37+
Credentials are stored in your OS keychain (macOS Keychain / Windows Credential Manager / libsecret on Linux) with a 0600 file fallback at `~/.config/pin/credentials.json` for headless boxes.
38+
39+
## How auth works
40+
41+
`pin login` does an OAuth 2.1 PKCE loopback flow against the pin server (which in turn handles Google SSO for the human). `pin login --device` falls back to the RFC 8628 device-code flow when no local browser is available.
42+
43+
Refresh tokens are rotated on every use; reuse triggers family revocation. If a CI script's stored token gets exfiltrated and used elsewhere, the next legitimate refresh detects it and locks everyone out — `pin login` again to recover.
44+
45+
## License
46+
47+
MIT

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/bitcomplete/pin-cli
2+
3+
go 1.25
4+
5+
require github.com/zalando/go-keyring v0.2.8
6+
7+
require (
8+
github.com/danieljoos/wincred v1.2.3 // indirect
9+
github.com/godbus/dbus/v5 v5.2.2 // indirect
10+
golang.org/x/sys v0.27.0 // indirect
11+
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
2+
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
6+
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
10+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
11+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
12+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
13+
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
14+
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
15+
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
16+
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
17+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

keychain.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/sha256"
6+
7+
"github.com/zalando/go-keyring"
8+
)
9+
10+
// Single "service" name in the OS keychain; each issuer (PIN_HOST) gets
11+
// its own row so multi-environment users (dev vs prod) don't collide.
12+
const keyringService = "pin"
13+
14+
func keychainSet(issuer, value string) error {
15+
return keyring.Set(keyringService, issuer, value)
16+
}
17+
18+
func keychainGet(issuer string) (string, error) {
19+
return keyring.Get(keyringService, issuer)
20+
}
21+
22+
func keychainDel(issuer string) error {
23+
return keyring.Delete(keyringService, issuer)
24+
}
25+
26+
// ----- pure crypto helpers (kept here to avoid pulling x/crypto in main.go) -----
27+
28+
func sha256Sum(s string) []byte {
29+
h := sha256.Sum256([]byte(s))
30+
return h[:]
31+
}
32+
33+
func readRandom(b []byte) (int, error) {
34+
return rand.Read(b)
35+
}

0 commit comments

Comments
 (0)