@snapcommit/cli 3.10.0 → 3.11.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.
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
@@ -0,0 +1,464 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.autopilotCommand = autopilotCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const child_process_1 = require("child_process");
9
+ const fs_1 = require("fs");
10
+ const path_1 = __importDefault(require("path"));
11
+ const auth_1 = require("../lib/auth");
12
+ const git_1 = require("../utils/git");
13
+ const ui_1 = require("../utils/ui");
14
+ const prompt_1 = require("../utils/prompt");
15
+ const conflict_1 = require("./conflict");
16
+ const commit_1 = require("./commit");
17
+ const quick_1 = require("./quick");
18
+ const memory_1 = require("../utils/memory");
19
+ const metrics_1 = require("../utils/metrics");
20
+ const WORKFLOWS = [
21
+ {
22
+ id: 'conflict-crusher',
23
+ name: 'Conflict Crusher',
24
+ headline: 'Resolve nasty merge conflicts with AI — fast.',
25
+ description: 'Diagnose merge conflicts, run the AI conflict wizard, and guide you through the final checks so you can finish the merge with confidence.',
26
+ idealFor: ['merge commits', 'rebases', 'cherry-picks'],
27
+ prerequisites: ['Repository has merge conflicts (status shows "both modified")'],
28
+ steps: [
29
+ {
30
+ id: 'scan',
31
+ title: 'Scan for conflicts',
32
+ description: 'Review git status and list conflicted files.',
33
+ action: async (ctx) => {
34
+ const conflicts = getConflictedFiles();
35
+ if (conflicts.length === 0) {
36
+ (0, ui_1.displayWarning)('No merge conflicts detected.', [
37
+ 'If you just merged, run `git status` to double-check.',
38
+ 'You can still run the wizard, but it may exit early.',
39
+ ]);
40
+ }
41
+ else {
42
+ ctx.recordInsight('conflictsBefore', conflicts);
43
+ (0, ui_1.displayInfo)('Conflicted files detected', conflicts);
44
+ }
45
+ },
46
+ },
47
+ {
48
+ id: 'ai-resolution',
49
+ title: 'Run AI conflict resolution',
50
+ description: 'Launch the AI-powered conflict wizard. It will attempt to resolve all conflicts automatically and only fall back to manual guidance if needed.',
51
+ action: async () => {
52
+ await (0, conflict_1.conflictCommand)();
53
+ },
54
+ },
55
+ {
56
+ id: 'verify',
57
+ title: 'Verify conflicts are resolved',
58
+ description: 'Double-check that no conflicted files remain.',
59
+ action: async (ctx) => {
60
+ const remaining = getConflictedFiles();
61
+ if (remaining.length === 0) {
62
+ ctx.recordInsight('conflictsAfter', 'None 🎉');
63
+ (0, ui_1.displaySuccess)('All conflicts resolved!', [
64
+ 'Run your tests and finish the merge when you are ready.',
65
+ ]);
66
+ }
67
+ else {
68
+ ctx.recordInsight('conflictsAfter', `${remaining.length} remaining`);
69
+ (0, ui_1.displayWarning)('Conflicts still remain.', remaining);
70
+ }
71
+ },
72
+ },
73
+ {
74
+ id: 'post-merge',
75
+ title: 'Optional: run safety checks',
76
+ description: 'Run tests or lint to ensure the merge is safe before committing.',
77
+ skippable: true,
78
+ action: async (ctx) => {
79
+ const defaultCommand = getPreferredTestCommand();
80
+ const shouldRun = ctx.autoContinue ||
81
+ (await (0, prompt_1.promptConfirm)('Run project tests now?', Boolean(defaultCommand)));
82
+ if (!shouldRun) {
83
+ ctx.recordInsight('postMergeChecks', 'Skipped');
84
+ return;
85
+ }
86
+ const command = ctx.autoContinue
87
+ ? defaultCommand || ''
88
+ : await (0, prompt_1.promptInput)('Command to run', defaultCommand || 'npm test');
89
+ if (!command) {
90
+ ctx.recordInsight('postMergeChecks', 'Skipped (no command)');
91
+ return;
92
+ }
93
+ try {
94
+ await runShellCommand(command);
95
+ (0, memory_1.rememberPreference)('autopilot', 'testCommand', command);
96
+ ctx.recordInsight('postMergeChecks', `Passed (${command})`);
97
+ (0, ui_1.displaySuccess)('Tests completed successfully.');
98
+ }
99
+ catch (error) {
100
+ ctx.recordInsight('postMergeChecks', `Failed (${command})`);
101
+ (0, ui_1.displayError)('Test command failed.', [
102
+ error.message,
103
+ 'Fix the failures and re-run the command.',
104
+ ]);
105
+ }
106
+ },
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ id: 'release-ready',
112
+ name: 'Release Readiness',
113
+ headline: 'Stage, test, and craft the perfect release commit.',
114
+ description: 'Stage changes, run automated checks, generate an AI commit message, and prepare to open a pull request.',
115
+ idealFor: ['release branches', 'end-of-day handoff', 'pre-PR polish'],
116
+ prerequisites: ['Project builds and tests locally', 'Authentication configured'],
117
+ steps: [
118
+ {
119
+ id: 'status',
120
+ title: 'Assess working tree',
121
+ description: 'Check for staged/unstaged files and current branch.',
122
+ action: async (ctx) => {
123
+ const status = (0, git_1.getGitStatus)();
124
+ const branch = (0, git_1.getCurrentBranch)();
125
+ ctx.recordInsight('branch', branch && branch !== 'unknown' ? branch : 'unknown branch');
126
+ (0, ui_1.displayInfo)('Repository status', [
127
+ `Branch: ${chalk_1.default.cyan(branch)}`,
128
+ `${status.staged} staged • ${status.unstaged} unstaged • ${status.untracked} untracked`,
129
+ ]);
130
+ },
131
+ },
132
+ {
133
+ id: 'stage',
134
+ title: 'Stage everything (optional)',
135
+ description: 'Stage all changes so the AI can review the full diff.',
136
+ skippable: true,
137
+ action: async (ctx) => {
138
+ const shouldStage = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Stage all changes?', true));
139
+ if (!shouldStage) {
140
+ ctx.recordInsight('stageAll', 'Skipped');
141
+ return;
142
+ }
143
+ try {
144
+ await runShellCommand('git add -A');
145
+ ctx.recordInsight('stageAll', 'Staged all changes');
146
+ (0, ui_1.displaySuccess)('All changes staged.');
147
+ }
148
+ catch (error) {
149
+ ctx.recordInsight('stageAll', 'Failed to stage');
150
+ (0, ui_1.displayError)('Failed to stage files.', [error.message]);
151
+ }
152
+ },
153
+ },
154
+ {
155
+ id: 'tests',
156
+ title: 'Run test suite',
157
+ description: 'Run your test command to ensure everything passes before committing.',
158
+ skippable: true,
159
+ action: async (ctx) => {
160
+ const defaultCommand = getPreferredTestCommand();
161
+ const shouldRun = ctx.autoContinue ||
162
+ (await (0, prompt_1.promptConfirm)('Run tests before committing?', Boolean(defaultCommand)));
163
+ if (!shouldRun) {
164
+ ctx.recordInsight('tests', 'Skipped');
165
+ return;
166
+ }
167
+ const command = ctx.autoContinue
168
+ ? defaultCommand || ''
169
+ : await (0, prompt_1.promptInput)('Test command', defaultCommand || 'npm test');
170
+ if (!command) {
171
+ ctx.recordInsight('tests', 'Skipped (no command)');
172
+ return;
173
+ }
174
+ try {
175
+ await runShellCommand(command);
176
+ (0, memory_1.rememberPreference)('autopilot', 'testCommand', command);
177
+ ctx.recordInsight('tests', `Passed (${command})`);
178
+ (0, ui_1.displaySuccess)('Tests passed successfully.');
179
+ }
180
+ catch (error) {
181
+ ctx.recordInsight('tests', `Failed (${command})`);
182
+ (0, ui_1.displayError)('Tests failed.', [
183
+ error.message,
184
+ 'Fix the tests or rerun with --auto to skip this step.',
185
+ ]);
186
+ }
187
+ },
188
+ },
189
+ {
190
+ id: 'commit',
191
+ title: 'Generate AI-powered commit',
192
+ description: 'Use the interactive commit flow to create a polished commit message and finalize your changes.',
193
+ action: async () => {
194
+ await (0, commit_1.commitCommand)();
195
+ },
196
+ },
197
+ {
198
+ id: 'quick-summary',
199
+ title: 'Optional: quick commit instead',
200
+ description: 'If you prefer a one-click commit, run the quick commit flow.',
201
+ skippable: true,
202
+ action: async (ctx) => {
203
+ const shouldRun = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Run quick commit now?', false));
204
+ if (!shouldRun) {
205
+ ctx.recordInsight('quickCommit', 'Skipped');
206
+ return;
207
+ }
208
+ await (0, quick_1.quickCommand)();
209
+ ctx.recordInsight('quickCommit', 'Executed');
210
+ },
211
+ },
212
+ ],
213
+ },
214
+ ];
215
+ const WORKFLOW_TIME_SAVINGS = {
216
+ 'conflict-crusher': 25,
217
+ 'release-ready': 18,
218
+ };
219
+ const WORKFLOW_EVENT_IDS = {
220
+ 'conflict-crusher': 'autopilot:conflict-crusher',
221
+ 'release-ready': 'autopilot:release-ready',
222
+ };
223
+ async function autopilotCommand(workflowId, rawOptions) {
224
+ const options = {
225
+ workflowId,
226
+ auto: rawOptions?.auto,
227
+ planOnly: rawOptions?.planOnly,
228
+ };
229
+ const authConfig = await (0, auth_1.ensureAuth)();
230
+ if (!authConfig) {
231
+ console.log(chalk_1.default.red('\n❌ Authentication required to use SnapCommit Autopilot\n'));
232
+ return;
233
+ }
234
+ if (!(0, git_1.isGitRepo)()) {
235
+ console.log(chalk_1.default.red('\n❌ Not a git repository'));
236
+ console.log(chalk_1.default.gray(' Autopilot needs to run inside an initialized git repo.\n'));
237
+ return;
238
+ }
239
+ const workflow = await selectWorkflow(options.workflowId);
240
+ if (!workflow) {
241
+ (0, ui_1.displayError)('No matching workflow found.', [
242
+ 'Run `snap autopilot` without arguments to see the available options.',
243
+ ]);
244
+ return;
245
+ }
246
+ console.log();
247
+ console.log(chalk_1.default.inverse(` AUTOPILOT • ${workflow.name.toUpperCase()} `));
248
+ console.log(chalk_1.default.gray(workflow.headline));
249
+ console.log();
250
+ if (workflow.prerequisites?.length) {
251
+ (0, ui_1.displayInfo)('Prerequisites', workflow.prerequisites);
252
+ }
253
+ (0, memory_1.rememberPreference)('autopilot', 'preferredWorkflow', workflow.id);
254
+ if (options.planOnly) {
255
+ presentPlan(workflow);
256
+ return;
257
+ }
258
+ const result = await runWorkflow(workflow, {
259
+ summary: workflow.headline,
260
+ autoContinue: options.auto ?? false,
261
+ });
262
+ presentSummary(workflow, result);
263
+ }
264
+ async function selectWorkflow(preferredId) {
265
+ if (preferredId) {
266
+ const byId = WORKFLOWS.find((wf) => wf.id === preferredId || wf.id === preferredId.toLowerCase());
267
+ if (byId) {
268
+ return byId;
269
+ }
270
+ const fuzzy = WORKFLOWS.find((wf) => wf.name.toLowerCase().startsWith(preferredId.toLowerCase()));
271
+ if (fuzzy) {
272
+ return fuzzy;
273
+ }
274
+ }
275
+ const choice = await (0, prompt_1.promptSelect)('Choose a workflow to run:', WORKFLOWS.map((wf) => ({
276
+ label: `${wf.name}`,
277
+ value: wf.id,
278
+ hint: wf.headline,
279
+ })));
280
+ return WORKFLOWS.find((wf) => wf.id === choice);
281
+ }
282
+ async function runWorkflow(workflow, baseContext) {
283
+ const statusByStep = {};
284
+ const insights = [];
285
+ const context = {
286
+ workflow,
287
+ summary: baseContext.summary,
288
+ autoContinue: baseContext.autoContinue,
289
+ stepStatus: statusByStep,
290
+ setStepStatus: (stepId, status) => {
291
+ statusByStep[stepId] = status;
292
+ },
293
+ recordInsight: (key, value) => {
294
+ const stringify = typeof value === 'string'
295
+ ? value
296
+ : Array.isArray(value)
297
+ ? value.join(', ')
298
+ : JSON.stringify(value, null, 2);
299
+ const existingIndex = insights.findIndex((insight) => insight.key === key);
300
+ const label = key
301
+ .split(/(?=[A-Z])/)
302
+ .join(' ')
303
+ .replace(/\b\w/g, (char) => char.toUpperCase());
304
+ if (existingIndex >= 0) {
305
+ insights[existingIndex] = { key, label, value: stringify };
306
+ }
307
+ else {
308
+ insights.push({ key, label, value: stringify });
309
+ }
310
+ },
311
+ };
312
+ for (let i = 0; i < workflow.steps.length; i += 1) {
313
+ const step = workflow.steps[i];
314
+ const position = `${i + 1}/${workflow.steps.length}`;
315
+ console.log(chalk_1.default.bold(`\n${position} • ${step.title}`));
316
+ console.log(chalk_1.default.gray(step.description));
317
+ context.setStepStatus(step.id, 'pending');
318
+ if (!context.autoContinue && step.skippable) {
319
+ const runStep = await (0, prompt_1.promptConfirm)('Run this step?', true);
320
+ if (!runStep) {
321
+ context.setStepStatus(step.id, 'skipped');
322
+ continue;
323
+ }
324
+ }
325
+ context.setStepStatus(step.id, 'running');
326
+ try {
327
+ await step.action(context);
328
+ context.setStepStatus(step.id, 'completed');
329
+ }
330
+ catch (error) {
331
+ context.setStepStatus(step.id, 'failed');
332
+ (0, ui_1.displayError)('Step failed.', [
333
+ error.message || 'Unknown error occurred',
334
+ 'Resolve the issue and re-run Autopilot.',
335
+ ]);
336
+ const continueWorkflow = context.autoContinue || (await (0, prompt_1.promptConfirm)('Continue to the next step?', true));
337
+ if (!continueWorkflow) {
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ return { statusByStep, insights };
343
+ }
344
+ function presentPlan(workflow) {
345
+ console.log(chalk_1.default.bold('\nWorkflow plan:\n'));
346
+ workflow.steps.forEach((step, index) => {
347
+ const bullet = chalk_1.default.gray(`${index + 1}.`);
348
+ const title = chalk_1.default.white(step.title);
349
+ const optional = step.skippable ? chalk_1.default.yellow(' (optional)') : '';
350
+ console.log(`${bullet} ${title}${optional}`);
351
+ console.log(chalk_1.default.gray(` ${step.description}`));
352
+ });
353
+ console.log();
354
+ }
355
+ function presentSummary(workflow, result) {
356
+ const rows = workflow.steps.map((step) => {
357
+ const status = result.statusByStep[step.id] ?? 'pending';
358
+ let symbol = chalk_1.default.gray('•');
359
+ switch (status) {
360
+ case 'completed':
361
+ symbol = chalk_1.default.green('✓');
362
+ break;
363
+ case 'skipped':
364
+ symbol = chalk_1.default.yellow('↷');
365
+ break;
366
+ case 'failed':
367
+ symbol = chalk_1.default.red('✗');
368
+ break;
369
+ case 'running':
370
+ symbol = chalk_1.default.blue('…');
371
+ break;
372
+ default:
373
+ symbol = chalk_1.default.gray('•');
374
+ }
375
+ return [symbol, step.title, step.description];
376
+ });
377
+ console.log();
378
+ console.log((0, ui_1.createTable)(['', 'Step', 'Details'], rows));
379
+ if (result.insights.length) {
380
+ console.log();
381
+ console.log(chalk_1.default.bold('Insights & artifacts:'));
382
+ result.insights.forEach((insight) => {
383
+ console.log(` ${chalk_1.default.cyan(insight.label)}: ${insight.value}`);
384
+ });
385
+ }
386
+ console.log();
387
+ const estimatedMinutes = WORKFLOW_TIME_SAVINGS[workflow.id];
388
+ const completedSteps = workflow.steps.filter((step) => result.statusByStep[step.id] === 'completed').length;
389
+ const eventId = WORKFLOW_EVENT_IDS[workflow.id];
390
+ if (estimatedMinutes && completedSteps > 0 && eventId) {
391
+ (0, metrics_1.logTimeSaved)(eventId, estimatedMinutes);
392
+ (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
393
+ `Time saved: ~${(0, metrics_1.formatMinutes)(estimatedMinutes)} compared to manual steps.`,
394
+ 'Re-run with --plan to preview or --auto to run without prompts.',
395
+ ]);
396
+ }
397
+ else {
398
+ (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
399
+ 'Re-run with --plan to preview or --auto to run without prompts.',
400
+ ]);
401
+ }
402
+ }
403
+ function getConflictedFiles() {
404
+ try {
405
+ const output = (0, child_process_1.execSync)('git status --short', {
406
+ encoding: 'utf-8',
407
+ stdio: ['ignore', 'pipe', 'pipe'],
408
+ });
409
+ const lines = output
410
+ .split('\n')
411
+ .map((line) => line.trim())
412
+ .filter(Boolean);
413
+ return lines
414
+ .filter((line) => line.startsWith('UU') || line.startsWith('AA') || line.startsWith('DD'))
415
+ .map((line) => line.substring(3));
416
+ }
417
+ catch {
418
+ return [];
419
+ }
420
+ }
421
+ async function runShellCommand(command) {
422
+ await new Promise((resolve, reject) => {
423
+ const child = (0, child_process_1.spawn)(command, { shell: true, stdio: 'inherit' });
424
+ child.on('close', (code) => {
425
+ if (code === 0) {
426
+ resolve();
427
+ }
428
+ else {
429
+ reject(new Error(`Command "${command}" exited with code ${code}`));
430
+ }
431
+ });
432
+ });
433
+ }
434
+ function getPreferredTestCommand() {
435
+ const remembered = (0, memory_1.recallPreference)('autopilot', 'testCommand');
436
+ if (remembered) {
437
+ return remembered;
438
+ }
439
+ return inferProjectTestCommand();
440
+ }
441
+ function inferProjectTestCommand() {
442
+ const cwd = process.cwd();
443
+ const pkgPath = path_1.default.join(cwd, 'package.json');
444
+ if (!(0, fs_1.existsSync)(pkgPath)) {
445
+ return null;
446
+ }
447
+ try {
448
+ const raw = (0, fs_1.readFileSync)(pkgPath, 'utf-8');
449
+ const parsed = JSON.parse(raw);
450
+ if (parsed.scripts?.test && parsed.scripts.test !== 'echo "Error: no test specified" && exit 1') {
451
+ return 'npm test';
452
+ }
453
+ if (parsed.scripts?.['test:ci']) {
454
+ return 'npm run test:ci';
455
+ }
456
+ if (parsed.scripts?.lint) {
457
+ return 'npm run lint';
458
+ }
459
+ }
460
+ catch {
461
+ return null;
462
+ }
463
+ return null;
464
+ }
@@ -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 {
@@ -75,6 +75,8 @@ async function onboardCommand() {
75
75
  console.log(chalk_1.default.gray('Other commands:'));
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
+ 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'));
78
80
  console.log(chalk_1.default.cyan(' snapcommit --help ') + chalk_1.default.gray('→ All commands'));
79
81
  console.log();
80
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/index.js CHANGED
@@ -24,6 +24,7 @@ const uninstall_1 = require("./commands/uninstall");
24
24
  const github_connect_1 = require("./commands/github-connect");
25
25
  const telemetry_1 = require("./commands/telemetry");
26
26
  const repl_1 = require("./repl");
27
+ const autopilot_1 = require("./commands/autopilot");
27
28
  // Load environment variables from root .env
28
29
  (0, dotenv_1.config)({ path: path_1.default.join(__dirname, '../../.env') });
29
30
  // Read version from package.json
@@ -128,6 +129,20 @@ program
128
129
  .action(async () => {
129
130
  await (0, conflict_1.conflictCommand)();
130
131
  });
132
+ // Command: snapcommit autopilot
133
+ program
134
+ .command('autopilot [workflowId]')
135
+ .description('Run multi-step AI-assisted workflows (merge recovery, release prep, etc.)')
136
+ .option('--auto', 'Run without confirmations where possible')
137
+ .option('--plan', 'Show the workflow plan without executing steps')
138
+ .action(async (workflowId, cmd) => {
139
+ const opts = cmd.opts();
140
+ await (0, autopilot_1.autopilotCommand)(workflowId, {
141
+ workflowId,
142
+ auto: Boolean(opts.auto),
143
+ planOnly: Boolean(opts.plan),
144
+ });
145
+ });
131
146
  // Command: snapcommit github
132
147
  const githubCmd = program
133
148
  .command('github')
package/dist/repl.js CHANGED
@@ -43,6 +43,9 @@ const auth_1 = require("./lib/auth");
43
43
  const natural_1 = require("./commands/natural");
44
44
  const github_connect_1 = require("./commands/github-connect");
45
45
  const version_1 = require("./utils/version");
46
+ const telemetry_1 = require("./utils/telemetry");
47
+ const settings_1 = require("./utils/settings");
48
+ const TELEMETRY_REPROMPT_INTERVAL_MS = 1000 * 60 * 60 * 24 * 14;
46
49
  /**
47
50
  * Start SnapCommit REPL (Read-Eval-Print-Loop)
48
51
  * Interactive mode for natural language Git commands
@@ -152,7 +155,6 @@ async function startREPL() {
152
155
  output: process.stdout,
153
156
  prompt: chalk_1.default.cyan('snap> '),
154
157
  });
155
- rl.prompt();
156
158
  rl.on('line', async (input) => {
157
159
  if (promptState.resolver) {
158
160
  const resolver = promptState.resolver;
@@ -374,8 +376,43 @@ async function startREPL() {
374
376
  }
375
377
  rl.prompt();
376
378
  });
379
+ await showTelemetryNotice(promptController);
380
+ rl.prompt();
377
381
  rl.on('close', () => {
378
382
  console.log(chalk_1.default.gray('\n👋 See you later! Happy coding!\n'));
379
383
  process.exit(0);
380
384
  });
381
385
  }
386
+ async function showTelemetryNotice(prompt) {
387
+ const settings = (0, settings_1.getSettings)();
388
+ const telemetryOn = (0, telemetry_1.isTelemetryEnabled)();
389
+ const lastPrompt = settings.telemetryPromptedAt ?? 0;
390
+ const shouldPrompt = !telemetryOn &&
391
+ (lastPrompt === 0 || Date.now() - lastPrompt > TELEMETRY_REPROMPT_INTERVAL_MS);
392
+ console.log(chalk_1.default.gray(`📊 Telemetry: ${telemetryOn ? chalk_1.default.green('enabled') : chalk_1.default.red('disabled')} • Manage anytime with ` +
393
+ chalk_1.default.cyan('snap telemetry status | enable | disable')));
394
+ if (!shouldPrompt) {
395
+ if (!telemetryOn) {
396
+ console.log(chalk_1.default.gray(' Help improve SnapCommit by enabling anonymous diagnostics when you are ready.'));
397
+ }
398
+ else {
399
+ console.log(chalk_1.default.gray(' Thanks for helping us prioritise new features!'));
400
+ }
401
+ console.log();
402
+ return;
403
+ }
404
+ console.log();
405
+ console.log(chalk_1.default.yellow.bold('Help us keep SnapCommit fast for everyone!'));
406
+ console.log(chalk_1.default.gray('We collect anonymous usage diagnostics (never your code, repo names, or secrets) to guide feature development.'));
407
+ console.log();
408
+ const enable = await prompt.confirm('Enable anonymous telemetry?', { defaultValue: true });
409
+ (0, telemetry_1.markTelemetryPrompted)();
410
+ if (enable) {
411
+ (0, telemetry_1.setTelemetryEnabled)(true);
412
+ (0, telemetry_1.recordTelemetry)('telemetry_opt_in');
413
+ console.log(chalk_1.default.green('\n✅ Telemetry enabled. Manage anytime with `snap telemetry disable`.\n'));
414
+ }
415
+ else {
416
+ console.log(chalk_1.default.gray('\nNo problem. Enable later with `snap telemetry enable` if you change your mind.\n'));
417
+ }
418
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadMemory = loadMemory;
7
+ exports.saveMemory = saveMemory;
8
+ exports.updateMemory = updateMemory;
9
+ exports.rememberPreference = rememberPreference;
10
+ exports.recallPreference = recallPreference;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const MEMORY_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit');
15
+ const MEMORY_FILE = path_1.default.join(MEMORY_DIR, 'memory.json');
16
+ const DEFAULT_MEMORY = {
17
+ version: 1,
18
+ workflows: {},
19
+ preferences: {},
20
+ metrics: {
21
+ minutesSavedTotal: 0,
22
+ events: {},
23
+ },
24
+ };
25
+ function loadMemory() {
26
+ try {
27
+ if (!fs_1.default.existsSync(MEMORY_FILE)) {
28
+ ensureMemoryFile();
29
+ return { ...DEFAULT_MEMORY };
30
+ }
31
+ const raw = fs_1.default.readFileSync(MEMORY_FILE, 'utf-8');
32
+ const parsed = JSON.parse(raw);
33
+ return {
34
+ ...DEFAULT_MEMORY,
35
+ ...parsed,
36
+ workflows: { ...DEFAULT_MEMORY.workflows, ...parsed.workflows },
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
+ },
46
+ };
47
+ }
48
+ catch {
49
+ return { ...DEFAULT_MEMORY };
50
+ }
51
+ }
52
+ function saveMemory(memory) {
53
+ ensureMemoryFile();
54
+ fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2), 'utf-8');
55
+ }
56
+ function updateMemory(mutator) {
57
+ const current = loadMemory();
58
+ const next = mutator({ ...current });
59
+ saveMemory(next);
60
+ return next;
61
+ }
62
+ function rememberPreference(namespace, key, value) {
63
+ updateMemory((memory) => {
64
+ const preferences = {
65
+ ...(memory.preferences || {}),
66
+ [namespace]: {
67
+ ...(typeof memory.preferences?.[namespace] === 'object'
68
+ ? memory.preferences?.[namespace]
69
+ : {}),
70
+ [key]: value,
71
+ },
72
+ };
73
+ return { ...memory, preferences };
74
+ });
75
+ }
76
+ function recallPreference(namespace, key) {
77
+ const memory = loadMemory();
78
+ const namespacePrefs = memory.preferences?.[namespace];
79
+ if (!namespacePrefs || typeof namespacePrefs !== 'object') {
80
+ return undefined;
81
+ }
82
+ const record = namespacePrefs;
83
+ return record[key];
84
+ }
85
+ function ensureMemoryFile() {
86
+ if (!fs_1.default.existsSync(MEMORY_DIR)) {
87
+ fs_1.default.mkdirSync(MEMORY_DIR, { recursive: true });
88
+ }
89
+ if (!fs_1.default.existsSync(MEMORY_FILE)) {
90
+ fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(DEFAULT_MEMORY, null, 2), 'utf-8');
91
+ }
92
+ }
@@ -0,0 +1,66 @@
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
+ 'commit:interactive': 4,
12
+ 'commit:quick': 2,
13
+ 'conflict:auto': 20,
14
+ };
15
+ function logTimeSaved(eventId, minutesOverride) {
16
+ const minutes = minutesOverride ?? DEFAULT_TIME_SAVED_MINUTES[eventId] ?? 0;
17
+ if (!minutes || minutes <= 0) {
18
+ return 0;
19
+ }
20
+ let recordedMinutes = minutes;
21
+ (0, memory_1.updateMemory)((memory) => {
22
+ const metrics = memory.metrics || { minutesSavedTotal: 0, events: {} };
23
+ const current = metrics.events?.[eventId] ?? {
24
+ minutes: 0,
25
+ count: 0,
26
+ lastUpdated: Date.now(),
27
+ };
28
+ const events = {
29
+ ...(metrics.events || {}),
30
+ [eventId]: {
31
+ minutes: current.minutes + minutes,
32
+ count: current.count + 1,
33
+ lastUpdated: Date.now(),
34
+ },
35
+ };
36
+ const minutesSavedTotal = (metrics.minutesSavedTotal ?? 0) + minutes;
37
+ recordedMinutes = minutes;
38
+ return {
39
+ ...memory,
40
+ metrics: {
41
+ minutesSavedTotal,
42
+ events,
43
+ },
44
+ };
45
+ });
46
+ (0, telemetry_1.recordTelemetry)('time_saved', { eventId, minutes: recordedMinutes });
47
+ return recordedMinutes;
48
+ }
49
+ function getTimeSavedSummary() {
50
+ const memory = (0, memory_1.loadMemory)();
51
+ return {
52
+ totalMinutes: memory.metrics?.minutesSavedTotal ?? 0,
53
+ breakdown: memory.metrics?.events ?? {},
54
+ };
55
+ }
56
+ function formatMinutes(minutes) {
57
+ if (minutes < 60) {
58
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
59
+ }
60
+ const hours = Math.floor(minutes / 60);
61
+ const remaining = minutes % 60;
62
+ if (remaining === 0) {
63
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
64
+ }
65
+ return `${hours}h ${remaining}m`;
66
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.promptConfirm = promptConfirm;
7
+ exports.promptSelect = promptSelect;
8
+ exports.promptInput = promptInput;
9
+ const readline_1 = __importDefault(require("readline"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ function createInterface() {
12
+ return readline_1.default.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+ }
17
+ async function promptConfirm(question, defaultValue = true) {
18
+ const rl = createInterface();
19
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
20
+ return new Promise((resolve) => {
21
+ rl.question(`${chalk_1.default.cyan(question)} ${chalk_1.default.gray(hint)} `, (answer) => {
22
+ rl.close();
23
+ const normalized = answer.trim().toLowerCase();
24
+ if (!normalized) {
25
+ resolve(defaultValue);
26
+ return;
27
+ }
28
+ if (['y', 'yes'].includes(normalized)) {
29
+ resolve(true);
30
+ return;
31
+ }
32
+ if (['n', 'no'].includes(normalized)) {
33
+ resolve(false);
34
+ return;
35
+ }
36
+ resolve(defaultValue);
37
+ });
38
+ });
39
+ }
40
+ async function promptSelect(question, choices, defaultIndex = 0) {
41
+ const rl = createInterface();
42
+ const safeIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
43
+ console.log(chalk_1.default.cyan(question));
44
+ choices.forEach((choice, index) => {
45
+ const prefix = index === safeIndex ? chalk_1.default.green('➤') : chalk_1.default.gray('•');
46
+ const label = `${choice.label}${choice.hint ? chalk_1.default.gray(` — ${choice.hint}`) : ''}`;
47
+ console.log(`${prefix} [${index + 1}] ${label}`);
48
+ });
49
+ console.log();
50
+ return new Promise((resolve) => {
51
+ rl.question(chalk_1.default.cyan(`Select an option [1-${choices.length}] (default: ${safeIndex + 1}): `), (answer) => {
52
+ rl.close();
53
+ const trimmed = answer.trim();
54
+ if (!trimmed) {
55
+ resolve(choices[safeIndex].value);
56
+ return;
57
+ }
58
+ const idx = parseInt(trimmed, 10);
59
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= choices.length) {
60
+ resolve(choices[idx - 1].value);
61
+ }
62
+ else {
63
+ resolve(choices[safeIndex].value);
64
+ }
65
+ });
66
+ });
67
+ }
68
+ async function promptInput(question, defaultValue = '') {
69
+ const rl = createInterface();
70
+ const suffix = defaultValue ? chalk_1.default.gray(` (default: ${defaultValue})`) : '';
71
+ return new Promise((resolve) => {
72
+ rl.question(chalk_1.default.cyan(`${question}${suffix}: `), (answer) => {
73
+ rl.close();
74
+ const trimmed = answer.trim();
75
+ resolve(trimmed || defaultValue);
76
+ });
77
+ });
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.10.0",
3
+ "version": "3.11.1",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {