vibechk 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,2553 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/storage/paths.ts
13
+ import { homedir } from "os";
14
+ import { join } from "path";
15
+ import { mkdirSync, existsSync } from "fs";
16
+ function ensureDir() {
17
+ if (!existsSync(VIBECHK_DIR)) {
18
+ mkdirSync(VIBECHK_DIR, { recursive: true, mode: 448 });
19
+ }
20
+ }
21
+ function dataExists() {
22
+ return existsSync(PROFILE_PATH);
23
+ }
24
+ var VIBECHK_SERVER, VIBECHK_DIR, PROFILE_PATH, STREAK_PATH, ACTIVITY_PATH, BADGES_PATH, LEADERBOARD_CACHE_PATH, FRIENDS_PATH, GIST_TOKEN_PATH;
25
+ var init_paths = __esm({
26
+ "src/storage/paths.ts"() {
27
+ "use strict";
28
+ VIBECHK_SERVER = process.env.VIBECHK_SERVER ? process.env.VIBECHK_SERVER.replace(/\/$/, "") : null;
29
+ VIBECHK_DIR = join(homedir(), ".vibechk");
30
+ PROFILE_PATH = join(VIBECHK_DIR, "profile.json");
31
+ STREAK_PATH = join(VIBECHK_DIR, "streak.json");
32
+ ACTIVITY_PATH = join(VIBECHK_DIR, "activity.jsonl");
33
+ BADGES_PATH = join(VIBECHK_DIR, "badges.json");
34
+ LEADERBOARD_CACHE_PATH = join(VIBECHK_DIR, "leaderboard.json");
35
+ FRIENDS_PATH = join(VIBECHK_DIR, "friends.json");
36
+ GIST_TOKEN_PATH = join(VIBECHK_DIR, "gist-token");
37
+ }
38
+ });
39
+
40
+ // src/storage/atomic.ts
41
+ import { writeFileSync, readFileSync, existsSync as existsSync2, renameSync, appendFileSync } from "fs";
42
+ import { randomBytes } from "crypto";
43
+ function writeJson(filePath, data) {
44
+ const tmp = filePath + ".tmp." + randomBytes(6).toString("hex");
45
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
46
+ renameSync(tmp, filePath);
47
+ }
48
+ function readJson(filePath) {
49
+ if (!existsSync2(filePath)) return null;
50
+ try {
51
+ return JSON.parse(readFileSync(filePath, "utf8"));
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ var init_atomic = __esm({
57
+ "src/storage/atomic.ts"() {
58
+ "use strict";
59
+ }
60
+ });
61
+
62
+ // src/storage/profile-store.ts
63
+ var profile_store_exports = {};
64
+ __export(profile_store_exports, {
65
+ loadProfile: () => loadProfile,
66
+ requireProfile: () => requireProfile,
67
+ saveProfile: () => saveProfile
68
+ });
69
+ function loadProfile() {
70
+ return readJson(PROFILE_PATH);
71
+ }
72
+ function saveProfile(profile) {
73
+ ensureDir();
74
+ writeJson(PROFILE_PATH, profile);
75
+ }
76
+ function requireProfile() {
77
+ const profile = loadProfile();
78
+ if (!profile) {
79
+ throw new Error(
80
+ "No vibechk profile found. Run `vibechk init` to get started."
81
+ );
82
+ }
83
+ return profile;
84
+ }
85
+ var init_profile_store = __esm({
86
+ "src/storage/profile-store.ts"() {
87
+ "use strict";
88
+ init_atomic();
89
+ init_paths();
90
+ }
91
+ });
92
+
93
+ // src/storage/streak-store.ts
94
+ function loadStreak() {
95
+ return readJson(STREAK_PATH) ?? { ...DEFAULT_STREAK };
96
+ }
97
+ function saveStreak(streak) {
98
+ ensureDir();
99
+ writeJson(STREAK_PATH, streak);
100
+ }
101
+ var DEFAULT_STREAK;
102
+ var init_streak_store = __esm({
103
+ "src/storage/streak-store.ts"() {
104
+ "use strict";
105
+ init_atomic();
106
+ init_paths();
107
+ DEFAULT_STREAK = {
108
+ currentStreak: 0,
109
+ longestStreak: 0,
110
+ lastActivityDate: null,
111
+ lastCheckInAt: null,
112
+ freezeTokens: 2,
113
+ totalCheckIns: 0,
114
+ graceUsedAt: null,
115
+ status: "new"
116
+ };
117
+ }
118
+ });
119
+
120
+ // src/core/date-utils.ts
121
+ import dayjs from "dayjs";
122
+ import utc from "dayjs/plugin/utc.js";
123
+ import timezone from "dayjs/plugin/timezone.js";
124
+ function todayInTz(tz) {
125
+ return dayjs().tz(tz).format("YYYY-MM-DD");
126
+ }
127
+ function dateInTz(isoTimestamp, tz) {
128
+ return dayjs(isoTimestamp).tz(tz).format("YYYY-MM-DD");
129
+ }
130
+ function daysBetween(a, b) {
131
+ const da = dayjs(a, "YYYY-MM-DD");
132
+ const db = dayjs(b, "YYYY-MM-DD");
133
+ return db.diff(da, "day");
134
+ }
135
+ function secondsUntilMidnight(tz) {
136
+ const now = dayjs().tz(tz);
137
+ const midnight = now.endOf("day");
138
+ return midnight.diff(now, "second");
139
+ }
140
+ function formatCountdown(seconds) {
141
+ const h = Math.floor(seconds / 3600);
142
+ const m = Math.floor(seconds % 3600 / 60);
143
+ if (h > 0) return `${h}h ${m}m`;
144
+ return `${m}m`;
145
+ }
146
+ function nowIso() {
147
+ return (/* @__PURE__ */ new Date()).toISOString();
148
+ }
149
+ function friendlyDate(date) {
150
+ return dayjs(date, "YYYY-MM-DD").format("MMMM D, YYYY");
151
+ }
152
+ function systemTimezone() {
153
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
154
+ }
155
+ var init_date_utils = __esm({
156
+ "src/core/date-utils.ts"() {
157
+ "use strict";
158
+ dayjs.extend(utc);
159
+ dayjs.extend(timezone);
160
+ }
161
+ });
162
+
163
+ // src/storage/activity-store.ts
164
+ import { appendFileSync as appendFileSync2, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
165
+ function appendActivity(entry) {
166
+ ensureDir();
167
+ const line = JSON.stringify(entry) + "\n";
168
+ appendFileSync2(ACTIVITY_PATH, line, { encoding: "utf8", mode: 384 });
169
+ }
170
+ function loadActivity() {
171
+ if (!existsSync4(ACTIVITY_PATH)) return [];
172
+ try {
173
+ const content = readFileSync3(ACTIVITY_PATH, "utf8");
174
+ return content.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+ var init_activity_store = __esm({
180
+ "src/storage/activity-store.ts"() {
181
+ "use strict";
182
+ init_paths();
183
+ }
184
+ });
185
+
186
+ // src/storage/badge-store.ts
187
+ function loadBadges() {
188
+ return readJson(BADGES_PATH) ?? { ...DEFAULT_BADGES };
189
+ }
190
+ function saveBadges(badges) {
191
+ ensureDir();
192
+ writeJson(BADGES_PATH, badges);
193
+ }
194
+ function appendBadges(newBadges) {
195
+ if (newBadges.length === 0) return;
196
+ const current = loadBadges();
197
+ current.earned.push(...newBadges);
198
+ saveBadges(current);
199
+ }
200
+ var DEFAULT_BADGES;
201
+ var init_badge_store = __esm({
202
+ "src/storage/badge-store.ts"() {
203
+ "use strict";
204
+ init_atomic();
205
+ init_paths();
206
+ DEFAULT_BADGES = { earned: [] };
207
+ }
208
+ });
209
+
210
+ // src/core/milestone-checker.ts
211
+ function getAllMilestones() {
212
+ return MILESTONES;
213
+ }
214
+ function checkNewMilestones(newStreak, badges) {
215
+ const all = getAllMilestones();
216
+ const earnedIds = new Set(badges.earned.map((b) => b.milestoneId));
217
+ return all.filter((m) => m.streakRequired <= newStreak && !earnedIds.has(m.id));
218
+ }
219
+ function createEarnedBadges(milestones, streak, nowIso2) {
220
+ return milestones.map((m) => ({
221
+ milestoneId: m.id,
222
+ earnedAt: nowIso2,
223
+ streakAtEarning: streak
224
+ }));
225
+ }
226
+ function totalFreezeReward(milestones) {
227
+ return milestones.reduce((sum, m) => sum + m.freezeTokenReward, 0);
228
+ }
229
+ var MILESTONES;
230
+ var init_milestone_checker = __esm({
231
+ "src/core/milestone-checker.ts"() {
232
+ "use strict";
233
+ MILESTONES = [
234
+ { id: "streak_3", name: "Warming Up", description: "3 days of vibe coding", streakRequired: 3, icon: "\u{1F331}", rarity: "common", freezeTokenReward: 0 },
235
+ { id: "streak_7", name: "Week Warrior", description: "7 days straight \u2014 that's a real habit", streakRequired: 7, icon: "\u26A1", rarity: "common", freezeTokenReward: 0 },
236
+ { id: "streak_14", name: "Fortnight Coder", description: "Two weeks of consistent vibe coding", streakRequired: 14, icon: "\u{1F680}", rarity: "common", freezeTokenReward: 0 },
237
+ { id: "streak_30", name: "Monthly Builder", description: "A full month of showing up every day", streakRequired: 30, icon: "\u{1F3C6}", rarity: "rare", freezeTokenReward: 1 },
238
+ { id: "streak_60", name: "Two Month Grind", description: "60 days \u2014 you're not messing around", streakRequired: 60, icon: "\u{1F48E}", rarity: "rare", freezeTokenReward: 1 },
239
+ { id: "streak_90", name: "Quarter Strong", description: "Three months of daily vibe coding", streakRequired: 90, icon: "\u{1F525}", rarity: "epic", freezeTokenReward: 1 },
240
+ { id: "streak_100", name: "Triple Digits", description: "100 days. This is who you are.", streakRequired: 100, icon: "\u{1F4AF}", rarity: "epic", freezeTokenReward: 0 },
241
+ { id: "streak_180", name: "Half Year Vibe", description: "Six months of consistent AI-assisted coding", streakRequired: 180, icon: "\u{1F31F}", rarity: "legendary", freezeTokenReward: 0 },
242
+ { id: "streak_365", name: "Year of the Vibe", description: "365 days. Legendary status achieved.", streakRequired: 365, icon: "\u{1F451}", rarity: "legendary", freezeTokenReward: 0 }
243
+ ];
244
+ }
245
+ });
246
+
247
+ // src/storage/friends-store.ts
248
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "fs";
249
+ function getPublishEndpoint() {
250
+ if (VIBECHK_SERVER) return VIBECHK_SERVER;
251
+ return loadFriends().publishEndpoint ?? null;
252
+ }
253
+ function loadFriends() {
254
+ if (!existsSync7(FRIENDS_PATH)) return { ...DEFAULT };
255
+ try {
256
+ return JSON.parse(readFileSync5(FRIENDS_PATH, "utf8"));
257
+ } catch {
258
+ return { ...DEFAULT };
259
+ }
260
+ }
261
+ function saveFriends(data) {
262
+ ensureDir();
263
+ writeFileSync3(FRIENDS_PATH, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
264
+ }
265
+ function getFriendByAlias(alias) {
266
+ const { friends } = loadFriends();
267
+ return friends.find((f) => f.alias.toLowerCase() === alias.toLowerCase()) ?? null;
268
+ }
269
+ function loadGistToken() {
270
+ if (!existsSync7(GIST_TOKEN_PATH)) return null;
271
+ try {
272
+ const t = readFileSync5(GIST_TOKEN_PATH, "utf8").trim();
273
+ return t || null;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ function saveGistToken(token) {
279
+ ensureDir();
280
+ writeFileSync3(GIST_TOKEN_PATH, token, { encoding: "utf8", mode: 384 });
281
+ }
282
+ var DEFAULT;
283
+ var init_friends_store = __esm({
284
+ "src/storage/friends-store.ts"() {
285
+ "use strict";
286
+ init_paths();
287
+ DEFAULT = {
288
+ version: 1,
289
+ friends: [],
290
+ myPublishUrl: null,
291
+ gistId: null,
292
+ publishEndpoint: null
293
+ };
294
+ }
295
+ });
296
+
297
+ // src/commands/publish.ts
298
+ var publish_exports = {};
299
+ __export(publish_exports, {
300
+ buildPublicProfile: () => buildPublicProfile,
301
+ runPublish: () => runPublish
302
+ });
303
+ import chalk4 from "chalk";
304
+ import { input as input2 } from "@inquirer/prompts";
305
+ function buildPublicProfile(username, currentStreak, longestStreak, lastActiveDate, totalCheckIns, badges, activeLast30) {
306
+ return {
307
+ version: 1,
308
+ username,
309
+ currentStreak,
310
+ longestStreak,
311
+ lastActiveDate,
312
+ consistencyLast30: Math.round(activeLast30 / 30 * 100),
313
+ totalCheckIns,
314
+ badges,
315
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString()
316
+ };
317
+ }
318
+ async function runPublish(options = {}) {
319
+ const profile = requireProfile();
320
+ const streak = loadStreak();
321
+ const activity = loadActivity();
322
+ const badges = loadBadges();
323
+ const today = todayInTz(profile.timezone);
324
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
325
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
326
+ const cutoff = thirtyDaysAgo.toISOString().slice(0, 10);
327
+ const activeLast30 = activity.filter((a) => a.date >= cutoff && a.date <= today).length;
328
+ const payload = buildPublicProfile(
329
+ profile.username,
330
+ streak.currentStreak,
331
+ streak.longestStreak,
332
+ streak.lastActivityDate,
333
+ streak.totalCheckIns,
334
+ badges.earned.map((b) => b.milestoneId),
335
+ activeLast30
336
+ );
337
+ const json = JSON.stringify(payload, null, 2);
338
+ if (options.stdout) {
339
+ console.log(json);
340
+ return null;
341
+ }
342
+ if (options.gist) {
343
+ return publishToGist(profile, json, options);
344
+ }
345
+ const endpoint = options.endpoint ? options.endpoint.replace(/\/$/, "") : getPublishEndpoint();
346
+ if (endpoint) {
347
+ return publishToEndpoint(profile, json, endpoint, options.silent ?? false);
348
+ }
349
+ return publishToGist(profile, json, options);
350
+ }
351
+ async function publishToEndpoint(profile, json, endpoint, silent) {
352
+ try {
353
+ if (!silent) process.stdout.write(chalk4.dim(` Publishing to ${endpoint}...`));
354
+ const res = await fetch(`${endpoint}/api/users/${profile.id}`, {
355
+ method: "PUT",
356
+ headers: {
357
+ "Content-Type": "application/json",
358
+ Authorization: `Bearer ${profile.id}`,
359
+ "User-Agent": "vibechk/0.1.0"
360
+ },
361
+ body: json,
362
+ signal: AbortSignal.timeout(1e4)
363
+ });
364
+ if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
365
+ const data = await res.json();
366
+ const friends = loadFriends();
367
+ friends.myPublishUrl = data.jsonUrl;
368
+ friends.publishEndpoint = endpoint;
369
+ saveFriends(friends);
370
+ if (!silent) {
371
+ console.log(chalk4.green(" \u2713"));
372
+ console.log("");
373
+ console.log(` ${chalk4.bold("Your profile:")} ${chalk4.cyan(data.url)}`);
374
+ console.log("");
375
+ console.log(chalk4.dim(" Friends on the same server can add you by username:"));
376
+ console.log(chalk4.dim(` vibechk friend add ${profile.username}`));
377
+ console.log("");
378
+ console.log(chalk4.dim(" Or share your direct URL:"));
379
+ console.log(chalk4.dim(` vibechk friend add ${profile.username} ${data.jsonUrl}`));
380
+ console.log("");
381
+ }
382
+ return data.jsonUrl;
383
+ } catch (err) {
384
+ if (!silent) {
385
+ console.log(chalk4.red(` \u2717 ${err.message}`));
386
+ console.log(chalk4.dim("\n Check that the endpoint is reachable and supports the vibechk API."));
387
+ console.log(chalk4.dim(" Use --gist to publish via GitHub Gist instead."));
388
+ }
389
+ return null;
390
+ }
391
+ }
392
+ async function publishToGist(profile, json, options) {
393
+ let token = options.token ?? loadGistToken();
394
+ if (!token) {
395
+ if (options.silent) return null;
396
+ console.log("");
397
+ console.log(chalk4.bold(" Publish your streak via GitHub Gist"));
398
+ console.log("");
399
+ console.log(chalk4.dim(" This creates a public JSON file that friends can subscribe to."));
400
+ console.log(chalk4.dim(" You need a GitHub personal access token with the `gist` scope."));
401
+ console.log("");
402
+ console.log(chalk4.dim(" 1. Go to https://github.com/settings/tokens/new?scopes=gist"));
403
+ console.log(chalk4.dim(' 2. Create a token with just the "gist" scope'));
404
+ console.log(chalk4.dim(" 3. Paste it below"));
405
+ console.log("");
406
+ token = await input2({ message: "GitHub token (gist scope):" });
407
+ if (!token.trim()) {
408
+ console.log(chalk4.dim(" Skipped. Run `vibechk publish` again when ready."));
409
+ return null;
410
+ }
411
+ saveGistToken(token.trim());
412
+ token = token.trim();
413
+ }
414
+ const friends = loadFriends();
415
+ try {
416
+ if (!options.silent) process.stdout.write(chalk4.dim(" Publishing to Gist..."));
417
+ const { gistId, rawUrl } = friends.gistId ? await updateGist(friends.gistId, json, token) : await createGist(json, token);
418
+ friends.gistId = gistId;
419
+ friends.myPublishUrl = rawUrl;
420
+ saveFriends(friends);
421
+ if (!options.silent) {
422
+ console.log(chalk4.green(" \u2713"));
423
+ console.log("");
424
+ console.log(` ${chalk4.bold("Your friend URL:")} ${chalk4.cyan(rawUrl)}`);
425
+ console.log("");
426
+ console.log(chalk4.dim(" Share this URL with friends so they can follow your streak:"));
427
+ console.log(chalk4.dim(` vibechk friend add ${profile.username} ${rawUrl}`));
428
+ console.log("");
429
+ }
430
+ return rawUrl;
431
+ } catch (err) {
432
+ if (!options.silent) {
433
+ console.log(chalk4.red(` \u2717 ${err.message}`));
434
+ if (err.message.includes("401") || err.message.includes("403")) {
435
+ console.log(chalk4.dim("\n Token may be expired or missing the gist scope."));
436
+ console.log(chalk4.dim(" Delete ~/.vibechk/gist-token and run `vibechk publish` again."));
437
+ }
438
+ }
439
+ return null;
440
+ }
441
+ }
442
+ async function createGist(json, token) {
443
+ const res = await fetch(`${GIST_API}/gists`, {
444
+ method: "POST",
445
+ headers: gistHeaders(token),
446
+ body: JSON.stringify({
447
+ description: "vibechk \u2014 my vibe coding streak",
448
+ public: true,
449
+ files: { [GIST_FILENAME]: { content: json } }
450
+ }),
451
+ signal: AbortSignal.timeout(1e4)
452
+ });
453
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
454
+ const data = await res.json();
455
+ const login = data.owner?.login ?? "me";
456
+ const rawUrl = `https://gist.githubusercontent.com/${login}/${data.id}/raw/${GIST_FILENAME}`;
457
+ return { gistId: data.id, rawUrl, login };
458
+ }
459
+ async function updateGist(gistId, json, token) {
460
+ const res = await fetch(`${GIST_API}/gists/${gistId}`, {
461
+ method: "PATCH",
462
+ headers: gistHeaders(token),
463
+ body: JSON.stringify({
464
+ files: { [GIST_FILENAME]: { content: json } }
465
+ }),
466
+ signal: AbortSignal.timeout(1e4)
467
+ });
468
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
469
+ const data = await res.json();
470
+ const login = data.owner?.login ?? "me";
471
+ const rawUrl = `https://gist.githubusercontent.com/${login}/${data.id}/raw/${GIST_FILENAME}`;
472
+ return { gistId: data.id, rawUrl, login };
473
+ }
474
+ function gistHeaders(token) {
475
+ return {
476
+ Authorization: `Bearer ${token}`,
477
+ Accept: "application/vnd.github+json",
478
+ "Content-Type": "application/json",
479
+ "User-Agent": "vibechk/0.1.0",
480
+ "X-GitHub-Api-Version": "2022-11-28"
481
+ };
482
+ }
483
+ var GIST_API, GIST_FILENAME;
484
+ var init_publish = __esm({
485
+ "src/commands/publish.ts"() {
486
+ "use strict";
487
+ init_profile_store();
488
+ init_streak_store();
489
+ init_activity_store();
490
+ init_badge_store();
491
+ init_friends_store();
492
+ init_date_utils();
493
+ GIST_API = "https://api.github.com";
494
+ GIST_FILENAME = "vibechk.json";
495
+ }
496
+ });
497
+
498
+ // src/commands/friend.ts
499
+ var friend_exports = {};
500
+ __export(friend_exports, {
501
+ runFriendAdd: () => runFriendAdd,
502
+ runFriendList: () => runFriendList,
503
+ runFriendPull: () => runFriendPull,
504
+ runFriendRemove: () => runFriendRemove
505
+ });
506
+ import chalk5 from "chalk";
507
+ async function runFriendAdd(alias, url) {
508
+ const normalized = alias.trim().toLowerCase();
509
+ if (!normalized || !/^[\w\-\.]+$/.test(normalized)) {
510
+ console.error(chalk5.red(" Alias must be letters, numbers, - or _ only."));
511
+ process.exit(1);
512
+ }
513
+ const existing = getFriendByAlias(normalized);
514
+ if (existing) {
515
+ console.log(chalk5.yellow(` Already following "${normalized}" at ${existing.url}`));
516
+ console.log(chalk5.dim(` Use \`vibechk friend remove ${normalized}\` first to replace them.`));
517
+ return;
518
+ }
519
+ let resolvedUrl;
520
+ if (url?.trim()) {
521
+ resolvedUrl = url.trim();
522
+ } else {
523
+ const endpoint = getPublishEndpoint();
524
+ if (endpoint) {
525
+ resolvedUrl = `${endpoint}/u/${normalized}.json`;
526
+ } else {
527
+ console.error(chalk5.red(" No URL provided and no remote endpoint configured."));
528
+ console.log("");
529
+ console.log(chalk5.dim(" Provide the friend's profile URL:"));
530
+ console.log(chalk5.dim(` vibechk friend add ${normalized} https://...`));
531
+ console.log("");
532
+ console.log(chalk5.dim(" Or set a remote endpoint via the VIBECHK_SERVER env var."));
533
+ process.exit(1);
534
+ }
535
+ }
536
+ const data = loadFriends();
537
+ const entry = {
538
+ alias: normalized,
539
+ url: resolvedUrl,
540
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
541
+ lastFetchedAt: null,
542
+ cached: null
543
+ };
544
+ data.friends.push(entry);
545
+ saveFriends(data);
546
+ console.log(chalk5.green(`
547
+ \u2713 Added ${normalized}.`));
548
+ console.log(chalk5.dim(" Fetching their streak now...\n"));
549
+ const result = await fetchOneFriend(entry);
550
+ if (result.cached) {
551
+ printFriendRow(result, todayInTz(requireProfile().timezone), true);
552
+ } else {
553
+ console.log(chalk5.yellow(` Could not fetch their data: ${result.lastFetchError ?? "unknown error"}`));
554
+ console.log(chalk5.dim(" Their data will be retried on the next `vibechk friend pull`."));
555
+ }
556
+ console.log("");
557
+ }
558
+ function runFriendRemove(alias) {
559
+ const normalized = alias.trim().toLowerCase();
560
+ const data = loadFriends();
561
+ const idx = data.friends.findIndex((f) => f.alias.toLowerCase() === normalized);
562
+ if (idx === -1) {
563
+ console.log(chalk5.yellow(` No friend found with alias "${normalized}".`));
564
+ return;
565
+ }
566
+ data.friends.splice(idx, 1);
567
+ saveFriends(data);
568
+ console.log(chalk5.green(`
569
+ \u2713 Removed ${normalized}.
570
+ `));
571
+ }
572
+ async function runFriendPull(options = {}) {
573
+ const data = loadFriends();
574
+ if (data.friends.length === 0) {
575
+ if (!options.quiet) {
576
+ console.log(chalk5.dim("\n No friends added yet. Run `vibechk friend add <alias> <url>`.\n"));
577
+ }
578
+ return;
579
+ }
580
+ if (!options.quiet) process.stdout.write(chalk5.dim(` Syncing ${data.friends.length} friend(s)...`));
581
+ const updated = await Promise.all(data.friends.map(fetchOneFriend));
582
+ data.friends = updated;
583
+ saveFriends(data);
584
+ if (!options.quiet) {
585
+ const ok = updated.filter((f) => f.cached !== null).length;
586
+ console.log(chalk5.green(` ${ok}/${updated.length} updated.
587
+ `));
588
+ }
589
+ }
590
+ async function runFriendList() {
591
+ const profile = requireProfile();
592
+ const myStreak = loadStreak();
593
+ const today = todayInTz(profile.timezone);
594
+ let data = loadFriends();
595
+ if (data.friends.length === 0) {
596
+ console.log("");
597
+ console.log(chalk5.dim(" No friends yet.\n"));
598
+ console.log(" Share your streak URL with friends and add theirs:");
599
+ if (data.myPublishUrl) {
600
+ console.log("");
601
+ console.log(` Your URL: ${chalk5.cyan(data.myPublishUrl)}`);
602
+ } else {
603
+ console.log(chalk5.dim(" Run `vibechk publish` to generate your shareable URL first."));
604
+ }
605
+ console.log(chalk5.dim("\n vibechk friend add <alias> <their-url>\n"));
606
+ return;
607
+ }
608
+ const needsRefresh = data.friends.some((f) => isStale(f) || !f.lastFetchedAt);
609
+ if (needsRefresh) {
610
+ process.stdout.write(chalk5.dim(` Refreshing ${data.friends.length} friend(s)...`));
611
+ const updated = await Promise.all(data.friends.map(fetchOneFriend));
612
+ data.friends = updated;
613
+ saveFriends(data);
614
+ const ok = updated.filter((f) => f.cached !== null).length;
615
+ console.log(chalk5.green(` ${ok}/${updated.length} updated.`));
616
+ }
617
+ console.log("");
618
+ const sorted = [...data.friends].sort((a, b) => {
619
+ const aToday = a.cached?.lastActiveDate === today ? 1 : 0;
620
+ const bToday = b.cached?.lastActiveDate === today ? 1 : 0;
621
+ if (bToday !== aToday) return bToday - aToday;
622
+ return (b.cached?.currentStreak ?? -1) - (a.cached?.currentStreak ?? -1);
623
+ });
624
+ const COL = { alias: 14, streak: 9, today: 8, consistency: 8, best: 7 };
625
+ const header = chalk5.dim(padR(" name", COL.alias)) + chalk5.dim(padR("streak", COL.streak)) + chalk5.dim(padR("today", COL.today)) + chalk5.dim(padR("30d%", COL.consistency)) + chalk5.dim("best");
626
+ console.log(header);
627
+ console.log(chalk5.dim(" " + "\u2500".repeat(48)));
628
+ for (const friend of sorted) {
629
+ printFriendRow(friend, today, false);
630
+ }
631
+ console.log(chalk5.dim(" " + "\u2500".repeat(48)));
632
+ const myCheckedIn = myStreak.lastActivityDate === today;
633
+ const myEmoji = myStreak.currentStreak >= 30 ? "\u{1F3C6}" : myStreak.currentStreak > 0 ? "\u{1F525}" : "\u{1F331}";
634
+ const myTodayLabel = myCheckedIn ? chalk5.green("\u2713") : chalk5.yellow("\u25CB");
635
+ console.log(
636
+ padR(` ${chalk5.bold("you")}`, COL.alias) + padR(`${myEmoji} ${myStreak.currentStreak}d`, COL.streak) + padR(myTodayLabel, COL.today) + padR(chalk5.dim("\u2014"), COL.consistency) + chalk5.dim(`${myStreak.longestStreak}d`)
637
+ );
638
+ console.log("");
639
+ if (data.friends.some((f) => f.lastFetchedAt)) {
640
+ const oldest = data.friends.filter((f) => f.lastFetchedAt).sort((a, b) => a.lastFetchedAt.localeCompare(b.lastFetchedAt)).at(0);
641
+ const age = Math.round((Date.now() - new Date(oldest.lastFetchedAt).getTime()) / 6e4);
642
+ console.log(chalk5.dim(` Last synced: ${age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`}`));
643
+ }
644
+ if (data.myPublishUrl) {
645
+ console.log(chalk5.dim(` Your URL: ${data.myPublishUrl}`));
646
+ } else {
647
+ console.log(chalk5.dim(" Run `vibechk publish` so friends can follow you."));
648
+ }
649
+ console.log("");
650
+ }
651
+ async function fetchOneFriend(entry) {
652
+ try {
653
+ const res = await fetch(entry.url, {
654
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
655
+ headers: { "User-Agent": "vibechk/0.1.0", Accept: "application/json" }
656
+ });
657
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
658
+ const json = await res.json();
659
+ if (typeof json.username !== "string" || typeof json.currentStreak !== "number") {
660
+ throw new Error("Invalid profile format");
661
+ }
662
+ return {
663
+ ...entry,
664
+ lastFetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
665
+ lastFetchError: void 0,
666
+ cached: json
667
+ };
668
+ } catch (err) {
669
+ return {
670
+ ...entry,
671
+ lastFetchError: err.message ?? "fetch failed"
672
+ // preserve old cached data — stale is better than empty
673
+ };
674
+ }
675
+ }
676
+ function printFriendRow(friend, today, verbose) {
677
+ const c = friend.cached;
678
+ const COL = { alias: 14, streak: 9, today: 8, consistency: 8 };
679
+ if (!c) {
680
+ const errLabel = friend.lastFetchError ? chalk5.red(`\u26A0 ${friend.lastFetchError.slice(0, 30)}`) : chalk5.dim("fetching...");
681
+ console.log(padR(` ${chalk5.bold(friend.alias)}`, COL.alias) + errLabel);
682
+ return;
683
+ }
684
+ const streakEmoji = c.currentStreak >= 30 ? "\u{1F3C6}" : c.currentStreak > 0 ? "\u{1F525}" : "\u{1F331}";
685
+ const checkedInToday = c.lastActiveDate === today;
686
+ const todayLabel = checkedInToday ? chalk5.green("\u2713") : chalk5.dim("\u25CB");
687
+ const consistencyColor = c.consistencyLast30 >= 80 ? chalk5.green : c.consistencyLast30 >= 50 ? chalk5.yellow : chalk5.red;
688
+ const prevNote = c.longestStreak > c.currentStreak && c.longestStreak > 0 ? chalk5.dim(` prev ${c.longestStreak}d`) : "";
689
+ const staleNote = isStale(friend) ? chalk5.yellow(" \u26A0") : "";
690
+ const theirName = c.username !== friend.alias ? chalk5.dim(` (${c.username})`) : "";
691
+ if (verbose) {
692
+ console.log(` ${streakEmoji} ${chalk5.bold(friend.alias)}${theirName}: ${chalk5.yellow.bold(c.currentStreak + " days")}`);
693
+ console.log(` Today: ${checkedInToday ? chalk5.green("checked in \u2713") : chalk5.dim("not yet \u25CB")}`);
694
+ console.log(` 30-day consistency: ${consistencyColor(c.consistencyLast30 + "%")}`);
695
+ console.log(` Best streak: ${c.longestStreak}d Check-ins: ${c.totalCheckIns}`);
696
+ const milestones = getAllMilestones();
697
+ const earned = c.badges.map((id) => milestones.find((m) => m.id === id)?.icon ?? "").filter(Boolean);
698
+ if (earned.length) console.log(` Badges: ${earned.join(" ")}`);
699
+ } else {
700
+ console.log(
701
+ padR(` ${chalk5.bold(friend.alias)}${theirName}`, COL.alias) + padR(`${streakEmoji} ${c.currentStreak}d${prevNote}`, COL.streak) + padR(todayLabel, COL.today) + padR(consistencyColor(c.consistencyLast30 + "%"), COL.consistency) + chalk5.dim(c.longestStreak + "d") + staleNote
702
+ );
703
+ }
704
+ }
705
+ function isStale(friend) {
706
+ if (!friend.lastFetchedAt) return false;
707
+ const ageMs = Date.now() - new Date(friend.lastFetchedAt).getTime();
708
+ return ageMs > STALE_HOURS * 3600 * 1e3;
709
+ }
710
+ function padR(s, width) {
711
+ const plainLen = s.replace(/\x1B\[[0-9;]*m/g, "").length;
712
+ const pad = Math.max(0, width - plainLen);
713
+ return s + " ".repeat(pad);
714
+ }
715
+ var FETCH_TIMEOUT_MS, STALE_HOURS;
716
+ var init_friend = __esm({
717
+ "src/commands/friend.ts"() {
718
+ "use strict";
719
+ init_friends_store();
720
+ init_profile_store();
721
+ init_streak_store();
722
+ init_date_utils();
723
+ init_milestone_checker();
724
+ FETCH_TIMEOUT_MS = 8e3;
725
+ STALE_HOURS = 25;
726
+ }
727
+ });
728
+
729
+ // src/cli.ts
730
+ init_paths();
731
+ import { Command } from "commander";
732
+ import chalk14 from "chalk";
733
+
734
+ // src/commands/init.ts
735
+ init_profile_store();
736
+ init_streak_store();
737
+ init_streak_store();
738
+ init_date_utils();
739
+ import { input, confirm } from "@inquirer/prompts";
740
+ import chalk2 from "chalk";
741
+ import { v4 as uuidv4 } from "uuid";
742
+
743
+ // src/commands/schedule.ts
744
+ import { execSync } from "child_process";
745
+ import { writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
746
+ import { join as join2 } from "path";
747
+ import { homedir as homedir2 } from "os";
748
+ import chalk from "chalk";
749
+ var LAUNCHD_LABEL = "com.vibechk.daily";
750
+ var LAUNCHD_PLIST_PATH = join2(homedir2(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
751
+ var CRON_MARKER = "# vibechk-daily";
752
+ async function installSchedule(time, profile) {
753
+ const [hour, minute] = parseTime(time);
754
+ const bin = findVibechkBin();
755
+ if (process.platform === "darwin") {
756
+ installLaunchd(bin, hour, minute, profile?.timezone);
757
+ } else {
758
+ installCron(bin, hour, minute, profile?.timezone);
759
+ }
760
+ }
761
+ async function uninstallSchedule() {
762
+ if (process.platform === "darwin") {
763
+ uninstallLaunchd();
764
+ } else {
765
+ uninstallCron();
766
+ }
767
+ }
768
+ function getScheduleStatus() {
769
+ if (process.platform === "darwin") {
770
+ return existsSync3(LAUNCHD_PLIST_PATH) ? `launchd plist: ${LAUNCHD_PLIST_PATH}` : null;
771
+ }
772
+ const crontab = readCrontab();
773
+ return crontab.includes(CRON_MARKER) ? "cron (user crontab)" : null;
774
+ }
775
+ function installLaunchd(bin, hour, minute, timezone3) {
776
+ if (existsSync3(LAUNCHD_PLIST_PATH)) {
777
+ try {
778
+ execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: "pipe" });
779
+ } catch {
780
+ }
781
+ }
782
+ const tzEnv = timezone3 ? `
783
+ <key>EnvironmentVariables</key>
784
+ <dict>
785
+ <key>TZ</key>
786
+ <string>${timezone3}</string>
787
+ </dict>` : "";
788
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
789
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
790
+ <plist version="1.0">
791
+ <dict>
792
+ <key>Label</key>
793
+ <string>${LAUNCHD_LABEL}</string>
794
+
795
+ <key>ProgramArguments</key>
796
+ <array>
797
+ <string>${bin}</string>
798
+ <string>check-in</string>
799
+ <string>--no-interactive</string>
800
+ <string>--quiet</string>
801
+ </array>
802
+
803
+ <key>StartCalendarInterval</key>
804
+ <dict>
805
+ <key>Hour</key>
806
+ <integer>${hour}</integer>
807
+ <key>Minute</key>
808
+ <integer>${minute}</integer>
809
+ </dict>
810
+ ${tzEnv}
811
+ <key>StandardOutPath</key>
812
+ <string>/tmp/vibechk-daily.log</string>
813
+ <key>StandardErrorPath</key>
814
+ <string>/tmp/vibechk-daily.log</string>
815
+
816
+ <!-- Run at load only if the scheduled time was missed while the machine was off -->
817
+ <key>RunAtLoad</key>
818
+ <false/>
819
+ </dict>
820
+ </plist>
821
+ `;
822
+ writeFileSync2(LAUNCHD_PLIST_PATH, plist, { encoding: "utf8", mode: 420 });
823
+ execSync(`launchctl load -w "${LAUNCHD_PLIST_PATH}"`, { stdio: "pipe" });
824
+ }
825
+ function uninstallLaunchd() {
826
+ if (!existsSync3(LAUNCHD_PLIST_PATH)) {
827
+ console.log(chalk.dim(" No launchd job found."));
828
+ return;
829
+ }
830
+ try {
831
+ execSync(`launchctl unload -w "${LAUNCHD_PLIST_PATH}"`, { stdio: "pipe" });
832
+ } catch {
833
+ }
834
+ unlinkSync(LAUNCHD_PLIST_PATH);
835
+ console.log(chalk.green("\u2713 launchd job removed."));
836
+ }
837
+ function installCron(bin, hour, minute, timezone3) {
838
+ const existing = readCrontab();
839
+ const cleaned = existing.split("\n").filter((line) => !line.includes(CRON_MARKER)).join("\n").trim();
840
+ const pathHint = `PATH=/usr/local/bin:/usr/bin:/bin:${join2(homedir2(), ".npm-global/bin")}:${join2(homedir2(), ".local/bin")}`;
841
+ const tzLine = timezone3 ? `TZ=${timezone3}` : "";
842
+ const cronLine = `${minute} ${hour} * * * ${bin} check-in --no-interactive --quiet >> /tmp/vibechk-daily.log 2>&1 ${CRON_MARKER}`;
843
+ const parts = [cleaned, pathHint, tzLine, cronLine].filter(Boolean);
844
+ const newCrontab = parts.join("\n") + "\n";
845
+ writeCrontab(newCrontab);
846
+ }
847
+ function uninstallCron() {
848
+ const existing = readCrontab();
849
+ if (!existing.includes(CRON_MARKER)) {
850
+ console.log(chalk.dim(" No cron job found."));
851
+ return;
852
+ }
853
+ const cleaned = existing.split("\n").filter((line) => !line.includes(CRON_MARKER)).join("\n").trimEnd() + "\n";
854
+ writeCrontab(cleaned);
855
+ console.log(chalk.green("\u2713 Cron job removed."));
856
+ }
857
+ function readCrontab() {
858
+ try {
859
+ return execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
860
+ } catch {
861
+ return "";
862
+ }
863
+ }
864
+ function writeCrontab(content) {
865
+ const tmp = `/tmp/vibechk-crontab-${Date.now()}`;
866
+ writeFileSync2(tmp, content, { encoding: "utf8" });
867
+ try {
868
+ execSync(`crontab "${tmp}"`, { stdio: "pipe" });
869
+ } finally {
870
+ try {
871
+ unlinkSync(tmp);
872
+ } catch {
873
+ }
874
+ }
875
+ }
876
+ function parseTime(time) {
877
+ const match = /^(\d{1,2}):(\d{2})$/.exec(time);
878
+ if (!match) throw new Error(`Invalid time format "${time}". Use HH:MM, e.g. 21:00`);
879
+ const h = parseInt(match[1], 10);
880
+ const m = parseInt(match[2], 10);
881
+ if (h < 0 || h > 23 || m < 0 || m > 59) {
882
+ throw new Error(`Time out of range. Hour must be 0\u201323, minute 0\u201359.`);
883
+ }
884
+ return [h, m];
885
+ }
886
+ function findVibechkBin() {
887
+ try {
888
+ const bin = execSync("which vibechk", { encoding: "utf8", stdio: "pipe" }).trim();
889
+ if (bin) return bin;
890
+ } catch {
891
+ }
892
+ const candidates = [
893
+ "/usr/local/bin/vibechk",
894
+ join2(homedir2(), ".npm-global/bin/vibechk"),
895
+ join2(homedir2(), ".local/bin/vibechk"),
896
+ join2(homedir2(), ".yarn/bin/vibechk")
897
+ ];
898
+ for (const c of candidates) {
899
+ if (existsSync3(c)) return c;
900
+ }
901
+ const nodeExec = process.execPath;
902
+ const distCli = new URL("../cli.js", import.meta.url).pathname;
903
+ return `${nodeExec} ${distCli}`;
904
+ }
905
+ async function runSchedule(options) {
906
+ if (options.status) {
907
+ const status = getScheduleStatus();
908
+ if (status) {
909
+ console.log(chalk.green(`\u2713 Daily auto-check-in is active`));
910
+ console.log(chalk.dim(` ${status}`));
911
+ } else {
912
+ console.log(chalk.yellow(" No daily schedule is installed."));
913
+ console.log(chalk.dim(" Run `vibechk schedule` to set it up."));
914
+ }
915
+ return;
916
+ }
917
+ if (options.remove) {
918
+ await uninstallSchedule();
919
+ return;
920
+ }
921
+ const time = options.time ?? "21:00";
922
+ const { requireProfile: requireProfile2 } = await Promise.resolve().then(() => (init_profile_store(), profile_store_exports));
923
+ const profile = requireProfile2();
924
+ try {
925
+ await installSchedule(time, profile);
926
+ const platform = process.platform === "darwin" ? "launchd" : "cron";
927
+ console.log("");
928
+ console.log(chalk.green(`\u2713 Daily auto-check-in scheduled via ${platform}`));
929
+ console.log(chalk.dim(` vibechk check-in will run at ${time} every day`));
930
+ console.log(chalk.dim(` It detects Claude Code and git commits automatically.`));
931
+ console.log(chalk.dim(` Logs: /tmp/vibechk-daily.log`));
932
+ console.log(chalk.dim(` To remove: vibechk schedule --remove`));
933
+ console.log("");
934
+ } catch (err) {
935
+ console.error(chalk.red("Failed to install schedule:"), err.message);
936
+ process.exit(1);
937
+ }
938
+ }
939
+
940
+ // src/commands/init.ts
941
+ async function runInit() {
942
+ console.log("");
943
+ console.log(chalk2.bold(" Welcome to vibechk! \u{1F525}"));
944
+ console.log(chalk2.dim(" Track your daily vibe coding streaks.\n"));
945
+ const username = await input({
946
+ message: "Choose a username (shown on leaderboard):",
947
+ validate: (v) => {
948
+ if (!v.trim()) return "Username cannot be empty";
949
+ if (v.length > 24) return "Max 24 characters";
950
+ if (!/^[\w\-\.]+$/.test(v)) return "Only letters, numbers, _ - . allowed";
951
+ return true;
952
+ }
953
+ });
954
+ const detectedTz = systemTimezone();
955
+ const tzInput = await input({
956
+ message: "Your timezone (IANA format):",
957
+ default: detectedTz,
958
+ validate: (v) => {
959
+ try {
960
+ Intl.DateTimeFormat(void 0, { timeZone: v });
961
+ return true;
962
+ } catch {
963
+ return `Invalid timezone. Example: America/New_York`;
964
+ }
965
+ }
966
+ });
967
+ const share = await confirm({
968
+ message: "Share your streak on the community leaderboard? (you can change this later)",
969
+ default: true
970
+ });
971
+ const profile = {
972
+ id: uuidv4(),
973
+ username: username.trim(),
974
+ timezone: tzInput,
975
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
976
+ cloudSync: null,
977
+ preferences: {
978
+ shareOnLeaderboard: share,
979
+ notificationsEnabled: false,
980
+ notificationTime: "21:00",
981
+ celebrationLevel: "normal",
982
+ sessionSources: ["claude-code", "git", "manual"],
983
+ weekendsCount: true,
984
+ watchedRepos: []
985
+ }
986
+ };
987
+ saveProfile(profile);
988
+ saveStreak({ ...DEFAULT_STREAK });
989
+ console.log("");
990
+ console.log(chalk2.green(`\u2713 Profile created! Welcome, ${chalk2.bold(profile.username)}.`));
991
+ console.log("");
992
+ const scheduleIt = await confirm({
993
+ message: "Set up automatic daily check-in at 9 PM? (recommended)",
994
+ default: true
995
+ });
996
+ if (scheduleIt) {
997
+ try {
998
+ await installSchedule("21:00", profile);
999
+ console.log(chalk2.green("\u2713 Daily auto-check-in scheduled."));
1000
+ console.log(chalk2.dim(" vibechk will detect your coding sessions automatically each evening."));
1001
+ console.log(chalk2.dim(" Run `vibechk schedule --help` to change the time or disable it."));
1002
+ } catch (err) {
1003
+ console.log(chalk2.yellow(` Could not install schedule automatically: ${err.message}`));
1004
+ console.log(chalk2.dim(" Run `vibechk schedule` manually to set it up."));
1005
+ }
1006
+ } else {
1007
+ console.log(chalk2.dim(" Run `vibechk schedule` any time to enable automatic daily tracking."));
1008
+ }
1009
+ console.log("");
1010
+ console.log(chalk2.dim(` Run ${chalk2.white("vibechk")} to check in your first session.
1011
+ `));
1012
+ }
1013
+
1014
+ // src/commands/check-in.ts
1015
+ init_profile_store();
1016
+ init_streak_store();
1017
+ init_activity_store();
1018
+ init_badge_store();
1019
+ init_badge_store();
1020
+ init_date_utils();
1021
+ import chalk6 from "chalk";
1022
+ import ora from "ora";
1023
+ import { confirm as confirm2 } from "@inquirer/prompts";
1024
+ import { v4 as uuidv42 } from "uuid";
1025
+
1026
+ // src/core/streak-calculator.ts
1027
+ init_date_utils();
1028
+ var GRACE_COOLDOWN_DAYS = 14;
1029
+ var MAX_FREEZE_TOKENS = 5;
1030
+ function calculateStreakImpact(streak, today, useFreeze = false) {
1031
+ const { lastActivityDate, currentStreak, graceUsedAt, freezeTokens } = streak;
1032
+ if (!lastActivityDate) {
1033
+ return { action: "started", newStreak: 1 };
1034
+ }
1035
+ const daysDiff = daysBetween(lastActivityDate, today);
1036
+ if (daysDiff === 0) {
1037
+ return { action: "already_checked_in", newStreak: currentStreak };
1038
+ }
1039
+ if (daysDiff === 1) {
1040
+ return { action: "continued", newStreak: currentStreak + 1 };
1041
+ }
1042
+ if (daysDiff === 2) {
1043
+ const graceAvailable = !graceUsedAt || daysBetween(graceUsedAt, today) >= GRACE_COOLDOWN_DAYS;
1044
+ if (graceAvailable) {
1045
+ return { action: "grace", newStreak: currentStreak + 1, usedGrace: true };
1046
+ }
1047
+ if (useFreeze && freezeTokens > 0) {
1048
+ return { action: "frozen", newStreak: currentStreak + 1, usedFreeze: true };
1049
+ }
1050
+ if (freezeTokens > 0 && !useFreeze) {
1051
+ return {
1052
+ action: "broken",
1053
+ newStreak: 1,
1054
+ previousStreak: currentStreak
1055
+ };
1056
+ }
1057
+ }
1058
+ return {
1059
+ action: "broken",
1060
+ newStreak: 1,
1061
+ previousStreak: currentStreak
1062
+ };
1063
+ }
1064
+ function applyStreakImpact(current, impact, today, nowIso2) {
1065
+ const newStreak = impact.newStreak;
1066
+ const newLongest = Math.max(current.longestStreak, newStreak);
1067
+ let freezeTokens = current.freezeTokens;
1068
+ if (impact.usedFreeze) {
1069
+ freezeTokens = Math.max(0, freezeTokens - 1);
1070
+ }
1071
+ let graceUsedAt = current.graceUsedAt;
1072
+ if (impact.usedGrace) {
1073
+ graceUsedAt = today;
1074
+ }
1075
+ let status = "active";
1076
+ if (impact.action === "frozen") status = "frozen";
1077
+ else if (impact.action === "grace") status = "grace";
1078
+ else if (impact.action === "broken") status = "broken";
1079
+ else if (impact.action === "started" || impact.action === "continued" || impact.action === "already_checked_in") status = "active";
1080
+ return {
1081
+ ...current,
1082
+ currentStreak: newStreak,
1083
+ longestStreak: newLongest,
1084
+ lastActivityDate: impact.action === "already_checked_in" ? current.lastActivityDate : today,
1085
+ lastCheckInAt: nowIso2,
1086
+ freezeTokens,
1087
+ totalCheckIns: impact.action === "already_checked_in" ? current.totalCheckIns : current.totalCheckIns + 1,
1088
+ graceUsedAt,
1089
+ status
1090
+ };
1091
+ }
1092
+ function addFreezeTokens(current, toAdd) {
1093
+ return Math.min(MAX_FREEZE_TOKENS, current + toAdd);
1094
+ }
1095
+
1096
+ // src/commands/check-in.ts
1097
+ init_milestone_checker();
1098
+
1099
+ // src/detection/claude-code.ts
1100
+ init_date_utils();
1101
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
1102
+ import { join as join3 } from "path";
1103
+ import { homedir as homedir3 } from "os";
1104
+ import { execSync as execSync2 } from "child_process";
1105
+ var CLAUDE_PROJECTS_DIR = join3(homedir3(), ".config", "claude", "projects");
1106
+ async function detectClaudeCodeUsage(date, timezone3) {
1107
+ const ccusageResult = tryCcusageCli(date);
1108
+ if (ccusageResult) return ccusageResult;
1109
+ return scanJsonlFiles(date, timezone3);
1110
+ }
1111
+ function tryCcusageCli(date) {
1112
+ try {
1113
+ const output = execSync2(`ccusage daily --date ${date} --json 2>/dev/null`, {
1114
+ timeout: 5e3,
1115
+ encoding: "utf8"
1116
+ });
1117
+ const data = JSON.parse(output);
1118
+ const entries = Array.isArray(data) ? data : [data];
1119
+ const total = entries.reduce((sum, e) => sum + (e.totalTokens || e.tokens || 0), 0);
1120
+ if (total > 0) {
1121
+ return {
1122
+ detected: true,
1123
+ source: "ccusage-cli",
1124
+ agentData: {
1125
+ sessions: entries.length,
1126
+ tokensApprox: total,
1127
+ models: [...new Set(entries.map((e) => e.model).filter(Boolean))]
1128
+ }
1129
+ };
1130
+ }
1131
+ return { detected: false, source: "ccusage-cli" };
1132
+ } catch {
1133
+ return null;
1134
+ }
1135
+ }
1136
+ function scanJsonlFiles(date, timezone3) {
1137
+ if (!existsSync5(CLAUDE_PROJECTS_DIR)) {
1138
+ return { detected: false, source: "none" };
1139
+ }
1140
+ let sessionCount = 0;
1141
+ let tokenCount = 0;
1142
+ try {
1143
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR);
1144
+ for (const project of projectDirs) {
1145
+ const projectPath = join3(CLAUDE_PROJECTS_DIR, project);
1146
+ if (!statSync(projectPath).isDirectory()) continue;
1147
+ const files = readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
1148
+ for (const file of files) {
1149
+ const filePath = join3(projectPath, file);
1150
+ const content = readFileSync4(filePath, "utf8");
1151
+ const lines = content.split("\n").filter((l) => l.trim());
1152
+ let fileHasActivity = false;
1153
+ for (const line of lines) {
1154
+ try {
1155
+ const entry = JSON.parse(line);
1156
+ const ts = entry.timestamp || entry.createdAt || entry.created_at;
1157
+ if (!ts) continue;
1158
+ const entryDate = dateInTz(ts, timezone3);
1159
+ if (entryDate === date) {
1160
+ fileHasActivity = true;
1161
+ if (typeof entry.usage?.input_tokens === "number") {
1162
+ tokenCount += entry.usage.input_tokens + (entry.usage.output_tokens || 0);
1163
+ } else if (typeof entry.content === "string") {
1164
+ tokenCount += Math.floor(entry.content.length / 4);
1165
+ }
1166
+ }
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ if (fileHasActivity) sessionCount++;
1171
+ }
1172
+ }
1173
+ } catch {
1174
+ return { detected: false, source: "none" };
1175
+ }
1176
+ if (sessionCount > 0) {
1177
+ return {
1178
+ detected: true,
1179
+ source: "jsonl-scan",
1180
+ agentData: { sessions: sessionCount, tokensApprox: tokenCount }
1181
+ };
1182
+ }
1183
+ return { detected: false, source: "jsonl-scan" };
1184
+ }
1185
+
1186
+ // src/detection/git.ts
1187
+ import { execSync as execSync3 } from "child_process";
1188
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1189
+ import { join as join4 } from "path";
1190
+ import { homedir as homedir4 } from "os";
1191
+ async function detectGitActivity(date, timezone3, watchedRepos) {
1192
+ const repoPaths = watchedRepos.length > 0 ? watchedRepos.map(expandHome) : discoverRepos();
1193
+ for (const repoPath of repoPaths) {
1194
+ if (!existsSync6(join4(repoPath, ".git"))) continue;
1195
+ try {
1196
+ const since = `${date} 00:00:00`;
1197
+ const until = `${date} 23:59:59`;
1198
+ const result = execSync3(
1199
+ `git -C ${JSON.stringify(repoPath)} log --oneline --after=${JSON.stringify(since)} --before=${JSON.stringify(until)} 2>/dev/null`,
1200
+ { encoding: "utf8", timeout: 5e3, env: { ...process.env, TZ: timezone3 } }
1201
+ ).trim();
1202
+ if (result.length > 0) {
1203
+ return { detected: true, repoPath };
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ return { detected: false };
1209
+ }
1210
+ function expandHome(p) {
1211
+ if (p.startsWith("~/") || p === "~") {
1212
+ return join4(homedir4(), p.slice(2));
1213
+ }
1214
+ return p;
1215
+ }
1216
+ function discoverRepos() {
1217
+ const candidates = [
1218
+ homedir4(),
1219
+ join4(homedir4(), "code"),
1220
+ join4(homedir4(), "projects"),
1221
+ join4(homedir4(), "dev"),
1222
+ join4(homedir4(), "workspace"),
1223
+ join4(homedir4(), "src")
1224
+ ];
1225
+ const repos = [];
1226
+ for (const dir of candidates) {
1227
+ if (!existsSync6(dir)) continue;
1228
+ if (existsSync6(join4(dir, ".git"))) {
1229
+ repos.push(dir);
1230
+ continue;
1231
+ }
1232
+ try {
1233
+ const entries = readdirSync2(dir);
1234
+ for (const entry of entries) {
1235
+ const full = join4(dir, entry);
1236
+ try {
1237
+ if (statSync2(full).isDirectory() && existsSync6(join4(full, ".git"))) {
1238
+ repos.push(full);
1239
+ }
1240
+ } catch {
1241
+ }
1242
+ }
1243
+ } catch {
1244
+ }
1245
+ if (repos.length >= 50) break;
1246
+ }
1247
+ return repos;
1248
+ }
1249
+
1250
+ // src/detection/index.ts
1251
+ async function detectTodaySession(date, timezone3, enabledSources, watchedRepos = []) {
1252
+ if (enabledSources.includes("claude-code")) {
1253
+ const result = await detectClaudeCodeUsage(date, timezone3);
1254
+ if (result.detected) {
1255
+ return { detected: true, source: "claude-code", agentData: result.agentData };
1256
+ }
1257
+ }
1258
+ if (enabledSources.includes("git")) {
1259
+ const result = await detectGitActivity(date, timezone3, watchedRepos);
1260
+ if (result.detected) {
1261
+ return { detected: true, source: "git" };
1262
+ }
1263
+ }
1264
+ return { detected: false, source: "manual" };
1265
+ }
1266
+
1267
+ // src/sync/client.ts
1268
+ function makeHeaders(apiKey) {
1269
+ return {
1270
+ "Content-Type": "application/json",
1271
+ Authorization: `Bearer ${apiKey}`,
1272
+ "User-Agent": "vibechk/0.1.0"
1273
+ };
1274
+ }
1275
+ function buildSyncPayload(profile, streak, badges) {
1276
+ const totalFreezesUsed = streak.totalCheckIns - streak.currentStreak;
1277
+ return {
1278
+ userId: profile.id,
1279
+ username: profile.username,
1280
+ currentStreak: streak.currentStreak,
1281
+ longestStreak: streak.longestStreak,
1282
+ lastActiveDate: streak.lastActivityDate,
1283
+ freezesUsed: totalFreezesUsed > 0 ? totalFreezesUsed : 0,
1284
+ badges: badges.earned.map((b) => b.milestoneId),
1285
+ shareOnLeaderboard: profile.preferences.shareOnLeaderboard
1286
+ };
1287
+ }
1288
+ async function pushStreak(profile, streak, badges) {
1289
+ const sync = profile.cloudSync;
1290
+ if (!sync) return false;
1291
+ const payload = buildSyncPayload(profile, streak, badges);
1292
+ try {
1293
+ const response = await fetch(`${sync.endpoint}/users/${profile.id}`, {
1294
+ method: "PUT",
1295
+ headers: makeHeaders(sync.apiKey),
1296
+ body: JSON.stringify(payload),
1297
+ signal: AbortSignal.timeout(8e3)
1298
+ });
1299
+ return response.ok;
1300
+ } catch {
1301
+ return false;
1302
+ }
1303
+ }
1304
+ async function fetchLeaderboard(profile) {
1305
+ const sync = profile.cloudSync;
1306
+ if (!sync) return null;
1307
+ try {
1308
+ const response = await fetch(`${sync.endpoint}/leaderboard`, {
1309
+ headers: makeHeaders(sync.apiKey),
1310
+ signal: AbortSignal.timeout(8e3)
1311
+ });
1312
+ if (!response.ok) return null;
1313
+ const data = await response.json();
1314
+ const entries = data.entries ?? [];
1315
+ const sorted = [...entries].sort((a, b) => b.currentStreak - a.currentStreak);
1316
+ sorted.forEach((e, i) => {
1317
+ e.rank = i + 1;
1318
+ e.isYou = e.userId === profile.id;
1319
+ });
1320
+ return {
1321
+ entries: sorted,
1322
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
1323
+ source: sync.endpoint,
1324
+ weekStart: data.weekStart ?? ""
1325
+ };
1326
+ } catch {
1327
+ return null;
1328
+ }
1329
+ }
1330
+
1331
+ // src/ui/renderer.ts
1332
+ init_date_utils();
1333
+ init_milestone_checker();
1334
+ import chalk3 from "chalk";
1335
+ import boxen from "boxen";
1336
+ import dayjs2 from "dayjs";
1337
+ import utc2 from "dayjs/plugin/utc.js";
1338
+ import timezone2 from "dayjs/plugin/timezone.js";
1339
+ dayjs2.extend(utc2);
1340
+ dayjs2.extend(timezone2);
1341
+ function flameBar(current, next) {
1342
+ if (next <= current) return "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2713";
1343
+ const pct = Math.min(current / next, 1);
1344
+ const filled = Math.round(pct * 12);
1345
+ const empty = 12 - filled;
1346
+ return chalk3.red("\u2588".repeat(filled)) + chalk3.gray("\u2591".repeat(empty)) + chalk3.dim(` \u2192 day ${next}`);
1347
+ }
1348
+ function weekCalendar(activity, tz) {
1349
+ const days = ["M", "T", "W", "T", "F", "S", "S"];
1350
+ const today = dayjs2().tz ? dayjs2().tz(tz) : dayjs2();
1351
+ const dayOfWeek = today.day();
1352
+ const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
1353
+ const activityDates = new Set(activity.map((a) => a.date));
1354
+ const row = days.map((label, i) => {
1355
+ const d = today.add(mondayOffset + i, "day");
1356
+ const dateStr = d.format("YYYY-MM-DD");
1357
+ const isToday = dateStr === today.format("YYYY-MM-DD");
1358
+ const isFuture = d.isAfter(today, "day");
1359
+ const hasActivity = activityDates.has(dateStr);
1360
+ let cell;
1361
+ if (isFuture) cell = chalk3.gray("\xB7");
1362
+ else if (hasActivity) cell = chalk3.green("\u2713");
1363
+ else cell = chalk3.red("\u2717");
1364
+ return isToday ? chalk3.bold.underline(`${label}:${cell}`) : `${label}:${cell}`;
1365
+ });
1366
+ return row.join(" ");
1367
+ }
1368
+ function nextMilestone(currentStreak) {
1369
+ const milestones = getAllMilestones();
1370
+ const next = milestones.find((m) => m.streakRequired > currentStreak);
1371
+ if (!next) return null;
1372
+ return { days: next.streakRequired, daysAway: next.streakRequired - currentStreak };
1373
+ }
1374
+ function renderStatus(profile, streak, activity, { quiet = false, json = false } = {}) {
1375
+ if (json) {
1376
+ return JSON.stringify({ streak, profile: { username: profile.username, timezone: profile.timezone } }, null, 2);
1377
+ }
1378
+ const today = todayInTz(profile.timezone);
1379
+ const secsLeft = secondsUntilMidnight(profile.timezone);
1380
+ const isProtected = streak.lastActivityDate === today;
1381
+ const isExpired = streak.status === "broken" && streak._expiredStreak !== void 0;
1382
+ const streakEmoji = isExpired ? "\u{1F331}" : streak.currentStreak >= 100 ? "\u{1F4AF}" : streak.currentStreak >= 30 ? "\u{1F3C6}" : "\u{1F525}";
1383
+ const statusLabel = isExpired ? chalk3.dim(`Last streak: ${streak._expiredStreak}d \u2014 run ${chalk3.white("vibechk")} to start fresh`) : isProtected ? chalk3.green("\u2713 Protected for today") : secsLeft < 3600 ? chalk3.red(`\u26A0 ${formatCountdown(secsLeft)} left to check in!`) : chalk3.yellow(`\u25CB Check in before midnight`);
1384
+ const next = nextMilestone(streak.currentStreak);
1385
+ const progressBar = next ? flameBar(streak.currentStreak, next.days) : chalk3.green("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 All milestones reached!");
1386
+ const freezeDisplay = "\u2744".repeat(streak.freezeTokens) + (streak.freezeTokens === 0 ? chalk3.gray("none") : "");
1387
+ const recentWeek = activity.filter((a) => {
1388
+ const diff = dayjs2(today).diff(dayjs2(a.date), "day");
1389
+ return diff >= 0 && diff < 7;
1390
+ });
1391
+ const thirtyDaysAgo = dayjs2(today).subtract(29, "day").format("YYYY-MM-DD");
1392
+ const activeLast30 = activity.filter((a) => a.date >= thirtyDaysAgo && a.date <= today).length;
1393
+ const consistencyPct = Math.round(activeLast30 / 30 * 100);
1394
+ const consistencyColor = consistencyPct >= 80 ? chalk3.green : consistencyPct >= 50 ? chalk3.yellow : chalk3.red;
1395
+ const consistencyLabel = consistencyColor(`${consistencyPct}%`) + chalk3.dim(` (${activeLast30}/30 days)`);
1396
+ if (quiet) {
1397
+ return `${streak.currentStreak}d streak`;
1398
+ }
1399
+ const lines = [
1400
+ `${streakEmoji} ${chalk3.bold.yellow(streak.currentStreak + "-day streak")} ${statusLabel}`,
1401
+ ``,
1402
+ `Progress ${progressBar}`,
1403
+ `Longest ${chalk3.cyan(streak.longestStreak + "d")} \u2502 Freezes ${freezeDisplay}`,
1404
+ `Consistency ${consistencyLabel}`,
1405
+ ``,
1406
+ weekCalendar(recentWeek, profile.timezone),
1407
+ ``,
1408
+ chalk3.dim(`${friendlyDate(today)} \u2502 ${profile.username}`)
1409
+ ];
1410
+ return boxen(lines.join("\n"), {
1411
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
1412
+ borderStyle: "round",
1413
+ borderColor: isExpired ? "gray" : isProtected ? "green" : streak.currentStreak > 0 ? "yellow" : "gray",
1414
+ title: " vibechk ",
1415
+ titleAlignment: "center"
1416
+ });
1417
+ }
1418
+ function renderCheckIn(action, streak, previousStreak, newMilestones = [], quiet = false) {
1419
+ if (quiet) return "";
1420
+ const lines = [];
1421
+ if (action === "already_checked_in") {
1422
+ lines.push(chalk3.green(`\u2713 Already checked in today \u2014 ${streak}-day streak protected.`));
1423
+ return lines.join("\n");
1424
+ }
1425
+ if (action === "broken") {
1426
+ if (previousStreak && previousStreak > 0) {
1427
+ lines.push(chalk3.cyan(`Your ${previousStreak}-day streak was real.`));
1428
+ lines.push(chalk3.dim(` Every day you showed up counted. That doesn't disappear.`));
1429
+ lines.push(``);
1430
+ }
1431
+ lines.push(chalk3.yellow(`\u{1F331} Day 1. Fresh start.`));
1432
+ return boxen(lines.join("\n"), { padding: 1, borderColor: "cyan", borderStyle: "round" });
1433
+ }
1434
+ const actionEmoji = action === "started" ? "\u{1F331}" : action === "grace" ? "\u2728" : action === "frozen" ? "\u2744\uFE0F" : "\u{1F525}";
1435
+ const actionLabel = action === "started" ? "Day 1! The journey begins." : action === "grace" ? `Grace period used \u2014 streak preserved!` : action === "frozen" ? `Freeze token used \u2014 streak preserved!` : `Day ${streak}! Streak protected.`;
1436
+ lines.push(`${actionEmoji} ${chalk3.bold(actionLabel)}`);
1437
+ lines.push(chalk3.dim(`\u{1F525} ${streak}-day streak`));
1438
+ return lines.join("\n");
1439
+ }
1440
+ function renderMilestone(milestone, streak) {
1441
+ const border = milestone.rarity === "legendary" ? "double" : milestone.rarity === "epic" ? "bold" : "round";
1442
+ const color = milestone.rarity === "legendary" ? "magentaBright" : milestone.rarity === "epic" ? "yellowBright" : "cyan";
1443
+ const lines = [
1444
+ chalk3.bold.yellow("\u2B50 ACHIEVEMENT UNLOCKED!"),
1445
+ ``,
1446
+ `${milestone.icon} ${chalk3.bold(milestone.name)}`,
1447
+ chalk3.dim(milestone.description),
1448
+ ``,
1449
+ chalk3.green(`${streak}-day streak reached!`)
1450
+ ];
1451
+ if (milestone.freezeTokenReward > 0) {
1452
+ lines.push(``);
1453
+ lines.push(chalk3.cyan(`+ ${milestone.freezeTokenReward} freeze token earned!`));
1454
+ }
1455
+ return boxen(lines.join("\n"), {
1456
+ padding: 1,
1457
+ borderStyle: border,
1458
+ borderColor: color,
1459
+ title: ` ${milestone.rarity.toUpperCase()} `,
1460
+ titleAlignment: "center"
1461
+ });
1462
+ }
1463
+
1464
+ // src/web/server.ts
1465
+ init_profile_store();
1466
+ init_streak_store();
1467
+ init_activity_store();
1468
+ init_badge_store();
1469
+ import { createServer } from "http";
1470
+
1471
+ // src/storage/leaderboard-cache.ts
1472
+ init_atomic();
1473
+ init_paths();
1474
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
1475
+ function loadLeaderboardCache() {
1476
+ return readJson(LEADERBOARD_CACHE_PATH);
1477
+ }
1478
+ function saveLeaderboardCache(cache) {
1479
+ ensureDir();
1480
+ writeJson(LEADERBOARD_CACHE_PATH, cache);
1481
+ }
1482
+ function isCacheStale(cache) {
1483
+ const age = Date.now() - new Date(cache.fetchedAt).getTime();
1484
+ return age > CACHE_TTL_MS;
1485
+ }
1486
+
1487
+ // src/web/server.ts
1488
+ init_milestone_checker();
1489
+ init_date_utils();
1490
+ async function startDashboard(autoClose = true) {
1491
+ const profile = requireProfile();
1492
+ const streak = loadStreak();
1493
+ const activity = loadActivity();
1494
+ const badges = loadBadges();
1495
+ const leaderboard = loadLeaderboardCache();
1496
+ const milestones = getAllMilestones();
1497
+ const today = todayInTz(profile.timezone);
1498
+ const dashboardData = {
1499
+ profile: { username: profile.username, timezone: profile.timezone },
1500
+ streak,
1501
+ recentActivity: activity.slice(-90),
1502
+ badges: badges.earned,
1503
+ milestones,
1504
+ leaderboard,
1505
+ today
1506
+ };
1507
+ const html = generateDashboardHtml(dashboardData);
1508
+ const server = createServer((req, res) => {
1509
+ if (req.url === "/data") {
1510
+ res.writeHead(200, { "Content-Type": "application/json" });
1511
+ res.end(JSON.stringify(dashboardData));
1512
+ return;
1513
+ }
1514
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1515
+ res.end(html);
1516
+ if (autoClose) {
1517
+ setTimeout(() => server.close(), 500);
1518
+ }
1519
+ });
1520
+ return new Promise((resolve) => {
1521
+ server.listen(0, "127.0.0.1", () => {
1522
+ const addr = server.address();
1523
+ const url = `http://127.0.0.1:${addr.port}`;
1524
+ resolve(url);
1525
+ });
1526
+ });
1527
+ }
1528
+ function generateDashboardHtml(data) {
1529
+ const { profile, streak, recentActivity, badges, milestones, leaderboard, today } = data;
1530
+ const activityMap = {};
1531
+ for (const a of recentActivity) {
1532
+ activityMap[a.date] = { checked: true, frozen: a.isFrozen, grace: a.isGrace, source: a.source };
1533
+ }
1534
+ const cells = [];
1535
+ for (let i = 89; i >= 0; i--) {
1536
+ const d = /* @__PURE__ */ new Date();
1537
+ d.setDate(d.getDate() - i);
1538
+ const dateStr = d.toISOString().slice(0, 10);
1539
+ const act = activityMap[dateStr];
1540
+ const isToday = dateStr === today;
1541
+ let cls = "day-empty";
1542
+ if (act?.frozen) cls = "day-frozen";
1543
+ else if (act?.grace) cls = "day-grace";
1544
+ else if (act?.checked) cls = "day-active";
1545
+ if (isToday) cls += " day-today";
1546
+ cells.push(`<div class="day ${cls}" title="${dateStr}"></div>`);
1547
+ }
1548
+ const lbRows = leaderboard ? leaderboard.entries.slice(0, 20).map((e) => {
1549
+ const isYou = e.userId === profile.id || e.displayName === profile.username;
1550
+ const badges_icons = (e.badges || []).slice(0, 3).map((id) => milestones.find((m) => m.id === id)?.icon ?? "").join("");
1551
+ return `<tr class="${isYou ? "you" : ""}">
1552
+ <td class="rank">${e.rank ?? "?"}</td>
1553
+ <td class="name">${isYou ? "\u2192 " : ""}${e.displayName}</td>
1554
+ <td class="streak">${e.currentStreak}d</td>
1555
+ <td class="longest">${e.longestStreak}d</td>
1556
+ <td class="badges">${badges_icons}${e.freezesUsed > 0 ? `<span class="freeze-used">${e.freezesUsed}\u2744</span>` : ""}</td>
1557
+ </tr>`;
1558
+ }).join("\n") : '<tr><td colspan="5" class="empty">No leaderboard data \u2014 run <code>vibechk sync</code></td></tr>';
1559
+ const earnedIds = new Set(badges.map((b) => b.milestoneId));
1560
+ const milestoneCards = milestones.map((m) => {
1561
+ const earned = earnedIds.has(m.id);
1562
+ return `<div class="milestone-card ${earned ? "earned" : "locked"} rarity-${m.rarity}">
1563
+ <div class="ms-icon">${earned ? m.icon : "\u{1F512}"}</div>
1564
+ <div class="ms-name">${m.name}</div>
1565
+ <div class="ms-req">${m.streakRequired}d</div>
1566
+ </div>`;
1567
+ }).join("\n");
1568
+ const isCheckedInToday = streak.lastActivityDate === today;
1569
+ const streakEmoji = streak.currentStreak >= 100 ? "\u{1F4AF}" : streak.currentStreak >= 30 ? "\u{1F3C6}" : "\u{1F525}";
1570
+ return `<!DOCTYPE html>
1571
+ <html lang="en">
1572
+ <head>
1573
+ <meta charset="UTF-8">
1574
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1575
+ <title>vibechk \u2014 ${profile.username}</title>
1576
+ <style>
1577
+ :root {
1578
+ --bg: #0d1117;
1579
+ --surface: #161b22;
1580
+ --border: #30363d;
1581
+ --text: #e6edf3;
1582
+ --muted: #8b949e;
1583
+ --accent: #f78166;
1584
+ --green: #3fb950;
1585
+ --yellow: #d29922;
1586
+ --blue: #58a6ff;
1587
+ --frozen: #79c0ff;
1588
+ --grace: #ffa657;
1589
+ --purple: #bc8cff;
1590
+ }
1591
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1592
+ body {
1593
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1594
+ background: var(--bg);
1595
+ color: var(--text);
1596
+ min-height: 100vh;
1597
+ padding: 24px;
1598
+ }
1599
+ h1, h2, h3 { font-weight: 600; }
1600
+ .container { max-width: 900px; margin: 0 auto; }
1601
+ header {
1602
+ display: flex;
1603
+ align-items: center;
1604
+ gap: 12px;
1605
+ margin-bottom: 32px;
1606
+ }
1607
+ header .logo {
1608
+ font-size: 1.5rem;
1609
+ font-weight: 700;
1610
+ background: linear-gradient(135deg, #f78166, #d29922);
1611
+ -webkit-background-clip: text;
1612
+ -webkit-text-fill-color: transparent;
1613
+ }
1614
+ header .username {
1615
+ color: var(--muted);
1616
+ font-size: 0.9rem;
1617
+ }
1618
+
1619
+ /* Streak hero card */
1620
+ .streak-hero {
1621
+ background: var(--surface);
1622
+ border: 1px solid var(--border);
1623
+ border-radius: 12px;
1624
+ padding: 28px 32px;
1625
+ margin-bottom: 24px;
1626
+ display: grid;
1627
+ grid-template-columns: 1fr auto;
1628
+ gap: 16px;
1629
+ align-items: center;
1630
+ }
1631
+ .streak-number {
1632
+ font-size: 4rem;
1633
+ font-weight: 700;
1634
+ line-height: 1;
1635
+ color: var(--accent);
1636
+ }
1637
+ .streak-label {
1638
+ color: var(--muted);
1639
+ font-size: 0.9rem;
1640
+ margin-top: 4px;
1641
+ }
1642
+ .streak-stats {
1643
+ display: flex;
1644
+ gap: 24px;
1645
+ }
1646
+ .stat { text-align: center; }
1647
+ .stat-value { font-size: 1.5rem; font-weight: 600; color: var(--blue); }
1648
+ .stat-label { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
1649
+
1650
+ .status-badge {
1651
+ display: inline-flex;
1652
+ align-items: center;
1653
+ gap: 6px;
1654
+ padding: 4px 12px;
1655
+ border-radius: 20px;
1656
+ font-size: 0.8rem;
1657
+ font-weight: 500;
1658
+ margin-top: 8px;
1659
+ }
1660
+ .status-badge.protected { background: rgba(63, 185, 80, 0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); }
1661
+ .status-badge.at-risk { background: rgba(210, 153, 34, 0.15); color: var(--yellow); border: 1px solid rgba(210,153,34,0.3); }
1662
+ .status-badge.broken { background: rgba(247, 129, 102, 0.15); color: var(--accent); border: 1px solid rgba(247,129,102,0.3); }
1663
+
1664
+ .freeze-tokens {
1665
+ font-size: 1.2rem;
1666
+ margin-top: 8px;
1667
+ }
1668
+
1669
+ /* Progress bar */
1670
+ .progress-section {
1671
+ margin-top: 16px;
1672
+ grid-column: 1 / -1;
1673
+ }
1674
+ .progress-label {
1675
+ font-size: 0.75rem;
1676
+ color: var(--muted);
1677
+ margin-bottom: 6px;
1678
+ }
1679
+ .progress-bar-track {
1680
+ height: 8px;
1681
+ background: var(--border);
1682
+ border-radius: 4px;
1683
+ overflow: hidden;
1684
+ }
1685
+ .progress-bar-fill {
1686
+ height: 100%;
1687
+ border-radius: 4px;
1688
+ background: linear-gradient(90deg, #f78166, #d29922);
1689
+ transition: width 0.6s ease;
1690
+ }
1691
+
1692
+ /* Calendar grid */
1693
+ .section {
1694
+ background: var(--surface);
1695
+ border: 1px solid var(--border);
1696
+ border-radius: 12px;
1697
+ padding: 20px 24px;
1698
+ margin-bottom: 24px;
1699
+ }
1700
+ .section h2 {
1701
+ font-size: 0.85rem;
1702
+ text-transform: uppercase;
1703
+ letter-spacing: 0.08em;
1704
+ color: var(--muted);
1705
+ margin-bottom: 16px;
1706
+ }
1707
+ .calendar-grid {
1708
+ display: grid;
1709
+ grid-template-columns: repeat(13, 1fr);
1710
+ gap: 3px;
1711
+ }
1712
+ .day {
1713
+ aspect-ratio: 1;
1714
+ border-radius: 3px;
1715
+ cursor: default;
1716
+ transition: transform 0.1s;
1717
+ }
1718
+ .day:hover { transform: scale(1.3); }
1719
+ .day-empty { background: var(--border); }
1720
+ .day-active { background: var(--green); }
1721
+ .day-frozen { background: var(--frozen); }
1722
+ .day-grace { background: var(--grace); }
1723
+ .day-today { outline: 2px solid var(--text); outline-offset: 1px; }
1724
+ .calendar-legend {
1725
+ display: flex;
1726
+ gap: 16px;
1727
+ margin-top: 12px;
1728
+ flex-wrap: wrap;
1729
+ }
1730
+ .legend-item {
1731
+ display: flex;
1732
+ align-items: center;
1733
+ gap: 6px;
1734
+ font-size: 0.75rem;
1735
+ color: var(--muted);
1736
+ }
1737
+ .legend-dot {
1738
+ width: 12px;
1739
+ height: 12px;
1740
+ border-radius: 3px;
1741
+ }
1742
+
1743
+ /* Leaderboard */
1744
+ table { width: 100%; border-collapse: collapse; }
1745
+ th, td { padding: 8px 12px; text-align: left; font-size: 0.875rem; }
1746
+ th { color: var(--muted); font-weight: 500; border-bottom: 1px solid var(--border); }
1747
+ tr.you { background: rgba(88, 166, 255, 0.08); }
1748
+ tr.you td { color: var(--blue); font-weight: 600; }
1749
+ td.rank { color: var(--muted); width: 40px; }
1750
+ td.streak { color: var(--accent); font-weight: 600; }
1751
+ td.longest { color: var(--muted); }
1752
+ td.badges { font-size: 1rem; letter-spacing: 2px; }
1753
+ td.empty { color: var(--muted); font-style: italic; padding: 20px 12px; }
1754
+ .freeze-used { font-size: 0.7rem; color: var(--frozen); margin-left: 4px; }
1755
+ tr:hover:not(.you) { background: rgba(255,255,255,0.03); }
1756
+
1757
+ /* Milestones */
1758
+ .milestones-grid {
1759
+ display: grid;
1760
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
1761
+ gap: 12px;
1762
+ }
1763
+ .milestone-card {
1764
+ text-align: center;
1765
+ padding: 16px 8px;
1766
+ border-radius: 10px;
1767
+ border: 1px solid var(--border);
1768
+ transition: transform 0.2s;
1769
+ }
1770
+ .milestone-card:hover { transform: translateY(-2px); }
1771
+ .milestone-card.locked { opacity: 0.35; filter: grayscale(1); }
1772
+ .milestone-card.earned { border-color: transparent; }
1773
+ .milestone-card.rarity-common.earned { background: rgba(63,185,80,0.1); border-color: rgba(63,185,80,0.3); }
1774
+ .milestone-card.rarity-rare.earned { background: rgba(88,166,255,0.1); border-color: rgba(88,166,255,0.3); }
1775
+ .milestone-card.rarity-epic.earned { background: rgba(210,153,34,0.1); border-color: rgba(210,153,34,0.3); }
1776
+ .milestone-card.rarity-legendary.earned { background: rgba(188,140,255,0.1); border-color: rgba(188,140,255,0.3); }
1777
+ .ms-icon { font-size: 1.75rem; margin-bottom: 6px; }
1778
+ .ms-name { font-size: 0.7rem; font-weight: 600; color: var(--text); }
1779
+ .ms-req { font-size: 0.65rem; color: var(--muted); margin-top: 2px; }
1780
+
1781
+ /* Animations */
1782
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
1783
+ .streak-number { animation: ${isCheckedInToday ? "none" : "pulse 2s ease-in-out infinite"}; }
1784
+
1785
+ @keyframes celebrate {
1786
+ 0% { transform: scale(1); }
1787
+ 50% { transform: scale(1.05); }
1788
+ 100% { transform: scale(1); }
1789
+ }
1790
+ .celebrate { animation: celebrate 0.5s ease-in-out; }
1791
+ </style>
1792
+ </head>
1793
+ <body>
1794
+ <div class="container">
1795
+ <header>
1796
+ <div class="logo">vibechk</div>
1797
+ <div class="username">@${profile.username}</div>
1798
+ </header>
1799
+
1800
+ <!-- Streak Hero -->
1801
+ <div class="streak-hero" id="heroCard">
1802
+ <div>
1803
+ <div class="streak-number">${streakEmoji} ${streak.currentStreak}</div>
1804
+ <div class="streak-label">day streak</div>
1805
+ <div class="status-badge ${isCheckedInToday ? "protected" : streak.currentStreak > 0 ? "at-risk" : "broken"}">
1806
+ ${isCheckedInToday ? "\u2713 Protected for today" : streak.currentStreak > 0 ? "\u25CB Check in to protect" : "\u25CF Start your streak"}
1807
+ </div>
1808
+ <div class="freeze-tokens" title="Freeze tokens available">
1809
+ ${"\u2744\uFE0F".repeat(streak.freezeTokens)}${streak.freezeTokens === 0 ? '<span style="color:var(--muted);font-size:0.8rem">No freezes</span>' : ""}
1810
+ </div>
1811
+ </div>
1812
+ <div class="streak-stats">
1813
+ <div class="stat">
1814
+ <div class="stat-value">${streak.longestStreak}d</div>
1815
+ <div class="stat-label">Longest</div>
1816
+ </div>
1817
+ <div class="stat">
1818
+ <div class="stat-value">${streak.totalCheckIns}</div>
1819
+ <div class="stat-label">Total days</div>
1820
+ </div>
1821
+ </div>
1822
+ </div>
1823
+
1824
+ <!-- Activity Calendar -->
1825
+ <div class="section">
1826
+ <h2>Last 90 Days</h2>
1827
+ <div class="calendar-grid">
1828
+ ${cells.join("\n ")}
1829
+ </div>
1830
+ <div class="calendar-legend">
1831
+ <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> Coded</div>
1832
+ <div class="legend-item"><div class="legend-dot" style="background:var(--frozen)"></div> Frozen</div>
1833
+ <div class="legend-item"><div class="legend-dot" style="background:var(--grace)"></div> Grace</div>
1834
+ <div class="legend-item"><div class="legend-dot" style="background:var(--border)"></div> Missed</div>
1835
+ </div>
1836
+ </div>
1837
+
1838
+ <!-- Leaderboard -->
1839
+ <div class="section">
1840
+ <h2>Leaderboard</h2>
1841
+ ${leaderboard ? `<table>
1842
+ <thead>
1843
+ <tr>
1844
+ <th>Rank</th>
1845
+ <th>Name</th>
1846
+ <th>Streak</th>
1847
+ <th>Longest</th>
1848
+ <th>Badges</th>
1849
+ </tr>
1850
+ </thead>
1851
+ <tbody>
1852
+ ${lbRows}
1853
+ </tbody>
1854
+ </table>` : '<p style="color:var(--muted);font-size:0.875rem">No leaderboard data yet. Configure cloud sync to compare with friends.</p>'}
1855
+ </div>
1856
+
1857
+ <!-- Milestones -->
1858
+ <div class="section">
1859
+ <h2>Milestones</h2>
1860
+ <div class="milestones-grid">
1861
+ ${milestoneCards}
1862
+ </div>
1863
+ </div>
1864
+ </div>
1865
+
1866
+ <script>
1867
+ // Celebrate if just reached a new milestone
1868
+ const urlParams = new URLSearchParams(window.location.search);
1869
+ if (urlParams.get('celebrate')) {
1870
+ document.getElementById('heroCard').classList.add('celebrate');
1871
+ }
1872
+
1873
+ // Auto-refresh every 60s
1874
+ setTimeout(() => window.location.reload(), 60000);
1875
+ </script>
1876
+ </body>
1877
+ </html>`;
1878
+ }
1879
+
1880
+ // src/commands/check-in.ts
1881
+ import openBrowser from "open";
1882
+ async function runCheckIn(options = {}) {
1883
+ const profile = requireProfile();
1884
+ const today = todayInTz(profile.timezone);
1885
+ const now = nowIso();
1886
+ let source = options.source ?? "manual";
1887
+ let agentData = void 0;
1888
+ if (!options.source || options.source === "claude-code") {
1889
+ const spinner = ora({ text: "Detecting coding session\u2026", isSilent: options.quiet }).start();
1890
+ const detection = await detectTodaySession(
1891
+ today,
1892
+ profile.timezone,
1893
+ profile.preferences.sessionSources,
1894
+ profile.preferences.watchedRepos ?? []
1895
+ );
1896
+ spinner.stop();
1897
+ if (detection.detected) {
1898
+ source = detection.source;
1899
+ agentData = detection.agentData;
1900
+ } else if (options.noInteractive) {
1901
+ if (!options.quiet) {
1902
+ console.log(chalk6.dim(" No coding session detected today. Streak unchanged."));
1903
+ }
1904
+ const currentStreak2 = loadStreak();
1905
+ return {
1906
+ action: "skipped",
1907
+ streak: currentStreak2.currentStreak,
1908
+ previousStreak: void 0,
1909
+ newMilestones: [],
1910
+ freezeTokensRemaining: currentStreak2.freezeTokens,
1911
+ agentData: void 0
1912
+ };
1913
+ } else if (!options.quiet) {
1914
+ const doManual = await confirm2({
1915
+ message: chalk6.dim("No AI session detected. Check in manually anyway?"),
1916
+ default: true
1917
+ });
1918
+ if (!doManual) {
1919
+ console.log(chalk6.dim(" Skipped. Run vibechk again when you've coded today."));
1920
+ process.exit(0);
1921
+ }
1922
+ source = "manual";
1923
+ }
1924
+ }
1925
+ const currentStreak = loadStreak();
1926
+ const currentBadges = loadBadges();
1927
+ let useFreeze = options.autoFreeze ?? false;
1928
+ const impact = calculateStreakImpact(currentStreak, today, false);
1929
+ if (impact.action === "broken" && impact.previousStreak && impact.previousStreak > 0 && currentStreak.freezeTokens > 0 && !options.noInteractive && !options.quiet) {
1930
+ const daysDiff = currentStreak.lastActivityDate ? Math.abs(new Date(today).getTime() - new Date(currentStreak.lastActivityDate).getTime()) / 864e5 : 99;
1931
+ if (daysDiff <= 2) {
1932
+ console.log(chalk6.yellow(`
1933
+ Missed yesterday. You have ${currentStreak.freezeTokens} freeze token(s).`));
1934
+ useFreeze = await confirm2({
1935
+ message: `Use a freeze token to preserve your ${impact.previousStreak}-day streak?`,
1936
+ default: true
1937
+ });
1938
+ }
1939
+ }
1940
+ const finalImpact = calculateStreakImpact(currentStreak, today, useFreeze);
1941
+ const newStreakRecord = applyStreakImpact(currentStreak, finalImpact, today, now);
1942
+ const newMilestones = checkNewMilestones(newStreakRecord.currentStreak, currentBadges);
1943
+ const newBadges = createEarnedBadges(newMilestones, newStreakRecord.currentStreak, now);
1944
+ const freezeReward = totalFreezeReward(newMilestones);
1945
+ if (freezeReward > 0) {
1946
+ newStreakRecord.freezeTokens = addFreezeTokens(newStreakRecord.freezeTokens, freezeReward);
1947
+ }
1948
+ if (!options.dryRun) {
1949
+ saveStreak(newStreakRecord);
1950
+ appendBadges(newBadges);
1951
+ if (finalImpact.action !== "already_checked_in") {
1952
+ appendActivity({
1953
+ date: today,
1954
+ checkedInAt: now,
1955
+ source,
1956
+ isFrozen: finalImpact.action === "frozen",
1957
+ isGrace: finalImpact.action === "grace",
1958
+ sessionId: uuidv42(),
1959
+ agentData,
1960
+ notes: options.notes
1961
+ });
1962
+ }
1963
+ }
1964
+ if (!options.quiet && !options.json) {
1965
+ console.log("");
1966
+ console.log(renderCheckIn(finalImpact.action, newStreakRecord.currentStreak, finalImpact.previousStreak, newMilestones));
1967
+ for (const milestone of newMilestones) {
1968
+ console.log("");
1969
+ console.log(renderMilestone(milestone, newStreakRecord.currentStreak));
1970
+ }
1971
+ console.log("");
1972
+ } else if (options.json) {
1973
+ const result = {
1974
+ action: finalImpact.action,
1975
+ streak: newStreakRecord.currentStreak,
1976
+ previousStreak: finalImpact.previousStreak,
1977
+ newMilestones: newMilestones.map((m) => m.id),
1978
+ freezeTokensRemaining: newStreakRecord.freezeTokens
1979
+ };
1980
+ console.log(JSON.stringify(result));
1981
+ }
1982
+ const shouldOpenBrowser = options.openDashboard || newMilestones.length > 0 && !options.quiet;
1983
+ if (shouldOpenBrowser && !options.dryRun) {
1984
+ try {
1985
+ const url = await startDashboard(true);
1986
+ const celebrateUrl = newMilestones.length > 0 ? `${url}?celebrate=1` : url;
1987
+ await openBrowser(celebrateUrl);
1988
+ if (!options.quiet) console.log(chalk6.dim(` Opening dashboard in browser\u2026`));
1989
+ } catch {
1990
+ }
1991
+ }
1992
+ if (!options.dryRun && profile.cloudSync) {
1993
+ const badges = loadBadges();
1994
+ pushStreak(profile, newStreakRecord, badges).catch(() => {
1995
+ });
1996
+ }
1997
+ if (!options.dryRun && options.noInteractive && finalImpact.action !== "already_checked_in") {
1998
+ Promise.resolve().then(() => (init_publish(), publish_exports)).then(({ runPublish: runPublish2 }) => runPublish2({ silent: true })).catch(() => {
1999
+ });
2000
+ Promise.resolve().then(() => (init_friend(), friend_exports)).then(({ runFriendPull: runFriendPull2 }) => runFriendPull2({ quiet: true })).catch(() => {
2001
+ });
2002
+ }
2003
+ return {
2004
+ action: finalImpact.action,
2005
+ streak: newStreakRecord.currentStreak,
2006
+ previousStreak: finalImpact.previousStreak,
2007
+ newMilestones,
2008
+ freezeTokensRemaining: newStreakRecord.freezeTokens,
2009
+ agentData
2010
+ };
2011
+ }
2012
+
2013
+ // src/commands/status.ts
2014
+ init_profile_store();
2015
+ init_streak_store();
2016
+ init_activity_store();
2017
+ init_date_utils();
2018
+ import openBrowser2 from "open";
2019
+ import chalk7 from "chalk";
2020
+ async function runStatus(options = {}) {
2021
+ const profile = requireProfile();
2022
+ const stored = loadStreak();
2023
+ const activity = loadActivity();
2024
+ const today = todayInTz(profile.timezone);
2025
+ const impact = calculateStreakImpact(stored, today, false);
2026
+ const streak = impact.action === "broken" ? { ...stored, currentStreak: 0, status: "broken", _expiredStreak: stored.currentStreak } : stored;
2027
+ if (options.web) {
2028
+ const url = await startDashboard(false);
2029
+ console.log(chalk7.dim(` Opening dashboard at ${url}`));
2030
+ await openBrowser2(url);
2031
+ await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1e3));
2032
+ process.exit(0);
2033
+ return;
2034
+ }
2035
+ const output = renderStatus(profile, streak, activity, {
2036
+ quiet: options.quiet,
2037
+ json: options.json
2038
+ });
2039
+ console.log(output);
2040
+ }
2041
+
2042
+ // src/commands/freeze.ts
2043
+ init_profile_store();
2044
+ init_streak_store();
2045
+ init_date_utils();
2046
+ import chalk8 from "chalk";
2047
+ import { confirm as confirm3 } from "@inquirer/prompts";
2048
+ async function runFreeze(options = {}) {
2049
+ const profile = requireProfile();
2050
+ const streak = loadStreak();
2051
+ if (streak.freezeTokens === 0) {
2052
+ console.log(chalk8.yellow(" No freeze tokens available."));
2053
+ console.log(chalk8.dim(" Earn tokens by reaching milestones (30, 60, 90 days)."));
2054
+ return;
2055
+ }
2056
+ const today = todayInTz(profile.timezone);
2057
+ console.log(`
2058
+ Freeze tokens: ${"\u2744\uFE0F".repeat(streak.freezeTokens)} (${streak.freezeTokens} available)`);
2059
+ console.log(chalk8.dim(` Current streak: ${streak.currentStreak} days
2060
+ `));
2061
+ if (options.tomorrow) {
2062
+ if (!options.noInteractive) {
2063
+ const ok = await confirm3({
2064
+ message: `Use 1 freeze token to protect your streak tomorrow?`,
2065
+ default: true
2066
+ });
2067
+ if (!ok) {
2068
+ console.log(chalk8.dim(" Cancelled."));
2069
+ return;
2070
+ }
2071
+ }
2072
+ const updated = {
2073
+ ...streak,
2074
+ freezeTokens: streak.freezeTokens - 1,
2075
+ status: "active"
2076
+ // Store scheduled freeze date in a field
2077
+ };
2078
+ saveStreak(updated);
2079
+ console.log(chalk8.green(`
2080
+ \u2713 Freeze token used. Your streak is protected for tomorrow.`));
2081
+ console.log(chalk8.dim(` Tokens remaining: ${updated.freezeTokens}
2082
+ `));
2083
+ return;
2084
+ }
2085
+ if (streak.lastActivityDate) {
2086
+ const daysDiff = daysBetween(streak.lastActivityDate, today);
2087
+ if (daysDiff === 2) {
2088
+ if (!options.noInteractive) {
2089
+ const ok = await confirm3({
2090
+ message: `Use 1 freeze token to recover your ${streak.currentStreak}-day streak? (missed 1 day)`,
2091
+ default: true
2092
+ });
2093
+ if (!ok) {
2094
+ console.log(chalk8.dim(" Cancelled."));
2095
+ return;
2096
+ }
2097
+ }
2098
+ const updated = {
2099
+ ...streak,
2100
+ freezeTokens: streak.freezeTokens - 1,
2101
+ lastActivityDate: today,
2102
+ status: "frozen"
2103
+ };
2104
+ saveStreak(updated);
2105
+ console.log(chalk8.green(`
2106
+ \u2713 Freeze applied. ${streak.currentStreak}-day streak preserved.`));
2107
+ console.log(chalk8.dim(` Tokens remaining: ${updated.freezeTokens}
2108
+ `));
2109
+ return;
2110
+ }
2111
+ }
2112
+ console.log(chalk8.dim(" Use --tomorrow to pre-apply a freeze for tomorrow."));
2113
+ console.log(chalk8.dim(" Freezes are auto-offered when you run `vibechk check-in` after missing a day.\n"));
2114
+ }
2115
+
2116
+ // src/commands/leaderboard.ts
2117
+ init_profile_store();
2118
+ import chalk10 from "chalk";
2119
+ import ora2 from "ora";
2120
+
2121
+ // src/ui/leaderboard-view.ts
2122
+ init_milestone_checker();
2123
+ import chalk9 from "chalk";
2124
+ import dayjs3 from "dayjs";
2125
+ function badgeIcons(badgeIds) {
2126
+ const milestones = getAllMilestones();
2127
+ return badgeIds.map((id) => milestones.find((m) => m.id === id)?.icon ?? "").filter(Boolean).slice(0, 4).join("");
2128
+ }
2129
+ function formatEntry(entry, today) {
2130
+ const rankStr = entry.rank ? String(entry.rank).padStart(3) : " ?";
2131
+ const nameStr = (entry.displayName ?? "anonymous").padEnd(16).slice(0, 16);
2132
+ const streakStr = `${entry.currentStreak}d`.padStart(6);
2133
+ const longestStr = `${entry.longestStreak}d`.padStart(8);
2134
+ const badges = badgeIcons(entry.badges ?? []).padEnd(6);
2135
+ const freeze = entry.freezesUsed > 0 ? chalk9.dim(` (${entry.freezesUsed}\u2744)`) : "";
2136
+ const activeToday = entry.lastActiveDate === today ? chalk9.green(" \u25CF") : "";
2137
+ const line = `${rankStr} ${nameStr} ${streakStr} ${longestStr} ${badges}${freeze}${activeToday}`;
2138
+ if (entry.isYou) return chalk9.bold.cyan("\u2192 " + line);
2139
+ return " " + line;
2140
+ }
2141
+ function renderLeaderboard(cache, myUserId) {
2142
+ const today = dayjs3().format("YYYY-MM-DD");
2143
+ const updatedAgo = Math.round((Date.now() - new Date(cache.fetchedAt).getTime()) / 6e4);
2144
+ const source = cache.source.replace(/^https?:\/\//, "");
2145
+ const header = [
2146
+ chalk9.bold("Leaderboard") + chalk9.dim(` (${source}) \u2014 updated ${updatedAgo}m ago`),
2147
+ ``,
2148
+ chalk9.dim(` Rank Name Streak Longest Badges`),
2149
+ chalk9.dim(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`)
2150
+ ].join("\n");
2151
+ const me = cache.entries.find((e) => e.userId === myUserId);
2152
+ const myRank = me?.rank ?? Infinity;
2153
+ const showSet = /* @__PURE__ */ new Set();
2154
+ cache.entries.slice(0, 5).forEach((e) => showSet.add(e.rank ?? 0));
2155
+ cache.entries.filter((e) => {
2156
+ const r = e.rank ?? 0;
2157
+ return r >= myRank - 5 && r <= myRank + 5;
2158
+ }).forEach((e) => showSet.add(e.rank ?? 0));
2159
+ const shown = cache.entries.filter((e) => showSet.has(e.rank ?? 0));
2160
+ let lastRank = 0;
2161
+ const rows = [];
2162
+ for (const entry of shown) {
2163
+ const rank = entry.rank ?? 0;
2164
+ if (lastRank > 0 && rank > lastRank + 1) {
2165
+ rows.push(chalk9.dim(" ..."));
2166
+ }
2167
+ rows.push(formatEntry(entry, today));
2168
+ lastRank = rank;
2169
+ }
2170
+ if (rows.length === 0) {
2171
+ rows.push(chalk9.dim(" No entries yet. Be the first!"));
2172
+ }
2173
+ return [header, ...rows, ""].join("\n");
2174
+ }
2175
+
2176
+ // src/commands/leaderboard.ts
2177
+ import openBrowser3 from "open";
2178
+ async function runLeaderboard(options = {}) {
2179
+ const profile = requireProfile();
2180
+ if (options.web) {
2181
+ const url = await startDashboard(false);
2182
+ console.log(chalk10.dim(` Opening leaderboard in browser at ${url}`));
2183
+ await openBrowser3(url + "#leaderboard");
2184
+ await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1e3));
2185
+ process.exit(0);
2186
+ return;
2187
+ }
2188
+ if (!profile.cloudSync) {
2189
+ console.log("");
2190
+ console.log(chalk10.yellow(" No cloud sync configured."));
2191
+ console.log(chalk10.dim(" Run `vibechk init` and provide a leaderboard endpoint to compare with friends."));
2192
+ console.log(chalk10.dim(" Or run `vibechk leaderboard --web` to see your local stats.\n"));
2193
+ return;
2194
+ }
2195
+ let cache = loadLeaderboardCache();
2196
+ if (!cache || options.refresh || isCacheStale(cache)) {
2197
+ const spinner = ora2("Fetching leaderboard\u2026").start();
2198
+ const fresh = await fetchLeaderboard(profile);
2199
+ spinner.stop();
2200
+ if (!fresh) {
2201
+ console.log(chalk10.red(" Could not reach leaderboard server."));
2202
+ if (cache) {
2203
+ console.log(chalk10.dim(" Showing cached data.\n"));
2204
+ } else {
2205
+ return;
2206
+ }
2207
+ } else {
2208
+ cache = fresh;
2209
+ saveLeaderboardCache(cache);
2210
+ }
2211
+ }
2212
+ if (!cache) return;
2213
+ if (options.hideMe) {
2214
+ cache = {
2215
+ ...cache,
2216
+ entries: cache.entries.map((e) => ({
2217
+ ...e,
2218
+ isYou: false
2219
+ }))
2220
+ };
2221
+ }
2222
+ console.log("");
2223
+ console.log(renderLeaderboard(cache, profile.id));
2224
+ }
2225
+
2226
+ // src/commands/export.ts
2227
+ init_profile_store();
2228
+ init_streak_store();
2229
+ init_activity_store();
2230
+ init_badge_store();
2231
+ async function runExport(options = {}) {
2232
+ const profile = requireProfile();
2233
+ const streak = loadStreak();
2234
+ const activity = loadActivity();
2235
+ const badges = loadBadges();
2236
+ const data = {
2237
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2238
+ version: "0.1.0",
2239
+ profile: {
2240
+ username: profile.username,
2241
+ timezone: profile.timezone,
2242
+ createdAt: profile.createdAt
2243
+ },
2244
+ streak,
2245
+ activity,
2246
+ badges
2247
+ };
2248
+ if (options.format === "csv") {
2249
+ const rows = ["date,source,isFrozen,isGrace"];
2250
+ for (const a of activity) {
2251
+ rows.push(`${a.date},${a.source},${a.isFrozen},${a.isGrace}`);
2252
+ }
2253
+ process.stdout.write(rows.join("\n") + "\n");
2254
+ } else {
2255
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
2256
+ }
2257
+ }
2258
+
2259
+ // src/commands/sync.ts
2260
+ init_profile_store();
2261
+ init_streak_store();
2262
+ init_badge_store();
2263
+ import chalk11 from "chalk";
2264
+ import ora3 from "ora";
2265
+ async function runSync() {
2266
+ const profile = requireProfile();
2267
+ if (!profile.cloudSync) {
2268
+ console.log(chalk11.yellow("\n No cloud sync configured."));
2269
+ console.log(chalk11.dim(" Run `vibechk init` to set up a leaderboard endpoint.\n"));
2270
+ return;
2271
+ }
2272
+ const streak = loadStreak();
2273
+ const badges = loadBadges();
2274
+ const spinner = ora3("Syncing to leaderboard\u2026").start();
2275
+ const ok = await pushStreak(profile, streak, badges);
2276
+ spinner.stop();
2277
+ if (ok) {
2278
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2279
+ profile.cloudSync.lastSyncedAt = now;
2280
+ saveProfile(profile);
2281
+ console.log(chalk11.green(`
2282
+ \u2713 Synced to ${profile.cloudSync.endpoint}
2283
+ `));
2284
+ } else {
2285
+ console.log(chalk11.red(`
2286
+ \u2717 Sync failed. Check your endpoint and API key.
2287
+ `));
2288
+ process.exitCode = 1;
2289
+ }
2290
+ }
2291
+
2292
+ // src/commands/log.ts
2293
+ init_profile_store();
2294
+ init_activity_store();
2295
+ init_date_utils();
2296
+ import chalk12 from "chalk";
2297
+ import dayjs4 from "dayjs";
2298
+ function runLog() {
2299
+ const profile = requireProfile();
2300
+ const activity = loadActivity();
2301
+ const today = todayInTz(profile.timezone);
2302
+ const activitySet = new Set(activity.map((a) => a.date));
2303
+ const days = [];
2304
+ for (let i = 29; i >= 0; i--) {
2305
+ const d = dayjs4(today).subtract(i, "day");
2306
+ const dateStr = d.format("YYYY-MM-DD");
2307
+ days.push({
2308
+ date: dateStr,
2309
+ label: d.format("MMM D"),
2310
+ active: activitySet.has(dateStr),
2311
+ isToday: dateStr === today
2312
+ });
2313
+ }
2314
+ const activeDays = days.filter((d) => d.active).length;
2315
+ const consistencyPct = Math.round(activeDays / 30 * 100);
2316
+ const startLabel = days[0].label;
2317
+ const endLabel = days[days.length - 1].label;
2318
+ console.log("");
2319
+ console.log(chalk12.bold(`Last 30 days`) + chalk12.dim(` ${startLabel} \u2013 ${endLabel}`));
2320
+ console.log(chalk12.dim(" \u2713 coded \xB7 missed"));
2321
+ console.log("");
2322
+ const WEEK_SIZE = 7;
2323
+ const DAYS_SHORT = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
2324
+ const firstDayOfWeek = dayjs4(days[0].date).day();
2325
+ const mondayAligned = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
2326
+ const headerOffset = " ".repeat(8 + mondayAligned * 3);
2327
+ console.log(chalk12.dim(headerOffset + DAYS_SHORT.join(" ")));
2328
+ for (let i = 0; i < days.length; i += WEEK_SIZE) {
2329
+ const week = days.slice(i, i + WEEK_SIZE);
2330
+ const rowLabel = chalk12.dim(week[0].label.padStart(6));
2331
+ let prefix = "";
2332
+ if (i === 0 && mondayAligned > 0) {
2333
+ prefix = " ".repeat(mondayAligned);
2334
+ }
2335
+ const cells = week.map((d) => {
2336
+ let cell;
2337
+ if (d.active) {
2338
+ cell = chalk12.green("\u2713");
2339
+ } else if (d.isToday) {
2340
+ cell = chalk12.yellow("\u25CB");
2341
+ } else {
2342
+ cell = chalk12.dim("\xB7");
2343
+ }
2344
+ return d.isToday ? chalk12.underline(cell) : cell;
2345
+ });
2346
+ console.log(` ${rowLabel} ${prefix}${cells.join(" ")}`);
2347
+ }
2348
+ console.log("");
2349
+ const consistencyColor = consistencyPct >= 80 ? chalk12.green : consistencyPct >= 50 ? chalk12.yellow : chalk12.red;
2350
+ console.log(
2351
+ ` Consistency: ` + consistencyColor(`${consistencyPct}%`) + chalk12.dim(` \u2014 ${activeDays} of 30 days`)
2352
+ );
2353
+ console.log("");
2354
+ }
2355
+
2356
+ // src/commands/share.ts
2357
+ init_profile_store();
2358
+ init_streak_store();
2359
+ init_activity_store();
2360
+ init_badge_store();
2361
+ init_milestone_checker();
2362
+ init_date_utils();
2363
+ import chalk13 from "chalk";
2364
+ import { execSync as execSync4 } from "child_process";
2365
+ function runShare(options = {}) {
2366
+ const profile = requireProfile();
2367
+ const streak = loadStreak();
2368
+ const activity = loadActivity();
2369
+ const badges = loadBadges();
2370
+ const today = todayInTz(profile.timezone);
2371
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
2372
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
2373
+ const cutoffStr = thirtyDaysAgo.toISOString().slice(0, 10);
2374
+ const activeLast30 = activity.filter((a) => a.date >= cutoffStr && a.date <= today).length;
2375
+ const consistencyPct = Math.round(activeLast30 / 30 * 100);
2376
+ const allMilestones = getAllMilestones();
2377
+ const earnedBadgeNames = badges.earned.map((b) => allMilestones.find((m) => m.id === b.milestoneId)).filter(Boolean).map((m) => `${m.icon} ${m.name}`);
2378
+ if (options.json) {
2379
+ console.log(JSON.stringify({
2380
+ currentStreak: streak.currentStreak,
2381
+ longestStreak: streak.longestStreak,
2382
+ consistencyPct,
2383
+ activeLast30,
2384
+ badges: earnedBadgeNames
2385
+ }, null, 2));
2386
+ return;
2387
+ }
2388
+ const streakLine = streak.currentStreak >= 30 ? `\u{1F3C6} ${streak.currentStreak}-day vibe coding streak!` : `\u{1F525} ${streak.currentStreak}-day vibe coding streak`;
2389
+ const statsLine = `\u{1F4CA} Best: ${streak.longestStreak}d | 30-day consistency: ${consistencyPct}%`;
2390
+ const badgeLine = earnedBadgeNames.length > 0 ? `\u{1F396} ${earnedBadgeNames.slice(-3).join(" ")}` : null;
2391
+ const ctaLine = `Built with vibechk \u2014 npm i -g vibechk`;
2392
+ const shareLines = [streakLine, statsLine, badgeLine, ctaLine].filter(Boolean);
2393
+ const shareText = shareLines.join("\n");
2394
+ console.log("");
2395
+ console.log(chalk13.bold(" Copy and share:"));
2396
+ console.log("");
2397
+ console.log(chalk13.dim(" \u250C" + "\u2500".repeat(Math.max(...shareLines.map((l) => l.length)) + 2) + "\u2510"));
2398
+ for (const line of shareLines) {
2399
+ console.log(chalk13.dim(" \u2502 ") + line + chalk13.dim(" \u2502"));
2400
+ }
2401
+ console.log(chalk13.dim(" \u2514" + "\u2500".repeat(Math.max(...shareLines.map((l) => l.length)) + 2) + "\u2518"));
2402
+ console.log("");
2403
+ tryCopyToClipboard(shareText);
2404
+ }
2405
+ function tryCopyToClipboard(text) {
2406
+ const escaped = JSON.stringify(text);
2407
+ const cmds = [
2408
+ `echo ${escaped} | pbcopy`,
2409
+ // macOS
2410
+ `echo ${escaped} | xclip -selection clipboard`,
2411
+ // Linux xclip
2412
+ `echo ${escaped} | xsel --clipboard --input`
2413
+ // Linux xsel
2414
+ ];
2415
+ for (const cmd of cmds) {
2416
+ try {
2417
+ execSync4(cmd, { stdio: "pipe" });
2418
+ console.log(chalk13.dim(" Copied to clipboard \u2713"));
2419
+ return;
2420
+ } catch {
2421
+ }
2422
+ }
2423
+ }
2424
+
2425
+ // src/cli.ts
2426
+ init_friend();
2427
+ init_publish();
2428
+ var program = new Command();
2429
+ program.name("vibechk").description("Daily streak tracker for vibe coders").version("0.1.0");
2430
+ program.action(async () => {
2431
+ if (!dataExists()) {
2432
+ console.log(chalk14.dim(" No profile found. Running setup...\n"));
2433
+ await runInit();
2434
+ return;
2435
+ }
2436
+ await runStatus({ quiet: false });
2437
+ });
2438
+ program.command("init").description("Set up your vibechk profile").action(async () => {
2439
+ await runInit();
2440
+ });
2441
+ program.command("check-in").alias("checkin").alias("ci").description("Record a vibe coding session (auto-detects Claude Code usage)").option("-m, --manual", "Skip auto-detection, check in manually").option("--note <text>", "Add a note to this session").option("--auto-freeze", "Automatically use a freeze token if needed").option("--dry-run", "Compute but do not save").option("-q, --quiet", "Minimal output").option("--json", "Output as JSON").option("--no-interactive", "Non-interactive mode (no prompts)").option("--web", "Open browser dashboard after check-in").action(async (opts) => {
2442
+ if (!dataExists()) {
2443
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2444
+ process.exit(1);
2445
+ }
2446
+ await runCheckIn({
2447
+ source: opts.manual ? "manual" : void 0,
2448
+ notes: opts.note,
2449
+ autoFreeze: opts.autoFreeze,
2450
+ dryRun: opts.dryRun,
2451
+ quiet: opts.quiet,
2452
+ json: opts.json,
2453
+ noInteractive: opts.noInteractive === false ? true : void 0,
2454
+ openDashboard: opts.web
2455
+ });
2456
+ });
2457
+ program.command("status").alias("s").description("Show your current streak and activity").option("-q, --quiet", "Single-line output").option("--json", "Output as JSON").option("--web", "Open browser dashboard").action(async (opts) => {
2458
+ if (!dataExists()) {
2459
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2460
+ process.exit(1);
2461
+ }
2462
+ await runStatus({ quiet: opts.quiet, json: opts.json, web: opts.web });
2463
+ });
2464
+ program.command("freeze").description("Manage freeze tokens (protect your streak against a missed day)").option("--tomorrow", "Pre-apply a freeze for tomorrow (planned absence)").option("--no-interactive", "Non-interactive mode").action(async (opts) => {
2465
+ if (!dataExists()) {
2466
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2467
+ process.exit(1);
2468
+ }
2469
+ await runFreeze({
2470
+ tomorrow: opts.tomorrow,
2471
+ noInteractive: opts.noInteractive === false ? true : void 0
2472
+ });
2473
+ });
2474
+ program.command("leaderboard").alias("lb").description("View the opt-in community streak leaderboard").option("--refresh", "Force refresh (ignore 1hr cache)").option("--hide-me", "Hide yourself from display").option("--web", "Open in browser").action(async (opts) => {
2475
+ if (!dataExists()) {
2476
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2477
+ process.exit(1);
2478
+ }
2479
+ await runLeaderboard({ refresh: opts.refresh, hideMe: opts.hideMe, web: opts.web });
2480
+ });
2481
+ program.command("sync").description("Push your streak data to the cloud leaderboard").action(async () => {
2482
+ if (!dataExists()) {
2483
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2484
+ process.exit(1);
2485
+ }
2486
+ await runSync();
2487
+ });
2488
+ program.command("export").description("Export all your streak data").option("--format <type>", "Output format: json (default) or csv", "json").action(async (opts) => {
2489
+ if (!dataExists()) {
2490
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2491
+ process.exit(1);
2492
+ }
2493
+ await runExport({ format: opts.format });
2494
+ });
2495
+ program.command("dashboard").alias("dash").description("Open the visual dashboard in your browser").action(async () => {
2496
+ if (!dataExists()) {
2497
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2498
+ process.exit(1);
2499
+ }
2500
+ await runStatus({ web: true });
2501
+ });
2502
+ program.command("log").alias("l").description("Show your last 30 days as a calendar with consistency %").action(() => {
2503
+ if (!dataExists()) {
2504
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2505
+ process.exit(1);
2506
+ }
2507
+ runLog();
2508
+ });
2509
+ program.command("share").description("Generate shareable text about your current streak").option("--json", "Output as JSON").action((opts) => {
2510
+ if (!dataExists()) {
2511
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2512
+ process.exit(1);
2513
+ }
2514
+ runShare({ json: opts.json });
2515
+ });
2516
+ program.command("schedule").description("Set up or manage the daily auto-check-in job (launchd on macOS, cron on Linux)").option("--time <HH:MM>", "Time to run daily check-in (24-hour format)", "21:00").option("--remove", "Remove the daily auto-check-in job").option("--status", "Show whether a schedule is installed").action(async (opts) => {
2517
+ await runSchedule({ time: opts.time, remove: opts.remove, status: opts.status });
2518
+ });
2519
+ program.command("friends").description("Show your friends' streaks").action(async () => {
2520
+ requireInit();
2521
+ await runFriendList();
2522
+ });
2523
+ var friendCmd = program.command("friend").description("Manage friend subscriptions");
2524
+ friendCmd.command("add <alias> [url]").description("Follow a friend's streak (URL required unless a remote endpoint is configured)").action(async (alias, url) => {
2525
+ requireInit();
2526
+ await runFriendAdd(alias, url);
2527
+ });
2528
+ friendCmd.command("remove <alias>").description("Unsubscribe from a friend").action((alias) => {
2529
+ requireInit();
2530
+ runFriendRemove(alias);
2531
+ });
2532
+ friendCmd.command("pull").description("Refresh all friends' streak data now").option("-q, --quiet", "Suppress output").action(async (opts) => {
2533
+ requireInit();
2534
+ await runFriendPull({ quiet: opts.quiet });
2535
+ });
2536
+ friendCmd.command("list").description("List all friends and their streaks").action(async () => {
2537
+ requireInit();
2538
+ await runFriendList();
2539
+ });
2540
+ program.command("publish").description("Publish your streak so friends can subscribe").option("--gist", "Publish to a GitHub Gist (default when no endpoint is configured)").option("--endpoint <url>", "Publish to a remote server endpoint (saved for future use)").option("--stdout", "Print the JSON payload to stdout").option("--token <token>", "GitHub personal access token (gist scope)").action(async (opts) => {
2541
+ requireInit();
2542
+ await runPublish({ gist: opts.gist, endpoint: opts.endpoint, stdout: opts.stdout, token: opts.token });
2543
+ });
2544
+ function requireInit() {
2545
+ if (!dataExists()) {
2546
+ console.log(chalk14.yellow(" Run `vibechk init` first.\n"));
2547
+ process.exit(1);
2548
+ }
2549
+ }
2550
+ program.parseAsync(process.argv).catch((err) => {
2551
+ console.error(chalk14.red("Error:"), err.message);
2552
+ process.exit(1);
2553
+ });