From 0d2879894f41a6d0575f30f842799686a33e0f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Fernando=20Gomes=20Marcial?= Date: Mon, 29 Jun 2026 12:32:06 -0400 Subject: [PATCH] feat: add macOS support (BSD date/stat, locale-safe rendering) Abstract date/stat over GNU vs BSD by probing behavior once (epoch_from_iso, file_mtime), so macOS works without coreutils. Pin LC_ALL=C so comma-decimal locales (e.g. pt_BR) render identically to the C-locale CI. install.sh accepts BSD date/stat; the npm CLI accepts darwin as a supported platform. Closes #2 --- .changeset/macos-support.md | 8 ++++++++ AGENTS.md | 5 +++-- README.md | 18 ++++++++++-------- install.sh | 24 ++++++++++++++---------- src/infra/system.ts | 6 +++--- tokenline.sh | 37 ++++++++++++++++++++++++++++++++++--- 6 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 .changeset/macos-support.md diff --git a/.changeset/macos-support.md b/.changeset/macos-support.md new file mode 100644 index 0000000..6d86e98 --- /dev/null +++ b/.changeset/macos-support.md @@ -0,0 +1,8 @@ +--- +"@inbrace-tech/tokenline": minor +--- + +Add macOS support. `tokenline.sh` now abstracts `date`/`stat` over GNU vs BSD by +probing behavior once (`epoch_from_iso`, `file_mtime`), pins `LC_ALL=C` so a +comma-decimal locale renders identically, and the installer accepts macOS +(`brew install bash jq`). Closes #2. diff --git a/AGENTS.md b/AGENTS.md index b9ed98d..f537c9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,8 +43,9 @@ Strip ANSI to check structure: pipe the output through `sed 's/\x1b\[[0-9;]*m//g ## Platform -v1 targets Linux / WSL2. macOS and Windows are tracked as roadmap issues — the -GNU-specific calls (`mapfile`, `date -d`, `stat -c`) are the things to abstract. +Runs on Linux / WSL2 and macOS. `date`/`stat` are abstracted over GNU vs BSD by +probing behavior once (`epoch_from_iso`, `file_mtime` in `tokenline.sh`); `mapfile` +still needs bash 4+, so macOS users `brew install bash`. Windows is a roadmap issue. ## The npm installer diff --git a/README.md b/README.md index ab524f8..faa2d1a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > See your AI coding costs in real time. Tokenline adds context usage, prompt-cache savings, TTL countdown and rate-limit pacing to Claude Code and Gemini CLI. ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg) -![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2-blue) +![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2%20%7C%20macOS-blue) ![Shell](https://img.shields.io/badge/shell-bash%204%2B-lightgrey) ## Quickstart @@ -78,14 +78,16 @@ If `tokenline` reveals you are getting 1-hour cache writes, keep in mind they bu ## Requirements -v1 targets **Linux / WSL2**: +Runs on **Linux / WSL2** and **macOS**: -- `bash` 4 or newer +- `bash` 4 or newer — macOS ships 3.2, so `brew install bash` - [`jq`](https://jqlang.github.io/jq/) -- GNU coreutils (`date -d`, `stat -c`) +- `date` and `stat` — GNU (`-d` / `-c`) or BSD (`-j` / `-f`); both are handled -> macOS and Windows support are on the [roadmap](#roadmap). `install.sh` checks all of -> the above and tells you exactly what's missing. +On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` needed. + +> Windows support is on the [roadmap](#roadmap). `install.sh` checks all of the +> above and tells you exactly what's missing. ## Advanced Installation @@ -139,11 +141,11 @@ On every refresh the host CLI pipes a JSON snapshot of the session to the script | Blank statusline, or `[tokenline] jq not found` | Install `jq` (`apt install jq` / `brew install jq`), then re-run `./install.sh`. | | Cache shows `COLD` immediately | Normal right after a long idle gap — the cache window has elapsed. It goes `HOT` again on your next turn. | | Colors look wrong / show escape codes | Your terminal must support 256-color ANSI. Most modern terminals do; check your `$TERM`. | -| Nothing renders on macOS | Expected on v1 — macOS uses BSD `date`/`stat`. See the [roadmap](#roadmap). | +| Nothing renders on macOS | Install `bash` 4+ and `jq`: `brew install bash jq`. Stock bash 3.2 lacks `mapfile`. | ## Roadmap -- [ ] macOS support (BSD `date`/`stat`, bash 3.2 fallback) +- [x] macOS support (BSD `date`/`stat`, Homebrew bash) - [ ] Windows support (Git Bash / PowerShell) - [ ] Configurable colors and thresholds via `TOKENLINE_*` env vars diff --git a/install.sh b/install.sh index 0d0e5ef..0d53e58 100755 --- a/install.sh +++ b/install.sh @@ -24,11 +24,11 @@ printf '%s\n' "--------------------------------" missing=0 -# bash 4+ (mapfile/associative features) +# bash 4+ (mapfile). macOS ships 3.2 — `brew install bash` provides 5.x. if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then ok "bash ${BASH_VERSION%%(*}" else - err "bash 4+ required (found ${BASH_VERSION%%(*}). macOS ships 3.2 — see README roadmap." + err "bash 4+ required (found ${BASH_VERSION%%(*}). On macOS: brew install bash" missing=1 fi @@ -40,19 +40,23 @@ else missing=1 fi -# GNU date (-d): used to parse ISO timestamps from the transcript +# date: GNU (-d) or BSD (-j) — tokenline handles both. if date -d "@0" >/dev/null 2>&1; then - ok "GNU date (-d)" + ok "date (GNU -d)" +elif date -j -f "%s" 0 >/dev/null 2>&1; then + ok "date (BSD -j)" else - err "GNU date (-d) missing — BSD/macOS date differs (see README roadmap)" + err "no usable date (need GNU -d or BSD -j)" missing=1 fi -# GNU stat (-c): mtime fallback for the cache timer +# stat: GNU (-c) or BSD (-f) — tokenline handles both. if stat -c %Y . >/dev/null 2>&1; then - ok "GNU stat (-c)" + ok "stat (GNU -c)" +elif stat -f %m . >/dev/null 2>&1; then + ok "stat (BSD -f)" else - err "GNU stat (-c) missing — BSD/macOS stat differs (see README roadmap)" + err "no usable stat (need GNU -c or BSD -f)" missing=1 fi @@ -67,8 +71,8 @@ fi printf '\n' if [ "$missing" -ne 0 ]; then - warn "Missing dependencies above. tokenline targets Linux/WSL2 for v1;" - warn "macOS/Windows support is on the roadmap (see README)." + warn "Missing dependencies above. On macOS: brew install bash jq." + warn "Windows support is on the roadmap (see README)." printf '\n' fi diff --git a/src/infra/system.ts b/src/infra/system.ts index d862710..438023e 100644 --- a/src/infra/system.ts +++ b/src/infra/system.ts @@ -5,13 +5,13 @@ import { ok, warn } from '../shared/logger' export function checkPlatform(): boolean { const p = platform() - if (p === 'linux') { + if (p === 'linux' || p === 'darwin') { ok(`platform: ${p} (supported)`) return true } warn( - `platform: ${p} — v1 targets Linux/WSL2; the bash statusline likely won't ` + - `render yet (see roadmap). Use --force to install anyway.`, + `platform: ${p} — supported on Linux/WSL2 and macOS; the bash statusline ` + + `likely won't render yet (see roadmap). Use --force to install anyway.`, ) return false } diff --git a/tokenline.sh b/tokenline.sh index c83d422..4e04023 100755 --- a/tokenline.sh +++ b/tokenline.sh @@ -9,9 +9,14 @@ # # Repo: https://github.com/inbrace-tech/tokenline # License: MIT -# Requires: bash 4+, jq, GNU coreutils (date -d, stat -c). Linux/WSL2 for v1. +# Requires: bash 4+, jq. Linux/WSL2 or macOS (brew install bash jq). # ============================================================================== +# Pin C locale: a comma-decimal locale (e.g. pt_BR) makes awk/printf emit +# "46,2k" and reject dotted input. LC_ALL beats LC_NUMERIC, so set LC_ALL to +# stay deterministic even when the user exports LC_ALL. Output is ASCII/bytes. +export LC_ALL=C + # --- Colors & Formatting Constants --- COLOR_GRAY=$'\033[38;5;244m' COLOR_DARK_GRAY=$'\033[38;5;240m' @@ -34,6 +39,32 @@ if ! command -v jq >/dev/null 2>&1; then exit 0 fi +# --- GNU vs BSD coreutils (Linux vs macOS) --- +# Probe behavior, not `uname`, so Homebrew coreutils is picked up automatically. +if date -d "@0" >/dev/null 2>&1; then _date_gnu=1; else _date_gnu=0; fi +if stat -c %Y . >/dev/null 2>&1; then _stat_gnu=1; else _stat_gnu=0; fi + +epoch_from_iso() { + # ISO-8601 -> epoch seconds; empty on failure (callers fall back to mtime). + local iso="$1" + if [ "$_date_gnu" -eq 1 ]; then + date -d "$iso" +%s 2>/dev/null + else + # BSD date needs an explicit format and rejects fractional secs / 'Z'. + # First 19 chars are always 'YYYY-MM-DDTHH:MM:SS'; transcripts are UTC. + date -u -j -f "%Y-%m-%dT%H:%M:%S" "${iso:0:19}" +%s 2>/dev/null + fi +} + +file_mtime() { + local f="$1" + if [ "$_stat_gnu" -eq 1 ]; then + stat -c %Y "$f" 2>/dev/null + else + stat -f %m "$f" 2>/dev/null + fi +} + # --- Runtime state directory --- # Per-turn timestamp / TTL are cached between the 1s refreshes. Prefer a # per-user dir (0700) under XDG_RUNTIME_DIR: it avoids predictable, world- @@ -176,10 +207,10 @@ compute_cache_timer() { | tail -n 1 ) if [ -n "$iso" ]; then - last_ts=$(date -d "$iso" +%s 2>/dev/null) + last_ts=$(epoch_from_iso "$iso") fi # Mtime fallback if parsing is unsuccessful - [ -z "$last_ts" ] && last_ts=$(stat -c %Y "$transcript_path" 2>/dev/null) + [ -z "$last_ts" ] && last_ts=$(file_mtime "$transcript_path") fi # Fallback to local session caching file if transcript read is unavailable or cached timestamp is newer