@snapcommit/cli 3.2.0 → 3.3.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.
@@ -140,38 +140,101 @@ 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
+ // Don't close - just resolve and let it clean up
194
+ setImmediate(() => rlFileSelect.close());
195
+ resolve(ans.trim());
196
+ });
197
+ });
198
+ // Process selection
199
+ let filesToStage = [];
200
+ if (!fileSelection || fileSelection.toLowerCase() === 'all' || fileSelection.toLowerCase() === 'a') {
201
+ // Stage all files
202
+ filesToStage = changedFiles.map(f => f.file);
203
+ }
204
+ else {
205
+ // Parse selection (e.g., "1,3,5" or "1-3")
206
+ const selections = fileSelection.split(',').map(s => s.trim());
207
+ const selectedIndices = new Set();
208
+ for (const sel of selections) {
209
+ if (sel.includes('-')) {
210
+ // Range (e.g., "1-3")
211
+ const [start, end] = sel.split('-').map(n => parseInt(n.trim()));
212
+ for (let i = start; i <= end; i++) {
213
+ if (i >= 1 && i <= changedFiles.length) {
214
+ selectedIndices.add(i - 1);
215
+ }
216
+ }
217
+ }
218
+ else {
219
+ // Single number
220
+ const index = parseInt(sel);
221
+ if (index >= 1 && index <= changedFiles.length) {
222
+ selectedIndices.add(index - 1);
223
+ }
224
+ }
225
+ }
226
+ filesToStage = Array.from(selectedIndices).map(i => changedFiles[i].file);
227
+ }
228
+ // Stage selected files
229
+ if (filesToStage.length === 0) {
230
+ console.log(chalk_1.default.gray('\nāœ“ No files selected\n'));
231
+ return;
232
+ }
173
233
  try {
174
- (0, git_1.stageAllChanges)();
234
+ for (const file of filesToStage) {
235
+ (0, child_process_1.execSync)(`git add "${file}"`, { encoding: 'utf-8', stdio: 'pipe' });
236
+ }
237
+ console.log(chalk_1.default.gray(`\nāœ“ Staged ${filesToStage.length} ${filesToStage.length === 1 ? 'file' : 'files'}\n`));
175
238
  }
176
239
  catch (error) {
177
240
  console.log(chalk_1.default.red(`āŒ ${error.message}\n`));
@@ -184,14 +247,15 @@ async function executeCommitWithAI(intent) {
184
247
  console.log(chalk_1.default.cyan('šŸ¤– ') + chalk_1.default.white.bold(commitMessage.split('\n')[0]));
185
248
  console.log();
186
249
  // ONE PROMPT - like Cursor's commit button
187
- const readline = await Promise.resolve().then(() => __importStar(require('readline')));
188
- const rl = readline.createInterface({
250
+ const rlModule2 = await Promise.resolve().then(() => __importStar(require('readline')));
251
+ const rlCommit = rlModule2.createInterface({
189
252
  input: process.stdin,
190
253
  output: process.stdout,
191
254
  });
192
255
  const response = await new Promise((resolve) => {
193
- rl.question(chalk_1.default.gray('→ Press Enter to commit, or edit message: '), (ans) => {
194
- rl.close();
256
+ rlCommit.question(chalk_1.default.gray('→ Press Enter to commit, or edit message: '), (ans) => {
257
+ // Don't close - just resolve and let it clean up
258
+ setImmediate(() => rlCommit.close());
195
259
  resolve(ans.trim());
196
260
  });
197
261
  });
@@ -203,6 +267,23 @@ async function executeCommitWithAI(intent) {
203
267
  try {
204
268
  (0, child_process_1.execSync)(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { encoding: 'utf-8', stdio: 'pipe' });
205
269
  console.log(chalk_1.default.green(`\nāœ“ Committed`));
270
+ // Log commit stats
271
+ try {
272
+ const { logCommit } = await Promise.resolve().then(() => __importStar(require('../db/database')));
273
+ const commitHash = (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
274
+ 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(' ');
275
+ logCommit({
276
+ message: commitMessage,
277
+ hash: commitHash,
278
+ files_changed: filesToStage.length,
279
+ insertions: parseInt(stats[0]) || 0,
280
+ deletions: parseInt(stats[1]) || 0,
281
+ timestamp: Date.now(),
282
+ });
283
+ }
284
+ catch {
285
+ // Silent fail - don't break user experience
286
+ }
206
287
  }
207
288
  catch (error) {
208
289
  console.log(chalk_1.default.red(`\nāŒ Commit failed: ${error.message}\n`));
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.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {