xoegit 1.1.3 → 1.1.4

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 CHANGED
@@ -15,6 +15,7 @@
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
19
20
  - **Smart Fallback** — Automatically switches between Gemini models when rate limits are hit
20
21
  - **Semantic Commits** — Strictly follows [Conventional Commits](https://www.conventionalcommits.org/)
@@ -22,7 +23,9 @@
22
23
 
23
24
  ## How It Works
24
25
 
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.
26
+ > **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.
27
+ >
28
+ > With the `--execute` flag, you can optionally let `xoegit` execute the suggested commits after your explicit confirmation.
26
29
 
27
30
  You remain in full control of your git workflow.
28
31
 
@@ -66,14 +69,15 @@ xoegit
66
69
 
67
70
  ### Options
68
71
 
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
- | `-s, --set-key <key>` | Save API key to config
74
- | `-d, --delete-key` | Delete saved API key from config |
75
- | `-V, --version` | Show version |
76
- | `-h, --help` | Show help |
72
+ | Option | Description |
73
+ | ---------------------- | --------------------------------------------------- |
74
+ | `-k, --api-key <key>` | Use specific API key for this session |
75
+ | `-c, --context <text>` | Provide context for more accurate suggestions |
76
+ | `-e, --execute` | Execute commits after confirmation prompt |
77
+ | `-s, --set-key <key>` | Save API key to config |
78
+ | `-d, --delete-key` | Delete saved API key from config |
79
+ | `-V, --version` | Show version |
80
+ | `-h, --help` | Show help |
77
81
 
78
82
  ### Examples
79
83
 
@@ -104,6 +108,14 @@ xoegit --set-key "YOUR_GEMINI_API_KEY"
104
108
  xoegit --delete-key
105
109
  ```
106
110
 
111
+ **Execute mode (auto-commit with confirmation):**
112
+
113
+ ```bash
114
+ xoegit --execute
115
+ xoegit -e
116
+ xoegit -e -c "fixing critical bug"
117
+ ```
118
+
107
119
  ### Sample Output
108
120
 
109
121
  ```
@@ -125,6 +137,23 @@ pr description: feat(auth): implement secure login
125
137
  - refactor(utils): improve error logging
126
138
  ```
127
139
 
140
+ ### Execute Mode Output
141
+
142
+ ```
143
+ ℹ Execute mode enabled. 2 commit(s) will be created.
144
+ Do you want to execute these commands? (y/n): y
145
+ ✔ [1/2] a1b2c3d feat(auth): add login validation
146
+ └─ src/
147
+ └─ auth/
148
+ └─ login.ts
149
+ ✔ [2/2] e4f5g6h refactor(utils): improve error logging
150
+ └─ src/
151
+ └─ utils/
152
+ └─ logger.ts
153
+
154
+ ✓ All 2 commit(s) executed successfully!
155
+ ```
156
+
128
157
  ## Troubleshooting
129
158
 
130
159
  ### "Current directory is not a git repository"
@@ -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
  /**
@@ -95,7 +95,13 @@ export async function analyzeAction() {
95
95
  spinner.stop();
96
96
  showSuccess('Suggestion generated!');
97
97
  showSuggestion(suggestion);
98
- showTip('Copy and execute the commands above. xoegit never runs commands automatically.');
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
+ }
@@ -8,4 +8,5 @@ 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');
@@ -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
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xoegit",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "AI-powered CLI tool for generating semantic git commit messages and PR descriptions",
5
5
  "license": "MIT",
6
6
  "author": "ujangdoubleday",