@snapcommit/cli 3.10.0 → 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
+ }
@@ -75,6 +75,7 @@ async function onboardCommand() {
75
75
  console.log(chalk_1.default.gray('Other commands:'));
76
76
  console.log(chalk_1.default.cyan(' snapcommit doctor ') + chalk_1.default.gray('→ Check setup'));
77
77
  console.log(chalk_1.default.cyan(' snapcommit stats ') + chalk_1.default.gray('→ See your progress'));
78
+ console.log(chalk_1.default.cyan(' snap autopilot ') + chalk_1.default.gray('→ AI workflows for conflicts & releases'));
78
79
  console.log(chalk_1.default.cyan(' snapcommit --help ') + chalk_1.default.gray('→ All commands'));
79
80
  console.log();
80
81
  console.log(chalk_1.default.yellow.bold('💡 Pro tip: ') + chalk_1.default.white('Use') + chalk_1.default.cyan(' bq ') + chalk_1.default.white('for instant commits!'));
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const uninstall_1 = require("./commands/uninstall");
24
24
  const github_connect_1 = require("./commands/github-connect");
25
25
  const telemetry_1 = require("./commands/telemetry");
26
26
  const repl_1 = require("./repl");
27
+ const autopilot_1 = require("./commands/autopilot");
27
28
  // Load environment variables from root .env
28
29
  (0, dotenv_1.config)({ path: path_1.default.join(__dirname, '../../.env') });
29
30
  // Read version from package.json
@@ -128,6 +129,20 @@ program
128
129
  .action(async () => {
129
130
  await (0, conflict_1.conflictCommand)();
130
131
  });
132
+ // Command: snapcommit autopilot
133
+ program
134
+ .command('autopilot [workflowId]')
135
+ .description('Run multi-step AI-assisted workflows (merge recovery, release prep, etc.)')
136
+ .option('--auto', 'Run without confirmations where possible')
137
+ .option('--plan', 'Show the workflow plan without executing steps')
138
+ .action(async (workflowId, cmd) => {
139
+ const opts = cmd.opts();
140
+ await (0, autopilot_1.autopilotCommand)(workflowId, {
141
+ workflowId,
142
+ auto: Boolean(opts.auto),
143
+ planOnly: Boolean(opts.plan),
144
+ });
145
+ });
131
146
  // Command: snapcommit github
132
147
  const githubCmd = program
133
148
  .command('github')
package/dist/repl.js CHANGED
@@ -43,6 +43,9 @@ const auth_1 = require("./lib/auth");
43
43
  const natural_1 = require("./commands/natural");
44
44
  const github_connect_1 = require("./commands/github-connect");
45
45
  const version_1 = require("./utils/version");
46
+ const telemetry_1 = require("./utils/telemetry");
47
+ const settings_1 = require("./utils/settings");
48
+ const TELEMETRY_REPROMPT_INTERVAL_MS = 1000 * 60 * 60 * 24 * 14;
46
49
  /**
47
50
  * Start SnapCommit REPL (Read-Eval-Print-Loop)
48
51
  * Interactive mode for natural language Git commands
@@ -152,7 +155,6 @@ async function startREPL() {
152
155
  output: process.stdout,
153
156
  prompt: chalk_1.default.cyan('snap> '),
154
157
  });
155
- rl.prompt();
156
158
  rl.on('line', async (input) => {
157
159
  if (promptState.resolver) {
158
160
  const resolver = promptState.resolver;
@@ -374,8 +376,43 @@ async function startREPL() {
374
376
  }
375
377
  rl.prompt();
376
378
  });
379
+ await showTelemetryNotice(promptController);
380
+ rl.prompt();
377
381
  rl.on('close', () => {
378
382
  console.log(chalk_1.default.gray('\n👋 See you later! Happy coding!\n'));
379
383
  process.exit(0);
380
384
  });
381
385
  }
386
+ async function showTelemetryNotice(prompt) {
387
+ const settings = (0, settings_1.getSettings)();
388
+ const telemetryOn = (0, telemetry_1.isTelemetryEnabled)();
389
+ const lastPrompt = settings.telemetryPromptedAt ?? 0;
390
+ const shouldPrompt = !telemetryOn &&
391
+ (lastPrompt === 0 || Date.now() - lastPrompt > TELEMETRY_REPROMPT_INTERVAL_MS);
392
+ console.log(chalk_1.default.gray(`📊 Telemetry: ${telemetryOn ? chalk_1.default.green('enabled') : chalk_1.default.red('disabled')} • Manage anytime with ` +
393
+ chalk_1.default.cyan('snap telemetry status | enable | disable')));
394
+ if (!shouldPrompt) {
395
+ if (!telemetryOn) {
396
+ console.log(chalk_1.default.gray(' Help improve SnapCommit by enabling anonymous diagnostics when you are ready.'));
397
+ }
398
+ else {
399
+ console.log(chalk_1.default.gray(' Thanks for helping us prioritise new features!'));
400
+ }
401
+ console.log();
402
+ return;
403
+ }
404
+ console.log();
405
+ console.log(chalk_1.default.yellow.bold('Help us keep SnapCommit fast for everyone!'));
406
+ console.log(chalk_1.default.gray('We collect anonymous usage diagnostics (never your code, repo names, or secrets) to guide feature development.'));
407
+ console.log();
408
+ const enable = await prompt.confirm('Enable anonymous telemetry?', { defaultValue: true });
409
+ (0, telemetry_1.markTelemetryPrompted)();
410
+ if (enable) {
411
+ (0, telemetry_1.setTelemetryEnabled)(true);
412
+ (0, telemetry_1.recordTelemetry)('telemetry_opt_in');
413
+ console.log(chalk_1.default.green('\n✅ Telemetry enabled. Manage anytime with `snap telemetry disable`.\n'));
414
+ }
415
+ else {
416
+ console.log(chalk_1.default.gray('\nNo problem. Enable later with `snap telemetry enable` if you change your mind.\n'));
417
+ }
418
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,80 @@
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.loadMemory = loadMemory;
7
+ exports.saveMemory = saveMemory;
8
+ exports.updateMemory = updateMemory;
9
+ exports.rememberPreference = rememberPreference;
10
+ exports.recallPreference = recallPreference;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const MEMORY_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit');
15
+ const MEMORY_FILE = path_1.default.join(MEMORY_DIR, 'memory.json');
16
+ const DEFAULT_MEMORY = {
17
+ version: 1,
18
+ workflows: {},
19
+ preferences: {},
20
+ };
21
+ function loadMemory() {
22
+ try {
23
+ if (!fs_1.default.existsSync(MEMORY_FILE)) {
24
+ ensureMemoryFile();
25
+ return { ...DEFAULT_MEMORY };
26
+ }
27
+ const raw = fs_1.default.readFileSync(MEMORY_FILE, 'utf-8');
28
+ const parsed = JSON.parse(raw);
29
+ return {
30
+ ...DEFAULT_MEMORY,
31
+ ...parsed,
32
+ workflows: { ...DEFAULT_MEMORY.workflows, ...parsed.workflows },
33
+ preferences: { ...DEFAULT_MEMORY.preferences, ...parsed.preferences },
34
+ };
35
+ }
36
+ catch {
37
+ return { ...DEFAULT_MEMORY };
38
+ }
39
+ }
40
+ function saveMemory(memory) {
41
+ ensureMemoryFile();
42
+ fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2), 'utf-8');
43
+ }
44
+ function updateMemory(mutator) {
45
+ const current = loadMemory();
46
+ const next = mutator({ ...current });
47
+ saveMemory(next);
48
+ return next;
49
+ }
50
+ function rememberPreference(namespace, key, value) {
51
+ updateMemory((memory) => {
52
+ const preferences = {
53
+ ...(memory.preferences || {}),
54
+ [namespace]: {
55
+ ...(typeof memory.preferences?.[namespace] === 'object'
56
+ ? memory.preferences?.[namespace]
57
+ : {}),
58
+ [key]: value,
59
+ },
60
+ };
61
+ return { ...memory, preferences };
62
+ });
63
+ }
64
+ function recallPreference(namespace, key) {
65
+ const memory = loadMemory();
66
+ const namespacePrefs = memory.preferences?.[namespace];
67
+ if (!namespacePrefs || typeof namespacePrefs !== 'object') {
68
+ return undefined;
69
+ }
70
+ const record = namespacePrefs;
71
+ return record[key];
72
+ }
73
+ function ensureMemoryFile() {
74
+ if (!fs_1.default.existsSync(MEMORY_DIR)) {
75
+ fs_1.default.mkdirSync(MEMORY_DIR, { recursive: true });
76
+ }
77
+ if (!fs_1.default.existsSync(MEMORY_FILE)) {
78
+ fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(DEFAULT_MEMORY, null, 2), 'utf-8');
79
+ }
80
+ }
@@ -0,0 +1,78 @@
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.promptConfirm = promptConfirm;
7
+ exports.promptSelect = promptSelect;
8
+ exports.promptInput = promptInput;
9
+ const readline_1 = __importDefault(require("readline"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ function createInterface() {
12
+ return readline_1.default.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+ }
17
+ async function promptConfirm(question, defaultValue = true) {
18
+ const rl = createInterface();
19
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
20
+ return new Promise((resolve) => {
21
+ rl.question(`${chalk_1.default.cyan(question)} ${chalk_1.default.gray(hint)} `, (answer) => {
22
+ rl.close();
23
+ const normalized = answer.trim().toLowerCase();
24
+ if (!normalized) {
25
+ resolve(defaultValue);
26
+ return;
27
+ }
28
+ if (['y', 'yes'].includes(normalized)) {
29
+ resolve(true);
30
+ return;
31
+ }
32
+ if (['n', 'no'].includes(normalized)) {
33
+ resolve(false);
34
+ return;
35
+ }
36
+ resolve(defaultValue);
37
+ });
38
+ });
39
+ }
40
+ async function promptSelect(question, choices, defaultIndex = 0) {
41
+ const rl = createInterface();
42
+ const safeIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
43
+ console.log(chalk_1.default.cyan(question));
44
+ choices.forEach((choice, index) => {
45
+ const prefix = index === safeIndex ? chalk_1.default.green('➤') : chalk_1.default.gray('•');
46
+ const label = `${choice.label}${choice.hint ? chalk_1.default.gray(` — ${choice.hint}`) : ''}`;
47
+ console.log(`${prefix} [${index + 1}] ${label}`);
48
+ });
49
+ console.log();
50
+ return new Promise((resolve) => {
51
+ rl.question(chalk_1.default.cyan(`Select an option [1-${choices.length}] (default: ${safeIndex + 1}): `), (answer) => {
52
+ rl.close();
53
+ const trimmed = answer.trim();
54
+ if (!trimmed) {
55
+ resolve(choices[safeIndex].value);
56
+ return;
57
+ }
58
+ const idx = parseInt(trimmed, 10);
59
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= choices.length) {
60
+ resolve(choices[idx - 1].value);
61
+ }
62
+ else {
63
+ resolve(choices[safeIndex].value);
64
+ }
65
+ });
66
+ });
67
+ }
68
+ async function promptInput(question, defaultValue = '') {
69
+ const rl = createInterface();
70
+ const suffix = defaultValue ? chalk_1.default.gray(` (default: ${defaultValue})`) : '';
71
+ return new Promise((resolve) => {
72
+ rl.question(chalk_1.default.cyan(`${question}${suffix}: `), (answer) => {
73
+ rl.close();
74
+ const trimmed = answer.trim();
75
+ resolve(trimmed || defaultValue);
76
+ });
77
+ });
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {