@snapcommit/cli 3.1.0 → 3.3.0

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.
@@ -140,38 +140,100 @@ async function executeCommitWithAI(intent) {
140
140
  }
141
141
  // Count files
142
142
  let fileCount = status.staged + status.unstaged + status.untracked;
143
- // Show changed files (like Cursor sidebar)
144
- console.log(chalk_1.default.blue(`\nšŸ“¦ Changes (${fileCount} ${fileCount === 1 ? 'file' : 'files'}):`));
143
+ // Get all changed files with details
144
+ let changedFiles = [];
145
145
  try {
146
146
  const output = (0, child_process_1.execSync)('git status --short', { encoding: 'utf-8' });
147
- const lines = output.split('\n').filter(line => line.trim()).slice(0, 10); // Show max 10
148
- lines.forEach(line => {
149
- const status = line.substring(0, 2);
147
+ const lines = output.split('\n').filter(line => line.trim());
148
+ changedFiles = lines.map(line => {
149
+ const fileStatus = line.substring(0, 2);
150
150
  const file = line.substring(3);
151
- if (status.includes('M')) {
152
- console.log(chalk_1.default.yellow(` āœŽ ${file}`));
151
+ let symbol = 'āœŽ';
152
+ let color = chalk_1.default.yellow;
153
+ if (fileStatus.includes('M')) {
154
+ symbol = 'āœŽ';
155
+ color = chalk_1.default.yellow;
153
156
  }
154
- else if (status.includes('?')) {
155
- console.log(chalk_1.default.green(` + ${file}`));
157
+ else if (fileStatus.includes('?')) {
158
+ symbol = '+';
159
+ color = chalk_1.default.green;
156
160
  }
157
- else if (status.includes('D')) {
158
- console.log(chalk_1.default.red(` - ${file}`));
161
+ else if (fileStatus.includes('D')) {
162
+ symbol = '-';
163
+ color = chalk_1.default.red;
159
164
  }
165
+ return { file, status: fileStatus, symbol, color };
160
166
  });
161
- if (fileCount > 10) {
162
- console.log(chalk_1.default.gray(` ... and ${fileCount - 10} more`));
163
- }
164
167
  }
165
168
  catch {
166
- if (status.unstaged > 0)
167
- console.log(chalk_1.default.yellow(` • ${status.unstaged} modified`));
168
- if (status.untracked > 0)
169
- console.log(chalk_1.default.green(` • ${status.untracked} new`));
169
+ // Fallback to staging all
170
+ try {
171
+ (0, git_1.stageAllChanges)();
172
+ }
173
+ catch (error) {
174
+ console.log(chalk_1.default.red(`āŒ ${error.message}\n`));
175
+ return;
176
+ }
170
177
  }
178
+ // Show interactive file selection
179
+ console.log(chalk_1.default.blue(`\nšŸ“¦ Changes (${fileCount} ${fileCount === 1 ? 'file' : 'files'}):\n`));
180
+ changedFiles.forEach((item, index) => {
181
+ console.log(chalk_1.default.gray(` ${index + 1}. `) + item.color(`${item.symbol} ${item.file}`));
182
+ });
171
183
  console.log();
172
- // Stage all
184
+ // Interactive file selection
185
+ const rlModule = await Promise.resolve().then(() => __importStar(require('readline')));
186
+ const rlFileSelect = rlModule.createInterface({
187
+ input: process.stdin,
188
+ output: process.stdout,
189
+ });
190
+ const fileSelection = await new Promise((resolve) => {
191
+ rlFileSelect.question(chalk_1.default.cyan('Select files: ') +
192
+ chalk_1.default.gray('(Enter=all, 1-3=specific, 1,3,5=multiple): '), (ans) => {
193
+ rlFileSelect.close();
194
+ resolve(ans.trim());
195
+ });
196
+ });
197
+ // Process selection
198
+ let filesToStage = [];
199
+ if (!fileSelection || fileSelection.toLowerCase() === 'all' || fileSelection.toLowerCase() === 'a') {
200
+ // Stage all files
201
+ filesToStage = changedFiles.map(f => f.file);
202
+ }
203
+ else {
204
+ // Parse selection (e.g., "1,3,5" or "1-3")
205
+ const selections = fileSelection.split(',').map(s => s.trim());
206
+ const selectedIndices = new Set();
207
+ for (const sel of selections) {
208
+ if (sel.includes('-')) {
209
+ // Range (e.g., "1-3")
210
+ const [start, end] = sel.split('-').map(n => parseInt(n.trim()));
211
+ for (let i = start; i <= end; i++) {
212
+ if (i >= 1 && i <= changedFiles.length) {
213
+ selectedIndices.add(i - 1);
214
+ }
215
+ }
216
+ }
217
+ else {
218
+ // Single number
219
+ const index = parseInt(sel);
220
+ if (index >= 1 && index <= changedFiles.length) {
221
+ selectedIndices.add(index - 1);
222
+ }
223
+ }
224
+ }
225
+ filesToStage = Array.from(selectedIndices).map(i => changedFiles[i].file);
226
+ }
227
+ // Stage selected files
228
+ if (filesToStage.length === 0) {
229
+ console.log(chalk_1.default.gray('\nāœ“ No files selected\n'));
230
+ return;
231
+ }
173
232
  try {
174
- (0, git_1.stageAllChanges)();
233
+ for (const file of filesToStage) {
234
+ (0, child_process_1.execSync)(`git add "${file}"`, { encoding: 'utf-8', stdio: 'pipe' });
235
+ }
236
+ console.log(chalk_1.default.gray(`\nāœ“ Staged ${filesToStage.length} ${filesToStage.length === 1 ? 'file' : 'files'}\n`));
175
237
  }
176
238
  catch (error) {
177
239
  console.log(chalk_1.default.red(`āŒ ${error.message}\n`));
@@ -184,14 +246,14 @@ async function executeCommitWithAI(intent) {
184
246
  console.log(chalk_1.default.cyan('šŸ¤– ') + chalk_1.default.white.bold(commitMessage.split('\n')[0]));
185
247
  console.log();
186
248
  // ONE PROMPT - like Cursor's commit button
187
- const readline = await Promise.resolve().then(() => __importStar(require('readline')));
188
- const rl = readline.createInterface({
249
+ const rlModule2 = await Promise.resolve().then(() => __importStar(require('readline')));
250
+ const rlCommit = rlModule2.createInterface({
189
251
  input: process.stdin,
190
252
  output: process.stdout,
191
253
  });
192
254
  const response = await new Promise((resolve) => {
193
- rl.question(chalk_1.default.gray('→ Press Enter to commit, or edit message: '), (ans) => {
194
- rl.close();
255
+ rlCommit.question(chalk_1.default.gray('→ Press Enter to commit, or edit message: '), (ans) => {
256
+ rlCommit.close();
195
257
  resolve(ans.trim());
196
258
  });
197
259
  });
@@ -203,6 +265,23 @@ async function executeCommitWithAI(intent) {
203
265
  try {
204
266
  (0, child_process_1.execSync)(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { encoding: 'utf-8', stdio: 'pipe' });
205
267
  console.log(chalk_1.default.green(`\nāœ“ Committed`));
268
+ // Log commit stats
269
+ try {
270
+ const { logCommit } = await Promise.resolve().then(() => __importStar(require('../db/database')));
271
+ const commitHash = (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
272
+ const stats = (0, child_process_1.execSync)(`git diff HEAD~1 HEAD --numstat | awk '{inserted+=$1; deleted+=$2} END {print inserted" "deleted}'`, { encoding: 'utf-8' }).trim().split(' ');
273
+ logCommit({
274
+ message: commitMessage,
275
+ hash: commitHash,
276
+ files_changed: filesToStage.length,
277
+ insertions: parseInt(stats[0]) || 0,
278
+ deletions: parseInt(stats[1]) || 0,
279
+ timestamp: Date.now(),
280
+ });
281
+ }
282
+ catch {
283
+ // Silent fail - don't break user experience
284
+ }
206
285
  }
207
286
  catch (error) {
208
287
  console.log(chalk_1.default.red(`\nāŒ Commit failed: ${error.message}\n`));
@@ -292,10 +371,14 @@ async function executeGitHubCommand(intent) {
292
371
  }
293
372
  break;
294
373
  case 'pr_merge':
295
- const prNumber = intent.target || intent.options?.number;
374
+ let prNumber = intent.target || intent.options?.number;
375
+ // Smart PR detection: "merge my PR" / "merge this PR"
296
376
  if (!prNumber) {
297
- console.log(chalk_1.default.red('\nāŒ PR number required\n'));
298
- return;
377
+ prNumber = await github.findPRNumber('current');
378
+ if (!prNumber) {
379
+ console.log(chalk_1.default.red('\nāŒ No PR found for current branch\n'));
380
+ return;
381
+ }
299
382
  }
300
383
  console.log(chalk_1.default.blue(`\nšŸ”„ Merging PR #${prNumber}...`));
301
384
  await github.mergePullRequest(prNumber);
@@ -362,10 +445,16 @@ async function executeGitHubCommand(intent) {
362
445
  console.log(chalk_1.default.green(`āœ“ Issue #${reopenIssueNum} reopened\n`));
363
446
  break;
364
447
  case 'pr_review':
365
- const reviewPrNum = intent.target || intent.options?.number;
448
+ let reviewPrNum = intent.target || intent.options?.number;
449
+ // Smart PR detection: "approve this PR" / "approve my PR"
366
450
  if (!reviewPrNum) {
367
- console.log(chalk_1.default.red('\nāŒ PR number required\n'));
368
- return;
451
+ const context = intent.options?.context || 'current';
452
+ reviewPrNum = await github.findPRNumber(context);
453
+ if (!reviewPrNum) {
454
+ console.log(chalk_1.default.red('\nāŒ No PR found for current branch'));
455
+ console.log(chalk_1.default.gray(' Try: "approve PR #123" or create a PR first\n'));
456
+ return;
457
+ }
369
458
  }
370
459
  const reviewType = intent.options?.type || 'APPROVE';
371
460
  const reviewBody = intent.options?.body || '';
@@ -385,10 +474,14 @@ async function executeGitHubCommand(intent) {
385
474
  }
386
475
  break;
387
476
  case 'pr_comment':
388
- const commentPrNum = intent.target || intent.options?.number;
477
+ let commentPrNum = intent.target || intent.options?.number;
478
+ // Smart PR detection
389
479
  if (!commentPrNum) {
390
- console.log(chalk_1.default.red('\nāŒ PR number required\n'));
391
- return;
480
+ commentPrNum = await github.findPRNumber('current');
481
+ if (!commentPrNum) {
482
+ console.log(chalk_1.default.red('\nāŒ No PR found for current branch\n'));
483
+ return;
484
+ }
392
485
  }
393
486
  const prComment = intent.options?.body || intent.options?.comment || '';
394
487
  if (!prComment) {
@@ -416,10 +509,14 @@ async function executeGitHubCommand(intent) {
416
509
  break;
417
510
  case 'pr_show':
418
511
  case 'pr_status':
419
- const showPrNum = intent.target || intent.options?.number;
512
+ let showPrNum = intent.target || intent.options?.number;
513
+ // Smart PR detection: "show my PR" / "show this PR"
420
514
  if (!showPrNum) {
421
- console.log(chalk_1.default.red('\nāŒ PR number required\n'));
422
- return;
515
+ showPrNum = await github.findPRNumber('current');
516
+ if (!showPrNum) {
517
+ console.log(chalk_1.default.red('\nāŒ No PR found for current branch\n'));
518
+ return;
519
+ }
423
520
  }
424
521
  console.log(chalk_1.default.blue(`\nšŸ“‹ PR #${showPrNum}:\n`));
425
522
  const prDetails = await github.getPullRequest(showPrNum);
@@ -442,10 +539,14 @@ async function executeGitHubCommand(intent) {
442
539
  }
443
540
  break;
444
541
  case 'pr_diff':
445
- const diffPrNum = intent.target || intent.options?.number;
542
+ let diffPrNum = intent.target || intent.options?.number;
543
+ // Smart PR detection: "what changed in my PR"
446
544
  if (!diffPrNum) {
447
- console.log(chalk_1.default.red('\nāŒ PR number required\n'));
448
- return;
545
+ diffPrNum = await github.findPRNumber('current');
546
+ if (!diffPrNum) {
547
+ console.log(chalk_1.default.red('\nāŒ No PR found for current branch\n'));
548
+ return;
549
+ }
449
550
  }
450
551
  console.log(chalk_1.default.blue(`\nšŸ“„ PR #${diffPrNum} changes:\n`));
451
552
  const files = await github.getPullRequestFiles(diffPrNum);
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getCurrentRepo = getCurrentRepo;
4
4
  exports.getCurrentBranch = getCurrentBranch;
5
+ exports.findPRNumber = findPRNumber;
6
+ exports.findIssueNumber = findIssueNumber;
5
7
  exports.createPullRequest = createPullRequest;
6
8
  exports.listPullRequests = listPullRequests;
7
9
  exports.getPullRequest = getPullRequest;
@@ -63,6 +65,41 @@ function getCurrentBranch() {
63
65
  return 'main';
64
66
  }
65
67
  }
68
+ /**
69
+ * Find PR number from context (current branch, latest, etc.)
70
+ */
71
+ async function findPRNumber(context) {
72
+ try {
73
+ if (context === 'current' || !context) {
74
+ // Find PR for current branch
75
+ const branch = getCurrentBranch();
76
+ const prs = await listPullRequests({ state: 'open', limit: 50 });
77
+ const pr = prs.find((p) => p.head.ref === branch);
78
+ return pr ? pr.number : null;
79
+ }
80
+ else if (context === 'latest' || context === 'mine') {
81
+ // Get latest PR
82
+ const prs = await listPullRequests({ state: 'open', limit: 1 });
83
+ return prs.length > 0 ? prs[0].number : null;
84
+ }
85
+ return null;
86
+ }
87
+ catch (error) {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Find issue number from context (latest, etc.)
93
+ */
94
+ async function findIssueNumber(context) {
95
+ try {
96
+ const issues = await listIssues({ state: 'open', limit: 1 });
97
+ return issues.length > 0 ? issues[0].number : null;
98
+ }
99
+ catch (error) {
100
+ return null;
101
+ }
102
+ }
66
103
  /**
67
104
  * GitHub API request helper - uses locally stored token
68
105
  */
package/dist/repl.js CHANGED
@@ -89,7 +89,7 @@ async function startREPL() {
89
89
  console.log(chalk_1.default.gray(' GitHub: ') + chalk_1.default.cyan('"create a PR to main"'));
90
90
  console.log(chalk_1.default.gray(' ') + chalk_1.default.cyan('"check CI status"'));
91
91
  }
92
- console.log(chalk_1.default.gray(' Other: ') + chalk_1.default.cyan('cd <path>') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('exit') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('help') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('update\n'));
92
+ console.log(chalk_1.default.gray(' Other: ') + chalk_1.default.cyan('stats') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('cd <path>') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('exit') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('help') + chalk_1.default.gray(' • ') + chalk_1.default.cyan('update\n'));
93
93
  // Show GitHub setup reminder if not connected
94
94
  if (!githubConnected) {
95
95
  console.log(chalk_1.default.yellow.bold('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
@@ -123,6 +123,56 @@ async function startREPL() {
123
123
  rl.close();
124
124
  process.exit(0);
125
125
  }
126
+ // Stats command - show commit stats
127
+ if (line === 'stats' || line.startsWith('stats ')) {
128
+ const parts = line.split(' ');
129
+ const period = parts[1] || 'week';
130
+ let daysBack = 7;
131
+ if (period === 'today')
132
+ daysBack = 1;
133
+ else if (period === 'week')
134
+ daysBack = 7;
135
+ else if (period === 'month')
136
+ daysBack = 30;
137
+ else if (period === 'year')
138
+ daysBack = 365;
139
+ try {
140
+ const { getStats } = await Promise.resolve().then(() => __importStar(require('./db/database')));
141
+ const stats = getStats(daysBack);
142
+ console.log(chalk_1.default.bold.cyan(`\nšŸ“Š Stats (${period}):\n`));
143
+ console.log(chalk_1.default.white(` • ${chalk_1.default.bold.green(stats.totalCommits)} commits`));
144
+ console.log(chalk_1.default.white(` • ${chalk_1.default.bold.green(stats.totalInsertions.toLocaleString())} lines added`));
145
+ console.log(chalk_1.default.white(` • ${chalk_1.default.bold.red(stats.totalDeletions.toLocaleString())} lines removed`));
146
+ // Calculate busiest day
147
+ if (stats.totalCommits > 0) {
148
+ const commitsByDay = {};
149
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
150
+ try {
151
+ const since = Date.now() - daysBack * 24 * 60 * 60 * 1000;
152
+ const sinceDate = new Date(since).toISOString().split('T')[0];
153
+ const logOutput = execSync(`git log --since="${sinceDate}" --format="%ai"`, { encoding: 'utf-8' });
154
+ logOutput.split('\n').filter(line => line.trim()).forEach(line => {
155
+ const day = line.split(' ')[0];
156
+ commitsByDay[day] = (commitsByDay[day] || 0) + 1;
157
+ });
158
+ const busiestDay = Object.entries(commitsByDay).sort(([, a], [, b]) => b - a)[0];
159
+ if (busiestDay) {
160
+ const dayName = new Date(busiestDay[0]).toLocaleDateString('en-US', { weekday: 'long' });
161
+ console.log(chalk_1.default.white(` • Busiest day: ${chalk_1.default.bold.cyan(dayName)} (${busiestDay[1]} commits)`));
162
+ }
163
+ }
164
+ catch {
165
+ // Silent fail
166
+ }
167
+ }
168
+ console.log();
169
+ }
170
+ catch (error) {
171
+ console.log(chalk_1.default.red(`\nāŒ Failed to get stats: ${error.message}\n`));
172
+ }
173
+ rl.prompt();
174
+ return;
175
+ }
126
176
  // CD command - navigate to different repo
127
177
  if (line.startsWith('cd ')) {
128
178
  const targetPath = line.substring(3).trim();
@@ -210,6 +260,7 @@ async function startREPL() {
210
260
  console.log(chalk_1.default.cyan(' "list my pull requests"'));
211
261
  console.log(chalk_1.default.cyan(' "merge PR #123"\n'));
212
262
  console.log(chalk_1.default.white.bold('System Commands:\n'));
263
+ console.log(chalk_1.default.gray(' • ') + chalk_1.default.cyan('stats') + chalk_1.default.gray(' - Show commit stats (stats today/week/month/year)'));
213
264
  console.log(chalk_1.default.gray(' • ') + chalk_1.default.cyan('cd <path>') + chalk_1.default.gray(' - Navigate to different repo'));
214
265
  console.log(chalk_1.default.gray(' • ') + chalk_1.default.cyan('update') + chalk_1.default.gray(' - Update to latest version'));
215
266
  console.log(chalk_1.default.gray(' • ') + chalk_1.default.cyan('exit/quit') + chalk_1.default.gray(' - Exit SnapCommit\n'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {