format-commit 0.4.0 → 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/lib/index.js CHANGED
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
- const { Command } = require('commander');
5
- const fs = require('fs');
6
- const utils = require('./utils');
7
- const setup = require('./setup');
8
- const commit = require('./commit');
9
- const options = require('./options.json');
3
+ import { Command } from 'commander';
4
+ import fs from 'fs';
5
+ import * as utils from './utils.js';
6
+ import setup from './setup.js';
7
+ import commit from './commit.js';
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ const options = JSON.parse(
17
+ readFileSync(join(__dirname, 'options.json'), 'utf-8')
18
+ );
19
+
10
20
 
11
21
  const program = new Command();
12
22
 
@@ -33,10 +43,7 @@ try {
33
43
  return;
34
44
  }
35
45
 
36
- /**
37
- * Get config from consumer package root
38
- * Generate new config file if not founded
39
- */
46
+ // Get config from consumer package root, generate new config file if not found
40
47
  fs.readFile(`./${options.configFile}.json`, async (err, data) => {
41
48
  if (err) {
42
49
  utils.log('no configuration found', 'warning');
@@ -46,7 +53,7 @@ try {
46
53
  }
47
54
  } else {
48
55
  if (opts.branch) {
49
- const createBranch = require('./create-branch');
56
+ const { default: createBranch } = await import('./create-branch.js');
50
57
  createBranch(JSON.parse(data), opts.test);
51
58
  return;
52
59
  }
package/lib/options.json CHANGED
@@ -15,6 +15,7 @@
15
15
  { "value": 2, "title": "type/scope/description" }
16
16
  ],
17
17
  "versionChangeMode": [
18
+ { "value": "ignore", "title": "never (ignore)" },
18
19
  { "value": "never", "title": "never (always ask)" },
19
20
  { "value": "releaseBranch", "title": "only on release branch" },
20
21
  { "value": "always" }
@@ -31,5 +32,31 @@
31
32
  { "value": "premajor" },
32
33
  { "value": "prerelease", "title": "prerelease <tag>" },
33
34
  { "value": "from-git" }
35
+ ],
36
+ "aiProviders": [
37
+ {
38
+ "value": "anthropic",
39
+ "title": "Anthropic (Claude)",
40
+ "models": [
41
+ { "value": "claude-haiku-4-5", "title": "Claude Haiku 4.5 (Fast & Cheap)" },
42
+ { "value": "claude-sonnet-4-5", "title": "Claude Sonnet 4.5 (Balanced)" }
43
+ ]
44
+ },
45
+ {
46
+ "value": "openai",
47
+ "title": "OpenAI (GPT)",
48
+ "models": [
49
+ { "value": "gpt-4o-mini", "title": "GPT-4o Mini (Fast & Cheap)" },
50
+ { "value": "gpt-4o", "title": "GPT-4o (More Capable)" }
51
+ ]
52
+ },
53
+ {
54
+ "value": "google",
55
+ "title": "Google (Gemini)",
56
+ "models": [
57
+ { "value": "gemini-2.0-flash", "title": "Gemini 2.0 Flash (Fast & Free)" },
58
+ { "value": "gemini-1.5-pro", "title": "Gemini 1.5 Pro (More Capable)" }
59
+ ]
60
+ }
34
61
  ]
35
62
  }
package/lib/setup.js CHANGED
@@ -1,18 +1,26 @@
1
- 'use strict';
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';
2
7
 
3
- const prompts = require('prompts');
4
- const fs = require('fs');
5
- const utils = require('./utils');
6
- const defaultConfig = require('./default-config.json');
7
- const options = require('./options.json');
8
8
 
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
9
11
 
10
- module.exports = async (askForCommitAfter) => {
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) {
11
21
  utils.log('create config file');
12
22
 
13
- /**
14
- * Get current git branch to pre-fill release branch option
15
- */
23
+ // Get current git branch to pre-fill release branch option
16
24
  const currentBranch = utils.getCurrentBranch();
17
25
 
18
26
  let cancelled = false;
@@ -61,6 +69,114 @@ module.exports = async (askForCommitAfter) => {
61
69
  message: 'Display all npm version types?',
62
70
  initial: defaultConfig.showAllVersionTypes,
63
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);
146
+
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
+ },
64
180
  {
65
181
  type: askForCommitAfter ? 'confirm' : null,
66
182
  name: 'commitAfter',
@@ -74,17 +190,12 @@ module.exports = async (askForCommitAfter) => {
74
190
  },
75
191
  });
76
192
 
77
- /**
78
- * Handle prompt cancellation and stop setup execution
79
- */
80
193
  if (cancelled) {
81
194
  utils.log('setup cancelled', 'error');
82
195
  return;
83
196
  }
84
197
 
