@alleyinteractive/changelog-extractor 0.0.1 → 0.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/package.json CHANGED
@@ -1,15 +1,24 @@
1
1
  {
2
2
  "name": "@alleyinteractive/changelog-extractor",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
+ "type": "module",
4
5
  "description": "Parse and extract structured information from Keep a Changelog formatted changelogs",
5
6
  "main": "src/parser.js",
6
7
  "bin": {
7
- "changelog-extractor": "./cli.js"
8
+ "changelog-extractor": "./src/cli.js"
8
9
  },
9
10
  "scripts": {
10
- "test": "jest",
11
- "test:watch": "jest --watch",
12
- "test:coverage": "jest --coverage"
11
+ "build": "npm run prettier && npm run package",
12
+ "fix-and-build": "npm run prettier:fix && npm run package",
13
+ "package:watch": "npm run package -- --watch",
14
+ "package": "npx rollup --config rollup.config.js",
15
+ "prepublishOnly": "npm test",
16
+ "prettier:fix": "npx prettier --write .",
17
+ "prettier": "npx prettier --check .",
18
+ "release:major": "npm run version:major && npm publish --access public && git push --follow-tags",
19
+ "release:minor": "npm run version:minor && npm publish --access public && git push --follow-tags",
20
+ "release:patch": "npm run version:patch && npm publish --access public && git push --follow-tags",
21
+ "test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest"
13
22
  },
14
23
  "keywords": [
15
24
  "changelog",
@@ -31,26 +40,27 @@
31
40
  "homepage": "https://github.com/alleyinteractive/action-changelog-extractor#readme",
32
41
  "files": [
33
42
  "src/",
43
+ "dist/",
34
44
  "cli.js",
45
+ "action.yml",
35
46
  "README.md",
36
47
  "LICENSE"
37
48
  ],
49
+ "exports": {
50
+ ".": "./dist/index.js"
51
+ },
38
52
  "devDependencies": {
53
+ "@jest/globals": "^30.2.0",
54
+ "@rollup/plugin-commonjs": "^29.0.0",
55
+ "@rollup/plugin-node-resolve": "^16.0.3",
39
56
  "@types/jest": "^29.5.12",
40
57
  "@types/node": "^20.11.24",
41
- "jest": "^29.7.0"
58
+ "jest": "^30.2.0",
59
+ "prettier": "^3.6.2",
60
+ "prettier-eslint": "^16.4.2",
61
+ "rollup": "^4.53.3"
42
62
  },
43
63
  "dependencies": {
44
64
  "@actions/core": "^1.10.1"
45
- },
46
- "jest": {
47
- "testEnvironment": "node",
48
- "coverageDirectory": "coverage",
49
- "collectCoverageFrom": [
50
- "src/**/*.js"
51
- ],
52
- "testMatch": [
53
- "**/__tests__/**/*.test.js"
54
- ]
55
65
  }
56
66
  }
package/src/cli.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Changelog Extractor CLI
5
+ *
6
+ * Parse and extract structured information from Keep a Changelog formatted files.
7
+ * Can be run via npx or installed globally.
8
+ *
9
+ * @package @alleyinteractive/changelog-extractor
10
+ * @author Alley Interactive
11
+ * @license GPL-3.0
12
+ */
13
+
14
+ import fs from 'fs'
15
+ import path from 'path'
16
+ import { parseChangelog } from './parser.js'
17
+
18
+ function printUsage() {
19
+ console.log(`
20
+ Usage: npx @alleyinteractive/changelog-extractor [options]
21
+
22
+ Options:
23
+ -f, --file <path> Path to changelog file (default: CHANGELOG.md)
24
+ -v, --version <ver> Specific version to extract (e.g., "1.14.0" or "v1.14.0")
25
+ Leave empty to return all versions
26
+ -l, --list List the count of versions found and exit
27
+ -h, --help Show this help message
28
+
29
+ Examples:
30
+ npx @alleyinteractive/changelog-extractor # Parse CHANGELOG.md, all versions
31
+ npx @alleyinteractive/changelog-extractor -l # Count versions in CHANGELOG.md
32
+ npx @alleyinteractive/changelog-extractor -v 1.14.0 # Extract specific version
33
+ npx @alleyinteractive/changelog-extractor -f /tmp/test.md # Parse specific file
34
+ npx @alleyinteractive/changelog-extractor -f CHANGELOG.md -v 1.14.0 # Both options
35
+ changelog-extractor -l # If installed globally
36
+ `)
37
+ }
38
+
39
+ // Parse command line arguments
40
+ const args = process.argv.slice(2)
41
+
42
+ if (args.includes('--help') || args.includes('-h')) {
43
+ printUsage()
44
+ process.exit(0)
45
+ }
46
+
47
+ const listMode = args.includes('-l') || args.includes('--list')
48
+
49
+ // Parse file option
50
+ let changelogFile = 'CHANGELOG.md'
51
+ const fileIndex = args.findIndex((arg) => arg === '-f' || arg === '--file')
52
+ if (fileIndex !== -1 && args[fileIndex + 1]) {
53
+ changelogFile = args[fileIndex + 1]
54
+ }
55
+
56
+ // Parse version option
57
+ let versionArg = null
58
+ const versionIndex = args.findIndex(
59
+ (arg) => arg === '-v' || arg === '--version'
60
+ )
61
+ if (versionIndex !== -1 && args[versionIndex + 1]) {
62
+ versionArg = args[versionIndex + 1]
63
+ }
64
+
65
+ const version = versionArg
66
+
67
+ // Read changelog file
68
+ const changelogPath = path.resolve(process.cwd(), changelogFile)
69
+
70
+ if (!fs.existsSync(changelogPath)) {
71
+ console.error(`āŒ Error: Changelog file not found: ${changelogPath}`)
72
+ console.error(
73
+ '\nTip: You can create a test changelog at /tmp/test-changelog.md'
74
+ )
75
+ process.exit(1)
76
+ }
77
+
78
+ const changelogContent = fs.readFileSync(changelogPath, 'utf8')
79
+
80
+ try {
81
+ const results = parseChangelog(changelogContent, version)
82
+
83
+ // List mode: just display count and exit
84
+ if (listMode) {
85
+ console.log(`${results.length}`)
86
+ process.exit(0)
87
+ }
88
+
89
+ // Parse changelog
90
+ console.log(`šŸ“– Parsing: ${changelogPath}`)
91
+ console.log(`šŸŽÆ Version: ${versionArg || 'all versions'}`)
92
+ console.log('')
93
+
94
+ if (results.length === 0) {
95
+ console.log('āš ļø No changelog entries found')
96
+ process.exit(1)
97
+ }
98
+
99
+ console.log(`āœ… Found ${results.length} version(s)\n`)
100
+ console.log('─'.repeat(80))
101
+
102
+ // Pretty print results
103
+ results.forEach((entry, index) => {
104
+ console.log(`\nšŸ“¦ Version: ${entry.name}`)
105
+ console.log(
106
+ `šŸ“ Sections: ${Object.keys(entry.sections).join(', ') || 'none'}`
107
+ )
108
+ console.log('')
109
+
110
+ // Print sections
111
+ for (const [sectionName, sectionContent] of Object.entries(
112
+ entry.sections
113
+ )) {
114
+ console.log(
115
+ ` ### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`
116
+ )
117
+ console.log('')
118
+ // Indent content
119
+ const lines = sectionContent.split('\n')
120
+ lines.forEach((line) => console.log(` ${line}`))
121
+ console.log('')
122
+ }
123
+
124
+ if (index < results.length - 1) {
125
+ console.log('─'.repeat(80))
126
+ }
127
+ })
128
+
129
+ // Output JSON at the end
130
+ console.log('\n' + '═'.repeat(80))
131
+ console.log('\nšŸ“‹ JSON Output:\n')
132
+ console.log(JSON.stringify(results, null, 2))
133
+ } catch (error) {
134
+ console.error('āŒ Error parsing changelog:', error.message)
135
+ process.exit(1)
136
+ }
package/src/index.js CHANGED
@@ -1,47 +1,7 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { parseChangelog } = require('./parser');
4
-
5
1
  /**
6
- * Main action entry point
2
+ * The entrypoint for the action. This file simply imports and runs the action's
3
+ * main logic.
7
4
  */
8
- async function run(core) {
9
- try {
10
- // Get inputs
11
- const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md';
12
- const versionInput = core.getInput('version');
13
-
14
- // Only pass version if explicitly specified, otherwise return all versions
15
- const version = versionInput || null;
16
-
17
- // Read changelog file
18
- const fullPath = path.resolve(process.cwd(), changelogPath);
19
-
20
- if (!fs.existsSync(fullPath)) {
21
- throw new Error(`Changelog file not found: ${fullPath}`);
22
- }
23
-
24
- const changelogContent = fs.readFileSync(fullPath, 'utf8');
25
-
26
- // Parse changelog
27
- const results = parseChangelog(changelogContent, version);
28
-
29
- if (results.length === 0) {
30
- core.warning(`No changelog entries found${version ? ` for version ${version}` : ''}`);
31
- }
32
-
33
- // Set output as JSON string
34
- core.setOutput('result', JSON.stringify(results));
35
-
36
- // Log summary
37
- core.info(`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`);
38
- for (const entry of results) {
39
- core.info(` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`);
40
- }
41
-
42
- } catch (error) {
43
- core.setFailed(error.message);
44
- }
45
- }
5
+ import { run } from './main.js'
46
6
 
47
- module.exports = run;
7
+ run()
package/src/main.js ADDED
@@ -0,0 +1,50 @@
1
+ import * as core from '@actions/core'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { parseChangelog } from './parser.js'
5
+
6
+ /**
7
+ * Main action entry point
8
+ */
9
+ export async function run() {
10
+ try {
11
+ const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md'
12
+ const versionInput = core.getInput('version')
13
+
14
+ // Only pass version if explicitly specified, otherwise return all versions
15
+ const version = versionInput || null
16
+
17
+ // Read changelog file
18
+ const fullPath = path.resolve(process.cwd(), changelogPath)
19
+
20
+ if (!fs.existsSync(fullPath)) {
21
+ throw new Error(`Changelog file not found: ${fullPath}`)
22
+ }
23
+
24
+ const changelogContent = fs.readFileSync(fullPath, 'utf8')
25
+
26
+ // Parse changelog
27
+ const results = parseChangelog(changelogContent, version)
28
+
29
+ if (results.length === 0) {
30
+ core.warning(
31
+ `No changelog entries found${version ? ` for version ${version}` : ''}`
32
+ )
33
+ }
34
+
35
+ // Set output as JSON string
36
+ core.setOutput('result', JSON.stringify(results))
37
+
38
+ // Log summary
39
+ core.info(
40
+ `Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`
41
+ )
42
+ for (const entry of results) {
43
+ core.info(
44
+ ` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`
45
+ )
46
+ }
47
+ } catch (error) {
48
+ core.setFailed(error.message)
49
+ }
50
+ }
package/src/parser.js CHANGED
@@ -5,31 +5,31 @@
5
5
  * @param {string|null} version - Specific version to extract (e.g., "v1.14.0" or "1.14.0"), or null/undefined for all versions
6
6
  * @returns {Array<{name: string, sections: Object, contents: string}>} Array of version objects
7
7
  */
8
- function parseChangelog(changelogContent, version = null) {
9
- const versions = [];
10
- const lines = changelogContent.split('\n');
8
+ export function parseChangelog(changelogContent, version = null) {
9
+ const versions = []
10
+ const lines = changelogContent.split('\n')
11
11
 
12
- let currentVersion = null;
13
- let currentSection = null;
14
- let currentSectionContent = [];
15
- let generalContent = []; // Content without section headers
12
+ let currentVersion = null
13
+ let currentSection = null
14
+ let currentSectionContent = []
15
+ let generalContent = [] // Content without section headers
16
16
 
17
17
  // Regex to match version headers: ## v1.14.0 or ## v1.14.0 - 2024-04-29
18
- const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))?$/;
18
+ const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))?$/
19
19
  // Regex to match section headers: ### Added, ### Changed, etc.
