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 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
- ### Install
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
- **or** install from source:
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
- Then, from whatever project you're working on, just run:
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
- | `-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 |
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"
@@ -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
- 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,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)');
@@ -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
+ }
@@ -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.
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xoegit",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "AI-powered CLI tool for generating semantic git commit messages and PR descriptions",
5
5
  "license": "MIT",
6
6
  "author": "ujangdoubleday",