85
- /**
86
- * Parse prompt data and write config file
87
- */
198
+ // Parse prompt data and write config file
88
199
  const config = {
89
200
  format: configChoices.format,
90
201
  branchFormat: configChoices.branchFormat,
@@ -98,6 +209,45 @@ module.exports = async (askForCommitAfter) => {
98
209
  releaseBranch: configChoices.releaseBranch,
99
210
  showAllVersionTypes: configChoices.showAllVersionTypes,
100
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');
222
+ return;
223
+ }
224
+ utils.log(`API key saved to ${envPath}`, 'success');
225
+ }
226
+
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,
243
+ };
244
+ } else {
245
+ config.ai = {
246
+ enabled: false,
247
+ largeDiffTokenThreshold: defaultConfig.ai.largeDiffTokenThreshold,
248
+ };
249
+ }
250
+
101
251
  const parsedConfig = JSON.stringify(config, null, 2);
102
252
 
103
253
  utils.log(`save ${options.configFile}.json file...`);
@@ -114,4 +264,4 @@ module.exports = async (askForCommitAfter) => {
114
264
  config,
115
265
  commitAfter: configChoices.commitAfter
116
266
  };
117
- };
267
+ }
package/lib/utils.js CHANGED
@@ -1,8 +1,8 @@
1
- 'use strict';
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
8
  if (
@@ -18,11 +18,20 @@ const getCurrentBranch = () => {
18
18
  return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
19
19
  };
20
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
+ }
28
+ };
29
+
21
30
  const validCommitTitle = (title, lenMin, lenMax) => {
22
31
  if (title.length < lenMin) {
23
- return 'Commit title too short';
32
+ return `Commit title too short (current ${title.length} - minimum ${lenMin})`;
24
33
  } else if (title.length > lenMax) {
25
- return 'Commit title too long';
34
+ return `Commit title too long (current ${title.length} - maximum ${lenMax})`;
26
35
  }
27
36
  return true;
28
37
  };
@@ -45,24 +54,31 @@ const validVersion = (version) => {
45
54
  };
46
55
 
47
56
  const formatCommitTitle = (type, title, format, scope = '*') => {
57
+ // Handle empty title
58
+ if (!title || title.trim().length === 0) {
59
+ return '';
60
+ }
61
+
62
+ const trimmedTitle = title.trim();
63
+
48
64
  switch (format) {
49
65
  case 1:
50
66
  default:
51
- return `(${type}) ${title[0].toUpperCase()}${title.substr(1).toLowerCase()}`;
67
+ return `(${type}) ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
52
68
  case 2:
53
- return `(${type}) ${title.toLowerCase()}`;
69
+ return `(${type}) ${trimmedTitle.toLowerCase()}`;
54
70
  case 3:
55
- return `${type}: ${title[0].toUpperCase()}${title.substr(1).toLowerCase()}`;
71
+ return `${type}: ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
56
72
  case 4:
57
- return `${type}: ${title.toLowerCase()}`;
73
+ return `${type}: ${trimmedTitle.toLowerCase()}`;
58
74
  case 5:
59
- return `${type}(${scope}) ${title[0].toUpperCase()}${title.substr(1).toLowerCase()}`;
75
+ return `${type}(${scope}) ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
60
76
  case 6:
61
- return `${type}(${scope}) ${title.toLowerCase()}`;
77
+ return `${type}(${scope}) ${trimmedTitle.toLowerCase()}`;
62
78
  case 7:
63
- return `${type}(${scope}): ${title[0].toUpperCase()}${title.substr(1).toLowerCase()}`;
79
+ return `${type}(${scope}): ${trimmedTitle[0].toUpperCase()}${trimmedTitle.slice(1).toLowerCase()}`;
64
80
  case 8:
65
- return `${type}(${scope}): ${title.toLowerCase()}`;
81
+ return `${type}(${scope}): ${trimmedTitle.toLowerCase()}`;
66
82
  }
67
83
  };
68
84
 
@@ -144,9 +160,10 @@ const checkBranchExists = (branchName) => {
144
160
  };
145
161
 
146
162
 
147
- module.exports = {
163
+ export {
148
164
  askForVersion,
149
165
  getCurrentBranch,
166
+ hasStagedChanges,
150
167
  validCommitTitle,
151
168
  validCommitTitleSetupLength,
152
169
  validBranchDescription,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "format-commit",
3
- "version": "0.4.0",
4
- "description": "Lightweight CLI to standardize commit messages",
5
- "license": "ISC",
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,7 +32,7 @@
27
32
  "url": "https://github.com/thomasbarkats/format-commit/issues"
28
33
  },
29
34
  "main": "./lib/index.js",
30
- "type": "commonjs",
35
+ "type": "module",
31
36
  "engines": {
32
37
  "node": ">=16.0.0"
33
38
  },
@@ -45,7 +50,7 @@
45
50
  "format-commit": "./lib/index.js"
46
51
  },
47
52
  "dependencies": {
48
- "commander": "^14.0.0",
53
+ "commander": "^14.0.3",
49
54
  "kleur": "^4.1.5",
50
55
  "prompts": "^2.4.2"
51
56
  },