20
- const sectionRegex = /^###\s+(.+)$/;
20
+ const sectionRegex = /^###\s+(.+)$/
21
21
 
22
22
  for (let i = 0; i < lines.length; i++) {
23
- const line = lines[i];
23
+ const line = lines[i]
24
24
 
25
25
  // Check for version header
26
- const versionMatch = line.match(versionRegex);
26
+ const versionMatch = line.match(versionRegex)
27
27
  if (versionMatch) {
28
28
  // Save previous version if exists
29
29
  if (currentVersion) {
30
- saveCurrentSection();
31
- saveGeneralContent();
32
- versions.push(currentVersion);
30
+ saveCurrentSection()
31
+ saveGeneralContent()
32
+ versions.push(currentVersion)
33
33
  }
34
34
 
35
35
  // Start new version
@@ -37,62 +37,68 @@ function parseChangelog(changelogContent, version = null) {
37
37
  name: versionMatch[1], // Version without 'v' prefix
38
38
  sections: {},
39
39
  contents: ''
40
- };
41
- currentSection = null;
42
- currentSectionContent = [];
43
- generalContent = [];
44
- continue;
40
+ }
41
+ currentSection = null
42
+ currentSectionContent = []
43
+ generalContent = []
44
+ continue
45
45
  }
46
46
 
