git-watchtower 1.6.1 → 1.7.1

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.
@@ -66,6 +66,9 @@ const casinoSounds = require('../src/casino/sounds');
66
66
  // Gitignore utilities for file watcher
67
67
  const { loadGitignorePatterns, shouldIgnoreFile } = require('../src/utils/gitignore');
68
68
 
69
+ // Telemetry (opt-in PostHog analytics)
70
+ const telemetry = require('../src/telemetry');
71
+
69
72
  // Extracted modules
70
73
  const { formatTimeAgo } = require('../src/utils/time');
71
74
  const { openInBrowser: openUrl } = require('../src/utils/browser');
@@ -195,6 +198,7 @@ async function runConfigurationWizard() {
195
198
 
196
199
  // Save configuration
197
200
  saveConfig(config);
201
+ telemetry.capture('config_wizard_completed', { server_mode: config.server.mode });
198
202
 
199
203
  console.log('\n✓ Configuration saved to ' + CONFIG_FILE_NAME);
200
204
  console.log(' You can edit this file manually or delete it to reconfigure.\n');
@@ -367,6 +371,10 @@ let AUTO_PULL = true;
367
371
  const MAX_LOG_ENTRIES = 10;
368
372
  const MAX_SERVER_LOG_LINES = 500;
369
373
 
374
+ // Telemetry session tracking
375
+ let branchSwitchCount = 0;
376
+ let sessionStartTime = null;
377
+
370
378
  // Server process management (for command mode)
371
379
  let serverProcess = null;
372
380
 
@@ -1419,6 +1427,7 @@ async function switchToBranch(branchName, recordHistory = true) {
1419
1427
  addLog(`Cannot switch: uncommitted changes in working directory`, 'error');
1420
1428
  pendingDirtyOperation = { type: 'switch', branch: branchName };
1421
1429
  showStashConfirm(`switch to ${branchName}`);
1430
+ telemetry.capture('dirty_repo_encountered');
1422
1431
  return { success: false, reason: 'dirty' };
1423
1432
  }
1424
1433
 
@@ -1451,6 +1460,8 @@ async function switchToBranch(branchName, recordHistory = true) {
1451
1460
  }
1452
1461
 
1453
1462
  addLog(`Switched to ${safeBranchName}`, 'success');
1463
+ telemetry.capture('branch_switched');
1464
+ branchSwitchCount++;
1454
1465
  pendingDirtyOperation = null;
1455
1466
 
1456
1467
  // Restart server if configured (command mode)
@@ -1480,6 +1491,7 @@ async function switchToBranch(branchName, recordHistory = true) {
1480
1491
  truncate(errMsg, 100),
1481
1492
  'Check the activity log for details'
1482
1493
  );
1494
+ telemetry.captureError(e);
1483
1495
  }
1484
1496
  return { success: false };
1485
1497
  }
@@ -1588,6 +1600,7 @@ async function stashAndRetry() {
1588
1600
  }
1589
1601
 
1590
1602
  addLog('Changes stashed successfully', 'success');
1603
+ telemetry.capture('stash_performed');
1591
1604
 
