ccclub 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +76 -53
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // src/commands/init.ts
7
7
  import { createInterface } from "readline/promises";
8
8
  import { stdin, stdout } from "process";
9
- import chalk2 from "chalk";
9
+ import chalk3 from "chalk";
10
10
  import ora2 from "ora";
11
11
 
12
12
  // src/config.ts
@@ -133,10 +133,30 @@ async function installHeartbeat() {
133
133
  return true;
134
134
  }
135
135
 
136
+ // src/global-install.ts
137
+ import { execFile as execFile2 } from "child_process";
138
+ import chalk from "chalk";
139
+ async function ensureGlobalInstall() {
140
+ const isInstalled = await new Promise((resolve) => {
141
+ execFile2("ccclub", ["--version"], (err) => resolve(!err));
142
+ });
143
+ if (isInstalled) return;
144
+ console.log(chalk.dim("\n Installing ccclub globally so you can run it directly..."));
145
+ const ok = await new Promise((resolve) => {
146
+ execFile2("npm", ["install", "-g", "ccclub"], (err) => resolve(!err));
147
+ });
148
+ if (ok) {
149
+ console.log(chalk.green(" \u2713 Installed!") + chalk.dim(" You can now use ") + chalk.white("ccclub rank") + chalk.dim(" directly."));
150
+ } else {
151
+ console.log(chalk.dim(" Could not install globally. You can do it manually:"));
152
+ console.log(chalk.dim(" npm install -g ccclub"));
153
+ }
154
+ }
155
+
136
156
  // src/commands/sync.ts
137
157
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
138
158
  import { existsSync as existsSync3 } from "fs";
139
- import chalk from "chalk";
159
+ import chalk2 from "chalk";
140
160
  import ora from "ora";
141
161
 
142
162
  // src/collector.ts
