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 CHANGED
@@ -1,13 +1,12 @@
1
1
  # format-commit
2
2
 
3
- [![npm version](https://badge.fury.io/js/format-commit.svg)](https://badge.fury.io/js/format-commit)
4
3
  [![Node.js Version](https://img.shields.io/node/v/format-commit.svg)](https://nodejs.org/)
5
4
  [![npm downloads](https://img.shields.io/npm/dm/format-commit.svg)](https://www.npmjs.com/package/format-commit)
6
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
6
 
8
- 🚀 Lightweight CLI for consistent Git workflow & and optional AI support.
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 just clean, consistent Git practices. Feel free to let AI suggest commit titles for you in the expected format.
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
- Then use:
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 formats, then create a `commit-config.json` file.
44
+ On first use, format-commit will prompt you to configure your commit and branch.
46
45
 
47
- To reconfigure later, run:
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
- | Property | Description |
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
- ### AI Suggestions
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
- | Property | Description |
69
- | :------- | :---------- |
70
- | **ai.enabled** | Enable AI commit title suggestions (default: `false`) |
71
- | **ai.provider** | AI provider:<br>`anthropic` (Claude)<br>`openai` (GPT)<br>`google` (Gemini) |
72
- | **ai.model** | Eg. `claude-haiku-4-5` or `gpt-4o-mini` |
73
- | **ai.envPath** | Path to .env file containing the AI provider API key (e.g., `.env`) |
74
- | **ai.envKeyName** | Name of the environment variable for the API key (e.g., `OPENAI_API_KEY`) |
75
- | **ai.largeDiffTokenThreshold** | Number of tokens from which not to use AI automatically |
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, format-commit will analyze your staged changes and suggest 4 complete commit titles that:
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:** Your AI provider API key is stored in a `.env` file (not versioned) and automatically added to `.gitignore`.
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
- | Option | Description |
92
- | :----- | :---------- |
93
- | `--config` / `-c` | Generate or update configuration file |
94
- | `--branch` / `-b` | Create a new standardized branch |
95
- | `--test` / `-t` | Test mode - preview without executing Git commands |
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
- - Available types: ${types}
86
- ${scopes ? `- Available scopes: ${scopes}` : '- No scopes - DO NOT include scope in output'}
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 a commit suggestion */
186
- const validateSuggestion = (suggestion, config) => {
187
- if (suggestion.length < config.minLength || suggestion.length > config.maxLength) {
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
- // Check that the suggestion contains a valid type
192
- const validTypes = config.types.map(t => t.value);
193
- const hasValidType = validTypes.some(type => {
194
- return suggestion.includes(type);
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
- return hasValidType;
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
- try {
203
- const diffData = getOptimizedGitDiff();
204
- if (!diffData) {
205
- return [];
206
- }
207
+ const diffData = getOptimizedGitDiff();
208
+ if (!diffData) {
209
+ return [];
210
+ }
207
211
 
208
- const apiKey = envUtils.getEnvKey(config.ai.envPath, config.ai.envKeyName);
209
- if (!apiKey) {
210
- utils.log('AI API key not found in .env', 'warning');
211
- return [];
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
- const prompt = buildPrompt(diffData, config);
215
- const estimatedTokens = Math.ceil(prompt.length / 4);
216
- const threshold = config.ai.largeDiffTokenThreshold || 20000;
217
-
218
- if (estimatedTokens > threshold) {
219
- const { confirm } = await prompts({
220
- type: 'confirm',
221
- name: 'confirm',
222
- message: `Large diff detected (~${estimatedTokens} tokens). Generate AI suggestions?`,
223
- initial: false,
224
- });
225
- if (!confirm) {
226
- return [];
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
- let responseText;
231
- if (config.ai.provider === 'anthropic') {
232
- responseText = await callAnthropicAPI(prompt, apiKey, config.ai.model);
233
- } else if (config.ai.provider === 'openai') {
234
- responseText = await callOpenAIAPI(prompt, apiKey, config.ai.model);
235
- } else if (config.ai.provider === 'google') {
236
- responseText = await callGeminiAPI(prompt, apiKey, config.ai.model);
237
- } else {
238
- throw new Error(`Unknown AI provider: ${config.ai.provider}`);
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
- const jsonMatch = responseText.match(/\[[\s\S]*\]/);
242
- if (!jsonMatch) {
243
- if (testMode) {
244
- utils.log(responseText, 'warning');
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
- const suggestions = JSON.parse(jsonMatch[0]);
252
+ const suggestions = JSON.parse(jsonMatch[0]);
250
253
 
251
- if (!Array.isArray(suggestions) || suggestions.length !== 4) {
252
- throw new Error('Invalid AI response format');
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
- const validSuggestions = suggestions.filter(s => validateSuggestion(s, config));
256
-
257
- if (validSuggestions.length < 4) {
258
- utils.log('Some AI suggestions were invalid', 'warning');
259
- return [];
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
- return validSuggestions;
276
+ const validSuggestions = validationResults
277
+ .filter(({ normalized }) => normalized !== null)
278
+ .map(({ normalized }) => normalized);
263
279
 
264
- } catch (error) {
265
- utils.log(`AI suggestion failed: ${error.message}`, 'warning');
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 (!testMode && !utils.hasStagedChanges()) {
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
- // Try to generate AI suggestions if enabled
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
- const suggestions = await aiService.generateCommitSuggestions(config, testMode);
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 && suggestions.length === 4) {
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 first
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, version and push
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,
@@ -77,7 +77,7 @@ export default async function createBranch(config, testMode) {
77
77
  );
78
78
 
79
79
  if (testMode) {
80
- utils.log(`Branch name: ${branchName}`, 'warning');
80
+ utils.log(`branch name: ${branchName}`, 'warning');
81
81
  return;
82
82
  }
83
83
 
package/lib/index.js CHANGED
@@ -30,8 +30,8 @@ program
30
30
 
31
31
  try {
32
32
  program.parse(process.argv);
33
- } catch (error) {
34
- console.error('Error parsing arguments:', error.message);
33
+ } catch (err) {
34
+ console.error('Error parsing arguments:', err.message);
35
35
  process.exit(1);
36
36
  }
37
37
 
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(`unable to save config file: ${err}`, 'error');
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "format-commit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Lightweight CLI to standardize commit messages with AI-powered suggestions",
5
5
  "license": "MIT",
6
6
  "author": "Thomas BARKATS",