format-commit 0.3.1 → 1.0.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/README.md +48 -10
- package/lib/ai-service.js +272 -0
- package/lib/commit.js +419 -148
- package/lib/create-branch.js +108 -0
- package/lib/default-config.json +23 -18
- package/lib/env-utils.js +109 -0
- package/lib/index.js +47 -34
- package/lib/options.json +60 -29
- package/lib/setup.js +260 -110
- package/lib/utils.js +146 -75
- package/package.json +14 -10
package/lib/setup.js
CHANGED
|
@@ -1,117 +1,267 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const options = require('./options.json');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module.exports = async (askForCommitAfter) => {
|
|
11
|
-
utils.log('create config file');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Get current git branch to pre-fill release branch option
|
|
15
|
-
*/
|
|
16
|
-
const currentBranch = utils.getCurrentBranch();
|
|
17
|
-
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
const configChoices = await prompts([
|
|
20
|
-
{
|
|
21
|
-
type: 'select',
|
|
22
|
-
name: 'format',
|
|
23
|
-
message: 'Commit format',
|
|
24
|
-
choices: options.commitFormats,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
type: 'number',
|
|
28
|
-
name: 'minLength',
|
|
29
|
-
message: 'Commit minimum length?',
|
|
30
|
-
validate: val => utils.validCommitTitleSetupLength(val),
|
|
31
|
-
initial: defaultConfig.minLength,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
type: 'number',
|
|
35
|
-
name: 'maxLength',
|
|
36
|
-
message: 'Commit maximum length?',
|
|
37
|
-
validate: val => utils.validCommitTitleSetupLength(val),
|
|
38
|
-
initial: defaultConfig.maxLength,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: 'confirm',
|
|
42
|
-
name: 'stageAllChanges',
|
|
43
|
-
message: 'Stage all changes before each commit?',
|
|
44
|
-
initial: defaultConfig.stageAllChanges,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
type: 'select',
|
|
48
|
-
name: 'changeVersion',
|
|
49
|
-
message: 'Change package version',
|
|
50
|
-
choices: options.versionChangeMode,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
type: prev => prev === 'releaseBranch' ? 'text' : null,
|
|
54
|
-
name: 'releaseBranch',
|
|
55
|
-
message: 'Release git branch ?',
|
|
56
|
-
initial: currentBranch,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
type: 'confirm',
|
|
60
|
-
name: 'showAllVersionTypes',
|
|
61
|
-
message: 'Display all npm version types?',
|
|
62
|
-
initial: defaultConfig.showAllVersionTypes,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: askForCommitAfter ? 'confirm' : null,
|
|
66
|
-
name: 'commitAfter',
|
|
67
|
-
message: 'Commit your changes now? (or exit the configuration without committing)',
|
|
68
|
-
initial: false,
|
|
69
|
-
},
|
|
70
|
-
], {
|
|
71
|
-
onCancel: () => {
|
|
72
|
-
cancelled = true;
|
|
73
|
-
return false;
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Handle prompt cancellation and stop setup execution
|
|
79
|
-
*/
|
|
80
|
-
if (cancelled) {
|
|
81
|
-
utils.log('setup cancelled', 'error');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import fs, { readFileSync } from 'fs';
|
|
3
|
+
import * as utils from './utils.js';
|
|
4
|
+
import * as envUtils from './env-utils.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
84
7
|
|
|
85
|
-
/**
|
|
86
|
-
* Parse prompt data and write config file
|
|
87
|
-
*/
|
|
88
|
-
const config = {
|
|
89
|
-
format: configChoices.format,
|
|
90
|
-
types: defaultConfig.types,
|
|
91
|
-
scopes: configChoices.format >= 5
|
|
92
|
-
? defaultConfig.scopes
|
|
93
|
-
: undefined,
|
|
94
|
-
minLength: configChoices.minLength,
|
|
95
|
-
maxLength: configChoices.maxLength,
|
|
96
|
-
changeVersion: configChoices.changeVersion,
|
|
97
|
-
releaseBranch: configChoices.releaseBranch,
|
|
98
|
-
showAllVersionTypes: configChoices.showAllVersionTypes,
|
|
99
|
-
stageAllChanges: configChoices.stageAllChanges,
|
|
100
|
-
};
|
|
101
|
-
const parsedConfig = JSON.stringify(config, null, 2);
|
|
102
8
|
|
|
103
|
-
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const defaultConfig = JSON.parse(
|
|
13
|
+
readFileSync(join(__dirname, 'default-config.json'), 'utf-8')
|
|
14
|
+
);
|
|
15
|
+
const options = JSON.parse(
|
|
16
|
+
readFileSync(join(__dirname, 'options.json'), 'utf-8')
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export default async function setup(askForCommitAfter) {
|
|
21
|
+
utils.log('create config file');
|
|
22
|
+
|
|
23
|
+
// Get current git branch to pre-fill release branch option
|
|
24
|
+
const currentBranch = utils.getCurrentBranch();
|
|
25
|
+
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
const configChoices = await prompts([
|
|
28
|
+
{
|
|
29
|
+
type: 'select',
|
|
30
|
+
name: 'format',
|
|
31
|
+
message: 'Commit messages nomenclature',
|
|
32
|
+
choices: options.commitFormats,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'select',
|
|
36
|
+
name: 'branchFormat',
|
|
37
|
+
message: 'Branch names nomenclature',
|
|
38
|
+
choices: options.branchFormats,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'number',
|
|
42
|
+
name: 'minLength',
|
|
43
|
+
message: 'Commit messages minimum length?',
|
|
44
|
+
validate: val => utils.validCommitTitleSetupLength(val),
|
|
45
|
+
initial: defaultConfig.minLength,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'number',
|
|
49
|
+
name: 'maxLength',
|
|
50
|
+
message: 'Commit messages maximum length?',
|
|
51
|
+
validate: val => utils.validCommitTitleSetupLength(val),
|
|
52
|
+
initial: defaultConfig.maxLength,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'select',
|
|
56
|
+
name: 'changeVersion',
|
|
57
|
+
message: 'Change package version when committing',
|
|
58
|
+
choices: options.versionChangeMode,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: prev => prev === 'releaseBranch' ? 'text' : null,
|
|
62
|
+
name: 'releaseBranch',
|
|
63
|
+
message: 'Release git branch?',
|
|
64
|
+
initial: currentBranch,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'confirm',
|
|
68
|
+
name: 'showAllVersionTypes',
|
|
69
|
+
message: 'Display all npm version types?',
|
|
70
|
+
initial: defaultConfig.showAllVersionTypes,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'enableAI',
|
|
75
|
+
message: 'Enable AI commit title suggestions? (API key required)',
|
|
76
|
+
initial: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: prev => prev ? 'select' : null,
|
|
80
|
+
name: 'aiProvider',
|
|
81
|
+
message: 'AI provider',
|
|
82
|
+
choices: options.aiProviders,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: prev => prev ? 'select' : null,
|
|
86
|
+
name: 'aiModel',
|
|
87
|
+
message: 'AI model',
|
|
88
|
+
choices: (prev, values) => {
|
|
89
|
+
const provider = options.aiProviders.find(p => p.value === values.aiProvider);
|
|
90
|
+
return provider ? provider.models : [];
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: (prev, values) => values.enableAI ? 'text' : null,
|
|
95
|
+
name: 'envPath',
|
|
96
|
+
message: 'Path to .env file for API key (will be created if doesn\'t exist)',
|
|
97
|
+
initial: '.env',
|
|
98
|
+
validate: (val) => {
|
|
99
|
+
if (!val || val.trim().length === 0) {
|
|
100
|
+
return 'Please provide a .env file path';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const envPath = val.trim();
|
|
104
|
+
const envExists = fs.existsSync(envPath);
|
|
105
|
+
|
|
106
|
+
// If .env exists, check that it is in .gitignore
|
|
107
|
+
if (envExists && !envUtils.isInGitignore(envPath)) {
|
|
108
|
+
return `${envPath} must be in .gitignore for security. Please add it first.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: (prev, values) => {
|
|
116
|
+
if (!values.enableAI) { return null; }
|
|
117
|
+
return fs.existsSync(values.envPath) ? 'text' : null;
|
|
118
|
+
},
|
|
119
|
+
name: 'envKeyName',
|
|
120
|
+
message: 'Name of the API key variable in .env (leave empty to create new)',
|
|
121
|
+
initial: (prev, values) => `${values.aiProvider.toUpperCase()}_API_KEY`,
|
|
122
|
+
validate: (val) => {
|
|
123
|
+
if (!val || val.trim().length === 0) {
|
|
124
|
+
return 'Please provide a variable name';
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: (prev, values) => {
|
|
131
|
+
if (!values.enableAI) { return null; }
|
|
132
|
+
|
|
133
|
+
const keyName = values.envKeyName || `${values.aiProvider.toUpperCase()}_API_KEY`;
|
|
134
|
+
const keyExists = envUtils.keyExistsInEnv(values.envPath, keyName);
|
|
135
|
+
|
|
136
|
+
if (keyExists) {
|
|
137
|
+
return 'confirm';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
},
|
|
142
|
+
name: 'updateApiKey',
|
|
143
|
+
message: (prev, values) => {
|
|
144
|
+
const keyName = values.envKeyName || `${values.aiProvider.toUpperCase()}_API_KEY`;
|
|
145
|
+
const keyExists = envUtils.keyExistsInEnv(values.envPath, keyName);
|
|
104
146
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
if (keyExists) {
|
|
148
|
+
return `${keyName} already exists in ${values.envPath}. Update it?`;
|
|
149
|
+
}
|
|
150
|
+
const providerNames = { anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google' };
|
|
151
|
+
return `Enter your ${providerNames[values.aiProvider] || values.aiProvider} API key`;
|
|
152
|
+
},
|
|
153
|
+
initial: false,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: (prev, values) => {
|
|
157
|
+
if (!values.enableAI) { return null; }
|
|
158
|
+
|
|
159
|
+
const keyName = values.envKeyName || `${values.aiProvider.toUpperCase()}_API_KEY`;
|
|
160
|
+
const keyExists = envUtils.keyExistsInEnv(values.envPath, keyName);
|
|
161
|
+
|
|
162
|
+
if (!keyExists || prev === true) {
|
|
163
|
+
return 'password';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
},
|
|
168
|
+
name: 'apiKey',
|
|
169
|
+
message: (prev, values) => {
|
|
170
|
+
const providerNames = { anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google' };
|
|
171
|
+
return `Enter your ${providerNames[values.aiProvider] || values.aiProvider} API key`;
|
|
172
|
+
},
|
|
173
|
+
validate: (val) => {
|
|
174
|
+
if (!val || val.trim().length < 20) {
|
|
175
|
+
return 'Please provide a valid API key';
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: askForCommitAfter ? 'confirm' : null,
|
|
182
|
+
name: 'commitAfter',
|
|
183
|
+
message: 'Commit your changes now? (or exit the configuration without committing)',
|
|
184
|
+
initial: false,
|
|
185
|
+
},
|
|
186
|
+
], {
|
|
187
|
+
onCancel: () => {
|
|
188
|
+
cancelled = true;
|
|
189
|
+
return false;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (cancelled) {
|
|
194
|
+
utils.log('setup cancelled', 'error');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Parse prompt data and write config file
|
|
199
|
+
const config = {
|
|
200
|
+
format: configChoices.format,
|
|
201
|
+
branchFormat: configChoices.branchFormat,
|
|
202
|
+
types: defaultConfig.types,
|
|
203
|
+
scopes: (configChoices.format >= 5 || configChoices.branchFormat === 2)
|
|
204
|
+
? defaultConfig.scopes
|
|
205
|
+
: undefined,
|
|
206
|
+
minLength: configChoices.minLength,
|
|
207
|
+
maxLength: configChoices.maxLength,
|
|
208
|
+
changeVersion: configChoices.changeVersion,
|
|
209
|
+
releaseBranch: configChoices.releaseBranch,
|
|
210
|
+
showAllVersionTypes: configChoices.showAllVersionTypes,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Handle AI configuration
|
|
214
|
+
if (configChoices.enableAI) {
|
|
215
|
+
const envPath = configChoices.envPath;
|
|
216
|
+
const keyName = configChoices.envKeyName || `${configChoices.aiProvider.toUpperCase()}_API_KEY`;
|
|
217
|
+
|
|
218
|
+
if (configChoices.apiKey) {
|
|
219
|
+
const saved = envUtils.setEnvKey(envPath, keyName, configChoices.apiKey);
|
|
220
|
+
if (!saved) {
|
|
221
|
+
utils.log(`Failed to save API key to ${envPath}`, 'error');
|
|
110
222
|
return;
|
|
223
|
+
}
|
|
224
|
+
utils.log(`API key saved to ${envPath}`, 'success');
|
|
111
225
|
}
|
|
112
226
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
227
|
+
if (!envUtils.isInGitignore(envPath)) {
|
|
228
|
+
const added = envUtils.addToGitignore(envPath);
|
|
229
|
+
if (added) {
|
|
230
|
+
utils.log(`${envPath} added to .gitignore`, 'success');
|
|
231
|
+
} else {
|
|
232
|
+
utils.log(`Failed to add ${envPath} to .gitignore`, 'warning');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
config.ai = {
|
|
237
|
+
enabled: true,
|
|
238
|
+
provider: configChoices.aiProvider,
|
|
239
|
+
model: configChoices.aiModel,
|
|
240
|
+
envPath: envPath,
|
|
241
|
+
envKeyName: keyName,
|
|
242
|
+
largeDiffTokenThreshold: defaultConfig.ai.largeDiffTokenThreshold,
|
|
116
243
|
};
|
|
117
|
-
}
|
|
244
|
+
} else {
|
|
245
|
+
config.ai = {
|
|
246
|
+
enabled: false,
|
|
247
|
+
largeDiffTokenThreshold: defaultConfig.ai.largeDiffTokenThreshold,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const parsedConfig = JSON.stringify(config, null, 2);
|
|
252
|
+
|
|
253
|
+
utils.log(`save ${options.configFile}.json file...`);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
fs.writeFileSync(`./${options.configFile}.json`, parsedConfig);
|
|
257
|
+
utils.log('config file successfully created', 'success');
|
|
258
|
+
} catch (err) {
|
|
259
|
+
utils.log(`unable to save config file: ${err}`, 'error');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
config,
|
|
265
|
+
commitAfter: configChoices.commitAfter
|
|
266
|
+
};
|
|
267
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -1,105 +1,176 @@
|
|
|
1
|
-
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import kleur from 'kleur';
|
|
2
3
|
|
|
3
|
-
const { execSync } = require('child_process');
|
|
4
|
-
const { gray, bold, red, green, yellow } = require('kleur');
|
|
5
4
|
|
|
5
|
+
const { gray, bold, red, green, yellow } = kleur;
|
|
6
6
|
|
|
7
7
|
const askForVersion = (config, branch) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
if (
|
|
9
|
+
config.changeVersion === 'always'
|
|
10
|
+
|| (config.changeVersion === 'releaseBranch' && branch === config.releaseBranch)
|
|
11
|
+
) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const getCurrentBranch = () => {
|
|
18
|
-
|
|
18
|
+
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const hasStagedChanges = () => {
|
|
22
|
+
try {
|
|
23
|
+
const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' });
|
|
24
|
+
return staged.trim().length > 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
const validCommitTitle = (title, lenMin, lenMax) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (title.length < lenMin) {
|
|
32
|
+
return `Commit title too short (current ${title.length} - minimum ${lenMin})`;
|
|
33
|
+
} else if (title.length > lenMax) {
|
|
34
|
+
return `Commit title too long (current ${title.length} - maximum ${lenMax})`;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
const validCommitTitleSetupLength = (len) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
if (len < 1) {
|
|
41
|
+
return `${len} isn't a valid length`;
|
|
42
|
+
} else if (len > 255) {
|
|
43
|
+
return 'length cannot be higher than 255';
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
37
46
|
};
|
|
38
47
|
|
|
39
48
|
const validVersion = (version) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
50
|
+
if (!regex.test(version)) {
|
|
51
|
+
return 'Version does not respect semantic versioning';
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
const formatCommitTitle = (type, title, format, scope = '*') => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
// Handle empty title
|
|
58
|
+
if (!title || title.trim().length === 0) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trimmedTitle = title.trim();
|
|
63
|
+
|
|
64
|
+
switch (format) {
|
|
65
|
+
case 1:
|
|
66
|
+
default:
|
|
67
|
+
return `(${type}) ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
|
|
68
|
+
case 2:
|
|
69
|
+
return `(${type}) ${trimmedTitle.toLowerCase()}`;
|
|
70
|
+
case 3:
|
|
71
|
+
return `${type}: ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
|
|
72
|
+
case 4:
|
|
73
|
+
return `${type}: ${trimmedTitle.toLowerCase()}`;
|
|
74
|
+
case 5:
|
|
75
|
+
return `${type}(${scope}) ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
|
|
76
|
+
case 6:
|
|
77
|
+
return `${type}(${scope}) ${trimmedTitle.toLowerCase()}`;
|
|
78
|
+
case 7:
|
|
79
|
+
return `${type}(${scope}): ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
|
|
80
|
+
case 8:
|
|
81
|
+
return `${type}(${scope}): ${trimmedTitle.toLowerCase()}`;
|
|
82
|
+
}
|
|
67
83
|
};
|
|
68
84
|
|
|
69
85
|
const handleCmdExec = (command) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
try {
|
|
87
|
+
const output = execSync(command);
|
|
88
|
+
return output.toString();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log(`Error\n${err.message ? err.message : err}`, 'error');
|
|
91
|
+
}
|
|
76
92
|
};
|
|
77
93
|
|
|
78
94
|
const log = (message, type) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
const date = gray(`[${new Date().toISOString()}]`);
|
|
96
|
+
let msg = `${bold('format-commit')}: ${message}`;
|
|
97
|
+
switch (type) {
|
|
98
|
+
case 'error':
|
|
99
|
+
msg = red(msg);
|
|
100
|
+
break;
|
|
101
|
+
case 'success':
|
|
102
|
+
msg = green(msg);
|
|
103
|
+
break;
|
|
104
|
+
case 'warning':
|
|
105
|
+
msg = yellow(msg);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
console.log(`${date} ${type === 'error' ? red(msg) : (type === 'success' ? green(msg) : msg)}`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const validBranchDescription = (description, maxLength) => {
|
|
112
|
+
if (description.length < 1) {
|
|
113
|
+
return 'Branch description cannot be empty';
|
|
114
|
+
}
|
|
115
|
+
if (description.length > maxLength) {
|
|
116
|
+
return `Branch description too long (max ${maxLength} characters)`;
|
|
117
|
+
}
|
|
118
|
+
const invalidChars = /[~^:?*[\\\s]/;
|
|
119
|
+
if (invalidChars.test(description)) {
|
|
120
|
+
return 'Branch description contains invalid characters (spaces, ~, ^, :, ?, *, [, \\)';
|
|
121
|
+
}
|
|
122
|
+
if (description.startsWith('.') || description.startsWith('-') ||
|
|
123
|
+
description.endsWith('.') || description.endsWith('-')) {
|
|
124
|
+
return 'Branch description cannot start or end with . or -';
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const formatBranchName = (type, description, format, scope = null) => {
|
|
130
|
+
const cleanDescription = description
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/\s+/g, '-')
|
|
133
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
134
|
+
.replace(/-+/g, '-')
|
|
135
|
+
.replace(/^-|-$/g, '');
|
|
136
|
+
|
|
137
|
+
switch (format) {
|
|
138
|
+
case 1:
|
|
139
|
+
default:
|
|
140
|
+
return `${type}/${cleanDescription}`;
|
|
141
|
+
case 2:
|
|
142
|
+
return scope ? `${type}/${scope}/${cleanDescription}` : `${type}/${cleanDescription}`;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const checkBranchExists = (branchName) => {
|
|
147
|
+
try {
|
|
148
|
+
const localBranches = execSync('git branch --list').toString();
|
|
149
|
+
if (localBranches.includes(branchName)) {
|
|
150
|
+
return true;
|
|
91
151
|
}
|
|
92
|
-
|
|
152
|
+
const remoteBranches = execSync('git branch -r --list').toString();
|
|
153
|
+
if (remoteBranches.includes(`origin/${branchName}`)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
93
160
|
};
|
|
94
161
|
|
|
95
162
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
163
|
+
export {
|
|
164
|
+
askForVersion,
|
|
165
|
+
getCurrentBranch,
|
|
166
|
+
hasStagedChanges,
|
|
167
|
+
validCommitTitle,
|
|
168
|
+
validCommitTitleSetupLength,
|
|
169
|
+
validBranchDescription,
|
|
170
|
+
validVersion,
|
|
171
|
+
formatCommitTitle,
|
|
172
|
+
formatBranchName,
|
|
173
|
+
checkBranchExists,
|
|
174
|
+
handleCmdExec,
|
|
175
|
+
log,
|
|
105
176
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "format-commit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lightweight CLI to standardize commit messages",
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight CLI to standardize commit messages with AI-powered suggestions",
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Thomas BARKATS",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"git",
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"workflow",
|
|
17
17
|
"tool",
|
|
18
18
|
"javascript",
|
|
19
|
-
"nodejs"
|
|
19
|
+
"nodejs",
|
|
20
|
+
"ai",
|
|
21
|
+
"gpt",
|
|
22
|
+
"claude",
|
|
23
|
+
"openai",
|
|
24
|
+
"anthropic"
|
|
20
25
|
],
|
|
21
26
|
"repository": {
|
|
22
27
|
"type": "git",
|
|
@@ -27,13 +32,12 @@
|
|
|
27
32
|
"url": "https://github.com/thomasbarkats/format-commit/issues"
|
|
28
33
|
},
|
|
29
34
|
"main": "./lib/index.js",
|
|
30
|
-
"type": "
|
|
35
|
+
"type": "module",
|
|
31
36
|
"engines": {
|
|
32
37
|
"node": ">=16.0.0"
|
|
33
38
|
},
|
|
34
39
|
"scripts": {
|
|
35
40
|
"start": "node lib/index.js",
|
|
36
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
37
41
|
"lint": "eslint . --ext .js --fix",
|
|
38
42
|
"prepublishOnly": "npm run lint",
|
|
39
43
|
"preversion": "npm run lint",
|
|
@@ -46,12 +50,12 @@
|
|
|
46
50
|
"format-commit": "./lib/index.js"
|
|
47
51
|
},
|
|
48
52
|
"dependencies": {
|
|
49
|
-
"commander": "^14.0.
|
|
50
|
-
"kleur": "^4.1.
|
|
53
|
+
"commander": "^14.0.3",
|
|
54
|
+
"kleur": "^4.1.5",
|
|
51
55
|
"prompts": "^2.4.2"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
54
|
-
"@eslint/js": "^9.
|
|
55
|
-
"eslint": "^9.
|
|
58
|
+
"@eslint/js": "^9.31.0",
|
|
59
|
+
"eslint": "^9.31.0"
|
|
56
60
|
}
|
|
57
61
|
}
|