format-commit 1.0.0 → 1.0.1
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 +86 -33
- package/lib/ai-service.js +83 -66
- package/lib/commit.js +21 -96
- package/lib/create-branch.js +1 -1
- package/lib/index.js +2 -2
- package/lib/setup.js +2 -1
- package/lib/utils.js +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# format-commit
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/js/format-commit)
|
|
4
3
|
[](https://nodejs.org/)
|
|
5
4
|
[](https://www.npmjs.com/package/format-commit)
|
|
6
5
|
[](https://opensource.org/licenses/MIT)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Lightweight CLI for consistent Git workflow & and optional AI support.
|
|
9
8
|
|
|
10
|
-
Standardize your commit messages and branch naming with configurable rules, and guide your development workflow through automated scripts. No bloat, no complexity
|
|
9
|
+
Standardize your commit messages and branch naming with configurable rules, and guide your development workflow through automated scripts. No bloat, no complexity - just clean, consistent Git practices. Feel free to let AI suggest commit titles for you in the expected format.
|
|
11
10
|
|
|
12
11
|
## Installation
|
|
13
12
|
|
|
@@ -25,7 +24,7 @@ Add to your `package.json` scripts:
|
|
|
25
24
|
}
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
And use:
|
|
29
28
|
```sh
|
|
30
29
|
npm run commit # to commit
|
|
31
30
|
npm run branch # to create a branch
|
|
@@ -42,39 +41,93 @@ format-commit --branch
|
|
|
42
41
|
|
|
43
42
|
### Initial Setup
|
|
44
43
|
|
|
45
|
-
On first use, format-commit will prompt you to configure your commit and branch
|
|
44
|
+
On first use, format-commit will prompt you to configure your commit and branch.
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
If you want to reconfigure later from scratch, run:
|
|
48
47
|
```sh
|
|
49
48
|
format-commit --config
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
## Configuration
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
| :------- | :---------- |
|
|
56
|
-
| **format** | Commit title format:<br>1 - `(type) Name` / 2 - `(type) name`<br>3 - `type: Name` / 4 - `type: name`<br>5 - `type(scope) Name` / 6 - `type(scope) name`<br>7 - `type(scope): Name` / 8 - `type(scope): name` |
|
|
57
|
-
| **branchFormat** | Branch naming format:<br>1 - `type/description`<br>2 - `type/scope/description` |
|
|
58
|
-
| **types** | Allowed commit and branch types (default: `feat`, `fix`, `core`, `test`, `config`, `doc`) |
|
|
59
|
-
| **scopes** | Scopes for commit and branch categorization (used in formats 5-8 for commits, format 2 for branches) |
|
|
60
|
-
| **minLength** | Minimum length required for the commit title |
|
|
61
|
-
| **maxLength** | Maximum length required for the commit title and branch description |
|
|
62
|
-
| **changeVersion** | Version change policy:<br>`never (ignore)` - Never change version, skip prompt (default)<br>`never (always ask)` - Always prompt for version change<br>`only on release branch` - Only release branch commits require version change<br>`always` - All commits require version change |
|
|
63
|
-
| **releaseBranch** | Main/release branch name (used if changeVersion = `only on release branch`) |
|
|
64
|
-
| **showAllVersionTypes** | Show all version types or only main ones (`major`/`minor`/`patch`/`custom`) |
|
|
53
|
+
All configuration is stored in the `commit-config.json` file. Here is the list of all options.
|
|
65
54
|
|
|
66
|
-
|
|
55
|
+
`format`
|
|
56
|
+
|
|
57
|
+
Commit title format:
|
|
58
|
+
- 1: `(type) Name` / 2: `(type) name`
|
|
59
|
+
- 3: `type: Name` / 4: `type: name`
|
|
60
|
+
- 5: `type(scope) Name` / 6: `type(scope) name`
|
|
61
|
+
- 7: `type(scope): Name` / 8: `type(scope): name`
|
|
62
|
+
|
|
63
|
+
`branchFormat`
|
|
64
|
+
|
|
65
|
+
Branch naming format:
|
|
66
|
+
- 1: `type/description`
|
|
67
|
+
- 2: `type/scope/description`
|
|
68
|
+
|
|
69
|
+
`types`
|
|
70
|
+
|
|
71
|
+
Allowed commit and branch types (default: `feat`, `fix`, `core`, `test`, `config`, `doc`)
|
|
72
|
+
|
|
73
|
+
`scopes`
|
|
74
|
+
|
|
75
|
+
Scopes for commit and branch categorization (used in formats 5-8 for commits, format 2 for branches)
|
|
76
|
+
|
|
77
|
+
`minLength`
|
|
78
|
+
|
|
79
|
+
Minimum length required for the commit title.
|
|
80
|
+
|
|
81
|
+
`maxLength`
|
|
82
|
+
|
|
83
|
+
Maximum length required for the commit title and branch description.
|
|
84
|
+
|
|
85
|
+
`changeVersion`
|
|
86
|
+
|
|
87
|
+
Version change policy:
|
|
88
|
+
- `never (ignore)`: Never change version, skip prompt (default)
|
|
89
|
+
- `never (always ask)`: Always prompt for version change
|
|
90
|
+
- `only on release branch`: Only release branch commits require version change
|
|
91
|
+
- `always`: All commits require version change
|
|
92
|
+
|
|
93
|
+
`releaseBranch`
|
|
94
|
+
|
|
95
|
+
Main/release branch name (used if changeVersion = `only on release branch`)
|
|
67
96
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
`showAllVersionTypes`
|
|
98
|
+
|
|
99
|
+
Show all version types or only main ones (`major`/`minor`/`patch`/`custom`)
|
|
100
|
+
|
|
101
|
+
`ai.enabled`
|
|
102
|
+
|
|
103
|
+
Enable AI commit title suggestions (default: `false`)
|
|
104
|
+
|
|
105
|
+
`ai.provider`
|
|
106
|
+
|
|
107
|
+
AI provider:
|
|
108
|
+
- `anthropic` (Claude)
|
|
109
|
+
- `openai` (GPT)
|
|
110
|
+
- `google` (Gemini)
|
|
111
|
+
|
|
112
|
+
`ai.model`
|
|
113
|
+
|
|
114
|
+
Model identifier (e.g., `claude-haiku-4-5` or `gpt-4o-mini`)
|
|
115
|
+
|
|
116
|
+
`ai.envPath`
|
|
117
|
+
|
|
118
|
+
Path to .env file containing the AI provider API key (e.g., `.env`)
|
|
119
|
+
|
|
120
|
+
`ai.envKeyName`
|
|
121
|
+
|
|
122
|
+
Name of the environment variable for the API key (e.g., `OPENAI_API_KEY`)
|
|
123
|
+
|
|
124
|
+
`ai.largeDiffTokenThreshold`
|
|
125
|
+
|
|
126
|
+
Number of tokens from which not to use AI automatically.
|
|
127
|
+
|
|
128
|
+
### AI Suggestions
|
|
76
129
|
|
|
77
|
-
When AI is enabled,
|
|
130
|
+
When AI is enabled, your staged changes will be processed by the defined AI to suggest commit titles that:
|
|
78
131
|
- Follow your configured format and naming conventions
|
|
79
132
|
- Automatically select appropriate types and scopes
|
|
80
133
|
- Respect your min/max length constraints
|
|
@@ -84,15 +137,15 @@ You can either:
|
|
|
84
137
|
- Choose one of the 4 AI suggestions for quick commits (and can edit it)
|
|
85
138
|
- Select "Custom" to enter commit details manually (classic flow)
|
|
86
139
|
|
|
87
|
-
**Security:**
|
|
140
|
+
**Security:** AI provider API key is stored in a `.env` file automatically added to `.gitignore`.
|
|
88
141
|
|
|
89
142
|
## CLI Options
|
|
90
143
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
144
|
+
| Short | Long | Description |
|
|
145
|
+
| :---- | :--- | :---------- |
|
|
146
|
+
| `-c` | `--config` | Generate or update configuration file |
|
|
147
|
+
| `-b` | `--branch` | Create a new standardized branch |
|
|
148
|
+
| `-t` | `--test` | Test mode - preview without executing Git commands |
|
|
96
149
|
|
|
97
150
|
## Contributing
|
|
98
151
|
|
package/lib/ai-service.js
CHANGED
|
@@ -31,7 +31,9 @@ const getOptimizedGitDiff = () => {
|
|
|
31
31
|
|
|
32
32
|
/** Build AI prompt */
|
|
33
33
|
const buildPrompt = (diffData, config) => {
|
|
34
|
+
const typesList = config.types.map(t => t.value).join(', ');
|
|
34
35
|
const types = config.types.map(t => `${t.value} (${t.description})`).join(', ');
|
|
36
|
+
const scopesList = config.scopes ? config.scopes.map(s => s.value).join(', ') : null;
|
|
35
37
|
const scopes = config.scopes
|
|
36
38
|
? config.scopes.map(s => `${s.value} (${s.description})`).join(', ')
|
|
37
39
|
: null;
|
|
@@ -82,8 +84,9 @@ ${diffData.diff}
|
|
|
82
84
|
STRICT REQUIREMENTS:
|
|
83
85
|
- ${formatInstruction}
|
|
84
86
|
- Example format: "${exampleTitle}"
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
+
- ONLY use these types (NO others): ${typesList}
|
|
88
|
+
- Type descriptions: ${types}
|
|
89
|
+
${scopes ? `- ONLY use these scopes (NO others): ${scopesList}\n- Scope descriptions: ${scopes}` : '- No scopes - DO NOT include scope in output'}
|
|
87
90
|
- Length: ${config.minLength}-${config.maxLength} chars per title
|
|
88
91
|
- Return exactly 4 different commit titles
|
|
89
92
|
- Output MUST be a raw JSON array with NO text before or after
|
|
@@ -182,89 +185,103 @@ const callGeminiAPI = async (prompt, apiKey, model) => {
|
|
|
182
185
|
return data.candidates[0].content.parts[0].text;
|
|
183
186
|
};
|
|
184
187
|
|
|
185
|
-
/** Validate
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
188
|
+
/** Validate and normalize AI suggestion */
|
|
189
|
+
const validateAndNormalizeSuggestion = (suggestion, config) => {
|
|
190
|
+
const result = utils.parseAndNormalizeCommitTitle(suggestion, config);
|
|
190
191
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
// Returns null if invalid format/type/scope/length
|
|
193
|
+
if (result.error) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const lengthCheck = utils.validCommitTitle(result.normalized, config.minLength, config.maxLength);
|
|
197
|
+
if (lengthCheck !== true) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
196
200
|
|
|
197
|
-
|
|
201
|
+
// Returns normalized title (auto-corrected case)
|
|
202
|
+
return result.normalized;
|
|
198
203
|
};
|
|
199
204
|
|
|
200
205
|
/** Generate commit title suggestions using AI */
|
|
201
206
|
const generateCommitSuggestions = async (config, testMode) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
+
const diffData = getOptimizedGitDiff();
|
|
208
|
+
if (!diffData) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
212
|
+
const apiKey = envUtils.getEnvKey(config.ai.envPath, config.ai.envKeyName);
|
|
213
|
+
if (!apiKey) {
|
|
214
|
+
throw new Error('AI API key not found in .env');
|
|
215
|
+
}
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
217
|
+
const prompt = buildPrompt(diffData, config);
|
|
218
|
+
const estimatedTokens = Math.ceil(prompt.length / 4);
|
|
219
|
+
const threshold = config.ai.largeDiffTokenThreshold || 20000;
|
|
220
|
+
|
|
221
|
+
if (estimatedTokens > threshold) {
|
|
222
|
+
const { confirm } = await prompts({
|
|
223
|
+
type: 'confirm',
|
|
224
|
+
name: 'confirm',
|
|
225
|
+
message: `Large diff detected (~${estimatedTokens} tokens). Generate AI suggestions?`,
|
|
226
|
+
initial: false,
|
|
227
|
+
});
|
|
228
|
+
if (!confirm) {
|
|
229
|
+
return [];
|
|
228
230
|
}
|
|
231
|
+
}
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
let responseText;
|
|
234
|
+
if (config.ai.provider === 'anthropic') {
|
|
235
|
+
responseText = await callAnthropicAPI(prompt, apiKey, config.ai.model);
|
|
236
|
+
} else if (config.ai.provider === 'openai') {
|
|
237
|
+
responseText = await callOpenAIAPI(prompt, apiKey, config.ai.model);
|
|
238
|
+
} else if (config.ai.provider === 'google') {
|
|
239
|
+
responseText = await callGeminiAPI(prompt, apiKey, config.ai.model);
|
|
240
|
+
} else {
|
|
241
|
+
throw new Error(`Unknown AI provider: ${config.ai.provider}`);
|
|
242
|
+
}
|
|
240
243
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
throw new Error('No JSON array found in AI response');
|
|
244
|
+
const jsonMatch = responseText.match(/\[[\s\S]*\]/);
|
|
245
|
+
if (!jsonMatch) {
|
|
246
|
+
if (testMode) {
|
|
247
|
+
utils.log(responseText, 'warning');
|
|
247
248
|
}
|
|
249
|
+
throw new Error('No JSON array found in AI response');
|
|
250
|
+
}
|
|
248
251
|
|
|
249
|
-
|
|
252
|
+
const suggestions = JSON.parse(jsonMatch[0]);
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
if (!Array.isArray(suggestions) || !suggestions.length) {
|
|
255
|
+
if (testMode) {
|
|
256
|
+
utils.log(responseText, 'warning');
|
|
253
257
|
}
|
|
258
|
+
throw new Error('Invalid AI response format');
|
|
259
|
+
}
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
// Validate and normalize each suggestion
|
|
262
|
+
const validationResults = suggestions.map(s => ({
|
|
263
|
+
original: s,
|
|
264
|
+
normalized: validateAndNormalizeSuggestion(s, config)
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
// Log rejected suggestions in test mode
|
|
268
|
+
if (testMode) {
|
|
269
|
+
validationResults.forEach(({ original, normalized }) => {
|
|
270
|
+
if (normalized === null) {
|
|
271
|
+
utils.log(`rejected: "${original}"`, 'warning');
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
261
275
|
|
|
262
|
-
|
|
276
|
+
const validSuggestions = validationResults
|
|
277
|
+
.filter(({ normalized }) => normalized !== null)
|
|
278
|
+
.map(({ normalized }) => normalized);
|
|
263
279
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return [];
|
|
280
|
+
if (validSuggestions.length === 0) {
|
|
281
|
+
throw new Error('All AI suggestions were invalid');
|
|
267
282
|
}
|
|
283
|
+
|
|
284
|
+
return validSuggestions;
|
|
268
285
|
};
|
|
269
286
|
|
|
270
287
|
export {
|
package/lib/commit.js
CHANGED
|
@@ -15,90 +15,6 @@ const options = JSON.parse(
|
|
|
15
15
|
readFileSync(join(__dirname, 'options.json'), 'utf-8')
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
/** Parse and validate commit title format, auto-correct case */
|
|
20
|
-
const parseAndNormalizeCommitTitle = (title, config) => {
|
|
21
|
-
let type, scope, message, detectedFormatGroup;
|
|
22
|
-
|
|
23
|
-
// Try different format patterns
|
|
24
|
-
const format7_8 = /^([^(]+)\(([^)]+)\):\s*(.+)$/; // type(scope): message
|
|
25
|
-
const format5_6 = /^([^(]+)\(([^)]+)\)\s+(.+)$/; // type(scope) message
|
|
26
|
-
const format3_4 = /^([^:]+):\s*(.+)$/; // type: message
|
|
27
|
-
const format1_2 = /^\(([^)]+)\)\s+(.+)$/; // (type) message
|
|
28
|
-
|
|
29
|
-
let match;
|
|
30
|
-
|
|
31
|
-
if ((match = title.match(format7_8))) {
|
|
32
|
-
[, type, scope, message] = match;
|
|
33
|
-
detectedFormatGroup = 'type(scope):';
|
|
34
|
-
} else if ((match = title.match(format5_6))) {
|
|
35
|
-
[, type, scope, message] = match;
|
|
36
|
-
detectedFormatGroup = 'type(scope)';
|
|
37
|
-
} else if ((match = title.match(format3_4))) {
|
|
38
|
-
[, type, message] = match;
|
|
39
|
-
detectedFormatGroup = 'type:';
|
|
40
|
-
} else if ((match = title.match(format1_2))) {
|
|
41
|
-
[, type, message] = match;
|
|
42
|
-
detectedFormatGroup = '(type)';
|
|
43
|
-
} else {
|
|
44
|
-
return { error: 'Invalid commit format. Expected format with type prefix.' };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Verify format matches config
|
|
48
|
-
let expectedFormatGroup;
|
|
49
|
-
if (config.format >= 7) {
|
|
50
|
-
expectedFormatGroup = 'type(scope):';
|
|
51
|
-
} else if (config.format >= 5) {
|
|
52
|
-
expectedFormatGroup = 'type(scope)';
|
|
53
|
-
} else if (config.format >= 3) {
|
|
54
|
-
expectedFormatGroup = 'type:';
|
|
55
|
-
} else {
|
|
56
|
-
expectedFormatGroup = '(type)';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (detectedFormatGroup !== expectedFormatGroup) {
|
|
60
|
-
const exampleTitle = utils.formatCommitTitle(
|
|
61
|
-
config.types[0].value,
|
|
62
|
-
'name',
|
|
63
|
-
config.format,
|
|
64
|
-
config.scopes?.[0]?.value
|
|
65
|
-
);
|
|
66
|
-
return { error: `Wrong format. Expected: "${exampleTitle}"` };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
type = type.trim();
|
|
70
|
-
message = message.trim();
|
|
71
|
-
if (scope) {
|
|
72
|
-
scope = scope.trim();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Validate type exists (case-insensitive)
|
|
76
|
-
const validType = config.types.find(t => t.value.toLowerCase() === type.toLowerCase());
|
|
77
|
-
if (!validType) {
|
|
78
|
-
const validTypes = config.types.map(t => t.value).join(', ');
|
|
79
|
-
return { error: `Invalid type "${type}". Valid types: ${validTypes}` };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Validate scope if present (case-insensitive)
|
|
83
|
-
let validScope = scope;
|
|
84
|
-
if (scope) {
|
|
85
|
-
if (!config.scopes || config.scopes.length === 0) {
|
|
86
|
-
return { error: 'Scope not allowed in current format configuration' };
|
|
87
|
-
}
|
|
88
|
-
const foundScope = config.scopes.find(s => s.value.toLowerCase() === scope.toLowerCase());
|
|
89
|
-
if (!foundScope) {
|
|
90
|
-
const validScopes = config.scopes.map(s => s.value).join(', ');
|
|
91
|
-
return { error: `Invalid scope "${scope}". Valid scopes: ${validScopes}` };
|
|
92
|
-
}
|
|
93
|
-
validScope = foundScope.value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Re-format with correct case
|
|
97
|
-
const normalized = utils.formatCommitTitle(validType.value, message, config.format, validScope);
|
|
98
|
-
|
|
99
|
-
return { normalized };
|
|
100
|
-
};
|
|
101
|
-
|
|
102
18
|
/** Prompt with pre-filled text (editable) using readline */
|
|
103
19
|
const promptWithPrefill = (message, prefill, validate) => {
|
|
104
20
|
return new Promise((resolve, reject) => {
|
|
@@ -192,7 +108,7 @@ export default async function commit(config, testMode) {
|
|
|
192
108
|
}
|
|
193
109
|
utils.log('new commit');
|
|
194
110
|
|
|
195
|
-
if (!
|
|
111
|
+
if (!utils.hasStagedChanges()) {
|
|
196
112
|
utils.log('No staged changes found - stage your changes with git add', 'error');
|
|
197
113
|
return;
|
|
198
114
|
}
|
|
@@ -218,16 +134,28 @@ export default async function commit(config, testMode) {
|
|
|
218
134
|
return;
|
|
219
135
|
}
|
|
220
136
|
|
|
137
|
+
// Warn if using default example scope and skip AI suggestions
|
|
138
|
+
const hasDefaultScope = config.scopes && config.scopes.length === 1 && config.scopes[0].value === 'example';
|
|
139
|
+
if (hasDefaultScope) {
|
|
140
|
+
utils.log('You are using the default scope "example" - customize your scopes in commit-config.json or run format-commit --config', 'warning');
|
|
141
|
+
utils.log('AI suggestions skipped - configure your scopes', 'warning');
|
|
142
|
+
}
|
|
143
|
+
|
|
221
144
|
let commitTitle = null;
|
|
222
145
|
let useAISuggestion = false;
|
|
223
146
|
|
|
224
|
-
|
|
225
|
-
if (config.ai?.enabled) {
|
|
147
|
+
if (config.ai?.enabled && !hasDefaultScope) {
|
|
226
148
|
utils.log('generating suggestions...');
|
|
227
149
|
const aiService = await import('./ai-service.js');
|
|
228
|
-
|
|
150
|
+
let suggestions = [];
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
suggestions = await aiService.generateCommitSuggestions(config, testMode);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
utils.log(`AI suggestions failed (${err.message})`, 'warning');
|
|
156
|
+
}
|
|
229
157
|
|
|
230
|
-
if (suggestions
|
|
158
|
+
if (suggestions?.length) {
|
|
231
159
|
let aiCancelled = false;
|
|
232
160
|
const aiChoice = await prompts({
|
|
233
161
|
type: 'select',
|
|
@@ -260,7 +188,7 @@ export default async function commit(config, testMode) {
|
|
|
260
188
|
}
|
|
261
189
|
|
|
262
190
|
// Parse and validate format, type, and scope
|
|
263
|
-
const result = parseAndNormalizeCommitTitle(val, config);
|
|
191
|
+
const result = utils.parseAndNormalizeCommitTitle(val, config);
|
|
264
192
|
if (result.error) {
|
|
265
193
|
return result.error;
|
|
266
194
|
}
|
|
@@ -276,7 +204,7 @@ export default async function commit(config, testMode) {
|
|
|
276
204
|
);
|
|
277
205
|
|
|
278
206
|
// Normalize the final title (correct case)
|
|
279
|
-
const result = parseAndNormalizeCommitTitle(rawTitle, config);
|
|
207
|
+
const result = utils.parseAndNormalizeCommitTitle(rawTitle, config);
|
|
280
208
|
commitTitle = result.normalized;
|
|
281
209
|
useAISuggestion = true;
|
|
282
210
|
} catch (err) {
|
|
@@ -286,8 +214,6 @@ export default async function commit(config, testMode) {
|
|
|
286
214
|
return;
|
|
287
215
|
}
|
|
288
216
|
}
|
|
289
|
-
} else {
|
|
290
|
-
utils.log('AI suggestions failed, using manual input', 'warning');
|
|
291
217
|
}
|
|
292
218
|
}
|
|
293
219
|
|
|
@@ -344,7 +270,7 @@ export default async function commit(config, testMode) {
|
|
|
344
270
|
return;
|
|
345
271
|
}
|
|
346
272
|
|
|
347
|
-
// Classic flow: ask for type and scope
|
|
273
|
+
// Classic flow: ask for type and scope
|
|
348
274
|
const typeScope = await prompts([
|
|
349
275
|
{
|
|
350
276
|
type: 'select',
|
|
@@ -370,7 +296,7 @@ export default async function commit(config, testMode) {
|
|
|
370
296
|
return;
|
|
371
297
|
}
|
|
372
298
|
|
|
373
|
-
// Ask for title with full formatted length validation, then description,
|
|
299
|
+
// Ask for title with full formatted length validation, then description, and options
|
|
374
300
|
const commit = await prompts([
|
|
375
301
|
{
|
|
376
302
|
type: 'text',
|
|
@@ -429,7 +355,6 @@ export default async function commit(config, testMode) {
|
|
|
429
355
|
return;
|
|
430
356
|
}
|
|
431
357
|
|
|
432
|
-
// Format changes message and commit it
|
|
433
358
|
commitTitle = utils.formatCommitTitle(
|
|
434
359
|
typeScope.type,
|
|
435
360
|
commit.title,
|
package/lib/create-branch.js
CHANGED
package/lib/index.js
CHANGED
package/lib/setup.js
CHANGED
|
@@ -255,8 +255,9 @@ export default async function setup(askForCommitAfter) {
|
|
|
255
255
|
try {
|
|
256
256
|
fs.writeFileSync(`./${options.configFile}.json`, parsedConfig);
|
|
257
257
|
utils.log('config file successfully created', 'success');
|
|
258
|
+
utils.log(`Customize default types and scopes in ${options.configFile}.json`);
|
|
258
259
|
} catch (err) {
|
|
259
|
-
utils.log(`
|
|
260
|
+
utils.log(`Unable to save config file: ${err.message}`, 'error');
|
|
260
261
|
return;
|
|
261
262
|
}
|
|
262
263
|
|
package/lib/utils.js
CHANGED
|
@@ -159,6 +159,89 @@ const checkBranchExists = (branchName) => {
|
|
|
159
159
|
}
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
+
/** Parse and validate commit title format, auto-correct case */
|
|
163
|
+
const parseAndNormalizeCommitTitle = (title, config) => {
|
|
164
|
+
let type, scope, message, detectedFormatGroup;
|
|
165
|
+
|
|
166
|
+
// Try different format patterns
|
|
167
|
+
const format7_8 = /^([^(]+)\(([^)]+)\):\s*(.+)$/; // type(scope): message
|
|
168
|
+
const format5_6 = /^([^(]+)\(([^)]+)\)\s+(.+)$/; // type(scope) message
|
|
169
|
+
const format3_4 = /^([^:]+):\s*(.+)$/; // type: message
|
|
170
|
+
const format1_2 = /^\(([^)]+)\)\s+(.+)$/; // (type) message
|
|
171
|
+
|
|
172
|
+
let match;
|
|
173
|
+
|
|
174
|
+
if ((match = title.match(format7_8))) {
|
|
175
|
+
[, type, scope, message] = match;
|
|
176
|
+
detectedFormatGroup = 'type(scope):';
|
|
177
|
+
} else if ((match = title.match(format5_6))) {
|
|
178
|
+
[, type, scope, message] = match;
|
|
179
|
+
detectedFormatGroup = 'type(scope)';
|
|
180
|
+
} else if ((match = title.match(format3_4))) {
|
|
181
|
+
[, type, message] = match;
|
|
182
|
+
detectedFormatGroup = 'type:';
|
|
183
|
+
} else if ((match = title.match(format1_2))) {
|
|
184
|
+
[, type, message] = match;
|
|
185
|
+
detectedFormatGroup = '(type)';
|
|
186
|
+
} else {
|
|
187
|
+
return { error: 'Invalid commit format. Expected format with type prefix.' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Verify format matches config
|
|
191
|
+
let expectedFormatGroup;
|
|
192
|
+
if (config.format >= 7) {
|
|
193
|
+
expectedFormatGroup = 'type(scope):';
|
|
194
|
+
} else if (config.format >= 5) {
|
|
195
|
+
expectedFormatGroup = 'type(scope)';
|
|
196
|
+
} else if (config.format >= 3) {
|
|
197
|
+
expectedFormatGroup = 'type:';
|
|
198
|
+
} else {
|
|
199
|
+
expectedFormatGroup = '(type)';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (detectedFormatGroup !== expectedFormatGroup) {
|
|
203
|
+
const exampleTitle = formatCommitTitle(
|
|
204
|
+
config.types[0].value,
|
|
205
|
+
'name',
|
|
206
|
+
config.format,
|
|
207
|
+
config.scopes?.[0]?.value
|
|
208
|
+
);
|
|
209
|
+
return { error: `Wrong format. Expected: "${exampleTitle}"` };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type = type.trim();
|
|
213
|
+
message = message.trim();
|
|
214
|
+
if (scope) {
|
|
215
|
+
scope = scope.trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate type exists (case-insensitive)
|
|
219
|
+
const validType = config.types.find(t => t.value.toLowerCase() === type.toLowerCase());
|
|
220
|
+
if (!validType) {
|
|
221
|
+
const validTypes = config.types.map(t => t.value).join(', ');
|
|
222
|
+
return { error: `Invalid type "${type}". Valid types: ${validTypes}` };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Validate scope if present (case-insensitive)
|
|
226
|
+
let validScope = scope;
|
|
227
|
+
if (scope) {
|
|
228
|
+
if (!config.scopes || config.scopes.length === 0) {
|
|
229
|
+
return { error: 'Scope not allowed in current format configuration' };
|
|
230
|
+
}
|
|
231
|
+
const foundScope = config.scopes.find(s => s.value.toLowerCase() === scope.toLowerCase());
|
|
232
|
+
if (!foundScope) {
|
|
233
|
+
const validScopes = config.scopes.map(s => s.value).join(', ');
|
|
234
|
+
return { error: `Invalid scope "${scope}". Valid scopes: ${validScopes}` };
|
|
235
|
+
}
|
|
236
|
+
validScope = foundScope.value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Re-format with correct case
|
|
240
|
+
const normalized = formatCommitTitle(validType.value, message, config.format, validScope);
|
|
241
|
+
|
|
242
|
+
return { normalized };
|
|
243
|
+
};
|
|
244
|
+
|
|
162
245
|
|
|
163
246
|
export {
|
|
164
247
|
askForVersion,
|
|
@@ -171,6 +254,7 @@ export {
|
|
|
171
254
|
formatCommitTitle,
|
|
172
255
|
formatBranchName,
|
|
173
256
|
checkBranchExists,
|
|
257
|
+
parseAndNormalizeCommitTitle,
|
|
174
258
|
handleCmdExec,
|
|
175
259
|
log,
|
|
176
260
|
};
|