47
47
  // Check for section header
48
- const sectionMatch = line.match(sectionRegex);
48
+ const sectionMatch = line.match(sectionRegex)
49
49
  if (sectionMatch && currentVersion) {
50
50
  // If we have general content, save it before starting a new section
51
51
  if (generalContent.length > 0) {
52
- saveGeneralContent();
52
+ saveGeneralContent()
53
53
  }
54
54
 
55
55
  // Save previous section if exists
56
- saveCurrentSection();
56
+ saveCurrentSection()
57
57
 
58
58
  // Start new section
59
- currentSection = sectionMatch[1].toLowerCase();
60
- currentSectionContent = [];
61
- continue;
59
+ currentSection = sectionMatch[1].toLowerCase()
60
+ currentSectionContent = []
61
+ continue
62
62
  }
63
63
 
64
64
  // Add content to current section or general content
65
65
  if (currentVersion) {
66
66
  if (currentSection) {
67
- currentSectionContent.push(line);
67
+ currentSectionContent.push(line)
68
68
  } else {
69
69
  // Content without a section header goes to general
70
- generalContent.push(line);
70
+ generalContent.push(line)
71
71
  }
72
72
  }
73
73
  }
74
74
 
75
75
  // Save final version and section
76
76
  if (currentVersion) {
77
- saveCurrentSection();
78
- saveGeneralContent();
79
- versions.push(currentVersion);
77
+ saveCurrentSection()
78
+ saveGeneralContent()
79
+ versions.push(currentVersion)
80
80
  }
