vibestreak 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/bin/vibestreak.mjs +35 -6
- package/package.json +2 -2
- package/src/version.mjs +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Any version runs with `npx vibestreak@<version>`; `npx vibestreak@latest autosync on`
|
|
4
|
+
moves the background sync to the newest one.
|
|
5
|
+
|
|
6
|
+
## 0.3.2 (2026-10-25)
|
|
7
|
+
|
|
8
|
+
- Linking says up front that your profile is public (and how to hide it), and
|
|
9
|
+
that linking means agreeing to the Terms and Privacy policy.
|
|
10
|
+
- If the server ever stops accepting this version, the background sync shows
|
|
11
|
+
a notification (once a day) with the command that updates it, instead of
|
|
12
|
+
failing quietly while your streak runs out.
|
|
13
|
+
|
|
14
|
+
## 0.3.1 (2026-10-25)
|
|
15
|
+
|
|
16
|
+
- `vibestreak --version` (or `-v`), and the version in `help` and `status`.
|
|
17
|
+
- Says when a newer version is out: after `vibestreak`, `sync` and in `status`.
|
|
18
|
+
- Sends its version with each sync. Settings shows each machine's version, and
|
|
19
|
+
the server can turn away versions too old to count correctly, with the
|
|
20
|
+
command that updates them.
|
|
21
|
+
|
|
22
|
+
## 0.3.0 (2026-10-25)
|
|
23
|
+
|
|
24
|
+
- First run: plain `npx vibestreak` signs in through the browser (no token to
|
|
25
|
+
copy), syncs, and offers background sync and the evening nudge.
|
|
26
|
+
- `vibestreak setup [--token <t>]`: all of that with the defaults and no
|
|
27
|
+
questions; commit addresses come from git config.
|
|
28
|
+
- `vibestreak login` signs in through the browser; `--token` still works.
|
|
29
|
+
- `vibestreak pause [1h|3d|tomorrow]` and `resume`: the background syncs and
|
|
30
|
+
nudge skip their runs for up to 7 days, and nothing is lost.
|
|
31
|
+
|
|
32
|
+
## 0.2.0 (2026-10-25)
|
|
33
|
+
|
|
34
|
+
- `vibestreak` shows a dashboard: streak, level, a 12-week heatmap and your rank.
|
|
35
|
+
- `vibestreak me`, `profile <handle>`, `board`, `today` in the terminal.
|
|
36
|
+
- Syncs after the first one send only the last 7 days: about 3 seconds.
|
|
37
|
+
|
|
38
|
+
## 0.1.2 (2026-10-25)
|
|
39
|
+
|
|
40
|
+
- `vibestreak nudge at <time>`: when the evening streak check runs (17:00 to
|
|
41
|
+
23:30; 20:30 by default).
|
|
42
|
+
|
|
43
|
+
## 0.1.1 (2026-10-25)
|
|
44
|
+
|
|
45
|
+
- `vibestreak status`; plain `vibestreak nudge` shows the nudge's status.
|
|
46
|
+
- The evening job runs `nudge run` (older schedules keep working).
|
|
47
|
+
|
|
48
|
+
## 0.1.0 (2026-10-25)
|
|
49
|
+
|
|
50
|
+
- First release: reads Claude Code, Codex, Cursor, Copilot (CLI and VS Code),
|
|
51
|
+
Gemini CLI, Antigravity and OpenCode history and git; syncs daily totals.
|
|
52
|
+
- Background sync on macOS (launchd), Linux (systemd or cron) and Windows
|
|
53
|
+
(Task Scheduler), with the evening streak nudge.
|
package/bin/vibestreak.mjs
CHANGED
|
@@ -10,12 +10,13 @@ import { CONFIG_PATH, loadConfig, saveConfig } from '../src/config.mjs'
|
|
|
10
10
|
import { configuredEmails, DEFAULT_ROOTS, globalEmail, scanEmails } from '../src/git.mjs'
|
|
11
11
|
import { deviceLogin } from '../src/login.mjs'
|
|
12
12
|
import { parsePause, pausedUntil, untilText } from '../src/pause.mjs'
|
|
13
|
+
import { updateCommand, updateNotice, VERSION } from '../src/version.mjs'
|
|
13
14
|
import { boardView, dashboardView, profileView, todayView } from '../src/views.mjs'
|
|
14
15
|
|
|
15
16
|
const BRAND = process.env.VIBESTREAK_BRAND ?? 'vibestreak'
|
|
16
17
|
const DEFAULT_SERVER = process.env.VIBESTREAK_SERVER ?? 'https://vibestreak.kingsbridge-consultancy.com'
|
|
17
18
|
|
|
18
|
-
const HELP = `${BRAND} — share your AI coding heatmap: what you shipped, and how steadily.
|
|
19
|
+
const HELP = `${BRAND} ${VERSION} — share your AI coding heatmap: what you shipped, and how steadily.
|
|
19
20
|
|
|
20
21
|
${BRAND} your streak, heatmap and rank at a glance
|
|
21
22
|
(the first time: sets this machine up)
|
|
@@ -44,6 +45,7 @@ const HELP = `${BRAND} — share your AI coding heatmap: what you shipped, and h
|
|
|
44
45
|
days; default until tomorrow 06:00)
|
|
45
46
|
${BRAND} resume end a pause now
|
|
46
47
|
${BRAND} whoami the profile this machine syncs to
|
|
48
|
+
${BRAND} --version this CLI's version
|
|
47
49
|
|
|
48
50
|
Reads: Claude Code, Codex, Cursor, Copilot (CLI and VS Code), Gemini CLI,
|
|
49
51
|
Antigravity and OpenCode history, and git.
|
|
@@ -59,10 +61,10 @@ async function api(config, path, init = {}) {
|
|
|
59
61
|
if (!config.server || !config.token) throw new Error(`Not linked yet: run \`${BRAND} login\`.`)
|
|
60
62
|
const response = await fetch(new URL(path, config.server), {
|
|
61
63
|
...init,
|
|
62
|
-
headers: { authorization: `Bearer ${config.token}`, 'content-type': 'application/json', ...init.headers },
|
|
64
|
+
headers: { authorization: `Bearer ${config.token}`, 'content-type': 'application/json', 'x-vibestreak-cli': VERSION, 'user-agent': `${BRAND}/${VERSION}`, ...init.headers },
|
|
63
65
|
})
|
|
64
66
|
const body = await response.json().catch(() => ({}))
|
|
65
|
-
if (!response.ok) throw new Error(body.error ?? `${response.status} ${response.statusText}`)
|
|
67
|
+
if (!response.ok) throw Object.assign(new Error(body.error ?? `${response.status} ${response.statusText}`), { status: response.status })
|
|
66
68
|
return body
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -83,6 +85,7 @@ async function main() {
|
|
|
83
85
|
// Set by the background jobs, so a pause skips them and nothing else.
|
|
84
86
|
scheduled: { type: 'boolean' },
|
|
85
87
|
yes: { type: 'boolean', short: 'y' },
|
|
88
|
+
version: { type: 'boolean', short: 'v' },
|
|
86
89
|
},
|
|
87
90
|
})
|
|
88
91
|
const [command = 'dashboard', ...rest] = positionals
|
|
@@ -90,6 +93,7 @@ async function main() {
|
|
|
90
93
|
const log = (...args) => values.quiet || console.log(...args)
|
|
91
94
|
|
|
92
95
|
if (command === 'help' || values.help) return console.log(HELP)
|
|
96
|
+
if (command === 'version' || values.version) return console.log(VERSION)
|
|
93
97
|
const columns = process.stdout.columns || 100
|
|
94
98
|
const server = config.server ?? DEFAULT_SERVER
|
|
95
99
|
|
|
@@ -101,7 +105,8 @@ async function main() {
|
|
|
101
105
|
return setup(config, values, { ask: !values.yes })
|
|
102
106
|
}
|
|
103
107
|
const me = await api(config, '/api/v1/me')
|
|
104
|
-
|
|
108
|
+
await dashboardView({ server, token: config.token, handle: me.handle, columns })
|
|
109
|
+
return notice(config, me.cli)
|
|
105
110
|
}
|
|
106
111
|
if (command === 'me') {
|
|
107
112
|
const me = await api(config, '/api/v1/me')
|
|
@@ -175,7 +180,7 @@ async function main() {
|
|
|
175
180
|
if (command === 'collect' || command === 'sync') {
|
|
176
181
|
const until = values.scheduled && pausedUntil(config)
|
|
177
182
|
if (until) return console.log(`${new Date().toISOString()} paused until ${until.toISOString()}; skipped`)
|
|
178
|
-
return sync(config, values, log, command === 'sync')
|
|
183
|
+
return sync(config, values, log, command === 'sync').catch(error => (values.scheduled ? tooOldNotice(config, error) : Promise.reject(error)))
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
if (command === 'status') return status(config)
|
|
@@ -208,7 +213,7 @@ async function main() {
|
|
|
208
213
|
// `nudge run`, the evening job: sync, then nudge if the streak would end at midnight.
|
|
209
214
|
const until = pausedUntil(config)
|
|
210
215
|
if (until) return console.log(`${new Date().toISOString()} paused until ${until.toISOString()}; skipped`)
|
|
211
|
-
const result = await sync(config, values, log, true)
|
|
216
|
+
const result = await sync(config, values, log, true).catch(error => tooOldNotice(config, error))
|
|
212
217
|
if (shouldNudge(result.streak, config)) {
|
|
213
218
|
await notify(nudgeText(result.streak))
|
|
214
219
|
config.lastNudge = result.streak.today
|
|
@@ -242,6 +247,9 @@ async function main() {
|
|
|
242
247
|
// settings page (for machines without one).
|
|
243
248
|
async function login(config, values, { addresses = true } = {}) {
|
|
244
249
|
config.server = values.server ?? config.server ?? DEFAULT_SERVER
|
|
250
|
+
const site = config.server.replace(/\/$/, '')
|
|
251
|
+
console.log(`Your profile (GitHub username, avatar, heatmap, streak) will be public on ${BRAND} and its leaderboard; you can hide it in Settings.`)
|
|
252
|
+
console.log(`Linking means you agree to the Terms (${site}/terms) and Privacy policy (${site}/privacy).\n`)
|
|
245
253
|
if (values.token) config.token = values.token
|
|
246
254
|
else config.token = (await deviceLogin({ server: config.server, machine: config.machineName })).token
|
|
247
255
|
const me = await api(config, '/api/v1/me')
|
|
@@ -323,9 +331,28 @@ async function sync(config, values, log, isSync) {
|
|
|
323
331
|
// Always one line, quiet or not: the background sync's log is how a person sees it ran.
|
|
324
332
|
const streak = result.streak ? ` · streak ${result.streak.current}d${result.streak.activeToday ? '' : ' (not yet today)'}` : ''
|
|
325
333
|
console.log(`${config.lastSync} uploaded ${result.days} days, ${result.commits} commits from ${s.repos} repos${streak} → ${result.profileUrl}`)
|
|
334
|
+
if (!values.quiet) notice(config, result.cli)
|
|
326
335
|
return result
|
|
327
336
|
}
|
|
328
337
|
|
|
338
|
+
// A background run the server turned away as too old: nobody reads the log,
|
|
339
|
+
// and the streak would end unnoticed, so say so on screen, once a day.
|
|
340
|
+
async function tooOldNotice(config, error) {
|
|
341
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
342
|
+
if (error.status === 426 && config.lastUpdateNotice !== today) {
|
|
343
|
+
await notify({ title: 'Update to keep your streak', message: `This version can no longer sync. Run: npx ${BRAND}@latest autosync on` })
|
|
344
|
+
config.lastUpdateNotice = today
|
|
345
|
+
await saveConfig(config)
|
|
346
|
+
}
|
|
347
|
+
throw error
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Says so when a newer CLI is out (the server checks npm).
|
|
351
|
+
function notice(config, cli) {
|
|
352
|
+
const line = updateNotice(cli, { brand: BRAND, autosync: config.autosync })
|
|
353
|
+
if (line) console.log(`\n${line}`)
|
|
354
|
+
}
|
|
355
|
+
|
|
329
356
|
const ago = iso => {
|
|
330
357
|
if (!iso) return 'never'
|
|
331
358
|
const minutes = Math.round((Date.now() - Date.parse(iso)) / 60000)
|
|
@@ -382,6 +409,8 @@ async function status(config) {
|
|
|
382
409
|
row('Nudge', nudgeOn ? `on · checks at ${clock(nudgeTimeOf(config))} · last nudged ${config.lastNudge ?? 'never'}` : config.nudge === false ? 'off' : 'off (needs autosync)')
|
|
383
410
|
const github = me.error ? 0 : (me.emails?.length ?? 0)
|
|
384
411
|
row('Addresses', `${config.emails.join(', ') || 'none'}${github ? ` · +${github} from GitHub` : ''}`)
|
|
412
|
+
const update = me.error ? undefined : updateNotice(me.cli, { brand: BRAND, autosync: config.autosync })
|
|
413
|
+
row('Version', update ? `${VERSION} · ${me.cli.latest} is out: ${updateCommand(BRAND, config.autosync)}` : `${VERSION}${me.cli?.latest ? ' (latest)' : ''}`)
|
|
385
414
|
row('Config', CONFIG_PATH)
|
|
386
415
|
}
|
|
387
416
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibestreak",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Your AI coding streak: a heatmap of the days you coded with Claude Code, Codex, Cursor, Copilot, Gemini and OpenCode, and the commits you shipped with them. Sends daily totals only.",
|
|
5
5
|
"keywords": ["claude-code", "codex", "cursor", "copilot", "gemini", "opencode", "ai-coding", "heatmap", "streak", "leaderboard", "cli"],
|
|
6
6
|
"homepage": "https://vibestreak.kingsbridge-consultancy.com/cli",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": "Kingsbridge Consultancy LLC <support@kingsbridge-consultancy.com>",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"bin": { "vibestreak": "bin/vibestreak.mjs" },
|
|
11
|
-
"files": ["bin", "src", "README.md", "LICENSE"],
|
|
11
|
+
"files": ["bin", "src", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
12
12
|
"engines": { "node": ">=22.13" },
|
|
13
13
|
"os": ["darwin", "linux", "win32"],
|
|
14
14
|
"scripts": { "test": "node --test test/*.test.mjs" }
|
package/src/version.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
4
|
+
|
|
5
|
+
// Release versions only (x.y.z): negative when a is older than b.
|
|
6
|
+
export function compareVersions(a, b) {
|
|
7
|
+
const pa = String(a).split('.').map(Number)
|
|
8
|
+
const pb = String(b).split('.').map(Number)
|
|
9
|
+
for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0)
|
|
10
|
+
return 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// One line when the server says a newer CLI is out; nothing otherwise. With
|
|
14
|
+
// autosync on, the update command also moves the background jobs to it.
|
|
15
|
+
export function updateNotice(cli, { brand = 'vibestreak', autosync = false, current = VERSION } = {}) {
|
|
16
|
+
if (!cli?.latest || compareVersions(current, cli.latest) >= 0) return undefined
|
|
17
|
+
return `${brand} ${cli.latest} is out (this is ${current}): ${updateCommand(brand, autosync)}.`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const updateCommand = (brand, autosync) =>
|
|
21
|
+
autosync ? `\`npx ${brand}@latest autosync on\` updates the background sync to it` : `\`npx ${brand}@latest\` runs it`
|