@snapcommit/cli 3.11.1 → 3.11.3

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.
@@ -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");
@@ -211,14 +245,182 @@ const WORKFLOWS = [
211
245
  },
212
246
  ],
213
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
+ try {
395
+ const pr = await github.createPullRequest({
396
+ title: title || defaultTitle,
397
+ body: body || defaultBody,
398
+ });
399
+ ctx.recordInsight('pullRequest', `#${pr.number} ${pr.title}`);
400
+ ctx.recordInsight('prUrl', pr.html_url);
401
+ (0, ui_1.displaySuccess)('Pull request created.', [
402
+ `#${pr.number} ${pr.title}`,
403
+ pr.html_url,
404
+ ]);
405
+ }
406
+ catch (error) {
407
+ ctx.recordInsight('pullRequest', 'Failed to open PR');
408
+ throw new Error(formatGitHubError(error));
409
+ }
410
+ },
411
+ },
412
+ ],
413
+ },
214
414
  ];
215
415
  const WORKFLOW_TIME_SAVINGS = {
216
416
  'conflict-crusher': 25,
217
417
  'release-ready': 18,
418
+ 'pr-polish': 15,
218
419
  };
219
420
  const WORKFLOW_EVENT_IDS = {
220
421
  'conflict-crusher': 'autopilot:conflict-crusher',
221
422
  'release-ready': 'autopilot:release-ready',
423
+ 'pr-polish': 'autopilot:pr-polish',
222
424
  };