81
81
 
82
82
  // Helper function to save the current section
83
83
  function saveCurrentSection() {
84
84
  if (currentVersion && currentSection && currentSectionContent.length > 0) {
85
85
  // Trim empty lines from start and end
86
- while (currentSectionContent.length > 0 && currentSectionContent[0].trim() === '') {
87
- currentSectionContent.shift();
86
+ while (
87
+ currentSectionContent.length > 0 &&
88
+ currentSectionContent[0].trim() === ''
89
+ ) {
90
+ currentSectionContent.shift()
88
91
  }
89
- while (currentSectionContent.length > 0 && currentSectionContent[currentSectionContent.length - 1].trim() === '') {
90
- currentSectionContent.pop();
92
+ while (
93
+ currentSectionContent.length > 0 &&
94
+ currentSectionContent[currentSectionContent.length - 1].trim() === ''
95
+ ) {
96
+ currentSectionContent.pop()
91
97
  }
92
98
 
93
- const content = currentSectionContent.join('\n');
99
+ const content = currentSectionContent.join('\n')
94
100
  if (content.trim()) {
95
- currentVersion.sections[currentSection] = content;
101
+ currentVersion.sections[currentSection] = content
96
102
  }
97
103
  }
98
104
  }
@@ -101,59 +107,71 @@ function parseChangelog(changelogContent, version = null) {
101
107
  function saveGeneralContent() {
102
108
  if (currentVersion && generalContent.length > 0) {
103
109
  // Trim empty lines from start and end
104
- let trimmedContent = [...generalContent];
110
+ let trimmedContent = [...generalContent]
105
111
  while (trimmedContent.length > 0 && trimmedContent[0].trim() === '') {
106
- trimmedContent.shift();
112
+ trimmedContent.shift()
107
113
  }
108
- while (trimmedContent.length > 0 && trimmedContent[trimmedContent.length - 1].trim() === '') {
109
- trimmedContent.pop();
114
+ while (
115
+ trimmedContent.length > 0 &&
116
+ trimmedContent[trimmedContent.length - 1].trim() === ''
117
+ ) {
118
+ trimmedContent.pop()
110
119
  }
111
120
 
112
- const content = trimmedContent.join('\n');
121
+ const content = trimmedContent.join('\n')
113
122
  if (content.trim()) {
114
- currentVersion.sections['general'] = content;
123
+ currentVersion.sections['general'] = content
115
124
  }
116
- generalContent = [];
125
+ generalContent = []
117
126
  }
118
127
  }
119
128
 
120
129
  // Build contents for each version (all sections combined)
121
130
  for (const ver of versions) {
122
- const contentParts = [];
131
+ const contentParts = []
123
132
 
124
133
  // Order sections in conventional order, with general at the end
125
- const sectionOrder = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security'];
134
+ const sectionOrder = [
135
+ 'added',
136
+ 'changed',
137
+ 'deprecated',
138
+ 'removed',
139
+ 'fixed',
140
+ 'security'
141
+ ]
126
142
 
127
143
  for (const sectionName of sectionOrder) {
128
144
  if (ver.sections[sectionName]) {
129
- contentParts.push(`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${ver.sections[sectionName]}`);
145
+ contentParts.push(
146
+ `### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${ver.sections[sectionName]}`
147
+ )
130
148
  }
131
149
  }
132
150
 
133
151
  // Add any sections not in the standard order (except 'general')
134
152
  for (const [sectionName, sectionContent] of Object.entries(ver.sections)) {
135
153
  if (!sectionOrder.includes(sectionName) && sectionName !== 'general') {
136
- contentParts.push(`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${sectionContent}`);
154
+ contentParts.push(
155
+ `### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${sectionContent}`
156
+ )
137
157
  }
138
158
  }
139
159
 
140
160
  // Add general section last if it exists (without a header since it's raw content)
141
161
  if (ver.sections['general']) {
142
- contentParts.push(ver.sections['general']);
162
+ contentParts.push(ver.sections['general'])
143
163
  }
144
164
 
145
- ver.contents = contentParts.join('\n\n');
165
+ ver.contents = contentParts.join('\n\n')
146
166
  }
147
167
 
148
168
  // Filter by specific version if requested
149
169
  if (version) {
150
- const normalizedVersion = version.replace(/^v/, ''); // Remove 'v' prefix if present
151
- const filtered = versions.filter(v => v.name === normalizedVersion);
152
- return filtered;
170
+ const normalizedVersion = version.replace(/^v/, '') // Remove 'v' prefix if present
171
+ const filtered = versions.filter((v) => v.name === normalizedVersion)
172
+ return filtered
153
173
  }
154
174
 
155
175
  // Return all versions by default
156
- return versions;
176
+ return versions
157
177
  }
158
-
159
- module.exports = { parseChangelog };
package/cli.js DELETED
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { parseChangelog } = require('./src/parser');
6
-
7
- /**
8
- * CLI for testing the changelog parser
9
- * Usage: node cli.js [changelog-file] [version]
10
- */
11
-
12
- function printUsage() {
13
- console.log(`
14
- Usage: node cli.js [options] [changelog-file] [version]
15
-
16
- Options:
17
- -l, --list List the count of versions found and exit
18
- -h, --help Show this help message
19
-
20
- Arguments:
21
- changelog-file Path to changelog file (default: CHANGELOG.md)
22
- version Specific version to extract (e.g., "1.14.0" or "v1.14.0")
23
- Leave empty to return all versions (default: all versions)
24
-
25
- Examples:
26
- node cli.js # Parse CHANGELOG.md, extract all versions
27
- node cli.js -l # Count versions in CHANGELOG.md
28
- node cli.js CHANGELOG.md # Same as above
29
- node cli.js CHANGELOG.md 1.14.0 # Extract specific version
30
- node cli.js /tmp/test-changelog.md # Parse specific file, all versions
31
- node cli.js -l /tmp/test-changelog.md # Count versions in specific file
32
- `);
33
- }
34
-
35
- // Parse command line arguments
36
- const args = process.argv.slice(2);
37
-
38
- if (args.includes('--help') || args.includes('-h')) {
39
- printUsage();
40
- process.exit(0);
41
- }
42
-
43
- const listMode = args.includes('-l') || args.includes('--list');
44
- const filteredArgs = args.filter(arg => arg !== '-l' && arg !== '--list');
45
-
46
- const changelogFile = filteredArgs[0] || 'CHANGELOG.md';
47
- const versionArg = filteredArgs[1] || null;
48
- const version = versionArg;
49
-
50
- // Read changelog file
51
- const changelogPath = path.resolve(process.cwd(), changelogFile);
52
-
53
- if (!fs.existsSync(changelogPath)) {
54
- console.error(`āŒ Error: Changelog file not found: ${changelogPath}`);
55
- console.error('\nTip: You can create a test changelog at /tmp/test-changelog.md');
56
- process.exit(1);
57
- }
58
-
59
- const changelogContent = fs.readFileSync(changelogPath, 'utf8');
60
-
61
- try {
62
- const results = parseChangelog(changelogContent, version);
63
-
64
- // List mode: just display count and exit
65
- if (listMode) {
66
- console.log(`${results.length}`);
67
- process.exit(0);
68
- }
69
-
70
- // Parse changelog
71
- console.log(`šŸ“– Parsing: ${changelogPath}`);
72
- console.log(`šŸŽÆ Version: ${versionArg || 'all versions'}`);
73
- console.log('');
74
-
75
- if (results.length === 0) {
76
- console.log('āš ļø No changelog entries found');
77
- process.exit(0);
78
- }
79
-
80
- console.log(`āœ… Found ${results.length} version(s)\n`);
81
- console.log('─'.repeat(80));
82
-
83
- // Pretty print results
84
- results.forEach((entry, index) => {
85
- console.log(`\nšŸ“¦ Version: ${entry.name}`);
86
- console.log(`šŸ“ Sections: ${Object.keys(entry.sections).join(', ') || 'none'}`);
87
- console.log('');
88
-
89
- // Print sections
90
- for (const [sectionName, sectionContent] of Object.entries(entry.sections)) {
91
- console.log(` ### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`);
92
- console.log('');
93
- // Indent content
94
- const lines = sectionContent.split('\n');
95
- lines.forEach(line => console.log(` ${line}`));
96
- console.log('');
97
- }
98
-
99
- if (index < results.length - 1) {
100
- console.log('─'.repeat(80));
101
- }
102
- });
103
-
104
- // Output JSON at the end
105
- console.log('\n' + '═'.repeat(80));
106
- console.log('\nšŸ“‹ JSON Output:\n');
107
- console.log(JSON.stringify(results, null, 2));
108
-
109
- } catch (error) {
110
- console.error('āŒ Error parsing changelog:', error.message);
111
- process.exit(1);
112
- }