buffswitch
v0.1.4
Published
A terminal account selector for Freebuff, written in Go with Bubble Tea
Readme
buffsw-cli — account selector bs
English · Bahasa Indonesia
A terminal account selector for Freebuff, written in Go with Bubble Tea.
bs manages multiple Freebuff accounts stored in
~/.config/manicode/credentials.json. Freebuff only reads the default
key in that file — so bs simply swaps the contents of default with
the account you pick, parking the previously active account in its own
slot.
Quick Start
Install the package globally from npm, then run bs:
npm i -g buffswitch
bsSebelumnya memakai nama package lama
buffsw-cli?npm remove -g buffsw-cli; npm i -g buffswitch
Dev mode — run straight from the source tree (Go 1.27+ and an interactive terminal required):
cd ~/projects/buffswitch go run .
Inside the TUI:
↑/↓ or j/k select · Enter activate & run freebuff · a add · d delete · r reload · q quit- Switch account — select one, press
Enter. - Add account — press
a: the TUI steps aside and hands the terminal over tofreebuff login; its output — including the login URL — appears right in your terminal. Finish the login there, then press Enter to return to the refreshed account list. - Different credentials file?
bs -creds /path/credentials.json
Full details below.
Table of Contents
- Features
- How it works
- File layout
- Requirements
- Usage
- Add-account flow
- credentials.json format
- Development
- Troubleshooting
Features
- Account list — every stored Freebuff account, active one marked
(active), in an 80-column window (auto-shrinks on narrow terminals). - Switch account (
Enter) — the selected account is copied into thedefaultkey; the previously active account is parked under its email key.bsthen exits and runs thefreebuffCLI with that account; whenfreebufffinishes,bsis done. - Usage stats — each row shows how often and how long the account was
used (
12x · 3h · 2h ago), tracked in a separatebuffswitch-stats.json; the list is sorted least-used first. - Add account (
a) — runs the officialfreebuff loginbinary by handing over the terminal, so its output and the login URL are shown right there; when it finishes,bsreturns to the TUI with the new account active. - Delete account (
d) — removes the selected account (parked or the active one) with ay/nconfirmation; if it was the active account, thedefaultkey is cleared too. - Reload (
r) — re-readscredentials.jsonfrom disk (useful if you log in outsidebs). - Automatic normalization — stray/random keys left by login (e.g.
defaults) are re-keyed by email; the file is never left messy. - Slot sync — refreshed sessions (new token for the same email) are copied into the parked slot too.
- Safe: atomic writes (temp + rename) and the file's original permissions are preserved.
How it works
credentials.json is a JSON map. Only one key matters to Freebuff:
"default": { ... the currently active account ... }Every other key is not read by Freebuff — those are bs's parking
slots. bs uses the account email as the slot key, so each account is
easy to find and can never be mixed up.
Core operations:
| Operation | What happens to the file |
|---|---|
| Switch to B | contents of default (account A) are copied to A's email slot; contents of B's email slot are copied into default |
| Before login (park) | the current default is copied to its email slot first, so it is not lost when freebuff login overwrites default |
| After login | the file is re-read, stray keys are re-keyed by email, a brand-new account becomes default (or a same-email session is refreshed), and slots are synced |
Invariant kept after every Save:
{
"default": <active account>,
"<email-1>": <parked account 1>,
"<email-2>": <parked account 2>,
...
}File layout
buffswitch/ repo; npm package name is `buffswitch`
├── go.mod module bs (bubbletea + lipgloss)
├── main.go Bubble Tea TUI: account list, keymap, login loop
├── store.go credentials.json logic: load/save/normalize/
│ switch/park/ingest-after-login
├── login.go terminal hand-off: runs `freebuff login`, ingests
│ the result, manages the temporary login-state file
├── store_test.go unit tests for the store logic
├── stats.go usage stats per account (sessions, counts, last used)
│ → buffswitch-stats.json next to credentials.json
├── stats_test.go unit tests for the stats logic
├── package.json npm wrapper: `bin.bs` → `bin/bs.exe` (tarball ships
│ no binaries — only the stub + postinstall)
├── postinstall.mjs downloads the binary matching the user's OS/arch
│ from the GitHub Release → `bin/bs.exe`
├── bin/
│ └── bs.exe plain-text placeholder; replaced by postinstall
└── dist/ built binaries, one per OS/arch (gitignored) —
upload these to the GitHub Release with `gh`Flow when you press a
[a] pressed
→ ParkDefault() + Save() (park the active account first)
→ snapshot emails/token → temporary state file (auto-cleaned)
→ TUI quits (alt screen off)
→ `freebuff login` runs in your terminal (URL shown there)
→ CLI exits after the browser login finishes
→ credentials.json re-read → IngestAfterLogin(pre) → Save()
→ press Enter → TUI relaunches with the updated account listRequirements
- To install & run: Node.js/npm (for
npm i -g buffswitch) - Dev mode / building from source: Go 1.27+ (module
bsis written with Go 1.27) - The Freebuff binary inside the manicode config directory (override with
the
FREEBUFF_BINenv var) credentials.jsoninside the manicode config directory (override with the-credsflag)- An interactive terminal (the TUI uses an alternate screen)
Usage
| Key | Action |
|---|---|
| ↑ / ↓ or j / k | select account |
| Enter | activate the selected account, exit bs, and run freebuff |
| a | add account: exit the TUI, run freebuff login in the terminal, then return |
| r | reload the list from disk |
| d | delete the selected account (confirm with y) |
| q / Ctrl+C | quit |
Add-account flow
- Press
ain the account list. bsparks the active account in its email slot (data is not lost) and snapshots the current accounts to a temporary state file.- The TUI closes and
freebuff logintakes over the terminal. Its output — including the login URL — is shown right there. Copy the URL, open it in your browser, and finish the login. Nothing is auto-opened. - When the CLI exits,
bsre-readscredentials.json, then:- a new account (email not seen before) → becomes
default; - the same email as an existing account → the session is refreshed (the new token is copied into the slot too);
- nothing changed (login canceled) →
defaultis left untouched.
- a new account (email not seen before) → becomes
- A one-line result is printed (e.g.
Akun baru aktif: ...). Press Enter to relaunch the TUI with the updated account list.
The temporary state file is removed automatically. bs never tries to
open a browser or capture the login output itself — freebuff login owns
the terminal, which is the most reliable way to run its interactive login.
credentials.json format
Example structure after bs has managed the file — all values below are
placeholders, not real data:
{
"default": {
"id": "00000000-0000-0000-0000-000000000000",
"name": "Active Account Name",
"email": "[email protected]",
"authToken": "secret-auth-token",
"fingerprintId": "enhanced-xxxxxxxx",
"fingerprintHash": "hash-xxxxxxxx"
},
"[email protected]": {
"id": "00000000-0000-0000-0000-000000000000",
"name": "Another Account Name",
"email": "[email protected]",
"authToken": "secret-auth-token",
"fingerprintId": "enhanced-xxxxxxxx",
"fingerprintHash": "hash-xxxxxxxx"
}
}Rules bs enforces:
- Every key other than
defaultis the account's email. - Stray keys left by login (e.g.
defaults) are re-keyed by email onSave. defaultalways has a synced parked copy under the same email slot.- The file is written atomically (
.tmp+ rename) keeping the original mode (0600 if the file does not exist yet). - Account field contents (
authToken,fingerprint*) are never modified — they are only moved between keys.
Development
Dev mode runs the Go source directly instead of the installed binary:
cd ~/projects/buffswitch
go run . # dev mode: run the TUI (needs a TTY)
gofmt -l . # format check (empty = tidy)
"$(go env GOBIN)/golangci-lint" run ./... # lint (baseline: 0 issues)Linting is the only test/verification command this project uses
(golangci-lint v2, installed via
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest).
go vet/go test are not part of the standard dev flow.
Store-logic unit tests exist in
store_test.goand can be run anytime withgo test ./...— but they are not part of the standard dev flow.
Release binaries (multi-OS)
The npm package ships no binaries at all. Each platform binary lives
as a GitHub Release asset (repo ozan-fn/buffswitch, tag
v<version>). During install, postinstall.mjs downloads only the one
matching the user's OS/arch and saves it as bin/bs.exe — so users
download a few MB, never all eight (~48 MB). bin/bs.exe in the tarball
is a plain-text stub (no shebang) that only prints an error if
postinstall never ran — it is a file, never a shell script, so Windows
never tries to run sh.
Build every platform into dist/ before a release:
for t in "linux amd64 buffsw-cli-linux-x64" \
"linux arm64 buffsw-cli-linux-arm64" \
"linux arm buffsw-cli-linux-arm" \
"linux 386 buffsw-cli-linux-386" \
"darwin amd64 buffsw-cli-darwin-x64" \
"darwin arm64 buffsw-cli-darwin-arm64" \
"windows amd64 buffsw-cli-win32-x64.exe" \
"windows arm64 buffsw-cli-win32-arm64.exe"; do
set -- $t
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -trimpath -o "dist/$3" .
doneCGO_ENABLED=0 produces static binaries, so a single Linux build runs
on both glibc and musl. Binaries are gitignored (dist/).
Optional: shrink with UPX before uploading (Linux and Windows-x64 only; darwin and win-arm64 are not supported):
upx --best --lzma dist/buffsw-cli-linux-* dist/buffsw-cli-win32-x64.exePublish a release (a single npm package):
- Make the release tag match the package version — bump
"version"inpackage.json, thengit tag v0.1.1(create the tag for the version you are releasing). - Upload the built binaries to the GitHub release:
gh release upload v0.1.1 dist/*(create the release first withgh release create v0.1.1if needed). - Publish the npm package:
npm publish.
postinstall.mjs builds its download URL from the package version
(https://github.com/ozan-fn/buffswitch/releases/download/v0.1.1/buffsw-cli-linux-x64),
so the release tag and the package version must always match. Override the
URL per install with the BUFFSW_CLI_BINARY_URL env var (e.g. a mirror).
File map for development:
| File | Responsibility |
|---|---|
| main.go | UI: Bubble Tea model, keymap, list rendering, login loop |
| store.go | Data rules: Load, Save, normalize, SwitchTo, ParkDefault, Delete, IngestAfterLogin |
| stats.go | Usage stats per account: session tracking, counts, last used → buffswitch-stats.json |
| login.go | Terminal hand-off: runs freebuff login, ingests the result, manages the temporary login-state file |
Troubleshooting
| Symptom | Fix |
|---|---|
| Gagal membaca ... : expected a JSON object | credentials.json is broken/invalid — check the JSON |
| Gagal menjalankan freebuff login | check FREEBUFF_BIN or that the freebuff binary exists |
| State login tidak ditemukan | the temporary state file was deleted while bs was closed — press r after relaunching to reload |
| An account is missing from the list | press r to reload from disk |
| Login finished but "No new account" | you logged in with an already-registered email — the session was refreshed, not added |
| Layout breaks in a narrow terminal | the window auto-shrinks; minimum width is ~20 columns |
| Error: bs is not installed correctly. / Windows The system cannot find the path specified. | postinstall never ran or the download failed — reinstall without --ignore-scripts (e.g. npm i -g --force buffswitch), check the release tag has the assets (see Release binaries), or run node postinstall.mjs inside the installed package |
| Install from a git clone/CI fails | dist/ is gitignored and the release may not have the assets yet — build the binary for your OS into dist/ and upload it to the release (see Release binaries) |
License
MIT.
