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.
Install the package globally from npm, then run bs:
npm i -g buffsw-cli
bsDev 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 · 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.
- Features
- How it works
- File layout
- Requirements
- Usage
- Add-account flow
- credentials.json format
- Development
- Troubleshooting
- 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. - 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.
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>,
...
}
buffswitch/ repo; npm package name is `buffsw-cli`
├── 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
├── 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`
[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 list
- To install & run: Node.js/npm (for
npm i -g buffsw-cli) - 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)
| Key | Action |
|---|---|
↑ / ↓ or j / k |
select account |
Enter |
activate the selected account (switch default) |
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 |
- 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.
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": "main-account@example.com",
"authToken": "secret-auth-token",
"fingerprintId": "enhanced-xxxxxxxx",
"fingerprintHash": "hash-xxxxxxxx"
},
"other-account@example.com": {
"id": "00000000-0000-0000-0000-000000000000",
"name": "Another Account Name",
"email": "other-account@example.com",
"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.
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.
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 |
login.go |
Terminal hand-off: runs freebuff login, ingests the result, manages the temporary login-state file |
| 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 buffsw-cli), 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) |
MIT.