1592
1605
  if (operation.type === 'switch') {
1593
1606
  const switchResult = await switchToBranch(operation.branch);
@@ -2248,6 +2261,7 @@ function setupKeyboardInput() {
2248
2261
  openInBrowser(prUrl);
2249
2262
  } else if (!prInfo && prLoaded && cliReady) {
2250
2263
  // Create PR — only if we've confirmed no PR exists (prLoaded=true)
2264
+ telemetry.capture('pr_action', { action: 'create' });
2251
2265
  addLog(`Creating ${prLabel} for ${aBranch.name}...`, 'update');
2252
2266
  render();
2253
2267
  try {
@@ -2289,6 +2303,7 @@ function setupKeyboardInput() {
2289
2303
  return;
2290
2304
  }
2291
2305
  if (key === 'a' && prInfo && cliReady) { // Approve PR
2306
+ telemetry.capture('pr_action', { action: 'approve' });
2292
2307
  addLog(`Approving ${prLabel} #${prInfo.number}...`, 'update');
2293
2308
  render();
2294
2309
  try {
@@ -2308,6 +2323,7 @@ function setupKeyboardInput() {
2308
2323
  return;
2309
2324
  }
2310
2325
  if (key === 'm' && prInfo && cliReady) { // Merge PR
2326
+ telemetry.capture('pr_action', { action: 'merge' });
2311
2327
  addLog(`Merging ${prLabel} #${prInfo.number}...`, 'update');
2312
2328
  render();
2313
2329
  try {
@@ -2406,6 +2422,7 @@ function setupKeyboardInput() {
2406
2422
  addLog(`Failed to delete ${f.name}: ${f.error}`, 'error');
2407
2423
  }
2408
2424
  if (result.deleted.length > 0) {
2425
+ telemetry.capture('cleanup_branches_deleted', { count: result.deleted.length });
2409
2426
  addLog(`Cleaned up ${result.deleted.length} branch${result.deleted.length === 1 ? '' : 'es'}`, 'success');
2410
2427
  await pollGitChanges();
2411
2428
  }
@@ -2528,12 +2545,14 @@ function setupKeyboardInput() {
2528
2545
  render();
2529
2546
  const pvData = await getPreviewData(branch.name);
2530
2547
  store.setState({ previewData: pvData, previewMode: true });
2548
+ telemetry.capture('preview_opened');
2531
2549
  render();
2532
2550
  }
2533
2551
  break;
2534
2552
 
2535
2553
  case '/': // Search mode
2536
2554
  applyUpdates(actions.enterSearchMode(actionState));
2555
+ telemetry.capture('search_used');
2537
2556
  render();
2538
2557
  break;
2539
2558
 
@@ -2548,11 +2567,13 @@ function setupKeyboardInput() {
2548
2567
  break;
2549
2568
 
2550
2569
  case 'u': // Undo last switch
2570
+ telemetry.capture('undo_branch_switch');
2551
2571
  await undoLastSwitch();
2552
2572
  await pollGitChanges();
2553
2573
  break;
2554
2574
 
2555
2575
  case 'p':
2576
+ telemetry.capture('pull_forced');
2556
2577
  await pullCurrentBranch();
2557
2578
  await pollGitChanges();
2558
2579
  break;
@@ -2591,6 +2612,7 @@ function setupKeyboardInput() {
2591
2612
  const branch = displayBranches.length > 0 && curSelIdx < displayBranches.length
2592
2613
  ? displayBranches[curSelIdx] : null;
2593
2614
  if (branch) {
2615
+ telemetry.capture('branch_actions_opened');
2594
2616
  // Phase 1: Open modal instantly with local/cached data
2595
2617
  const localData = gatherLocalActionData(branch);
2596
2618
  store.setState({ actionData: localData, actionMode: true, actionLoading: !localData.prLoaded });
@@ -2625,8 +2647,10 @@ function setupKeyboardInput() {
2625
2647
 
2626
2648
  case 's': {
2627
2649
  applyUpdates(actions.toggleSound(actionState));
2628
- addLog(`Sound notifications ${store.get('soundEnabled') ? 'enabled' : 'disabled'}`, 'info');
2629
- if (store.get('soundEnabled')) playSound();
2650
+ const soundNowEnabled = store.get('soundEnabled');
2651
+ addLog(`Sound notifications ${soundNowEnabled ? 'enabled' : 'disabled'}`, 'info');
2652
+ telemetry.capture('sound_toggled', { enabled: soundNowEnabled });
2653
+ if (soundNowEnabled) playSound();
2630
2654
  render();
2631
2655
  break;
2632
2656
  }
@@ -2645,6 +2669,7 @@ function setupKeyboardInput() {
2645
2669
  case 'c': { // Toggle casino mode
2646
2670
  const newCasinoState = casino.toggle();
2647
2671
  store.setState({ casinoModeEnabled: newCasinoState });
2672
+ telemetry.capture('casino_mode_toggled', { enabled: newCasinoState });
2648
2673
  addLog(`Casino mode ${newCasinoState ? '🎰 ENABLED' : 'disabled'}`, newCasinoState ? 'success' : 'info');
2649
2674
  if (newCasinoState) {
2650
2675
  addLog(`Have you noticed this game has that 'variable rewards' thing going on? 🤔😉`, 'info');
@@ -2754,6 +2779,14 @@ async function shutdown() {
2754
2779
  await Promise.race([serverClosePromise, timeoutPromise]);
2755
2780
  }
2756
2781
 
2782
+ // Flush telemetry
2783
+ telemetry.capture('session_ended', {
2784
+ duration_seconds: sessionStartTime ? Math.round((Date.now() - sessionStartTime) / 1000) : 0,
2785
+ branch_switches: branchSwitchCount,
2786
+ branches_count: store.get('branches').length,
2787
+ });
2788
+ await telemetry.shutdown();
2789
+
2757
2790
  console.log('\n✓ Git Watchtower stopped\n');
2758
2791
  process.exit(0);
2759
2792
  }
@@ -2761,6 +2794,7 @@ async function shutdown() {
2761
2794
  process.on('SIGINT', shutdown);
2762
2795
  process.on('SIGTERM', shutdown);
2763
2796
  process.on('uncaughtException', (err) => {
2797
+ telemetry.captureError(err);
2764
2798
  write(ansi.showCursor);
2765
2799
  write(ansi.restoreScreen);
2766
2800
  restoreTerminalTitle();
@@ -2787,6 +2821,19 @@ async function start() {
2787
2821
  const config = await ensureConfig(cliArgs);
2788
2822
  applyConfig(config);
2789
2823
 
2824
+ // Telemetry: opt-in prompt (first run only) and initialization
2825
+ await telemetry.promptIfNeeded(promptYesNo);
2826
+ telemetry.init({ version: PACKAGE_VERSION });
2827
+ sessionStartTime = Date.now();
2828
+ telemetry.capture('tool_launched', {
2829
+ version: PACKAGE_VERSION,
2830
+ node_version: process.version,
2831
+ os: process.platform,
2832
+ server_mode: SERVER_MODE,
2833
+ has_config: !!loadConfig(),
2834
+ casino_mode: config.casinoMode || false,
2835
+ });
2836
+
2790
2837
  // Set up casino mode render callback for animations
2791
2838
  casino.setRenderCallback(render);
2792
2839
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -52,6 +52,8 @@
52
52
  },
53
53
  "files": [
54
54
  "bin/git-watchtower.js",
55
+ "src/",
56
+ "sounds/",
55
57
  "README.md",
56
58
  "LICENSE"
57
59
  ],
@@ -85,5 +87,8 @@
85
87
  }
86
88
  ]
87
89
  ]
90
+ },
91
+ "dependencies": {
92
+ "posthog-node": "^5.28.0"
88
93
  }
89
94
  }
@@ -0,0 +1,34 @@
1
+ # Casino Mode Sound Effects
2
+
3
+ Casino mode uses system sounds by default. For custom sounds, add `.wav` files to this directory:
4
+
5
+ ## Sound Files
6
+
7
+ | File | Description |
8
+ |------|-------------|
9
+ | `win.wav` | Short victory sound (small/medium wins) |
10
+ | `jackpot.wav` | Exciting fanfare (big wins, jackpots) |
11
+ | `spin.wav` | Slot machine spinning sound |
12
+ | `loss.wav` | Sad trombone / failure sound |
13
+
14
+ ## Free Sound Sources
15
+
16
+ - [Freesound.org](https://freesound.org/) - Search for "slot machine", "casino win", "coin"
17
+ - [Mixkit](https://mixkit.co/free-sound-effects/) - Game sounds category
18
+ - [Zapsplat](https://www.zapsplat.com/) - Royalty-free effects
19
+
20
+ ## Recommended Search Terms
21
+
22
+ - "slot machine win"
23
+ - "casino jackpot"
24
+ - "coin drop"
25
+ - "cha-ching"
26
+ - "sad trombone"
27
+ - "wah wah"
28
+ - "game over"
29
+
30
+ ## Format Notes
31
+
32
+ - WAV format recommended for best compatibility
33
+ - Keep files short (1-3 seconds)
34
+ - Mono or stereo, any sample rate works