format-commit 1.0.0 → 1.1.0

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/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) => {
@@ -108,7 +24,7 @@ const promptWithPrefill = (message, prefill, validate) => {
108
24
  output: process.stdout
109
25
  });
110
26
 
111
- const questionText = `${kleur.bold(message)}\n `;
27
+ const questionText = `${kleur.bold(message)}\n> `;
112
28
 
113
29
  // Handle Ctrl+C to cancel
114
30
  rl.on('SIGINT', () => {
@@ -186,19 +102,23 @@ const finalizeCommit = async (title, description, commitData, currentBranch, tes
186
102
  };
187
103
 
188
104
 
189
- export default async function commit(config, testMode) {
105
+ export default async function commit(config, testMode, debugMode) {
190
106
  if (!config) {
191
107
  return;
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
  }
199
115
 
200
116
  if (testMode) {
201
- utils.log('test mode enabled - commit will not be performed', 'warning');
117
+ utils.log('test mode enabled (commit will not be performed)', 'warning');
118
+ }
119
+
120
+ if (debugMode) {
121
+ utils.log('debug mode enabled (additional visible logs)', 'debug');
202
122
  }
203
123
 
204
124
  // Get current git branch for version change option "only on release branch"
@@ -212,22 +132,71 @@ export default async function commit(config, testMode) {
212
132
  return;
213
133
  }
214
134
 
135
+ if (config.format === 'custom') {
136
+ const formatValid = utils.validateCustomFormatPattern(config.customFormat);
137
+ if (formatValid !== true) {
138
+ utils.log(`Invalid custom format - ${formatValid}`, 'error');
139
+ return;
140
+ }
141
+ }
142
+
143
+ const formatNeedsScope = config.format === 'custom'
144
+ ? utils.customFormatHasScope(config.customFormat)
145
+ : config.format >= 5;
146
+
215
147
  const noScope = !config.scopes || (config.scopes && config.scopes.length === 0);
216
- if (config.format >= 5 && noScope) {
148
+ if (formatNeedsScope && noScope) {
217
149
  utils.log('no scopes defined - update config or format option', 'error');
218
150
  return;
219
151
  }
220
152
 
153
+ // Warn if using default example scope and skip AI suggestions
154
+ const hasDefaultScope = config.scopes && config.scopes.length === 1 && config.scopes[0].value === 'example';
155
+ if (hasDefaultScope) {
156
+ utils.log('You are using the default scope "example" - customize your scopes in commit-config.json or run `format-commit --config`', 'warning');
157
+ utils.log('AI suggestions skipped - configure your scopes', 'warning');
158
+ }
159
+
160
+ // Collect custom field values early (needed before AI and classic flows)
161
+ let cancelled = false;
162
+ let customFieldValues = {};
163
+ if (config.format === 'custom') {
164
+ const fields = utils.getCustomFields(config.customFormat);
165
+ for (const label of fields) {
166
+ const resp = await prompts({
167
+ type: 'text',
168
+ name: 'value',
169
+ message: `${label}?`,
170
+ validate: v => (!v || !v.trim()) ? `${label} cannot be empty` : true,
171
+ }, {
172
+ onCancel: () => {
173
+ cancelled = true;
174
+ return false;
175
+ },
176
+ });
177
+ if (cancelled) {
178
+ utils.log('commit cancelled', 'error');
179
+ return;
180
+ }
181
+ customFieldValues[label] = resp.value;
182
+ }
183
+ }
184
+
221
185
  let commitTitle = null;
222
186
  let useAISuggestion = false;
223
187
 
224
- // Try to generate AI suggestions if enabled
225
- if (config.ai?.enabled) {
188
+ if (config.ai?.enabled && !hasDefaultScope) {
226
189
  utils.log('generating suggestions...');
227
190
  const aiService = await import('./ai-service.js');
228
- const suggestions = await aiService.generateCommitSuggestions(config, testMode);
191
+ let suggestions = [];
192
+
193
+ try {
194
+ suggestions = await aiService.generateCommitSuggestions(config, debugMode, customFieldValues);
195
+ } catch (err) {
196
+ utils.log(`AI suggestions failed (${err.message})`, 'warning');
197
+ }
229
198
 
230
- if (suggestions && suggestions.length === 4) {
199
+ if (suggestions?.length) {
231
200
  let aiCancelled = false;
232
201
  const aiChoice = await prompts({
233
202
  type: 'select',
@@ -260,7 +229,7 @@ export default async function commit(config, testMode) {
260
229
  }
261
230
 
262
231
  // Parse and validate format, type, and scope
263
- const result = parseAndNormalizeCommitTitle(val, config);
232
+ const result = utils.parseAndNormalizeCommitTitle(val, config, customFieldValues);
264
233
  if (result.error) {
265
234
  return result.error;
266
235
  }
@@ -276,7 +245,7 @@ export default async function commit(config, testMode) {
276
245
  );
277
246
 
278
247
  // Normalize the final title (correct case)
279
- const result = parseAndNormalizeCommitTitle(rawTitle, config);
248
+ const result = utils.parseAndNormalizeCommitTitle(rawTitle, config, customFieldValues);
280
249
  commitTitle = result.normalized;
281
250
  useAISuggestion = true;
282
251
  } catch (err) {
@@ -286,13 +255,9 @@ export default async function commit(config, testMode) {
286
255
  return;
287
256
  }
288
257
  }
289
- } else {
290
- utils.log('AI suggestions failed, using manual input', 'warning');
291
258
  }
292
259
  }
293
260
 
294
- let cancelled = false;
295
-
296
261
  // If AI suggestion was accepted, only ask for description, version, and push
297
262
  if (useAISuggestion) {
298
263
  const commit = await prompts([
@@ -323,7 +288,7 @@ export default async function commit(config, testMode) {
323
288
  validate: val => utils.validVersion(val),
324
289
  },
325
290
  {
326
- type: 'confirm',
291
+ type: testMode ? null : 'confirm',
327
292
  name: 'pushAfterCommit',
328
293
  message: 'Push changes?',
329
294
  initial: false,
@@ -344,7 +309,7 @@ export default async function commit(config, testMode) {
344
309
  return;
345
310
  }
346
311
 
347
- // Classic flow: ask for type and scope first
312
+ // Classic flow: ask for type and scope
348
313
  const typeScope = await prompts([
349
314
  {
350
315
  type: 'select',
@@ -353,7 +318,7 @@ export default async function commit(config, testMode) {
353
318
  choices: config.types,
354
319
  },
355
320
  {
356
- type: config.format >= 5 ? 'select' : null,
321
+ type: formatNeedsScope ? 'select' : null,
357
322
  name: 'scope',
358
323
  message: 'Scope',
359
324
  choices: config.scopes,
@@ -370,7 +335,7 @@ export default async function commit(config, testMode) {
370
335
  return;
371
336
  }
372
337
 
373
- // Ask for title with full formatted length validation, then description, version and push
338
+ // Ask for title with full formatted length validation, then description, and options
374
339
  const commit = await prompts([
375
340
  {
376
341
  type: 'text',
@@ -380,7 +345,10 @@ export default async function commit(config, testMode) {
380
345
  if (!val || val.trim().length === 0) {
381
346
  return 'Commit title cannot be empty';
382
347
  }
383
- const fullTitle = utils.formatCommitTitle(typeScope.type, val, config.format, typeScope.scope);
348
+ const fullTitle = utils.formatCommitTitle(
349
+ typeScope.type, val, config.format, typeScope.scope,
350
+ config.customFormat, customFieldValues
351
+ );
384
352
  return utils.validCommitTitle(fullTitle, config.minLength, config.maxLength);
385
353
  },
386
354
  },
@@ -429,12 +397,13 @@ export default async function commit(config, testMode) {
429
397
  return;
430
398
  }
431
399
 
432
- // Format changes message and commit it
433
400
  commitTitle = utils.formatCommitTitle(
434
401
  typeScope.type,
435
402
  commit.title,
436
403
  config.format,
437
- typeScope.scope
404
+ typeScope.scope,
405
+ config.customFormat,
406
+ customFieldValues
438
407
  );
439
408
 
440
409
  await finalizeCommit(commitTitle, commit.description, commit, currentBranch, testMode);
@@ -23,14 +23,36 @@ export default async function createBranch(config, testMode) {
23
23
  return;
24
24
  }
25
25
 
26
+ const isCustom = config.branchFormat === 'custom';
27
+ const customHasScope = isCustom && config.customBranchFormat && utils.customBranchFormatHasScope(config.customBranchFormat);
28
+ const needsScope = config.branchFormat === 2 || customHasScope;
29
+
26
30
  const noScope = !config.scopes || (config.scopes && config.scopes.length === 0);
27
- if (config.branchFormat === 2 && noScope) {
31
+ if (needsScope && noScope) {
28
32
  utils.log('no scopes defined - update config or branch format option', 'error');
29
33
  return;
30
34
  }
31
35
 
36
+ if (isCustom) {
37
+ const formatValid = utils.validateCustomBranchFormatPattern(config.customBranchFormat);
38
+ if (formatValid !== true) {
39
+ utils.log(`Invalid custom branch format - ${formatValid}`, 'error');
40
+ return;
41
+ }
42
+ }
43
+
44
+ // Collect custom fields if custom format
45
+ const customFields = isCustom ? utils.getCustomBranchFields(config.customBranchFormat) : [];
46
+ const customFieldPrompts = customFields.map(field => ({
47
+ type: 'text',
48
+ name: `custom_${field}`,
49
+ message: `${field}?`,
50
+ validate: val => utils.validBranchCustomField(val, field),
51
+ }));
52
+
32
53
  let cancelled = false;
33
54
  const branch = await prompts([
55
+ ...customFieldPrompts,
34
56
  {
35
57
  type: 'select',
36
58
  name: 'type',
@@ -38,7 +60,7 @@ export default async function createBranch(config, testMode) {
38
60
  choices: config.types,
39
61
  },
40
62
  {
41
- type: config.branchFormat === 2 ? 'select' : null,
63
+ type: needsScope ? 'select' : null,
42
64
  name: 'scope',
43
65
  message: 'Scope',
44
66
  choices: config.scopes,
@@ -50,7 +72,7 @@ export default async function createBranch(config, testMode) {
50
72
  validate: val => utils.validBranchDescription(val, config.maxLength),
51
73
  },
52
74
  {
53
- type: 'confirm',
75
+ type: testMode ? null : 'confirm',
54
76
  name: 'checkoutAfterCreate',
55
77
  message: 'Switch to the new branch after creation?',
56
78
  initial: true,
@@ -67,17 +89,25 @@ export default async function createBranch(config, testMode) {
67
89
  return;
68
90
  }
69
91
 
92
+ // Build custom field values from prompt answers
93
+ const customFieldValues = {};
94
+ for (const field of customFields) {
95
+ customFieldValues[field] = branch[`custom_${field}`];
96
+ }
97
+
70
98
  // Format branch name and create it
71
99
  utils.log('create branch...');
72
100
  const branchName = utils.formatBranchName(
73
101
  branch.type,
74
102
  branch.description,
75
103
  config.branchFormat,
76
- branch.scope
104
+ branch.scope,
105
+ config.customBranchFormat,
106
+ customFieldValues
77
107
  );
78
108
 
79
109
  if (testMode) {
80
- utils.log(`Branch name: ${branchName}`, 'warning');
110
+ utils.log(`branch name: ${branchName}`, 'warning');
81
111
  return;
82
112
  }
83
113
 
package/lib/index.js CHANGED
@@ -26,12 +26,13 @@ program
26
26
  .version('0.3.1')
27
27
  .option('-b, --branch', 'create a new branch with standardized naming')
28
28
  .option('-c, --config', 'generate a configuration file on your project for format-commit')
29
- .option('-t, --test', 'start without finalize commit (for tests)');
29
+ .option('-t, --test', 'start without finalize commit (for tests)')
30
+ .option('-d, --debug', 'display additional logs');
30
31
 
31
32
  try {
32
33
  program.parse(process.argv);
33
- } catch (error) {
34
- console.error('Error parsing arguments:', error.message);
34
+ } catch (err) {
35
+ console.error('Error parsing arguments:', err.message);
35
36
  process.exit(1);
36
37
  }
37
38
 
@@ -49,7 +50,7 @@ try {
49
50
  utils.log('no configuration found', 'warning');
50
51
  const setupResult = await setup(true);
51
52
  if (setupResult && setupResult.commitAfter) {
52
- commit(setupResult.config, opts.test);
53
+ commit(setupResult.config, opts.test, opts.debug);
53
54
  }
54
55
  } else {
55
56
  if (opts.branch) {
@@ -57,7 +58,7 @@ try {
57
58
  createBranch(JSON.parse(data), opts.test);
58
59
  return;
59
60
  }
60
- commit(JSON.parse(data), opts.test);
61
+ commit(JSON.parse(data), opts.test, opts.debug);
61
62
  }
62
63
  });
63
64
  })();
package/lib/options.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "configFile": "commit-config",
3
3
  "commitFormats": [
4
- { "value": 1, "title": "(type) Name" },
5
- { "value": 2, "title": "(type) name" },
6
- { "value": 3, "title": "type: Name" },
7
- { "value": 4, "title": "type: name" },
8
- { "value": 5, "title": "type(scope) Name" },
9
- { "value": 6, "title": "type(scope) name" },
10
- { "value": 7, "title": "type(scope): Name" },
11
- { "value": 8, "title": "type(scope): name" }
4
+ { "value": 1, "title": "(type) Description" },
5
+ { "value": 2, "title": "(type) description" },
6
+ { "value": 3, "title": "type: Description" },
7
+ { "value": 4, "title": "type: description" },
8
+ { "value": 5, "title": "type(scope) Description" },
9
+ { "value": 6, "title": "type(scope) description" },
10
+ { "value": 7, "title": "type(scope): Description" },
11
+ { "value": 8, "title": "type(scope): description" }
12
12
  ],
13
13
  "branchFormats": [
14
14
  { "value": 1, "title": "type/description" },
package/lib/setup.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import prompts from 'prompts';
2
+ import kleur from 'kleur';
2
3
  import fs, { readFileSync } from 'fs';
3
4
  import * as utils from './utils.js';
4
5
  import * as envUtils from './env-utils.js';
@@ -29,13 +30,31 @@ export default async function setup(askForCommitAfter) {
29
30
  type: 'select',
30
31
  name: 'format',
31
32
  message: 'Commit messages nomenclature',
32
- choices: options.commitFormats,
33
+ choices: [
34
+ ...options.commitFormats,
35
+ { value: 'custom', title: kleur.gray('Custom - define your own pattern') },
36
+ ],
37
+ },
38
+ {
39
+ type: prev => prev === 'custom' ? 'text' : null,
40
+ name: 'customFormat',
41
+ message: 'Custom format pattern (e.g. `{Issue ID} - type - scope - Description`)',
42
+ validate: val => utils.validateCustomFormatPattern(val),
33
43
  },
34
44
  {
35
45
  type: 'select',
36
46
  name: 'branchFormat',
37
47
  message: 'Branch names nomenclature',
38
- choices: options.branchFormats,
48
+ choices: [
49
+ ...options.branchFormats,
50
+ { value: 'custom', title: kleur.gray('Custom - define your own pattern') },
51
+ ],
52
+ },
53
+ {
54
+ type: prev => prev === 'custom' ? 'text' : null,
55
+ name: 'customBranchFormat',
56
+ message: 'Custom branch format pattern (e.g. `{Issue ID}-type/description`)',
57
+ validate: val => utils.validateCustomBranchFormatPattern(val),
39
58
  },
40
59
  {
41
60
  type: 'number',
@@ -196,13 +215,20 @@ export default async function setup(askForCommitAfter) {
196
215
  }
197
216
 
198
217
  // Parse prompt data and write config file
218
+ const needsScope = (
219
+ (configChoices.format !== 'custom' && configChoices.format >= 5) ||
220
+ (configChoices.format === 'custom' && utils.customFormatHasScope(configChoices.customFormat)) ||
221
+ configChoices.branchFormat === 2 ||
222
+ (configChoices.branchFormat === 'custom' && utils.customBranchFormatHasScope(configChoices.customBranchFormat))
223
+ );
224
+
199
225
  const config = {
200
226
  format: configChoices.format,
227
+ customFormat: configChoices.format === 'custom' ? configChoices.customFormat : undefined,
201
228
  branchFormat: configChoices.branchFormat,
229
+ customBranchFormat: configChoices.branchFormat === 'custom' ? configChoices.customBranchFormat : undefined,
202
230
  types: defaultConfig.types,
203
- scopes: (configChoices.format >= 5 || configChoices.branchFormat === 2)
204
- ? defaultConfig.scopes
205
- : undefined,
231
+ scopes: needsScope ? defaultConfig.scopes : undefined,
206
232
  minLength: configChoices.minLength,
207
233
  maxLength: configChoices.maxLength,
208
234
  changeVersion: configChoices.changeVersion,
@@ -255,8 +281,9 @@ export default async function setup(askForCommitAfter) {
255
281
  try {
256
282
  fs.writeFileSync(`./${options.configFile}.json`, parsedConfig);
257
283
  utils.log('config file successfully created', 'success');
284
+ utils.log(`Customize default types and scopes in ${options.configFile}.json`);
258
285
  } catch (err) {
259
- utils.log(`unable to save config file: ${err}`, 'error');
286
+ utils.log(`Unable to save config file: ${err.message}`, 'error');
260
287
  return;
261
288
  }
262
289