@snapcommit/cli 3.11.0 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -148,7 +148,7 @@ Full docs at [snapcommit.dev/docs](https://snapcommit.dev/docs)
148
148
  ## Support
149
149
 
150
150
  - 🐦 [Follow on X/Twitter](https://x.com/Arjun06061)
151
- - 📧 [Email support](mailto:support@snapcommit.dev)
151
+ - 📧 [Email support](mailto:karjunvarma2001@gmail.com)
152
152
  - 💡 [Suggestions & Feedback](https://x.com/Arjun06061)
153
153
 
154
154
  ## License
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -9,6 +42,7 @@ const child_process_1 = require("child_process");
9
42
  const fs_1 = require("fs");
10
43
  const path_1 = __importDefault(require("path"));
11
44
  const auth_1 = require("../lib/auth");
45
+ const github = __importStar(require("../lib/github"));
12
46
  const git_1 = require("../utils/git");
13
47
  const ui_1 = require("../utils/ui");
14
48
  const prompt_1 = require("../utils/prompt");
@@ -16,6 +50,7 @@ const conflict_1 = require("./conflict");
16
50
  const commit_1 = require("./commit");
17
51
  const quick_1 = require("./quick");
18
52
  const memory_1 = require("../utils/memory");
53
+ const metrics_1 = require("../utils/metrics");
19
54
  const WORKFLOWS = [
20
55
  {
21
56
  id: 'conflict-crusher',
@@ -210,7 +245,177 @@ const WORKFLOWS = [
210
245
  },
211
246
  ],
212
247
  },
248
+ {
249
+ id: 'pr-polish',
250
+ name: 'PR Polish',
251
+ headline: 'Push, summarize, and open a reviewer-ready pull request.',
252
+ description: 'Ensure your working tree is clean, run optional tests, capture reviewer notes, push your branch, and open a GitHub PR with consistent copy.',
253
+ idealFor: ['feature branches', 'handoffs to reviewers', 'Product Hunt launches'],
254
+ prerequisites: [
255
+ 'Branch has commits ready for review',
256
+ 'GitHub authentication configured (`snap github connect`)',
257
+ ],
258
+ steps: [
259
+ {
260
+ id: 'review-status',
261
+ title: 'Confirm branch status',
262
+ description: 'Check branch tracking info and ensure the working tree is ready.',
263
+ action: async (ctx) => {
264
+ const status = (0, git_1.getGitStatus)();
265
+ const branch = (0, git_1.getCurrentBranch)();
266
+ const tracking = getBranchTrackingStatus();
267
+ const dirty = status.unstaged > 0 || status.untracked > 0;
268
+ ctx.recordInsight('branch', branch && branch !== 'unknown' ? branch : 'unknown');
269
+ if (tracking) {
270
+ ctx.recordInsight('tracking', tracking);
271
+ (0, ui_1.displayInfo)('Branch tracking', [tracking]);
272
+ }
273
+ if (dirty) {
274
+ const warnings = [];
275
+ if (status.unstaged > 0)
276
+ warnings.push(`${status.unstaged} unstaged file(s)`);
277
+ if (status.untracked > 0)
278
+ warnings.push(`${status.untracked} untracked file(s)`);
279
+ (0, ui_1.displayWarning)('Working tree has pending changes.', warnings);
280
+ const continueDirty = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Continue with dirty working tree?', false));
281
+ if (!continueDirty) {
282
+ throw new Error('Please commit, stash, or clean your working tree before continuing.');
283
+ }
284
+ ctx.recordInsight('workingTree', 'Dirty (user accepted)');
285
+ }
286
+ else {
287
+ ctx.recordInsight('workingTree', 'Clean');
288
+ (0, ui_1.displaySuccess)('Working tree clean.');
289
+ }
290
+ },
291
+ },
292
+ {
293
+ id: 'pr-tests',
294
+ title: 'Optional: run tests',
295
+ description: 'Run your preferred test command so reviewers trust the PR.',
296
+ skippable: true,
297
+ action: async (ctx) => {
298
+ const defaultCommand = getPreferredTestCommand();
299
+ const shouldRun = ctx.autoContinue ||
300
+ (await (0, prompt_1.promptConfirm)('Run tests before pushing?', Boolean(defaultCommand)));
301
+ if (!shouldRun) {
302
+ ctx.recordInsight('tests', 'Skipped');
303
+ ctx.prTestResult = 'Tests not run';
304
+ return;
305
+ }
306
+ const command = ctx.autoContinue
307
+ ? defaultCommand || ''
308
+ : await (0, prompt_1.promptInput)('Test command', defaultCommand || 'npm test');
309
+ if (!command) {
310
+ ctx.recordInsight('tests', 'Skipped (no command)');
311
+ ctx.prTestResult = 'Tests not run';
312
+ return;
313
+ }
314
+ try {
315
+ await runShellCommand(command);
316
+ (0, memory_1.rememberPreference)('autopilot', 'testCommand', command);
317
+ ctx.recordInsight('tests', `Passed (${command})`);
318
+ ctx.prTestResult = `✅ ${command}`;
319
+ (0, ui_1.displaySuccess)('Tests passed successfully.');
320
+ }
321
+ catch (error) {
322
+ ctx.recordInsight('tests', `Failed (${command})`);
323
+ ctx.prTestResult = `⚠️ Failed (${command})`;
324
+ (0, ui_1.displayError)('Test command failed.', [
325
+ error.message,
326
+ 'Fix the failures or continue at your own risk.',
327
+ ]);
328
+ const continueAnyway = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Continue despite failing tests?', false));
329
+ if (!continueAnyway) {
330
+ throw new Error('Aborted because tests failed.');
331
+ }
332
+ }
333
+ },
334
+ },
335
+ {
336
+ id: 'sync',
337
+ title: 'Push branch to origin',
338
+ description: 'Make sure your branch is on GitHub before opening a PR.',
339
+ action: async (ctx) => {
340
+ const branch = (0, git_1.getCurrentBranch)();
341
+ const hasUpstream = branchHasUpstream();
342
+ const command = hasUpstream ? 'git push' : `git push --set-upstream origin ${branch}`;
343
+ try {
344
+ await runShellCommand(command);
345
+ ctx.recordInsight('push', `Pushed via "${command}"`);
346
+ (0, ui_1.displaySuccess)(`Branch ${chalk_1.default.cyan(branch)} pushed to origin.`);
347
+ }
348
+ catch (error) {
349
+ ctx.recordInsight('push', 'Failed');
350
+ throw new Error(error.message || 'Failed to push branch.');
351
+ }
352
+ },
353
+ },
354
+ {
355
+ id: 'capture-summary',
356
+ title: 'Capture reviewer summary',
357
+ description: 'Gather highlights so reviewers instantly understand the change.',
358
+ action: async (ctx) => {
359
+ const defaultSummary = getRecentCommitSummary();
360
+ let summary = defaultSummary;
361
+ if (!ctx.autoContinue) {
362
+ summary =
363
+ (await (0, prompt_1.promptInput)('Describe what reviewers should know (markdown ok)', defaultSummary || 'Add a quick summary of your changes')) || defaultSummary;
364
+ }
365
+ summary = (summary || defaultSummary || '').trim();
366
+ if (!summary) {
367
+ summary = '- Updated files\n- Ready for review';
368
+ }
369
+ ctx.prSummary = summary;
370
+ ctx.recordInsight('prSummary', summary);
371
+ (0, ui_1.displayInfo)('PR summary captured', [summary]);
372
+ },
373
+ },
374
+ {
375
+ id: 'open-pr',
376
+ title: 'Create pull request',
377
+ description: 'Generate title + body and open a GitHub PR with one approval.',
378
+ action: async (ctx) => {
379
+ const branch = (0, git_1.getCurrentBranch)();
380
+ const defaultTitle = getDefaultPRTitle();
381
+ const storedSummary = ctx.prSummary || getRecentCommitSummary();
382
+ const testResult = ctx.prTestResult;
383
+ const defaultBody = buildPRBody(storedSummary, testResult);
384
+ const title = ctx.autoContinue
385
+ ? defaultTitle
386
+ : await (0, prompt_1.promptInput)('PR title', defaultTitle || `Updates from ${branch}`);
387
+ const body = ctx.autoContinue
388
+ ? defaultBody
389
+ : await (0, prompt_1.promptInput)('PR body (markdown supported)', defaultBody);
390
+ (0, ui_1.displayInfo)('Opening pull request on GitHub...', [
391
+ `Title: ${title}`,
392
+ testResult ? `Tests: ${testResult}` : 'Tests: not provided',
393
+ ]);
394
+ const pr = await github.createPullRequest({
395
+ title: title || defaultTitle,
396
+ body: body || defaultBody,
397
+ });
398
+ ctx.recordInsight('pullRequest', `#${pr.number} ${pr.title}`);
399
+ ctx.recordInsight('prUrl', pr.html_url);
400
+ (0, ui_1.displaySuccess)('Pull request created.', [
401
+ `#${pr.number} ${pr.title}`,
402
+ pr.html_url,
403
+ ]);
404
+ },
405
+ },
406
+ ],
407
+ },
213
408
  ];
409
+ const WORKFLOW_TIME_SAVINGS = {
410
+ 'conflict-crusher': 25,
411
+ 'release-ready': 18,
412
+ 'pr-polish': 15,
413
+ };
414
+ const WORKFLOW_EVENT_IDS = {
415
+ 'conflict-crusher': 'autopilot:conflict-crusher',
416
+ 'release-ready': 'autopilot:release-ready',
417
+ 'pr-polish': 'autopilot:pr-polish',
418
+ };
214
419
  async function autopilotCommand(workflowId, rawOptions) {
215
420
  const options = {
216
421
  workflowId,
@@ -375,9 +580,21 @@ function presentSummary(workflow, result) {
375
580
  });
376
581
  }
377
582
  console.log();
378
- (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
379
- 'Re-run with --plan to preview or --auto to run without prompts.',
380
- ]);
583
+ const estimatedMinutes = WORKFLOW_TIME_SAVINGS[workflow.id];
584
+ const completedSteps = workflow.steps.filter((step) => result.statusByStep[step.id] === 'completed').length;
585
+ const eventId = WORKFLOW_EVENT_IDS[workflow.id];
586
+ if (estimatedMinutes && completedSteps > 0 && eventId) {
587
+ (0, metrics_1.logTimeSaved)(eventId, estimatedMinutes);
588
+ (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
589
+ `Time saved: ~${(0, metrics_1.formatMinutes)(estimatedMinutes)} compared to manual steps.`,
590
+ 'Re-run with --plan to preview or --auto to run without prompts.',
591
+ ]);
592
+ }
593
+ else {
594
+ (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
595
+ 'Re-run with --plan to preview or --auto to run without prompts.',
596
+ ]);
597
+ }
381
598
  }
382
599
  function getConflictedFiles() {
383
600
  try {
@@ -441,3 +658,89 @@ function inferProjectTestCommand() {
441
658
  }
442
659
  return null;
443
660
  }
661
+ function branchHasUpstream() {
662
+ try {
663
+ (0, child_process_1.execSync)('git rev-parse --abbrev-ref --symbolic-full-name @{u}', {
664
+ stdio: ['ignore', 'ignore', 'ignore'],
665
+ });
666
+ return true;
667
+ }
668
+ catch {
669
+ return false;
670
+ }
671
+ }
672
+ function getBranchTrackingStatus() {
673
+ try {
674
+ const firstLine = (0, child_process_1.execSync)('git status -sb', {
675
+ encoding: 'utf-8',
676
+ stdio: ['ignore', 'pipe', 'ignore'],
677
+ })
678
+ .split('\n')[0]
679
+ .trim();
680
+ const match = firstLine.match(/\[(.+)\]/);
681
+ if (match && match[1]) {
682
+ return match[1].replace(',', ' • ');
683
+ }
684
+ return 'Up to date with upstream';
685
+ }
686
+ catch {
687
+ return null;
688
+ }
689
+ }
690
+ function getRecentCommitSummary(limit = 3) {
691
+ try {
692
+ const log = (0, child_process_1.execSync)(`git log -${limit} --oneline`, {
693
+ encoding: 'utf-8',
694
+ stdio: ['ignore', 'pipe', 'ignore'],
695
+ })
696
+ .trim()
697
+ .split('\n')
698
+ .filter(Boolean)
699
+ .map((line) => `- ${line.trim()}`)
700
+ .join('\n');
701
+ return log;
702
+ }
703
+ catch {
704
+ return '';
705
+ }
706
+ }
707
+ function getDefaultPRTitle() {
708
+ try {
709
+ const title = (0, child_process_1.execSync)('git log -1 --pretty=%s', {
710
+ encoding: 'utf-8',
711
+ stdio: ['ignore', 'pipe', 'ignore'],
712
+ }).trim();
713
+ if (title) {
714
+ return title;
715
+ }
716
+ }
717
+ catch {
718
+ // no-op
719
+ }
720
+ const branch = (0, git_1.getCurrentBranch)();
721
+ return branch && branch !== 'unknown' ? `Updates from ${branch}` : 'Updates from SnapCommit';
722
+ }
723
+ function buildPRBody(summary, tests) {
724
+ const lines = [];
725
+ lines.push('## Summary');
726
+ if (summary && summary.trim()) {
727
+ lines.push(summary.trim());
728
+ }
729
+ else {
730
+ lines.push('- Describe the key changes');
731
+ }
732
+ lines.push('');
733
+ lines.push('## Testing');
734
+ if (tests && tests.trim()) {
735
+ lines.push(`- ${tests.trim()}`);
736
+ }
737
+ else {
738
+ lines.push('- [ ] Tests not run');
739
+ }
740
+ lines.push('');
741
+ lines.push('## Checklist');
742
+ lines.push('- [ ] Linked issue / ticket');
743
+ lines.push('- [ ] Added or updated tests');
744
+ lines.push('- [ ] Updated documentation (if needed)');
745
+ return lines.join('\n');
746
+ }
@@ -44,6 +44,7 @@ const manager_1 = require("../license/manager");
44
44
  const rate_limit_1 = require("../utils/rate-limit");
45
45
  const analytics_1 = require("../utils/analytics");
46
46
  const readline_1 = __importDefault(require("readline"));
47
+ const metrics_1 = require("../utils/metrics");
47
48
  async function commitCommand() {
48
49
  // Check license before proceeding
49
50
  const { allowed, reason, usage } = (0, manager_1.canUseCommit)();
@@ -226,6 +227,11 @@ async function commitCommand() {
226
227
  catch (error) {
227
228
  // Silent fail - cloud sync is optional
228
229
  }
230
+ const minutesSaved = (0, metrics_1.logTimeSaved)('commit:interactive');
231
+ if (minutesSaved > 0) {
232
+ console.log(chalk_1.default.gray(` ⏱ Saved about ${(0, metrics_1.formatMinutes)(minutesSaved)} compared to drafting this commit by hand.`));
233
+ console.log();
234
+ }
229
235
  // Show dopamine stats
230
236
  const { displayQuickDopamine } = await Promise.resolve().then(() => __importStar(require('../utils/dopamine')));
231
237
  displayQuickDopamine();
@@ -14,6 +14,7 @@ const fs_1 = __importDefault(require("fs"));
14
14
  const readline_1 = __importDefault(require("readline"));
15
15
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
16
16
  const analytics_1 = require("../utils/analytics");
17
+ const metrics_1 = require("../utils/metrics");
17
18
  const anthropic = new sdk_1.default({
18
19
  apiKey: process.env.ANTHROPIC_API_KEY || '',
19
20
  });
@@ -74,6 +75,11 @@ async function conflictCommand() {
74
75
  }
75
76
  // Final commit
76
77
  console.log(chalk_1.default.blue('\n✅ All conflicts resolved!\n'));
78
+ const minutesSaved = (0, metrics_1.logTimeSaved)('conflict:auto');
79
+ if (minutesSaved > 0) {
80
+ console.log(chalk_1.default.gray(`⏱ Saved roughly ${(0, metrics_1.formatMinutes)(minutesSaved)} by letting SnapCommit handle the merge.`));
81
+ console.log();
82
+ }
77
83
  const shouldCommit = await askQuestion(chalk_1.default.yellow('Commit the merge? (Y/n): '));
78
84
  if (shouldCommit.toLowerCase() !== 'n') {
79
85
  try {
@@ -113,7 +113,7 @@ async function handleAICommand(userInput) {
113
113
  async function showStatus() {
114
114
  const status = (0, git_1.getGitStatus)();
115
115
  const branch = (0, git_1.getCurrentBranch)();
116
- const hasChanges = status.staged > 0 || status.unstaged > 0 || status.untracked > 0;
116
+ const hasChanges = status.entries.length > 0;
117
117
  console.log(chalk_1.default.blue(`\nBranch: ${branch}`));
118
118
  if (!hasChanges) {
119
119
  console.log(chalk_1.default.gray('✓ Branch clean - no changes\n'));
@@ -123,10 +123,48 @@ async function showStatus() {
123
123
  if (status.unstaged > 0)
124
124
  console.log(chalk_1.default.yellow(` • ${status.unstaged} modified`));
125
125
  if (status.untracked > 0)
126
- console.log(chalk_1.default.yellow(` • ${status.untracked} new`));
126
+ console.log(chalk_1.default.cyan(` • ${status.untracked} new`));
127
127
  if (status.staged > 0)
128
128
  console.log(chalk_1.default.green(` • ${status.staged} staged`));
129
129
  console.log();
130
+ const stagedFiles = [];
131
+ const unstagedFiles = [];
132
+ const untrackedFiles = [];
133
+ const pushUnique = (list, value) => {
134
+ if (!list.includes(value)) {
135
+ list.push(value);
136
+ }
137
+ };
138
+ status.entries.forEach((entry) => {
139
+ const file = entry.file;
140
+ const stageCode = entry.stagedCode;
141
+ const worktreeCode = entry.worktreeCode;
142
+ if (stageCode === '?' && worktreeCode === '?') {
143
+ pushUnique(untrackedFiles, file);
144
+ return;
145
+ }
146
+ if (stageCode !== ' ' && stageCode !== '?') {
147
+ pushUnique(stagedFiles, file);
148
+ }
149
+ if (worktreeCode !== ' ' && worktreeCode !== '?') {
150
+ pushUnique(unstagedFiles, file);
151
+ }
152
+ });
153
+ const printSection = (label, files, formatter) => {
154
+ if (!files.length) {
155
+ return;
156
+ }
157
+ console.log(label);
158
+ files.forEach((file) => {
159
+ console.log(formatter(file));
160
+ });
161
+ console.log();
162
+ };
163
+ printSection(chalk_1.default.green('Staged:'), stagedFiles, (file) => chalk_1.default.green(` ✓ ${file}`));
164
+ printSection(chalk_1.default.yellow('Modified (unstaged):'), unstagedFiles, (file) => chalk_1.default.yellow(` ✎ ${file}`));
165
+ printSection(chalk_1.default.cyan('Untracked:'), untrackedFiles, (file) => chalk_1.default.cyan(` + ${file}`));
166
+ console.log(chalk_1.default.gray('Tip: say "show diff <file>", "stage <file>", or "commit these changes" next.'));
167
+ console.log();
130
168
  }
131
169
  /**
132
170
  * Execute commit - EXACTLY like Cursor!
@@ -76,6 +76,7 @@ async function onboardCommand() {
76
76
  console.log(chalk_1.default.cyan(' snapcommit doctor ') + chalk_1.default.gray('→ Check setup'));
77
77
  console.log(chalk_1.default.cyan(' snapcommit stats ') + chalk_1.default.gray('→ See your progress'));
78
78
  console.log(chalk_1.default.cyan(' snap autopilot ') + chalk_1.default.gray('→ AI workflows for conflicts & releases'));
79
+ console.log(chalk_1.default.cyan(' snap stats ') + chalk_1.default.gray('→ Track hours saved by AI'));
79
80
  console.log(chalk_1.default.cyan(' snapcommit --help ') + chalk_1.default.gray('→ All commands'));
80
81
  console.log();
81
82
  console.log(chalk_1.default.yellow.bold('💡 Pro tip: ') + chalk_1.default.white('Use') + chalk_1.default.cyan(' bq ') + chalk_1.default.white('for instant commits!'));
@@ -9,6 +9,7 @@ const git_1 = require("../utils/git");
9
9
  const database_1 = require("../db/database");
10
10
  const auth_1 = require("../lib/auth");
11
11
  const analytics_1 = require("../utils/analytics");
12
+ const metrics_1 = require("../utils/metrics");
12
13
  // API URL - defaults to production, can be overridden for development
13
14
  const API_BASE_URL = process.env.SNAPCOMMIT_API_URL || 'https://www.snapcommit.dev';
14
15
  /**
@@ -123,6 +124,11 @@ async function quickCommand() {
123
124
  deletions: stats.deletions,
124
125
  timestamp: Date.now(),
125
126
  });
127
+ const minutesSaved = (0, metrics_1.logTimeSaved)('commit:quick');
128
+ if (minutesSaved > 0) {
129
+ console.log(chalk_1.default.gray(` ⏱ Saved ~${(0, metrics_1.formatMinutes)(minutesSaved)} with quick commit automation.`));
130
+ console.log();
131
+ }
126
132
  }
127
133
  catch (error) {
128
134
  console.log(chalk_1.default.red('\n❌ Commit failed\n'));
@@ -8,6 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const database_1 = require("../db/database");
9
9
  const dopamine_1 = require("../utils/dopamine");
10
10
  const heatmap_1 = require("../utils/heatmap");
11
+ const metrics_1 = require("../utils/metrics");
11
12
  function statsCommand() {
12
13
  console.log(chalk_1.default.blue.bold('\n📊 Your SnapCommit Stats\n'));
13
14
  // Show dopamine stats first (most engaging)
@@ -30,6 +31,18 @@ function statsCommand() {
30
31
  { label: 'Commits', value: statsAll.totalCommits, color: 'green' },
31
32
  { label: 'Commands', value: statsAll.totalCommands, color: 'cyan' },
32
33
  ]));
34
+ const timeSaved = (0, metrics_1.getTimeSavedSummary)();
35
+ if (timeSaved.totalMinutes > 0) {
36
+ console.log(chalk_1.default.white.bold('\nTime Saved With SnapCommit:'));
37
+ console.log(chalk_1.default.green(` ≈ ${(0, metrics_1.formatMinutes)(Math.round(timeSaved.totalMinutes))} saved by AI assistance so far`));
38
+ const topEvents = Object.entries(timeSaved.breakdown)
39
+ .sort(([, a], [, b]) => (b.minutes ?? 0) - (a.minutes ?? 0))
40
+ .slice(0, 3);
41
+ topEvents.forEach(([eventId, data]) => {
42
+ console.log(chalk_1.default.gray(` • ${formatEventLabel(eventId)} → ${(0, metrics_1.formatMinutes)(Math.round(data.minutes))} (${data.count} run${data.count === 1 ? '' : 's'})`));
43
+ });
44
+ console.log();
45
+ }
33
46
  // Commit streak
34
47
  const streak = calculateStreak(statsAll.recentCommits);
35
48
  if (streak > 0) {
@@ -71,6 +84,16 @@ function statsCommand() {
71
84
  console.log(chalk_1.default.gray('💡 Keep building with SnapCommit!'));
72
85
  console.log();
73
86
  }
87
+ function formatEventLabel(eventId) {
88
+ const table = {
89
+ 'autopilot:conflict-crusher': 'Autopilot – Conflict Crusher',
90
+ 'autopilot:release-ready': 'Autopilot – Release Ready',
91
+ 'commit:interactive': 'Interactive AI commits',
92
+ 'commit:quick': 'Quick commits',
93
+ 'conflict:auto': 'AI conflict resolution',
94
+ };
95
+ return table[eventId] ?? eventId;
96
+ }
74
97
  function createStatCard(title, stats) {
75
98
  const width = 32;
76
99
  const border = '─'.repeat(width);
@@ -91,7 +91,7 @@ async function uninstallCommand() {
91
91
  }
92
92
  // Feedback request
93
93
  console.log(chalk_1.default.yellow('💬 We\'d love to know why you\'re leaving:'));
94
- console.log(chalk_1.default.gray(' Email: feedback@snapcommit.dev'));
94
+ console.log(chalk_1.default.gray(' Email: karjunvarma2001@gmail.com'));
95
95
  console.log(chalk_1.default.gray(' Or just reply to this prompt:\n'));
96
96
  const feedback = await askQuestion(chalk_1.default.yellow('Why are you uninstalling? (optional): '));
97
97
  if (feedback.trim()) {
package/dist/utils/git.js CHANGED
@@ -38,8 +38,16 @@ function getGitStatus() {
38
38
  let staged = 0;
39
39
  let unstaged = 0;
40
40
  let untracked = 0;
41
+ const entries = [];
41
42
  lines.forEach((line) => {
42
43
  const statusCode = line.substring(0, 2);
44
+ const file = line.substring(3).trim();
45
+ entries.push({
46
+ code: statusCode,
47
+ file,
48
+ stagedCode: statusCode[0],
49
+ worktreeCode: statusCode[1],
50
+ });
43
51
  if (statusCode[0] !== ' ' && statusCode[0] !== '?')
44
52
  staged++;
45
53
  if (statusCode[1] !== ' ')
@@ -47,7 +55,7 @@ function getGitStatus() {
47
55
  if (statusCode[0] === '?' && statusCode[1] === '?')
48
56
  untracked++;
49
57
  });
50
- return { staged, unstaged, untracked };
58
+ return { staged, unstaged, untracked, entries };
51
59
  }
52
60
  catch (error) {
53
61
  throw new Error(`Git error: ${error.message}`);
@@ -17,6 +17,10 @@ const DEFAULT_MEMORY = {
17
17
  version: 1,
18
18
  workflows: {},
19
19
  preferences: {},
20
+ metrics: {
21
+ minutesSavedTotal: 0,
22
+ events: {},
23
+ },
20
24
  };
21
25
  function loadMemory() {
22
26
  try {
@@ -31,6 +35,14 @@ function loadMemory() {
31
35
  ...parsed,
32
36
  workflows: { ...DEFAULT_MEMORY.workflows, ...parsed.workflows },
33
37
  preferences: { ...DEFAULT_MEMORY.preferences, ...parsed.preferences },
38
+ metrics: {
39
+ ...DEFAULT_MEMORY.metrics,
40
+ ...parsed.metrics,
41
+ events: {
42
+ ...(DEFAULT_MEMORY.metrics?.events ?? {}),
43
+ ...(parsed.metrics?.events ?? {}),
44
+ },
45
+ },
34
46
  };
35
47
  }
36
48
  catch {
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logTimeSaved = logTimeSaved;
4
+ exports.getTimeSavedSummary = getTimeSavedSummary;
5
+ exports.formatMinutes = formatMinutes;
6
+ const memory_1 = require("./memory");
7
+ const telemetry_1 = require("./telemetry");
8
+ const DEFAULT_TIME_SAVED_MINUTES = {
9
+ 'autopilot:conflict-crusher': 25,
10
+ 'autopilot:release-ready': 18,
11
+ 'autopilot:pr-polish': 15,
12
+ 'commit:interactive': 4,
13
+ 'commit:quick': 2,
14
+ 'conflict:auto': 20,
15
+ };
16
+ function logTimeSaved(eventId, minutesOverride) {
17
+ const minutes = minutesOverride ?? DEFAULT_TIME_SAVED_MINUTES[eventId] ?? 0;
18
+ if (!minutes || minutes <= 0) {
19
+ return 0;
20
+ }
21
+ let recordedMinutes = minutes;
22
+ (0, memory_1.updateMemory)((memory) => {
23
+ const metrics = memory.metrics || { minutesSavedTotal: 0, events: {} };
24
+ const current = metrics.events?.[eventId] ?? {
25
+ minutes: 0,
26
+ count: 0,
27
+ lastUpdated: Date.now(),
28
+ };
29
+ const events = {
30
+ ...(metrics.events || {}),
31
+ [eventId]: {
32
+ minutes: current.minutes + minutes,
33
+ count: current.count + 1,
34
+ lastUpdated: Date.now(),
35
+ },
36
+ };
37
+ const minutesSavedTotal = (metrics.minutesSavedTotal ?? 0) + minutes;
38
+ recordedMinutes = minutes;
39
+ return {
40
+ ...memory,
41
+ metrics: {
42
+ minutesSavedTotal,
43
+ events,
44
+ },
45
+ };
46
+ });
47
+ (0, telemetry_1.recordTelemetry)('time_saved', { eventId, minutes: recordedMinutes });
48
+ return recordedMinutes;
49
+ }
50
+ function getTimeSavedSummary() {
51
+ const memory = (0, memory_1.loadMemory)();
52
+ return {
53
+ totalMinutes: memory.metrics?.minutesSavedTotal ?? 0,
54
+ breakdown: memory.metrics?.events ?? {},
55
+ };
56
+ }
57
+ function formatMinutes(minutes) {
58
+ if (minutes < 60) {
59
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
60
+ }
61
+ const hours = Math.floor(minutes / 60);
62
+ const remaining = minutes % 60;
63
+ if (remaining === 0) {
64
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
65
+ }
66
+ return `${hours}h ${remaining}m`;
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.11.0",
3
+ "version": "3.11.2",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {