xoegit 1.1.3 → 1.1.5
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.
- package/README.md +83 -21
- package/dist/cli/analyze.js +142 -4
- package/dist/cli/program.js +3 -1
- package/dist/git/service.js +13 -0
- package/dist/prompts/service.js +29 -2
- package/dist/utils/input.js +46 -0
- package/dist/utils/ui.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,14 +15,18 @@
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
17
|
- **Atomic Commits** — Automatically suggests splitting large changes into multiple logical commits
|
|
18
|
+
- **Execute Mode** — Optionally execute commits with confirmation using `--execute`
|
|
18
19
|
- **Context Aware** — Provide context with `--context` for more accurate commit messages
|
|
20
|
+
- **Explain Mode** — Learn commit crafting with `--explain` to see reasoning behind each grouping
|
|
19
21
|
- **Smart Fallback** — Automatically switches between Gemini models when rate limits are hit
|
|
20
22
|
- **Semantic Commits** — Strictly follows [Conventional Commits](https://www.conventionalcommits.org/)
|
|
21
23
|
- **PR Ready** — Generates ready-to-use PR title and description
|
|
22
24
|
|
|
23
25
|
## How It Works
|
|
24
26
|
|
|
25
|
-
> **Important:** `xoegit` **never** stages your files, commits your changes, or modifies your repository in any way. It only analyzes your changes and provides recommendations that you can review and execute yourself.
|
|
27
|
+
> **Important:** By default, `xoegit` **never** stages your files, commits your changes, or modifies your repository in any way. It only analyzes your changes and provides recommendations that you can review and execute yourself.
|
|
28
|
+
>
|
|
29
|
+
> With the `--execute` flag, you can optionally let `xoegit` execute the suggested commits after your explicit confirmation.
|
|
26
30
|
|
|
27
31
|
You remain in full control of your git workflow.
|
|
28
32
|
|
|
@@ -34,13 +38,19 @@ You remain in full control of your git workflow.
|
|
|
34
38
|
- **Git**: Must be installed and available in your PATH
|
|
35
39
|
- **API Key**: A Google Gemini API key ([get one here](https://aistudio.google.com/))
|
|
36
40
|
|
|
37
|
-
###
|
|
41
|
+
### Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx xoegit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Global Installation (Optional)
|
|
38
48
|
|
|
39
49
|
```bash
|
|
40
50
|
npm install -g xoegit
|
|
41
51
|
```
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
### Install from Source
|
|
44
54
|
|
|
45
55
|
```bash
|
|
46
56
|
git clone git@github.com:ujangdoubleday/xoegit.git
|
|
@@ -56,7 +66,13 @@ Simply run `xoegit` for the first time. It will prompt you for your API Key secu
|
|
|
56
66
|
|
|
57
67
|
## Usage
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
From any git repository, just run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx xoegit
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**or** if installed globally:
|
|
60
76
|
|
|
61
77
|
```bash
|
|
62
78
|
xoegit
|
|
@@ -66,42 +82,57 @@ xoegit
|
|
|
66
82
|
|
|
67
83
|
### Options
|
|
68
84
|
|
|
69
|
-
| Option | Description
|
|
70
|
-
| ---------------------- |
|
|
71
|
-
| `-k, --api-key <key>` | Use specific API key for this session
|
|
72
|
-
| `-c, --context <text>` | Provide context for more accurate suggestions
|
|
73
|
-
| `-
|
|
74
|
-
|
|
|
75
|
-
| `-
|
|
76
|
-
| `-
|
|
85
|
+
| Option | Description |
|
|
86
|
+
| ---------------------- | --------------------------------------------------- |
|
|
87
|
+
| `-k, --api-key <key>` | Use specific API key for this session |
|
|
88
|
+
| `-c, --context <text>` | Provide context for more accurate suggestions |
|
|
89
|
+
| `-e, --execute` | Execute commits after confirmation prompt |
|
|
90
|
+
| `--explain` | Show reasoning behind each commit grouping |
|
|
91
|
+
| `-s, --set-key <key>` | Save API key to config |
|
|
92
|
+
| `-d, --delete-key` | Delete saved API key from config |
|
|
93
|
+
| `-V, --version` | Show version |
|
|
94
|
+
| `-h, --help` | Show help |
|
|
77
95
|
|
|
78
96
|
### Examples
|
|
79
97
|
|
|
80
98
|
**Basic usage:**
|
|
81
99
|
|
|
82
100
|
```bash
|
|
83
|
-
xoegit
|
|
101
|
+
npx xoegit
|
|
84
102
|
```
|
|
85
103
|
|
|
86
104
|
**With context for better commit type detection:**
|
|
87
105
|
|
|
88
106
|
```bash
|
|
89
|
-
xoegit --context "refactoring folder structure"
|
|
90
|
-
xoegit -c "fixing authentication bug"
|
|
91
|
-
xoegit -c "adding new payment feature"
|
|
107
|
+
npx xoegit --context "refactoring folder structure"
|
|
108
|
+
npx xoegit -c "fixing authentication bug"
|
|
109
|
+
npx xoegit -c "adding new payment feature"
|
|
92
110
|
```
|
|
93
111
|
|
|
94
|
-
**Use API key for this session only (not saved):**
|
|
95
|
-
|
|
96
112
|
```bash
|
|
97
|
-
xoegit --api-key "YOUR_GEMINI_API_KEY"
|
|
113
|
+
npx xoegit --api-key "YOUR_GEMINI_API_KEY"
|
|
98
114
|
```
|
|
99
115
|
|
|
100
116
|
**Manage API key:**
|
|
101
117
|
|
|
102
118
|
```bash
|
|
103
|
-
xoegit --set-key "YOUR_GEMINI_API_KEY"
|
|
104
|
-
xoegit --delete-key
|
|
119
|
+
npx xoegit --set-key "YOUR_GEMINI_API_KEY"
|
|
120
|
+
npx xoegit --delete-key
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Execute mode (auto-commit with confirmation):**
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx xoegit --execute
|
|
127
|
+
npx xoegit -e
|
|
128
|
+
npx xoegit -e -c "fixing critical bug"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Explain mode (verbose reasoning):**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx xoegit --explain
|
|
135
|
+
npx xoegit --explain -c "refactoring auth module"
|
|
105
136
|
```
|
|
106
137
|
|
|
107
138
|
### Sample Output
|
|
@@ -125,6 +156,37 @@ pr description: feat(auth): implement secure login
|
|
|
125
156
|
- refactor(utils): improve error logging
|
|
126
157
|
```
|
|
127
158
|
|
|
159
|
+
### Explain Mode Output
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
commit 1
|
|
163
|
+
git add src/auth/login.ts src/auth/validator.ts
|
|
164
|
+
git commit -m "feat(auth): add login validation"
|
|
165
|
+
why: I grouped these files because they both handle authentication logic and the validator is directly used by login.ts.
|
|
166
|
+
|
|
167
|
+
commit 2
|
|
168
|
+
git add package.json package-lock.json
|
|
169
|
+
git commit -m "chore: update dependencies"
|
|
170
|
+
why: I separated dependency updates from code changes to keep the commit atomic and make it easy to revert if needed.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Execute Mode Output
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
ℹ Execute mode enabled. 2 commit(s) will be created.
|
|
177
|
+
Do you want to execute these commands? (y/n): y
|
|
178
|
+
✔ [1/2] a1b2c3d feat(auth): add login validation
|
|
179
|
+
└─ src/
|
|
180
|
+
└─ auth/
|
|
181
|
+
└─ login.ts
|
|
182
|
+
✔ [2/2] e4f5g6h refactor(utils): improve error logging
|
|
183
|
+
└─ src/
|
|
184
|
+
└─ utils/
|
|
185
|
+
└─ logger.ts
|
|
186
|
+
|
|
187
|
+
✓ All 2 commit(s) executed successfully!
|
|
188
|
+
```
|
|
189
|
+
|
|
128
190
|
## Troubleshooting
|
|
129
191
|
|
|
130
192
|
### "Current directory is not a git repository"
|
package/dist/cli/analyze.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import { program } from './program.js';
|
|
3
3
|
import { ConfigService } from '../config/index.js';
|
|
4
|
-
import { isValidApiKey, promptApiKey, showBanner, showSuggestion, showSuccess, showError, showWarning, showInfo, showTip, spinnerText, } from '../utils/index.js';
|
|
5
|
-
import { getGitDiff, getGitLog, getGitStatus, isGitRepository } from '../git/index.js';
|
|
4
|
+
import { isValidApiKey, promptApiKey, promptConfirm, showBanner, showSuggestion, showSuccess, showError, showWarning, showInfo, showTip, spinnerText, } from '../utils/index.js';
|
|
5
|
+
import { getGitDiff, getGitLog, getGitStatus, isGitRepository, executeGitAdd, executeGitCommit, } from '../git/index.js';
|
|
6
6
|
import { generateSystemPrompt } from '../prompts/index.js';
|
|
7
7
|
import { generateCommitSuggestion } from '../providers/index.js';
|
|
8
8
|
/**
|
|
@@ -80,7 +80,7 @@ export async function analyzeAction() {
|
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
// 3. Generate Prompt
|
|
83
|
-
const systemPrompt = await generateSystemPrompt();
|
|
83
|
+
const systemPrompt = await generateSystemPrompt({ explain: options.explain });
|
|
84
84
|
// Get user context if provided
|
|
85
85
|
const userContext = options.context || '';
|
|
86
86
|
if (userContext) {
|
|
@@ -95,7 +95,13 @@ export async function analyzeAction() {
|
|
|
95
95
|
spinner.stop();
|
|
96
96
|
showSuccess('Suggestion generated!');
|
|
97
97
|
showSuggestion(suggestion);
|
|
98
|
-
|
|
98
|
+
// Handle -e (execute) flag
|
|
99
|
+
if (options.execute) {
|
|
100
|
+
await handleExecuteMode(suggestion);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
showTip('Copy and execute the commands above. xoegit never runs commands automatically.');
|
|
104
|
+
}
|
|
99
105
|
}
|
|
100
106
|
catch (error) {
|
|
101
107
|
spinner.stop();
|
|
@@ -107,3 +113,135 @@ export async function analyzeAction() {
|
|
|
107
113
|
process.exit(1);
|
|
108
114
|
}
|
|
109
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse all commit operations from suggestion
|
|
118
|
+
* Each "commit N" section contains git add and git commit commands
|
|
119
|
+
*/
|
|
120
|
+
function parseCommitOperations(suggestion) {
|
|
121
|
+
const operations = [];
|
|
122
|
+
const lines = suggestion.split('\n');
|
|
123
|
+
let currentFiles = [];
|
|
124
|
+
let currentMessage = null;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
// Check for commit section header (e.g., "commit 1", "commit 2")
|
|
128
|
+
if (trimmed.match(/^commit\s+\d+$/i)) {
|
|
129
|
+
// Save previous operation if complete
|
|
130
|
+
if (currentFiles.length > 0 && currentMessage) {
|
|
131
|
+
operations.push({ files: [...currentFiles], message: currentMessage });
|
|
132
|
+
}
|
|
133
|
+
// Reset for new commit
|
|
134
|
+
currentFiles = [];
|
|
135
|
+
currentMessage = null;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Parse git add command
|
|
139
|
+
if (trimmed.startsWith('git add ')) {
|
|
140
|
+
const filesStr = trimmed.slice(8).trim();
|
|
141
|
+
const matches = filesStr.match(/(?:[^\s"]+|"[^"]*")+/g);
|
|
142
|
+
if (matches) {
|
|
143
|
+
currentFiles.push(...matches.map((f) => f.replace(/^"|"$/g, '')));
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Parse git commit command
|
|
148
|
+
if (trimmed.startsWith('git commit -m ')) {
|
|
149
|
+
const match = trimmed.match(/git commit -m ["'](.+)["']/);
|
|
150
|
+
if (match) {
|
|
151
|
+
currentMessage = match[1];
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Don't forget the last operation
|
|
157
|
+
if (currentFiles.length > 0 && currentMessage) {
|
|
158
|
+
operations.push({ files: [...currentFiles], message: currentMessage });
|
|
159
|
+
}
|
|
160
|
+
return operations;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Handle execute mode - prompt user and execute git commands for all commits
|
|
164
|
+
*/
|
|
165
|
+
async function handleExecuteMode(suggestion) {
|
|
166
|
+
const operations = parseCommitOperations(suggestion);
|
|
167
|
+
if (operations.length === 0) {
|
|
168
|
+
showWarning('Could not parse git commands from suggestion.');
|
|
169
|
+
showTip('Copy and execute the commands manually.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
showInfo(`Execute mode enabled. ${operations.length} commit(s) will be created.`);
|
|
174
|
+
const confirmed = await promptConfirm('Do you want to execute these commands?');
|
|
175
|
+
if (!confirmed) {
|
|
176
|
+
showInfo('Execution cancelled.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
for (let i = 0; i < operations.length; i++) {
|
|
181
|
+
const op = operations[i];
|
|
182
|
+
const commitNum = i + 1;
|
|
183
|
+
// Add files silently
|
|
184
|
+
await executeGitAdd(op.files);
|
|
185
|
+
// Create commit with spinner
|
|
186
|
+
const commitSpinner = ora({
|
|
187
|
+
text: `[${commitNum}/${operations.length}] Creating commit...`,
|
|
188
|
+
spinner: 'dots12',
|
|
189
|
+
}).start();
|
|
190
|
+
const commitHash = await executeGitCommit(op.message);
|
|
191
|
+
const shortHash = commitHash.slice(0, 7);
|
|
192
|
+
commitSpinner.succeed(`[${commitNum}/${operations.length}] ${shortHash} ${op.message}`);
|
|
193
|
+
// Display file tree
|
|
194
|
+
printFileTree(op.files);
|
|
195
|
+
}
|
|
196
|
+
console.log('');
|
|
197
|
+
showSuccess(`All ${operations.length} commit(s) executed successfully!`);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
showError('Execution Failed', error.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Print files in a tree format similar to husky
|
|
205
|
+
*/
|
|
206
|
+
function printFileTree(files) {
|
|
207
|
+
// Sort files for consistent display
|
|
208
|
+
const sortedFiles = [...files].sort();
|
|
209
|
+
// Build tree structure
|
|
210
|
+
const tree = {};
|
|
211
|
+
for (const file of sortedFiles) {
|
|
212
|
+
const parts = file.split('/');
|
|
213
|
+
let current = tree;
|
|
214
|
+
for (let i = 0; i < parts.length; i++) {
|
|
215
|
+
const part = parts[i];
|
|
216
|
+
const isLast = i === parts.length - 1;
|
|
217
|
+
if (isLast) {
|
|
218
|
+
current[part] = null; // File (leaf node)
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
if (!current[part]) {
|
|
222
|
+
current[part] = {};
|
|
223
|
+
}
|
|
224
|
+
current = current[part];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Print tree
|
|
229
|
+
const lines = [];
|
|
230
|
+
printTreeNode(tree, '', lines);
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
console.log(line);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function printTreeNode(node, prefix, lines) {
|
|
236
|
+
const entries = Object.entries(node);
|
|
237
|
+
entries.forEach(([name, children], index) => {
|
|
238
|
+
const isLast = index === entries.length - 1;
|
|
239
|
+
const connector = isLast ? '└─' : '├─';
|
|
240
|
+
const isDir = children !== null;
|
|
241
|
+
lines.push(`${prefix}${connector} ${name}${isDir ? '/' : ''}`);
|
|
242
|
+
if (isDir) {
|
|
243
|
+
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
244
|
+
printTreeNode(children, newPrefix, lines);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
package/dist/cli/program.js
CHANGED
|
@@ -8,4 +8,6 @@ program
|
|
|
8
8
|
.option('-k, --api-key <key>', 'Gemini API Key')
|
|
9
9
|
.option('-c, --context <context>', 'Context for the changes (e.g., "refactoring folder structure")')
|
|
10
10
|
.option('-s, --set-key <key>', 'Save Gemini API Key to config (overwrites existing)')
|
|
11
|
-
.option('-d, --delete-key', 'Delete saved API Key from config')
|
|
11
|
+
.option('-d, --delete-key', 'Delete saved API Key from config')
|
|
12
|
+
.option('-e, --execute', 'Execute commit after confirmation prompt')
|
|
13
|
+
.option('--explain', 'Show explanation for each commit grouping (verbose mode)');
|
package/dist/git/service.js
CHANGED
|
@@ -38,3 +38,16 @@ export async function getGitLog(maxCount = 5) {
|
|
|
38
38
|
return 'No commits yet.';
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Execute git add with specified files
|
|
43
|
+
*/
|
|
44
|
+
export async function executeGitAdd(files) {
|
|
45
|
+
await git.add(files);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute git commit with specified message
|
|
49
|
+
*/
|
|
50
|
+
export async function executeGitCommit(message) {
|
|
51
|
+
const result = await git.commit(message);
|
|
52
|
+
return result.commit;
|
|
53
|
+
}
|
package/dist/prompts/service.js
CHANGED
|
@@ -4,7 +4,7 @@ import url from 'url';
|
|
|
4
4
|
/**
|
|
5
5
|
* Generates the system prompt for the AI
|
|
6
6
|
*/
|
|
7
|
-
export async function generateSystemPrompt() {
|
|
7
|
+
export async function generateSystemPrompt(options = {}) {
|
|
8
8
|
let rulesContent = '';
|
|
9
9
|
try {
|
|
10
10
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
@@ -15,6 +15,33 @@ export async function generateSystemPrompt() {
|
|
|
15
15
|
console.warn('Could not read RULES.md, using default rules.');
|
|
16
16
|
rulesContent = 'Follow conventional commits.';
|
|
17
17
|
}
|
|
18
|
+
// Build explain mode instructions
|
|
19
|
+
const explainInstructions = options.explain
|
|
20
|
+
? `
|
|
21
|
+
EXPLAIN MODE ENABLED:
|
|
22
|
+
For each commit, you MUST add a "why:" line directly after the git commit command (no blank line in between).
|
|
23
|
+
This line should explain the logical reasoning behind grouping these specific files together.
|
|
24
|
+
|
|
25
|
+
Format:
|
|
26
|
+
commit N
|
|
27
|
+
git add <files>
|
|
28
|
+
git commit -m "<message>"
|
|
29
|
+
why: <one sentence explaining why these files are grouped together>
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
commit 1
|
|
33
|
+
git add src/auth/login.ts src/auth/utils.ts
|
|
34
|
+
git commit -m "feat(auth): add login validation"
|
|
35
|
+
why: I grouped these files because they both handle authentication logic and the validation directly depends on the auth utilities.
|
|
36
|
+
|
|
37
|
+
commit 2
|
|
38
|
+
git add package.json package-lock.json
|
|
39
|
+
git commit -m "chore: update dependencies"
|
|
40
|
+
why: I separated dependency updates from code changes to keep the commit atomic and make it easy to revert if needed.
|
|
41
|
+
|
|
42
|
+
The explanation should help developers understand your commit crafting logic for educational purposes.
|
|
43
|
+
`
|
|
44
|
+
: '';
|
|
18
45
|
return `
|
|
19
46
|
You are a Git Commit Assistant for the 'xoegit' CLI.
|
|
20
47
|
Your goal is to suggest git commands and commit messages based on the provided changes.
|
|
@@ -28,7 +55,7 @@ Your goal is to suggest git commands and commit messages based on the provided c
|
|
|
28
55
|
RULES FROM USER:
|
|
29
56
|
${rulesContent}
|
|
30
57
|
---
|
|
31
|
-
|
|
58
|
+
${explainInstructions}
|
|
32
59
|
IMPORTANT:
|
|
33
60
|
- You definitely MUST NOT execute commands. You only suggest them.
|
|
34
61
|
- Output MUST be strictly valid shell commands or clear instructions as per the examples in RULES.md.
|
package/dist/utils/input.js
CHANGED
|
@@ -65,3 +65,49 @@ export async function promptApiKey() {
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Prompts user for yes/no confirmation
|
|
70
|
+
*/
|
|
71
|
+
export async function promptConfirm(message) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
process.stdout.write(`${message} (y/n): `);
|
|
74
|
+
if (process.stdin.setRawMode && process.stdout.isTTY) {
|
|
75
|
+
process.stdin.setRawMode(true);
|
|
76
|
+
process.stdin.resume();
|
|
77
|
+
const onData = (char) => {
|
|
78
|
+
const charStr = char.toString('utf-8').toLowerCase();
|
|
79
|
+
// Ctrl+C
|
|
80
|
+
if (charStr === '\u0003') {
|
|
81
|
+
process.stdout.write('\n');
|
|
82
|
+
process.stdin.setRawMode(false);
|
|
83
|
+
process.stdin.removeListener('data', onData);
|
|
84
|
+
process.stdin.pause();
|
|
85
|
+
resolve(false);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (charStr === 'y' || charStr === 'n') {
|
|
89
|
+
process.stdout.write(charStr + '\n');
|
|
90
|
+
process.stdin.setRawMode(false);
|
|
91
|
+
process.stdin.removeListener('data', onData);
|
|
92
|
+
process.stdin.pause();
|
|
93
|
+
resolve(charStr === 'y');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
process.stdin.on('data', onData);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Fallback for non-TTY environments
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout,
|
|
104
|
+
terminal: false,
|
|
105
|
+
});
|
|
106
|
+
rl.question('', (answer) => {
|
|
107
|
+
rl.close();
|
|
108
|
+
const normalized = answer.trim().toLowerCase();
|
|
109
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/dist/utils/ui.js
CHANGED
|
@@ -53,6 +53,10 @@ function formatSuggestionLine(line) {
|
|
|
53
53
|
if (line.startsWith('pr title:') || line.startsWith('pr description:')) {
|
|
54
54
|
return chalk.yellow(line);
|
|
55
55
|
}
|
|
56
|
+
// Format explanation lines from --explain mode
|
|
57
|
+
if (line.startsWith('why:')) {
|
|
58
|
+
return brand.muted(' ') + chalk.italic.hex('#A78BFA')(line);
|
|
59
|
+
}
|
|
56
60
|
return line;
|
|
57
61
|
}
|
|
58
62
|
/**
|