223
425
  async function autopilotCommand(workflowId, rawOptions) {
224
426
  const options = {
@@ -462,3 +664,108 @@ function inferProjectTestCommand() {
462
664
  }
463
665
  return null;
464
666
  }
667
+ function branchHasUpstream() {
668
+ try {
669
+ (0, child_process_1.execSync)('git rev-parse --abbrev-ref --symbolic-full-name @{u}', {
670
+ stdio: ['ignore', 'ignore', 'ignore'],
671
+ });
672
+ return true;
673
+ }
674
+ catch {
675
+ return false;
676
+ }
677
+ }
678
+ function getBranchTrackingStatus() {
679
+ try {
680
+ const firstLine = (0, child_process_1.execSync)('git status -sb', {
681
+ encoding: 'utf-8',
682
+ stdio: ['ignore', 'pipe', 'ignore'],
683
+ })
684
+ .split('\n')[0]
685
+ .trim();
686
+ const match = firstLine.match(/\[(.+)\]/);
687
+ if (match && match[1]) {
688
+ return match[1].replace(',', ' • ');
689
+ }
690
+ return 'Up to date with upstream';
691
+ }
692
+ catch {
693
+ return null;
694
+ }
695
+ }
696
+ function getRecentCommitSummary(limit = 3) {
697
+ try {
698
+ const log = (0, child_process_1.execSync)(`git log -${limit} --oneline`, {
699
+ encoding: 'utf-8',
700
+ stdio: ['ignore', 'pipe', 'ignore'],
701
+ })
702
+ .trim()
703
+ .split('\n')
704
+ .filter(Boolean)
705
+ .map((line) => `- ${line.trim()}`)
706
+ .join('\n');
707
+ return log;
708
+ }
709
+ catch {
710
+ return '';
711
+ }
712
+ }
713
+ function getDefaultPRTitle() {
714
+ try {
715
+ const title = (0, child_process_1.execSync)('git log -1 --pretty=%s', {
716
+ encoding: 'utf-8',
717
+ stdio: ['ignore', 'pipe', 'ignore'],
718
+ }).trim();
719
+ if (title) {
720
+ return title;
721
+ }
722
+ }
723
+ catch {
724
+ // no-op
725
+ }
726
+ const branch = (0, git_1.getCurrentBranch)();
727
+ return branch && branch !== 'unknown' ? `Updates from ${branch}` : 'Updates from SnapCommit';
728
+ }
729
+ function buildPRBody(summary, tests) {
730
+ const lines = [];
731
+ lines.push('## Summary');
732
+ if (summary && summary.trim()) {
733
+ lines.push(summary.trim());
734
+ }
735
+ else {
736
+ lines.push('- Describe the key changes');
737
+ }
738
+ lines.push('');
739
+ lines.push('## Testing');
740
+ if (tests && tests.trim()) {
741
+ lines.push(`- ${tests.trim()}`);
742
+ }
743
+ else {
744
+ lines.push('- [ ] Tests not run');
745
+ }
746
+ lines.push('');
747
+ lines.push('## Checklist');
748
+ lines.push('- [ ] Linked issue / ticket');
749
+ lines.push('- [ ] Added or updated tests');
750
+ lines.push('- [ ] Updated documentation (if needed)');
751
+ return lines.join('\n');
752
+ }
753
+ function formatGitHubError(error) {
754
+ const raw = (error?.message || String(error) || '').trim();
755
+ if (!raw) {
756
+ return 'GitHub request failed. Verify your connection with `snap github status`.';
757
+ }
758
+ if (/not connected/i.test(raw) || /github connect/i.test(raw)) {
759
+ return 'GitHub not connected. Run `snap github connect` and try again.';
760
+ }
761
+ if (/bad credentials/i.test(raw)) {
762
+ return 'GitHub authentication failed. Re-authorize with `snap github connect`.';
763
+ }
764
+ if (/not a github repository/i.test(raw)) {
765
+ return 'Origin remote is missing or not a GitHub repo. Add a GitHub remote and retry.';
766
+ }
767
+ if (/resource not accessible/i.test(raw)) {
768
+ return `${raw} — check that your GitHub user has access to this repository.`;
769
+ }
770
+ return raw;
771
+ }
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.executeCursorStyle = executeCursorStyle;
44
+ exports.showStatus = showStatus;
44
45
  const chalk_1 = __importDefault(require("chalk"));
45
46
  const fs_1 = __importDefault(require("fs"));
46
47
  const child_process_1 = require("child_process");
@@ -113,7 +114,7 @@ async function handleAICommand(userInput) {
113
114
  async function showStatus() {
114
115
  const status = (0, git_1.getGitStatus)();
115
116
  const branch = (0, git_1.getCurrentBranch)();
116
- const hasChanges = status.staged > 0 || status.unstaged > 0 || status.untracked > 0;
117
+ const hasChanges = status.entries.length > 0;
117
118
  console.log(chalk_1.default.blue(`\nBranch: ${branch}`));
118
119
  if (!hasChanges) {
119
120
  console.log(chalk_1.default.gray('✓ Branch clean - no changes\n'));
@@ -123,10 +124,48 @@ async function showStatus() {
123
124
  if (status.unstaged > 0)
124
125
  console.log(chalk_1.default.yellow(` • ${status.unstaged} modified`));
125
126
  if (status.untracked > 0)
126
- console.log(chalk_1.default.yellow(` • ${status.untracked} new`));
127
+ console.log(chalk_1.default.cyan(` • ${status.untracked} new`));
127
128
  if (status.staged > 0)
128
129
  console.log(chalk_1.default.green(` • ${status.staged} staged`));
129
130
  console.log();
131
+ const stagedFiles = [];
132
+ const unstagedFiles = [];
133
+ const untrackedFiles = [];
134
+ const pushUnique = (list, value) => {
135
+ if (!list.includes(value)) {
136
+ list.push(value);
137
+ }
138
+ };
139
+ status.entries.forEach((entry) => {
140
+ const file = entry.file;
141
+ const stageCode = entry.stagedCode;
142
+ const worktreeCode = entry.worktreeCode;
143
+ if (stageCode === '?' && worktreeCode === '?') {
144
+ pushUnique(untrackedFiles, file);
145
+ return;
146
+ }
147
+ if (stageCode !== ' ' && stageCode !== '?') {
148
+ pushUnique(stagedFiles, file);
149
+ }
150
+ if (worktreeCode !== ' ' && worktreeCode !== '?') {
151
+ pushUnique(unstagedFiles, file);
152
+ }
153
+ });
154
+ const printSection = (label, files, formatter) => {
155
+ if (!files.length) {
156
+ return;
157
+ }
158
+ console.log(label);
159
+ files.forEach((file) => {
160
+ console.log(formatter(file));
161
+ });
162
+ console.log();
163
+ };
164
+ printSection(chalk_1.default.green('Staged:'), stagedFiles, (file) => chalk_1.default.green(` ✓ ${file}`));
165
+ printSection(chalk_1.default.yellow('Modified (unstaged):'), unstagedFiles, (file) => chalk_1.default.yellow(` ✎ ${file}`));
166
+ printSection(chalk_1.default.cyan('Untracked:'), untrackedFiles, (file) => chalk_1.default.cyan(` + ${file}`));
167
+ console.log(chalk_1.default.gray('Tip: say "show diff <file>", "stage <file>", or "commit these changes" next.'));
168
+ console.log();
130
169
  }
131
170
  /**
132
171
  * Execute commit - EXACTLY like Cursor!
@@ -19,6 +19,7 @@ exports.switchBranch = switchBranch;
19
19
  exports.mergeBranch = mergeBranch;
20
20
  exports.showLog = showLog;
21
21
  exports.showDiff = showDiff;
22
+ exports.showDiffForFile = showDiffForFile;
22
23
  const child_process_1 = require("child_process");
23
24
  const chalk_1 = __importDefault(require("chalk"));
24
25
  const readline_1 = __importDefault(require("readline"));
@@ -294,6 +295,21 @@ function showDiff(cached = false) {
294
295
  console.log(chalk_1.default.red(`\n❌ Failed to show diff: ${error.message}\n`));
295
296
  }
296
297
  }
298
+ /**
299
+ * Show diff for a specific file
300
+ */
301
+ function showDiffForFile(filePath, cached = false) {
302
+ try {
303
+ const flag = cached ? '--cached' : '';
304
+ const escapedPath = escapeFilePath(filePath.trim());
305
+ console.log(chalk_1.default.bold(`\n📄 Changes in ${filePath.trim()}:\n`));
306
+ (0, child_process_1.execSync)(`git diff ${flag} -- "${escapedPath}"`, { stdio: 'inherit' });
307
+ console.log('');
308
+ }
309
+ catch (error) {
310
+ console.log(chalk_1.default.red(`\n❌ Failed to show diff for ${filePath.trim()}: ${error.message}\n`));
311
+ }
312
+ }
297
313
  /**
298
314
  * Helper: Confirm action
299
315
  */
@@ -309,3 +325,6 @@ async function confirmAction(message) {
309
325
  });
310
326
  });
311
327
  }
