@logickernel/agileflow 0.2.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logickernel/agileflow",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Automatic semantic versioning and changelog generation based on conventional commits",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -12,9 +12,9 @@ Usage:
12
12
 
13
13
  Commands:
14
14
  <none> Prints the current version, next version, commits, and changelog
15
- push Push a semantic version tag to the remote repository (native git)
16
- gitlab Push a semantic version tag via GitLab API (for GitLab CI)
17
- github Push a semantic version tag via GitHub API (for GitHub Actions)
15
+ push Push a semantic version tag to the remote repository
16
+ gitlab Create a semantic version tag via GitLab API (for GitLab CI)
17
+ github Create a semantic version tag via GitHub API (for GitHub Actions)
18
18
 
19
19
  Options:
20
20
  --quiet Only output the next version (or empty if no bump)
@@ -26,11 +26,32 @@ For more information, visit: https://code.logickernel.com/tools/agileflow
26
26
  }
27
27
 
28
28
  /**
29
- * Parses command line arguments.
29
+ * Valid options that can be passed to commands.
30
+ */
31
+ const VALID_OPTIONS = ['--quiet', '--help', '-h', '--version', '-v'];
32
+
33
+ /**
34
+ * Valid commands.
35
+ */
36
+ const VALID_COMMANDS = ['push', 'gitlab', 'github'];
37
+
38
+ /**
39
+ * Parses command line arguments and validates them.
30
40
  * @param {Array<string>} args - Command line arguments
31
41
  * @returns {{quiet: boolean}}
42
+ * @throws {Error} If invalid options are found
32
43
  */
