@snapcommit/cli 3.9.21 → 3.11.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.
@@ -0,0 +1,443 @@
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 WORKFLOWS = [
20
+ {
21
+ id: 'conflict-crusher',
22
+ name: 'Conflict Crusher',
23
+ headline: 'Resolve nasty merge conflicts with AI — fast.',
24
+ description: 'Diagnose merge conflicts, run the AI conflict wizard, and guide you through the final checks so you can finish the merge with confidence.',
25
+ idealFor: ['merge commits', 'rebases', 'cherry-picks'],
26
+ prerequisites: ['Repository has merge conflicts (status shows "both modified")'],
27
+ steps: [
28
+ {
29
+ id: 'scan',
30
+ title: 'Scan for conflicts',
31
+ description: 'Review git status and list conflicted files.',
32
+ action: async (ctx) => {
33
+ const conflicts = getConflictedFiles();
34
+ if (conflicts.length === 0) {
35
+ (0, ui_1.displayWarning)('No merge conflicts detected.', [
36
+ 'If you just merged, run `git status` to double-check.',
37
+ 'You can still run the wizard, but it may exit early.',
38
+ ]);
39
+ }
40
+ else {
41
+ ctx.recordInsight('conflictsBefore', conflicts);
42
+ (0, ui_1.displayInfo)('Conflicted files detected', conflicts);
43
+ }
44
+ },
45
+ },
46
+ {
47
+ id: 'ai-resolution',
48
+ title: 'Run AI conflict resolution',
49
+ description: 'Launch the AI-powered conflict wizard. It will attempt to resolve all conflicts automatically and only fall back to manual guidance if needed.',
50
+ action: async () => {
51
+ await (0, conflict_1.conflictCommand)();
52
+ },
53
+ },
54
+ {
55
+ id: 'verify',
56
+ title: 'Verify conflicts are resolved',
57
+ description: 'Double-check that no conflicted files remain.',
58
+ action: async (ctx) => {
59
+ const remaining = getConflictedFiles();
60
+ if (remaining.length === 0) {
61
+ ctx.recordInsight('conflictsAfter', 'None 🎉');
62
+ (0, ui_1.displaySuccess)('All conflicts resolved!', [
63
+ 'Run your tests and finish the merge when you are ready.',
64
+ ]);
65
+ }
66
+ else {
67
+ ctx.recordInsight('conflictsAfter', `${remaining.length} remaining`);
68
+ (0, ui_1.displayWarning)('Conflicts still remain.', remaining);
69
+ }
70
+ },
71
+ },
72
+ {
73
+ id: 'post-merge',
74
+ title: 'Optional: run safety checks',
75
+ description: 'Run tests or lint to ensure the merge is safe before committing.',
76
+ skippable: true,
77
+ action: async (ctx) => {
78
+ const defaultCommand = getPreferredTestCommand();
79
+ const shouldRun = ctx.autoContinue ||
80
+ (await (0, prompt_1.promptConfirm)('Run project tests now?', Boolean(defaultCommand)));
81
+ if (!shouldRun) {
82
+ ctx.recordInsight('postMergeChecks', 'Skipped');
83
+ return;
84
+ }
85
+ const command = ctx.autoContinue
86
+ ? defaultCommand || ''
87
+ : await (0, prompt_1.promptInput)('Command to run', defaultCommand || 'npm test');
88
+ if (!command) {
89
+ ctx.recordInsight('postMergeChecks', 'Skipped (no command)');
90
+ return;
91
+ }
92
+ try {
93
+ await runShellCommand(command);
94
+ (0, memory_1.rememberPreference)('autopilot', 'testCommand', command);
95
+ ctx.recordInsight('postMergeChecks', `Passed (${command})`);
96
+ (0, ui_1.displaySuccess)('Tests completed successfully.');
97
+ }
98
+ catch (error) {
99
+ ctx.recordInsight('postMergeChecks', `Failed (${command})`);
100
+ (0, ui_1.displayError)('Test command failed.', [
101
+ error.message,
102
+ 'Fix the failures and re-run the command.',
103
+ ]);
104
+ }
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ id: 'release-ready',
111
+ name: 'Release Readiness',
112
+ headline: 'Stage, test, and craft the perfect release commit.',
113
+ description: 'Stage changes, run automated checks, generate an AI commit message, and prepare to open a pull request.',
114
+ idealFor: ['release branches', 'end-of-day handoff', 'pre-PR polish'],
115
+ prerequisites: ['Project builds and tests locally', 'Authentication configured'],
116
+ steps: [
117
+ {
118
+ id: 'status',
119
+ title: 'Assess working tree',
120
+ description: 'Check for staged/unstaged files and current branch.',
121
+ action: async (ctx) => {
122
+ const status = (0, git_1.getGitStatus)();
123
+ const branch = (0, git_1.getCurrentBranch)();
124
+ ctx.recordInsight('branch', branch && branch !== 'unknown' ? branch : 'unknown branch');
125
+ (0, ui_1.displayInfo)('Repository status', [
126
+ `Branch: ${chalk_1.default.cyan(branch)}`,
127
+ `${status.staged} staged • ${status.unstaged} unstaged • ${status.untracked} untracked`,
128
+ ]);
129
+ },
130
+ },
131
+ {
132
+ id: 'stage',
133
+ title: 'Stage everything (optional)',
134
+ description: 'Stage all changes so the AI can review the full diff.',
135
+ skippable: true,
136
+ action: async (ctx) => {
137
+ const shouldStage = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Stage all changes?', true));
138
+ if (!shouldStage) {
139
+ ctx.recordInsight('stageAll', 'Skipped');
140
+ return;
141
+ }
142
+ try {
143
+ await runShellCommand('git add -A');
144
+ ctx.recordInsight('stageAll', 'Staged all changes');
145
+ (0, ui_1.displaySuccess)('All changes staged.');
146
+ }
147
+ catch (error) {
148
+ ctx.recordInsight('stageAll', 'Failed to stage');
149
+ (0, ui_1.displayError)('Failed to stage files.', [error.message]);
150
+ }
151
+ },
152
+ },
153
+ {
154
+ id: 'tests',
155
+ title: 'Run test suite',
156
+ description: 'Run your test command to ensure everything passes before committing.',
157
+ skippable: true,
158
+ action: async (ctx) => {
159
+ const defaultCommand = getPreferredTestCommand();
160
+ const shouldRun = ctx.autoContinue ||
161
+ (await (0, prompt_1.promptConfirm)('Run tests before committing?', Boolean(defaultCommand)));
162
+ if (!shouldRun) {
163
+ ctx.recordInsight('tests', 'Skipped');
164
+ return;
165
+ }
166
+ const command = ctx.autoContinue
167
+ ? defaultCommand || ''
168
+ : await (0, prompt_1.promptInput)('Test command', defaultCommand || 'npm test');
169
+ if (!command) {
170
+ ctx.recordInsight('tests', 'Skipped (no command)');
171
+ return;
172
+ }
173
+ try {
174
+ await runShellCommand(command);
175
+ (0, memory_1.rememberPreference)('autopilot', 'testCommand', command);
176
+ ctx.recordInsight('tests', `Passed (${command})`);
177
+ (0, ui_1.displaySuccess)('Tests passed successfully.');
178
+ }
179
+ catch (error) {
180
+ ctx.recordInsight('tests', `Failed (${command})`);
181
+ (0, ui_1.displayError)('Tests failed.', [
182
+ error.message,
183
+ 'Fix the tests or rerun with --auto to skip this step.',
184
+ ]);
185
+ }
186
+ },
187
+ },
188
+ {
189
+ id: 'commit',
190
+ title: 'Generate AI-powered commit',
191
+ description: 'Use the interactive commit flow to create a polished commit message and finalize your changes.',
192
+ action: async () => {
193
+ await (0, commit_1.commitCommand)();
194
+ },
195
+ },
196
+ {
197
+ id: 'quick-summary',
198
+ title: 'Optional: quick commit instead',
199
+ description: 'If you prefer a one-click commit, run the quick commit flow.',
200
+ skippable: true,
201
+ action: async (ctx) => {
202
+ const shouldRun = ctx.autoContinue || (await (0, prompt_1.promptConfirm)('Run quick commit now?', false));
203
+ if (!shouldRun) {
204
+ ctx.recordInsight('quickCommit', 'Skipped');
205
+ return;
206
+ }
207
+ await (0, quick_1.quickCommand)();
208
+ ctx.recordInsight('quickCommit', 'Executed');
209
+ },
210
+ },
211
+ ],
212
+ },
213
+ ];
214
+ async function autopilotCommand(workflowId, rawOptions) {
215
+ const options = {
216
+ workflowId,
217
+ auto: rawOptions?.auto,
218
+ planOnly: rawOptions?.planOnly,
219
+ };
220
+ const authConfig = await (0, auth_1.ensureAuth)();
221
+ if (!authConfig) {
222
+ console.log(chalk_1.default.red('\n❌ Authentication required to use SnapCommit Autopilot\n'));
223
+ return;
224
+ }
225
+ if (!(0, git_1.isGitRepo)()) {
226
+ console.log(chalk_1.default.red('\n❌ Not a git repository'));
227
+ console.log(chalk_1.default.gray(' Autopilot needs to run inside an initialized git repo.\n'));
228
+ return;
229
+ }
230
+ const workflow = await selectWorkflow(options.workflowId);
231
+ if (!workflow) {
232
+ (0, ui_1.displayError)('No matching workflow found.', [
233
+ 'Run `snap autopilot` without arguments to see the available options.',
234
+ ]);
235
+ return;
236
+ }
237
+ console.log();
238
+ console.log(chalk_1.default.inverse(` AUTOPILOT • ${workflow.name.toUpperCase()} `));
239
+ console.log(chalk_1.default.gray(workflow.headline));
240
+ console.log();
241
+ if (workflow.prerequisites?.length) {
242
+ (0, ui_1.displayInfo)('Prerequisites', workflow.prerequisites);
243
+ }
244
+ (0, memory_1.rememberPreference)('autopilot', 'preferredWorkflow', workflow.id);
245
+ if (options.planOnly) {
246
+ presentPlan(workflow);
247
+ return;
248
+ }
249
+ const result = await runWorkflow(workflow, {
250
+ summary: workflow.headline,
251
+ autoContinue: options.auto ?? false,
252
+ });
253
+ presentSummary(workflow, result);
254
+ }
255
+ async function selectWorkflow(preferredId) {
256
+ if (preferredId) {
257
+ const byId = WORKFLOWS.find((wf) => wf.id === preferredId || wf.id === preferredId.toLowerCase());
258
+ if (byId) {
259
+ return byId;
260
+ }
261
+ const fuzzy = WORKFLOWS.find((wf) => wf.name.toLowerCase().startsWith(preferredId.toLowerCase()));
262
+ if (fuzzy) {
263
+ return fuzzy;
264
+ }
265
+ }
266
+ const choice = await (0, prompt_1.promptSelect)('Choose a workflow to run:', WORKFLOWS.map((wf) => ({
267
+ label: `${wf.name}`,
268
+ value: wf.id,
269
+ hint: wf.headline,
270
+ })));
271
+ return WORKFLOWS.find((wf) => wf.id === choice);
272
+ }
273
+ async function runWorkflow(workflow, baseContext) {
274
+ const statusByStep = {};
275
+ const insights = [];
276
+ const context = {
277
+ workflow,
278
+ summary: baseContext.summary,
279
+ autoContinue: baseContext.autoContinue,
280
+ stepStatus: statusByStep,
281
+ setStepStatus: (stepId, status) => {
282
+ statusByStep[stepId] = status;
283
+ },
284
+ recordInsight: (key, value) => {
285
+ const stringify = typeof value === 'string'
286
+ ? value
287
+ : Array.isArray(value)
288
+ ? value.join(', ')
289
+ : JSON.stringify(value, null, 2);
290
+ const existingIndex = insights.findIndex((insight) => insight.key === key);
291
+ const label = key
292
+ .split(/(?=[A-Z])/)
293
+ .join(' ')
294
+ .replace(/\b\w/g, (char) => char.toUpperCase());
295
+ if (existingIndex >= 0) {
296
+ insights[existingIndex] = { key, label, value: stringify };
297
+ }
298
+ else {
299
+ insights.push({ key, label, value: stringify });
300
+ }
301
+ },
302
+ };
303
+ for (let i = 0; i < workflow.steps.length; i += 1) {
304
+ const step = workflow.steps[i];
305
+ const position = `${i + 1}/${workflow.steps.length}`;
306
+ console.log(chalk_1.default.bold(`\n${position} • ${step.title}`));
307
+ console.log(chalk_1.default.gray(step.description));
308
+ context.setStepStatus(step.id, 'pending');
309
+ if (!context.autoContinue && step.skippable) {
310
+ const runStep = await (0, prompt_1.promptConfirm)('Run this step?', true);
311
+ if (!runStep) {
312
+ context.setStepStatus(step.id, 'skipped');
313
+ continue;
314
+ }
315
+ }
316
+ context.setStepStatus(step.id, 'running');
317
+ try {
318
+ await step.action(context);
319
+ context.setStepStatus(step.id, 'completed');
320
+ }
321
+ catch (error) {
322
+ context.setStepStatus(step.id, 'failed');
323
+ (0, ui_1.displayError)('Step failed.', [
324
+ error.message || 'Unknown error occurred',
325
+ 'Resolve the issue and re-run Autopilot.',
326
+ ]);
327
+ const continueWorkflow = context.autoContinue || (await (0, prompt_1.promptConfirm)('Continue to the next step?', true));
328
+ if (!continueWorkflow) {
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ return { statusByStep, insights };
334
+ }
335
+ function presentPlan(workflow) {
336
+ console.log(chalk_1.default.bold('\nWorkflow plan:\n'));
337
+ workflow.steps.forEach((step, index) => {
338
+ const bullet = chalk_1.default.gray(`${index + 1}.`);
339
+ const title = chalk_1.default.white(step.title);
340
+ const optional = step.skippable ? chalk_1.default.yellow(' (optional)') : '';
341
+ console.log(`${bullet} ${title}${optional}`);
342
+ console.log(chalk_1.default.gray(` ${step.description}`));
343
+ });
344
+ console.log();
345
+ }
346
+ function presentSummary(workflow, result) {
347
+ const rows = workflow.steps.map((step) => {
348
+ const status = result.statusByStep[step.id] ?? 'pending';
349
+ let symbol = chalk_1.default.gray('•');
350
+ switch (status) {
351
+ case 'completed':
352
+ symbol = chalk_1.default.green('✓');
353
+ break;
354
+ case 'skipped':
355
+ symbol = chalk_1.default.yellow('↷');
356
+ break;
357
+ case 'failed':
358
+ symbol = chalk_1.default.red('✗');
359
+ break;
360
+ case 'running':
361
+ symbol = chalk_1.default.blue('…');
362
+ break;
363
+ default:
364
+ symbol = chalk_1.default.gray('•');
365
+ }
366
+ return [symbol, step.title, step.description];
367
+ });
368
+ console.log();
369
+ console.log((0, ui_1.createTable)(['', 'Step', 'Details'], rows));
370
+ if (result.insights.length) {
371
+ console.log();
372
+ console.log(chalk_1.default.bold('Insights & artifacts:'));
373
+ result.insights.forEach((insight) => {
374
+ console.log(` ${chalk_1.default.cyan(insight.label)}: ${insight.value}`);
375
+ });
376
+ }
377
+ console.log();
378
+ (0, ui_1.displaySuccess)(`${workflow.name} workflow completed.`, [
379
+ 'Re-run with --plan to preview or --auto to run without prompts.',
380
+ ]);
381
+ }
382
+ function getConflictedFiles() {
383
+ try {
384
+ const output = (0, child_process_1.execSync)('git status --short', {
385
+ encoding: 'utf-8',
386
+ stdio: ['ignore', 'pipe', 'pipe'],
387
+ });
388
+ const lines = output
389
+ .split('\n')
390
+ .map((line) => line.trim())
391
+ .filter(Boolean);
392
+ return lines
393
+ .filter((line) => line.startsWith('UU') || line.startsWith('AA') || line.startsWith('DD'))
394
+ .map((line) => line.substring(3));
395
+ }
396
+ catch {
397
+ return [];
398
+ }
399
+ }
400
+ async function runShellCommand(command) {
401
+ await new Promise((resolve, reject) => {
402
+ const child = (0, child_process_1.spawn)(command, { shell: true, stdio: 'inherit' });
403
+ child.on('close', (code) => {
404
+ if (code === 0) {
405
+ resolve();
406
+ }
407
+ else {
408
+ reject(new Error(`Command "${command}" exited with code ${code}`));
409
+ }
410
+ });
411
+ });
412
+ }
413
+ function getPreferredTestCommand() {
414
+ const remembered = (0, memory_1.recallPreference)('autopilot', 'testCommand');
415
+ if (remembered) {
416
+ return remembered;
417
+ }
418
+ return inferProjectTestCommand();
419
+ }
420
+ function inferProjectTestCommand() {
421
+ const cwd = process.cwd();
422
+ const pkgPath = path_1.default.join(cwd, 'package.json');
423
+ if (!(0, fs_1.existsSync)(pkgPath)) {
424
+ return null;
425
+ }
426
+ try {
427
+ const raw = (0, fs_1.readFileSync)(pkgPath, 'utf-8');
428
+ const parsed = JSON.parse(raw);
429
+ if (parsed.scripts?.test && parsed.scripts.test !== 'echo "Error: no test specified" && exit 1') {
430
+ return 'npm test';
431
+ }
432
+ if (parsed.scripts?.['test:ci']) {
433
+ return 'npm run test:ci';
434
+ }
435
+ if (parsed.scripts?.lint) {
436
+ return 'npm run lint';
437
+ }
438
+ }
439
+ catch {
440
+ return null;
441
+ }
442
+ return null;
443
+ }