@snapcommit/cli 3.11.1 → 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.
@@ -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,176 @@ 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
+ 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
+ },
214
408
  ];
215
409
  const WORKFLOW_TIME_SAVINGS = {
216
410
  'conflict-crusher': 25,
217
411
  'release-ready': 18,
412
+ 'pr-polish': 15,
218
413
  };
219
414
  const WORKFLOW_EVENT_IDS = {
220
415
  'conflict-crusher': 'autopilot:conflict-crusher',
221
416
  'release-ready': 'autopilot:release-ready',
417
+ 'pr-polish': 'autopilot:pr-polish',
222
418
  };
223
419
  async function autopilotCommand(workflowId, rawOptions) {
224
420
  const options = {
@@ -462,3 +658,89 @@ function inferProjectTestCommand() {
462
658
  }
463
659
  return null;
464
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
+ }
@@ -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.staged > 0 || status.unstaged > 0 || status.untracked > 0;
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.yellow(` • ${status.untracked} new`));
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/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.2",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {