@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 +1 -1
- package/dist/commands/autopilot.js +306 -3
- package/dist/commands/commit.js +6 -0
- package/dist/commands/conflict.js +6 -0
- package/dist/commands/cursor-style.js +40 -2
- package/dist/commands/onboard.js +1 -0
- package/dist/commands/quick.js +6 -0
- package/dist/commands/stats.js +23 -0
- package/dist/commands/uninstall.js +1 -1
- package/dist/utils/git.js +9 -1
- package/dist/utils/memory.js +12 -0
- package/dist/utils/metrics.js +67 -0
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
379
|
-
|
|
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
|
+
}
|
package/dist/commands/commit.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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!
|
package/dist/commands/onboard.js
CHANGED
|
@@ -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!'));
|
package/dist/commands/quick.js
CHANGED
|
@@ -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'));
|
package/dist/commands/stats.js
CHANGED
|
@@ -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:
|
|
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}`);
|
package/dist/utils/memory.js
CHANGED
|
@@ -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
|
+
}
|