@@ -314,7 +334,7 @@ async function doSync(firstSync = false, silent = false) {
314
334
  const totalCost = blocksToSync.reduce((s, b) => s + b.costUSD, 0);
315
335
  if (spinner) {
316
336
  spinner.succeed(`Synced ${data.synced} blocks`);
317
- log(chalk.dim(` Tokens: ${totalTokens.toLocaleString()} Cost: $${totalCost.toFixed(4)}`));
337
+ log(chalk2.dim(` Tokens: ${totalTokens.toLocaleString()} Cost: $${totalCost.toFixed(4)}`));
318
338
  }
319
339
  } catch (err) {
320
340
  if (spinner) spinner.fail(`Sync error: ${err instanceof Error ? err.message : err}`);
@@ -325,17 +345,18 @@ async function doSync(firstSync = false, silent = false) {
325
345
  async function initCommand() {
326
346
  const existing = await loadConfig();
327
347
  if (existing) {
328
- console.log(chalk2.yellow("Already initialized!"));
348
+ console.log(chalk3.yellow("Already initialized!"));
329
349
  console.log(` User: ${existing.displayName}`);
330
350
  console.log(` Groups: ${existing.groups.join(", ") || "(none)"}`);
331
- console.log(chalk2.dim('\n Run "ccclub rank" to see rankings'));
351
+ console.log(chalk3.dim('\n Run "ccclub rank" to see rankings'));
352
+ await ensureGlobalInstall();
332
353
  return;
333
354
  }
334
355
  const rl = createInterface({ input: stdin, output: stdout });
335
356
  try {
336
- const displayName = await rl.question(chalk2.bold("Your display name: "));
357
+ const displayName = await rl.question(chalk3.bold("Your display name: "));
337
358
  if (!displayName.trim()) {
338
- console.error(chalk2.red("Name cannot be empty"));
359
+ console.error(chalk3.red("Name cannot be empty"));
339
360
  return;
340
361
  }
341
362
  const spinner = ora2("Setting up...").start();
@@ -362,18 +383,19 @@ async function initCommand() {
362
383
  const heartbeatOk = await installHeartbeat();
363
384
  spinner.succeed("CCClub initialized!");
364
385
  console.log("");
365
- console.log(chalk2.bold(" Your invite code:"));
366
- console.log(chalk2.cyan.bold(`
386
+ console.log(chalk3.bold(" Your invite code:"));
387
+ console.log(chalk3.cyan.bold(`
367
388
  ${data.groupCode}
368
389
  `));
369
- console.log(chalk2.dim(" Share with friends: ") + chalk2.white(`npx ccclub join ${data.groupCode}`));
390
+ console.log(chalk3.dim(" Share with friends: ") + chalk3.white(`npx ccclub join ${data.groupCode}`));
370
391
  if (heartbeatOk) {
371
- console.log(chalk2.dim(" Heartbeat: installed (syncs every hour)"));
392
+ console.log(chalk3.dim(" Heartbeat: installed (syncs every hour)"));
372
393
  } else {
373
- console.log(chalk2.dim(' Tip: run "ccclub sync" periodically to update data'));
394
+ console.log(chalk3.dim(' Tip: run "ccclub sync" periodically to update data'));
374
395
  }
375
396
  console.log("");
376
397
  await doSync(true);
398
+ await ensureGlobalInstall();
377
399
  } finally {
378
400
  rl.close();
379
401
  }
@@ -382,7 +404,7 @@ async function initCommand() {
382
404
  // src/commands/join.ts
383
405
  import { createInterface as createInterface2 } from "readline/promises";
384
406
  import { stdin as stdin2, stdout as stdout2 } from "process";
385
- import chalk3 from "chalk";
407
+ import chalk4 from "chalk";
386
408
  import ora3 from "ora";
387
409
  async function joinCommand(inviteCode) {
388
410
  let config = await loadConfig();
@@ -395,9 +417,9 @@ async function joinCommand(inviteCode) {
395
417
  } else {
396
418
  const rl = createInterface2({ input: stdin2, output: stdout2 });
397
419
  try {
398
- displayName = await rl.question(chalk3.bold("Your display name: "));
420
+ displayName = await rl.question(chalk4.bold("Your display name: "));
399
421
  if (!displayName.trim()) {
400
- console.error(chalk3.red("Name cannot be empty"));
422
+ console.error(chalk4.red("Name cannot be empty"));
401
423
  return;
402
424
  }
403
425
  displayName = displayName.trim();
@@ -434,17 +456,18 @@ async function joinCommand(inviteCode) {
434
456
  await installHeartbeat();
435
457
  }
436
458
  spinner.succeed(`Joined "${data.groupName}"!`);
437
- console.log(chalk3.dim(`
459
+ console.log(chalk4.dim(`
438
460
  Dashboard: ${apiUrl}/g/${data.groupCode}`));
439
- console.log(chalk3.dim(' Run "ccclub rank" to see rankings'));
461
+ console.log(chalk4.dim(' Run "ccclub rank" to see rankings'));
440
462
  if (!config) {
441
463
  console.log("");
442
464
  await doSync(true);
443
465
  }
466
+ await ensureGlobalInstall();
444
467
  }
445
468
 
446
469
  // src/commands/rank.ts
447
- import chalk4 from "chalk";
470
+ import chalk5 from "chalk";
448
471
  import Table from "cli-table3";
449
472
  import ora4 from "ora";
450
473
  async function rankCommand(options) {
@@ -467,24 +490,24 @@ async function rankCommand(options) {
467
490
  const data = await res.json();
468
491
  spinner.stop();
469
492
  if (data.rankings.length === 0) {
470
- console.log(chalk4.yellow("\n No rankings data for this period"));
471
- console.log(chalk4.dim(' Run "ccclub sync" to upload your usage data'));
493
+ console.log(chalk5.yellow("\n No rankings data for this period"));
494
+ console.log(chalk5.dim(' Run "ccclub sync" to upload your usage data'));
472
495
  return;
473
496
  }
474
- console.log(chalk4.bold(`
497
+ console.log(chalk5.bold(`
475
498
  ${data.group.name}`));
476
- console.log(chalk4.dim(` ${period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members
499
+ console.log(chalk5.dim(` ${period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members
477
500
  `));
478
501
  const table = new Table({
479
- head: ["#", "Name", "Tokens", "Cost", "Calls"].map((h) => chalk4.cyan(h)),
502
+ head: ["#", "Name", "Tokens", "Cost", "Calls"].map((h) => chalk5.cyan(h)),
480
503
  style: { head: [], border: [] },
481
504
  colWidths: [5, 20, 16, 12, 8]
482
505
  });
483
506
  for (const entry of data.rankings) {
484
507
  const isMe = entry.userId === config.userId;
485
- const marker = isMe ? chalk4.green("\u2192") : " ";
486
- const name = isMe ? chalk4.green.bold(entry.displayName) : entry.displayName;
487
- const rankColor = entry.rank <= 3 ? chalk4.yellow : chalk4.white;
508
+ const marker = isMe ? chalk5.green("\u2192") : " ";
509
+ const name = isMe ? chalk5.green.bold(entry.displayName) : entry.displayName;
510
+ const rankColor = entry.rank <= 3 ? chalk5.yellow : chalk5.white;
488
511
  table.push([
489
512
  `${marker}${rankColor(String(entry.rank))}`,
490
513
  name,
@@ -494,7 +517,7 @@ async function rankCommand(options) {
494
517
  ]);
495
518
  }
496
519
  console.log(table.toString());
497
- console.log(chalk4.dim(`
520
+ console.log(chalk5.dim(`
498
521
  Dashboard: ${config.apiUrl}/g/${groupCode}`));
499
522
  } catch (err) {
500
523
  spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
@@ -502,7 +525,7 @@ async function rankCommand(options) {
502
525
  }
503
526
 
504
527
  // src/commands/profile.ts
505
- import chalk5 from "chalk";
528
+ import chalk6 from "chalk";
506
529
  import ora5 from "ora";
507
530
  async function profileCommand(options) {
508
531
  const config = await requireConfig();
@@ -519,10 +542,10 @@ async function profileCommand(options) {
519
542
  }
520
543
  const profile = await res.json();
521
544
  spinner2.stop();
522
- console.log(chalk5.bold("\n Your Profile"));
545
+ console.log(chalk6.bold("\n Your Profile"));
523
546
  console.log(` Name: ${profile.displayName}`);
524
- console.log(` Avatar: ${profile.avatar || chalk5.dim("(default)")}`);
525
- console.log(` Visibility: ${profile.visibility === "public" ? chalk5.green("public") : chalk5.dim("private")}`);
547
+ console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
548
+ console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
526
549
  console.log();
527
550
  } catch (err) {
528
551
  spinner2.fail(`Error: ${err instanceof Error ? err.message : err}`);
@@ -554,10 +577,10 @@ async function profileCommand(options) {
554
577
  config.displayName = body.displayName;
555
578
  await saveConfig(config);
556
579
  }
557
- console.log(chalk5.green("\n Profile updated!"));
580
+ console.log(chalk6.green("\n Profile updated!"));
558
581
  console.log(` Name: ${profile.displayName}`);
559
- console.log(` Avatar: ${profile.avatar || chalk5.dim("(default)")}`);
560
- console.log(` Visibility: ${profile.visibility === "public" ? chalk5.green("public") : chalk5.dim("private")}`);
582
+ console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
583
+ console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
561
584
  console.log();
562
585
  } catch (err) {
563
586
  spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
@@ -565,44 +588,44 @@ async function profileCommand(options) {
565
588
  }
566
589
 
567
590
  // src/commands/show-data.ts
568
- import chalk6 from "chalk";
591
+ import chalk7 from "chalk";
569
592
  async function showDataCommand() {
570
- console.log(chalk6.bold("\n What CCClub uploads:\n"));
571
- console.log(chalk6.dim(" Only aggregated 5-hour block summaries. No conversation content,"));
572
- console.log(chalk6.dim(" no file paths, no project names, no session details.\n"));
593
+ console.log(chalk7.bold("\n What CCClub uploads:\n"));
594
+ console.log(chalk7.dim(" Only aggregated 5-hour block summaries. No conversation content,"));
595
+ console.log(chalk7.dim(" no file paths, no project names, no session details.\n"));
573
596
  const entries = await collectUsageEntries();
574
597
  const blocks = aggregateToBlocks(entries);
575
598
  if (blocks.length === 0) {
576
- console.log(chalk6.yellow(" No usage data found in ~/.claude/projects/"));
599
+ console.log(chalk7.yellow(" No usage data found in ~/.claude/projects/"));
577
600
  return;
578
601
  }
579
- console.log(chalk6.dim(` Total entries found: ${entries.length}`));
580
- console.log(chalk6.dim(` Aggregated into: ${blocks.length} blocks
602
+ console.log(chalk7.dim(` Total entries found: ${entries.length}`));
603
+ console.log(chalk7.dim(` Aggregated into: ${blocks.length} blocks
581
604
  `));
582
605
  const recent = blocks.slice(-5);
583
- console.log(chalk6.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
606
+ console.log(chalk7.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
584
607
  for (const block of recent) {
585
- console.log(chalk6.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
586
- console.log(chalk6.dim(` tokens: ${block.totalTokens.toLocaleString()} cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} models: ${block.models.join(", ")}`));
608
+ console.log(chalk7.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
609
+ console.log(chalk7.dim(` tokens: ${block.totalTokens.toLocaleString()} cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} models: ${block.models.join(", ")}`));
587
610
  }
588
611
  const totalTokens = blocks.reduce((s, b) => s + b.totalTokens, 0);
589
612
  const totalCost = blocks.reduce((s, b) => s + b.costUSD, 0);
590
- console.log(chalk6.bold(`
613
+ console.log(chalk7.bold(`
591
614
  All-time total: ${totalTokens.toLocaleString()} tokens \xB7 $${totalCost.toFixed(2)}`));
592
615
  }
593
616
 
594
617
  // src/commands/group.ts
595
618
  import { createInterface as createInterface3 } from "readline/promises";
596
619
  import { stdin as stdin3, stdout as stdout3 } from "process";
597
- import chalk7 from "chalk";
620
+ import chalk8 from "chalk";
598
621
  import ora6 from "ora";
599
622
  async function createGroupCommand() {
600
623
  const config = await requireConfig();
601
624
  const rl = createInterface3({ input: stdin3, output: stdout3 });
602
625
  try {
603
- const name = await rl.question(chalk7.bold("Group name: "));
626
+ const name = await rl.question(chalk8.bold("Group name: "));
604
627
  if (!name.trim()) {
605
- console.error(chalk7.red("Name cannot be empty"));
628
+ console.error(chalk8.red("Name cannot be empty"));
606
629
  return;
607
630
  }
608
631
  const spinner = ora6("Creating group...").start();
@@ -625,10 +648,10 @@ async function createGroupCommand() {
625
648
  await saveConfig(config);
626
649
  }
627
650
  spinner.succeed(`Created "${data.groupName}"`);
628
- console.log(chalk7.bold(`
629
- Invite code: `) + chalk7.cyan.bold(data.groupCode));
630
- console.log(chalk7.dim(` Share: npx ccclub join ${data.groupCode}`));
631
- console.log(chalk7.dim(` Dashboard: ${config.apiUrl}/g/${data.groupCode}`));
651
+ console.log(chalk8.bold(`
652
+ Invite code: `) + chalk8.cyan.bold(data.groupCode));
653
+ console.log(chalk8.dim(` Share: npx ccclub join ${data.groupCode}`));
654
+ console.log(chalk8.dim(` Dashboard: ${config.apiUrl}/g/${data.groupCode}`));
632
655
  } finally {
633
656
  rl.close();
634
657
  }
@@ -636,7 +659,7 @@ async function createGroupCommand() {
636
659
 
637
660
  // src/index.ts
638
661
  var program = new Command();
639
- program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.1.0");
662
+ program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.1.2");
640
663
  program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
641
664
  program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
642
665
  program.command("sync").description("Sync local usage data to server").option("-s, --silent", "No output (used by heartbeat)").action(syncCommand);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "See how much Claude Code you and your friends are using",
6
6
  "bin": {
7
- "ccclub": "./dist/index.js"
7
+ "ccclub": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"