328
+ function escapeFilePath(path) {
329
+ return path.replace(/(["\\$`])/g, '\\$1');
330
+ }
@@ -88,6 +88,7 @@ function formatEventLabel(eventId) {
88
88
  const table = {
89
89
  'autopilot:conflict-crusher': 'Autopilot – Conflict Crusher',
90
90
  'autopilot:release-ready': 'Autopilot – Release Ready',
91
+ 'autopilot:pr-polish': 'Autopilot – PR Polish',
91
92
  'commit:interactive': 'Interactive AI commits',
92
93
  'commit:quick': 'Quick commits',
93
94
  'conflict:auto': 'AI conflict resolution',
@@ -210,13 +210,12 @@ async function tryQuickCommands(input, context) {
210
210
  // Status commands
211
211
  if (lower === 'status' || lower === 'show status' || lower === 'what changed') {
212
212
  try {
213
- console.log('');
214
- (0, child_process_1.execSync)('git status', { stdio: 'inherit' });
215
- console.log('');
213
+ const { showStatus } = await Promise.resolve().then(() => __importStar(require('../commands/cursor-style')));
214
+ await showStatus();
216
215
  return true;
217
216
  }
218
217
  catch (error) {
219
- console.log(chalk_1.default.red('\n❌ Failed to get status\n'));
218
+ console.log(chalk_1.default.red(`\n❌ Failed to get status: ${error.message}\n`));
220
219
  return true;
221
220
  }
222
221
  }
@@ -345,7 +344,23 @@ async function tryQuickCommands(input, context) {
345
344
  gitAdvanced.showLog();
346
345
  return true;
347
346
  }
348
- // Git Advanced: Show diff
347
+ // Git Advanced: Show diff for specific file
348
+ const diffFileMatch = input.match(/(?:show|what).*(?:diff|changed).*(?:in|for|of)\s+(.+)/i) ||
349
+ input.match(/show diff (?:for|of|on)\s+(.+)/i) ||
350
+ input.match(/diff (?:for|of|on)\s+(.+)/i);
351
+ if (diffFileMatch) {
352
+ const raw = diffFileMatch[1]
353
+ .trim()
354
+ .replace(/^the\s+/i, '')
355
+ .replace(/^(file|files)\s+/i, '')
356
+ .replace(/\s+please$/i, '')
357
+ .replace(/[?]+$/g, '');
358
+ if (raw.length > 0) {
359
+ gitAdvanced.showDiffForFile(raw);
360
+ return true;
361
+ }
362
+ }
363
+ // Git Advanced: Show diff (general)
349
364
  if (lower === 'diff' || lower === 'show diff' || lower === 'what changed' || lower === 'changes') {
350
365
  gitAdvanced.showDiff();
351
366
  return true;
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}`);
@@ -8,6 +8,7 @@ const telemetry_1 = require("./telemetry");
8
8
  const DEFAULT_TIME_SAVED_MINUTES = {
9
9
  'autopilot:conflict-crusher': 25,
10
10
  'autopilot:release-ready': 18,
11
+ 'autopilot:pr-polish': 15,
11
12
  'commit:interactive': 4,
12
13
  'commit:quick': 2,
13
14
  'conflict:auto': 20,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.11.1",
3
+ "version": "3.11.3",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {