GUESTBOOK.EXEcontact.exe
GITPULSE

</gitpulse>

GITPULSE

Any GitHub profile as a terminal report card — every number states its window.

3

runtime dependencies — chalk, commander, ora

0

install-time lifecycle scripts, so npx works under npm v12 defaults

0

network calls on the --demo path, asserted by CI

3

GitHub REST endpoints read; everything else computed locally

SPECIFICATIONS

ROLESOLO BUILD
YEAR2026
TYPECLI TOOL
STATUSPUBLISHED ON NPM · MIT
STACKtypescript · node.js · esm · cli · github-api · commander · chalk · ora +2 more
LINKS[live ↗][github ↗]
AVAILABILITYnpm · @aethereumdev/gitpulse

A GitHub profile is a scroll.

gitpulse renders any public GitHub profile as a single-screen report card in the terminal: profile, statistics, language share, ranked repositories, a code-activity heatmap, commit patterns, streaks, and a 0–100 score with a published methodology.

README.TXT: GITPULSE (11 KB)[full readme →]

== WHAT IS THIS ==

────────────────────────────────────────

gitpulse renders any public GitHub profile as a single-screen report card in the terminal: profile, statistics, language share, ranked repositories, a code-activity heatmap, commit patterns, streaks, and a 0–100 score with a published methodology. Its differentiator is labelling — GitHub's public Events API reaches back a variable and often short distance, so gitpulse derives the real window from the data and prints it next to the number instead of stamping a fixed "last 90 days" header over whatever it got. Published on npm as @aethereumdev/gitpulse under MIT, with three runtime dependencies and no install-time scripts, so `npx @aethereumdev/gitpulse torvalds` works on a machine that has never seen it. `--demo` renders the whole report offline with no token and no network calls, and CI fails the build if that path ever reaches the wire.

== </the problem> ==

────────────────────────────────────────

A GitHub profile is a scroll. It gives you a follower count and a green calendar and leaves you to assemble everything else across a dozen tabs. The terminal tools that try to fix this mostly inherit a subtler problem: they are built on the public Events API, which returns at most 300 events over a variable and often short period, and then print a fixed "last 90 days" header over whatever they happened to get. The number on screen and the label above it describe different things, on a page about a real person.

role & context

Solo build. The interesting half was not the features — it was a measured audit of a tool that already looked finished, and the repairs that audit forced before it was fit to publish.

== </my approach> ==

────────────────────────────────────────

A TypeScript CLI that reads three public REST endpoints — user, repositories, public events — and computes everything else locally, rendering one screen: profile, statistics, language share, ranked repositories, a code-activity heatmap, commit patterns by day and hour, coding streaks, and a 0–100 score with a letter grade. The rule the whole thing is built on is that when the label and the data would disagree, the label changes and the data does not: the activity span is derived from the oldest event the feed actually returned and printed next to the number, and a streak carries "measured within the 30-day event window above, not all-time". The score's methodology, inputs and weights ship inside the package as SCORING.md, so the number is checkable rather than asserted.

== </the story> ==

────────────────────────────────────────

GitHub's own profile page will tell you a follower count and show you a green calendar, then leave you to assemble everything else across a dozen tabs. gitpulse renders the same public data as one screen in the terminal: profile, statistics, language share, ranked repositories, a code-activity heatmap, commit patterns by day and hour, coding streaks, and a 0–100 score with a letter grade.

What separates it from the other terminal GitHub tools is not the rendering, it is the labelling. The public Events API returns at most 300 events and reaches back a limited and variable period — often far less than 90 days for an active account. Most tools built on it print a fixed "last 90 days" header over whatever they got. gitpulse derives the window from the oldest event the feed actually returned and prints that window beside the number, so a heatmap says `last 30 days of public code events 2026-07-16 → 2026-08-14` and a streak says `measured within the 30-day event window above, not all-time`. When the label and the data would disagree, the label changes.

The output is the product, so the output is what CI enforces. Committed snapshots pin what every renderer emits byte for byte, the demo is rendered on every push, and the piped output is scanned for emoji and stray ANSI escapes so scripts get clean text. The score is the part that needed a caveat rather than a feature bullet, so it has one: it reads public metadata only, never opens a line of anyone's code, and SCORING.md ships inside the package documenting every input and weight — and what the number cannot see — before it gives any formula.

== </architecture> ==

────────────────────────────────────────

TypeScript, ESM, strict mode, Node 20+. Three runtime dependencies: chalk for colour, commander for the flag surface, ora for the spinner.

One network surface, `src/api/github.ts`, reading three REST endpoints — `/users/:name`, `/users/:name/repos` (owner-type, capped at 200) and `/users/:name/events/public` (capped at 300, tolerant of the endpoint's known flakiness). Nothing else touches the wire. Results cache for 30 minutes under `~/.gitpulse/cache`, evicting at 50 profiles.

The pipeline is deliberately cut in the middle: `buildProfile()` is split out of `fetchUserProfile()`, so the network path and the bundled `--demo` fixture converge on the same derivation. `src/utils/scoring.ts` computes language share, the contribution window, streaks, commit patterns and the five score components from that single structure. Nine renderers in `src/ui/` turn it into text; `src/utils/anim.ts` stages the reveal.

The animation contract is carried by the type signature: every animated renderer takes `progress: number = 1`, and at `progress === 1` returns exactly the string the static path returns. The last frame is the real output, so the animated and unanimated paths cannot drift.

CI runs on every push to every branch across Node 20, 22 and 24: lint, typecheck of source and tests, build, the suite, an assertion that `dist/__tests__` does not exist, a real render of the demo, a render with `fetch` stubbed to throw, a scan of the piped output for emoji and ANSI escapes, and `npm pack --dry-run`.

== </key features> ==

────────────────────────────────────────

Every number carries the window it was measured over

The report prints spans like `last 30 days of public code events 2026-07-16 → 2026-08-14`, derived from the data rather than assumed, plus an on-screen note that the feed reaches no further back — that earlier activity is invisible, not absent.

Runs on a machine that has never seen it

`npx @aethereumdev/gitpulse torvalds`. Three runtime dependencies, no install-time lifecycle scripts, Node 20 or newer, MIT. The package is scoped; the binary is plain `gitpulse`.

`--demo` renders the whole report offline

No token, no network, no account — the bundled fixture runs the exact same derivation as the live path. CI stubs fetch to throw and fails the build if that path ever reaches the wire.

A score that publishes its own methodology

Five weighted components summing to 100 — repo quality, consistency, language diversity, readme quality, recent activity — with SCORING.md stating what the score does not measure before it states any formula.

Staged render that never diverges from the static one

Sections arrive in reading order, the heatmap paints column by column, the grade lands last. `--no-anim` is byte-identical because the last animation frame is the static output, and staging disables itself automatically when the output is piped, redirected, or run under CI or NO_COLOR.

Machine-readable exits

`--json` emits curated output rather than a raw API dump, `--minimal` collapses the card to five lines, `--compare` runs two profiles head to head, and `--export` emits a Three.js scene graph with every derived float quantised so the same profile produces the same bytes on any platform.

== </key decisions> ==

────────────────────────────────────────

DECISION 01

REST only, with no GraphQL path for contribution data. GraphQL would give a true contribution calendar but demands a token on every single call, which would kill both the unauthenticated path and the offline demo. The cost is a narrower, variable window — and the whole product is built around labelling that window honestly rather than hiding it.

DECISION 02

`buildProfile()` split out of `fetchUserProfile()`. The demo fixture runs the exact same derivation as the live path, so the demo cannot drift from the product; it is the output, with three fetches replaced by a captured snapshot. The same split made the pipeline testable without a network and forced an injectable clock, without which the fixture's absolute timestamps would have decayed into an empty heatmap within months.

DECISION 03

Every animated renderer takes `progress: number = 1`, where `progress === 1` returns the static string. `--no-anim` must be byte-identical to the staged render. Structuring it this way makes divergence impossible rather than merely tested, and the identity is asserted directly on top of that.

DECISION 04

Publish the scoring methodology instead of softening the score. An undocumented 0–100 number about a real person is an assertion nobody can check. SCORING.md ships inside the npm tarball, documents every input and weight, and states what the score does not measure first — including that one sub-item saturates at 40 stars, which was discovered while writing the document rather than before it.

DECISION 05

Scope the package rather than rename the tool. npm's similarity filter reserves the unscoped `gitpulse` against the existing `git-pulse`, so the registry returns 404 for the bare name permanently. Scoping to `@aethereumdev` keeps the binary plain `gitpulse` — only the install string changes, and the command in the docs is still the command you type.

DECISION 06

Quantise every derived float in the scene export to six decimal places. Matrix CI on Node 20/22/24 proved Math.sin/cos/sqrt/log2 differ in the last digit between macOS and Linux. A scene that changes between machines cannot be diffed or cached, which defeats the point of a format another tool consumes.

== </what i learned> ==

────────────────────────────────────────
>

An acceptance gate is worth more than a green test suite. The gate caught this project's own rewrite fabricating 60 days of inactivity for a feed that only reached back 30 — a bug in the plan, not in the code, and no unit test was ever going to notice it.

>

A passing test can verify nothing at all. Two of the then-33 tests asserted an inline expression and imported no product code; the suite was green and the coverage was imaginary.

>

The interesting defects came from rendering real output and reading it, not from the suite: phantom runtime dependencies, test helpers leaking into the packaged tarball, three renderers stamping wall-clock time so the same profile produced different bytes every run.

>

Checking a weight is different from describing one. Writing the scoring document exposed that the star input saturates at 40 stars rather than ~215, which changes how the whole number should be read — and that only surfaced because the methodology had to be written down for strangers.

>

Cross-platform float divergence is real and only a matrix catches it. Math.sin/cos/sqrt/log2 differed in the last digit between macOS and Linux, which is invisible until an export is meant to be diffable.

next:

The release path is written but has not run: 1.0.0 was published by hand, so moving npm releases onto trusted publishing (OIDC) — which would make every version after the first carry a provenance attestation and remove the need for any long-lived token — is the first open item, along with revoking the bootstrap token that exists only to have solved that chicken-and-egg. After that: a human confirmation that the heatmap's column paint does not flicker in Terminal.app at 52 columns, which an agent cannot check, and the decision of whether the bundled demo fixture stays `torvalds` or becomes its author's own profile.

typescript · node.js · esm · cli · github-api · commander · chalk · ora · vitest · github-actions

BUILD.TXT: GITPULSE (6 KB)[the build record →]

== </what the audit found> ==

────────────────────────────────────────

The work did not start from a feature list. It started from a measured audit (RESEARCH-CONTEXT.md, committed to the public repo) whose verdict was uncomfortable: the tool worked, looked finished, and its own documented first command did not run. `gitpulse torvalds` with no token hit GitHub's 60-requests-per-hour unauthenticated cap, because only the `--token` flag was honoured and most machines have already spent that per-IP budget on something else. A first-time user ran the command printed in the README, got an error, and concluded the tool was broken. Everything else in the backlog was worth less than that.

== </what was repaired> ==

────────────────────────────────────────

The first-run fix was roughly twenty lines: read GITHUB_TOKEN and GH_TOKEN from the environment, split the single 403 branch into primary rate limit, secondary rate limit and bad credentials so the message names the actual problem, and add `--demo` so the output is reachable with no setup at all.

Then the numbers were made to agree with their labels. `generateContributions` had been counting every public event type as a "contribution" under a hardcoded `Last 90 days:` header, while reporting an active-day count derived from a third window — three windows, one label, on a screen about a real person. The rewrite counts only code events and derives the span from real coverage. Its own first implementation then widened the window to GitHub's nominal 90-day retention whenever the 300-event cap was not hit, which rendered torvalds as a 90-day grid containing 60 days of fabricated inactivity for a feed that only reaches back 30. The sprint's acceptance gate caught it before it shipped, which is the single most useful thing that happened in this project.

Four more defects surfaced the same way — by rendering real profiles and reading the result rather than by trusting a green suite. `src/__tests__/cache.test.ts` was asserting an inline expression and importing no product code at all, so two of the then-33 passing tests verified nothing; it was replaced with tests against the real module. Test helpers were compiling into `dist/` and would have shipped inside the packed tarball, so CI now fails if `dist/__tests__` reappears. `boxen` and `node-fetch` were declared as runtime dependencies and imported nowhere, worth 24 packages in the install tree of a project whose first rule is no runtime bloat — removing them left three. And three separate renderers stamped wall-clock time into their output, so the same profile produced different bytes on every run.

A second independent re-audit at the end, run against a live render rather than against the draft, found seven further defects inside gates that had already been marked passed. That pass is written up in masterplan.md, which is committed, so the failures are readable by anyone who wants to check the account.

== </what was measured> ==

────────────────────────────────────────

Matrix CI on Node 20, 22 and 24 earned its place on the first run by failing on all three. The failure was real rather than flaky: Math.sin, cos, sqrt and log2 are not required to be bit-identical across platforms and are not, so the Three.js scene export produced x = 4.044661788320042 on macOS and …043 on Linux. A scene that differs between machines cannot be diffed or cached, which defeats the point of a contract another tool consumes, so every derived float is now quantised to six decimal places (src/ui/export.ts) with a regression test behind it.

Writing the scoring methodology meant checking the weights instead of describing them, which produced the most interesting correction in the project. The star sub-item is `Math.min(8, log2(stars + 1) * 1.5)`, which saturates at 40 stars — not the ~215 the first draft of the document claimed. Above 40 stars that input stops discriminating entirely, so a 250,000-star repository scores identically to a 40-star one. SCORING.md now says so, because it materially changes how the number should be read.

`--no-anim` was verified byte-identical to the staged render by replaying the animated stream through a minimal ANSI interpreter and diffing the result, rather than by asserting that it ought to be.

== </decisions> ==

────────────────────────────────────────

DECISION 01

REST only — no GraphQL path for contribution data

because GraphQL would give a true contribution calendar, but it requires a token on every single call. Staying on REST keeps both the unauthenticated path and `--demo` working, at the cost of a narrower and variable window — which is then labelled honestly rather than papered over. The narrow window is the price of the tool being runnable by a stranger with no setup.

DECISION 02

`buildProfile()` split out of `fetchUserProfile()`

because The bundled demo fixture runs the exact same derivation as the network path, so the demo is structurally incapable of drifting from the product — it is not a mock-up of the output, it is the output with three fetches replaced by a captured snapshot. It also made the whole pipeline testable without a network, and it forced an injectable clock, without which the demo would have decayed into an empty heatmap within months.

DECISION 03

Every animated renderer takes `progress: number = 1`, and `progress === 1` returns exactly the static string

because The requirement was that `--no-anim` be byte-identical to the staged render. Structuring it this way makes divergence impossible rather than merely tested — the last frame is the real output — and turns a property you verify after the fact into one the type signature enforces.

DECISION 04

Publish the scoring methodology instead of softening the score

because An undocumented 0–100 number about a real person is an assertion nobody can check. SCORING.md ships inside the npm tarball and documents every input and weight — and states what the score does not measure before it states any formula, including that it reads no code and cannot see private work.

DECISION 05

Scope the package to @aethereumdev rather than rename the tool

because npm's similarity filter reserves the unscoped `gitpulse` against the existing `git-pulse` and rejects it at publish. The registry returns 404 for the bare name and always will, because it is unregistered rather than available. Scoping the package keeps the binary plain `gitpulse` — only the install string carries the scope, so the command in the docs is still the command you type.

LIMITS.TXT: GITPULSE (1 KB)[where it doesn't work →]

== </where it doesn’t work> ==

────────────────────────────────────────

Everything time-based comes out of GitHub's public Events API, which caps at 300 events and often reaches back far less than 90 days for a busy account. The heatmap, commit patterns, streaks and two of the five score components therefore describe a narrow measured window, not a year — gitpulse prints the true span rather than assuming one, but it cannot widen it without GraphQL, and activity older than the window is invisible to the tool rather than absent from someone's life. "Languages" counts repositories, not lines of code: each non-fork repo contributes 1 to its GitHub-assigned primary language, so a 300,000-line C project and a one-file C script weigh the same. The hire-ability score reads public metadata only, never opens a line of code, cannot see private work — a staff engineer whose entire output is private will score badly — and one sub-item stops discriminating above 40 stars. The bundled `--demo` fixture is `torvalds`: public data and the profile the docs already use, but it is another person's profile shipping inside the package, and that is an open decision rather than a settled one. No end-to-end test drives the CLI as a subprocess (the byte-identity check does, but it needs a forced TTY and lives outside the suite), nothing enforces that the recorded demo asset stays current, and the release workflow is written but has never fired — 1.0.0 was published by hand, so it carries a registry signature but no provenance attestation.

PUBLISHED ON NPM · MIT · npmjs.com