33
44
  function parseArgs(args) {
45
+ // Check for invalid options
46
+ for (const arg of args) {
47
+ if (arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
48
+ throw new Error(`Unknown option: ${arg}`);
49
+ }
50
+ if (arg.startsWith('-') && !arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
51
+ throw new Error(`Unknown option: ${arg}`);
52
+ }
53
+ }
54
+
34
55
  return {
35
56
  quiet: args.includes('--quiet'),
36
57
  };
@@ -61,7 +82,7 @@ function displayVersionInfo(info, quiet) {
61
82
  }
62
83
 
63
84
  console.log(`\nCurrent version: ${currentVersion || 'none'}`);
64
- console.log(`New version: ${newVersion || 'no bump needed'}`);
85
+ console.log(`New version: ${newVersion || 'no bump needed'}`);
65
86
  if (changelog) {
66
87
  console.log(`\nChangelog:\n\n${changelog}`);
67
88
  }
@@ -113,7 +134,17 @@ async function handlePushCommand(pushType, options) {
113
134
 
114
135
  async function main() {
115
136
  const [, , cmd, ...rest] = process.argv;
116
- const options = parseArgs(cmd ? [cmd, ...rest] : rest);
137
+
138
+ let options;
139
+ try {
140
+ options = parseArgs(cmd ? [cmd, ...rest] : rest);
141
+ } catch (err) {
142
+ console.error(`Error: ${err.message}`);
143
+ console.error();
144
+ printHelp();
145
+ process.exit(1);
146
+ return;
147
+ }
117
148
 
118
149
  // Handle help
119
150
  if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
@@ -134,13 +165,22 @@ async function main() {
134
165
  }
135
166
 
136
167
  // Unknown command (not an option)
137
- if (cmd && !cmd.startsWith('--')) {
168
+ if (cmd && !cmd.startsWith('--') && !cmd.startsWith('-')) {
138
169
  console.error(`Error: Unknown command "${cmd}"`);
139
170
  console.error();
140
171
  printHelp();
141
172
  process.exit(1);
142
173
  }
143
174
 
175
+ // Invalid option (starts with -- but not valid)
176
+ if (cmd && cmd.startsWith('--') && !VALID_OPTIONS.includes(cmd)) {
177
+ console.error(`Error: Unknown option "${cmd}"`);
178
+ console.error();
179
+ printHelp();
180
+ process.exit(1);
181
+ return;
182
+ }
183
+
144
184
  // Default: show version info
145
185
  const info = await processVersionInfo();
146
186
  displayVersionInfo(info, options.quiet);
package/src/utils.js CHANGED
@@ -170,13 +170,16 @@ function extractIssueReference(message) {
170
170
  * @param {string} subject - First line of commit message
171
171
  * @param {Object} parsed - Parsed conventional commit info
172
172
  * @param {string} fullMessage - Full commit message
173
+ * @param {boolean} isBreakingSection - Whether this is for the breaking changes section
173
174
  * @returns {string} Formatted description
174
175
  */
175
- function formatChangelogDescription(subject, parsed, fullMessage) {
176
+ function formatChangelogDescription(subject, parsed, fullMessage, isBreakingSection = false) {
176
177
  if (!parsed) return subject;
177
178
  let description = parsed.description;
178
179
  const isBreaking = parsed.breaking || /BREAKING CHANGE:/i.test(fullMessage);
179
- if (isBreaking) {
180
+
181
+ // Only add BREAKING prefix if not in breaking changes section
182
+ if (isBreaking && !isBreakingSection) {
180
183
  description = `BREAKING: ${description}`;
181
184
  }
182
185
  return description;
@@ -229,13 +232,25 @@ function applyVersionBump(current, bump) {
229
232
  }
230
233
  }
231
234
 
235
+ /**
236
+ * Checks if a commit is a breaking change.
237
+ * @param {Object} commit - Commit object
238
+ * @param {Object} parsed - Parsed conventional commit info
239
+ * @returns {boolean}
240
+ */
241
+ function isBreakingChange(commit, parsed) {
242
+ if (!parsed) return false;
243
+ return parsed.breaking || /BREAKING CHANGE:/i.test(commit.message);
244
+ }
245
+
232
246
  /**
233
247
  * Analyzes commits to determine version bump requirements.
234
248
  * @param {Array} commits - Array of commit objects
235
- * @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object}}
249
+ * @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object, breakingCommits: Array}}
236
250
  */
237
251
  function analyzeCommitsForVersioning(commits) {
238
252
  const commitsByType = Object.fromEntries(TYPE_ORDER.map(t => [t, []]));
253
+ const breakingCommits = [];
239
254
  let hasBreaking = false, hasFeat = false, hasPatchTypes = false;
240
255
 
241
256
  for (const commit of commits) {
@@ -243,18 +258,23 @@ function analyzeCommitsForVersioning(commits) {
243
258
  if (!parsed) continue;
244
259
 
245
260
  const { type, breaking } = parsed;
246
- const isBreaking = breaking || /BREAKING CHANGE:/i.test(commit.message);
261
+ const isBreaking = isBreakingChange(commit, parsed);
247
262
 
248
- if (isBreaking) hasBreaking = true;
249
- if (type === 'feat') hasFeat = true;
250
- else if (PATCH_TYPES.includes(type)) hasPatchTypes = true;
251
-
252
- if (commitsByType[type]) {
253
- commitsByType[type].push(commit);
263
+ if (isBreaking) {
264
+ hasBreaking = true;
265
+ breakingCommits.push(commit);
266
+ } else {
267
+ // Only add to type sections if not breaking
268
+ if (type === 'feat') hasFeat = true;
269
+ else if (PATCH_TYPES.includes(type)) hasPatchTypes = true;
270
+
271
+ if (commitsByType[type]) {
272
+ commitsByType[type].push(commit);
273
+ }
254
274
  }
255
275
  }
256
276
 
257
- return { hasBreaking, hasFeat, hasPatchTypes, commitsByType };
277
+ return { hasBreaking, hasFeat, hasPatchTypes, commitsByType, breakingCommits };
258
278
  }
259
279
 
260
280
  /**
@@ -270,9 +290,10 @@ function capitalize(str) {
270
290
  /**
271
291
  * Generates changelog entries for a commit type section.
272
292
  * @param {Array} commits - Commits of this type
293
+ * @param {boolean} isBreakingSection - Whether this is for the breaking changes section
273
294
  * @returns {Array<string>} Changelog lines
274
295
  */
275
- function generateTypeChangelog(commits) {
296
+ function generateTypeChangelog(commits, isBreakingSection = false) {
276
297
  const byScope = {};
277
298
  const noScope = [];
278
299
 
@@ -283,7 +304,7 @@ function generateTypeChangelog(commits) {
283
304
  const subject = commit.message.split('\n')[0].trim();
284
305
  const entry = {
285
306
  scope: parsed.scope,
286
- description: formatChangelogDescription(subject, parsed, commit.message),
307
+ description: formatChangelogDescription(subject, parsed, commit.message, isBreakingSection),
287
308
  issueRef: extractIssueReference(commit.message) || '',
288
309
  };
289
310
 
@@ -323,6 +344,15 @@ function calculateNextVersionAndChangelog(expandedInfo) {
323
344
 
324
345
  // Generate changelog
325
346
  const changelogLines = [];
347
+
348
+ // Add breaking changes section first if any
349
+ if (analysis.breakingCommits.length > 0) {
350
+ changelogLines.push('BREAKING CHANGES:');
351
+ changelogLines.push(...generateTypeChangelog(analysis.breakingCommits, true));
352
+ changelogLines.push('');
353
+ }
354
+
355
+ // Add regular type sections
326
356
  for (const type of TYPE_ORDER) {
327
357
  const typeCommits = analysis.commitsByType[type];
328
358
  if (!typeCommits?.length) continue;