@theihtisham/ai-release-notes 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/LICENSE +21 -0
- package/README.md +1493 -0
- package/__tests__/analyzer.test.js +63 -0
- package/__tests__/categorizer.test.js +93 -0
- package/__tests__/config.test.js +92 -0
- package/__tests__/formatter.test.js +63 -0
- package/__tests__/formatters.test.js +394 -0
- package/__tests__/migration.test.js +322 -0
- package/__tests__/semver.test.js +94 -0
- package/__tests__/tones.test.js +252 -0
- package/action.yml +113 -0
- package/index.js +73 -0
- package/jest.config.js +10 -0
- package/package.json +41 -0
- package/src/ai-writer.js +108 -0
- package/src/analyzer.js +232 -0
- package/src/analyzers/migration.js +355 -0
- package/src/categorizer.js +182 -0
- package/src/config.js +162 -0
- package/src/constants.js +137 -0
- package/src/contributor.js +144 -0
- package/src/diff-analyzer.js +202 -0
- package/src/formatter.js +336 -0
- package/src/formatters/discord.js +174 -0
- package/src/formatters/html.js +195 -0
- package/src/formatters/index.js +42 -0
- package/src/formatters/markdown.js +123 -0
- package/src/formatters/slack.js +176 -0
- package/src/formatters/twitter.js +242 -0
- package/src/formatters/types.js +48 -0
- package/src/generator.js +297 -0
- package/src/integrations/changelog.js +125 -0
- package/src/integrations/discord.js +96 -0
- package/src/integrations/github-release.js +75 -0
- package/src/integrations/indexer.js +119 -0
- package/src/integrations/slack.js +112 -0
- package/src/integrations/twitter.js +128 -0
- package/src/logger.js +52 -0
- package/src/prompts.js +210 -0
- package/src/rate-limiter.js +92 -0
- package/src/semver.js +129 -0
- package/src/tones/casual.js +114 -0
- package/src/tones/humorous.js +164 -0
- package/src/tones/index.js +38 -0
- package/src/tones/professional.js +125 -0
- package/src/tones/technical.js +164 -0
- package/src/tones/types.js +26 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Simple rate limiter for API calls.
|
|
7
|
+
* Tracks calls per time window and enforces delays when approaching limits.
|
|
8
|
+
*/
|
|
9
|
+
class RateLimiter {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.maxRequests = options.maxRequests || 60;
|
|
12
|
+
this.windowMs = options.windowMs || 60 * 1000; // 1 minute
|
|
13
|
+
this.minDelay = options.minDelay || 100; // 100ms between calls
|
|
14
|
+
this.calls = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wait before making the next API call if needed.
|
|
19
|
+
* Call this before each API request.
|
|
20
|
+
*/
|
|
21
|
+
async wait() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
|
|
24
|
+
// Clean up old calls outside the window
|
|
25
|
+
this.calls = this.calls.filter(time => now - time < this.windowMs);
|
|
26
|
+
|
|
27
|
+
// If at limit, wait until oldest call expires
|
|
28
|
+
if (this.calls.length >= this.maxRequests) {
|
|
29
|
+
const oldestCall = this.calls[0];
|
|
30
|
+
const waitTime = this.windowMs - (now - oldestCall) + 100;
|
|
31
|
+
if (waitTime > 0) {
|
|
32
|
+
logger.debug(`Rate limit approaching โ waiting ${waitTime}ms`);
|
|
33
|
+
await this.sleep(waitTime);
|
|
34
|
+
}
|
|
35
|
+
// Clean again after waiting
|
|
36
|
+
this.calls = this.calls.filter(time => Date.now() - time < this.windowMs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Enforce minimum delay between calls
|
|
40
|
+
if (this.calls.length > 0) {
|
|
41
|
+
const lastCall = this.calls[this.calls.length - 1];
|
|
42
|
+
const elapsed = Date.now() - lastCall;
|
|
43
|
+
if (elapsed < this.minDelay) {
|
|
44
|
+
await this.sleep(this.minDelay - elapsed);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Record this call
|
|
49
|
+
this.calls.push(Date.now());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the number of calls made in the current window.
|
|
54
|
+
*/
|
|
55
|
+
getCallCount() {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
this.calls = this.calls.filter(time => now - time < this.windowMs);
|
|
58
|
+
return this.calls.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reset the rate limiter.
|
|
63
|
+
*/
|
|
64
|
+
reset() {
|
|
65
|
+
this.calls = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sleep for a given number of milliseconds.
|
|
70
|
+
*/
|
|
71
|
+
sleep(ms) {
|
|
72
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a rate limiter configured for GitHub API.
|
|
78
|
+
* GitHub allows ~60 requests per minute for unauthenticated,
|
|
79
|
+
* and ~5000 per hour for authenticated (but we conservatively limit per minute).
|
|
80
|
+
*/
|
|
81
|
+
function createGitHubRateLimiter() {
|
|
82
|
+
return new RateLimiter({
|
|
83
|
+
maxRequests: 60,
|
|
84
|
+
windowMs: 60 * 1000,
|
|
85
|
+
minDelay: 100,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
RateLimiter,
|
|
91
|
+
createGitHubRateLimiter,
|
|
92
|
+
};
|
package/src/semver.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a version string or tag into components.
|
|
7
|
+
* Handles: v1.2.3, 1.2.3, v1.2.3-beta.1, v1.2.3-alpha, etc.
|
|
8
|
+
*/
|
|
9
|
+
function parseVersion(tag) {
|
|
10
|
+
if (!tag || typeof tag !== 'string') {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cleaned = tag.replace(/^v/, '').trim();
|
|
15
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:[-.]([\w.]+))?$/i);
|
|
16
|
+
|
|
17
|
+
if (!match) {
|
|
18
|
+
logger.debug(`Could not parse version from: ${tag}`);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
major: parseInt(match[1], 10),
|
|
24
|
+
minor: parseInt(match[2], 10),
|
|
25
|
+
patch: parseInt(match[3], 10),
|
|
26
|
+
prerelease: match[4] || null,
|
|
27
|
+
raw: tag,
|
|
28
|
+
clean: cleaned,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compare two version strings or parsed version objects.
|
|
34
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
35
|
+
*/
|
|
36
|
+
function compareVersions(a, b) {
|
|
37
|
+
const vA = typeof a === 'string' ? parseVersion(a) : a;
|
|
38
|
+
const vB = typeof b === 'string' ? parseVersion(b) : b;
|
|
39
|
+
|
|
40
|
+
if (!vA && !vB) return 0;
|
|
41
|
+
if (!vA) return -1;
|
|
42
|
+
if (!vB) return 1;
|
|
43
|
+
|
|
44
|
+
if (vA.major !== vB.major) return vA.major > vB.major ? 1 : -1;
|
|
45
|
+
if (vA.minor !== vB.minor) return vA.minor > vB.minor ? 1 : -1;
|
|
46
|
+
if (vA.patch !== vB.patch) return vA.patch > vB.patch ? 1 : -1;
|
|
47
|
+
|
|
48
|
+
// No prerelease > prerelease
|
|
49
|
+
if (!vA.prerelease && vB.prerelease) return 1;
|
|
50
|
+
if (vA.prerelease && !vB.prerelease) return -1;
|
|
51
|
+
if (vA.prerelease && vB.prerelease) {
|
|
52
|
+
if (vA.prerelease < vB.prerelease) return -1;
|
|
53
|
+
if (vA.prerelease > vB.prerelease) return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Determine the next version based on commit analysis.
|
|
61
|
+
* Breaking changes -> major bump
|
|
62
|
+
* Features -> minor bump
|
|
63
|
+
* Fixes only -> patch bump
|
|
64
|
+
*/
|
|
65
|
+
function getNextVersion(commits, currentVersion) {
|
|
66
|
+
const parsed = typeof currentVersion === 'string' ? parseVersion(currentVersion) : currentVersion;
|
|
67
|
+
if (!parsed) {
|
|
68
|
+
logger.warn(`Cannot determine next version: invalid current version "${currentVersion}"`);
|
|
69
|
+
return currentVersion;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let hasBreaking = false;
|
|
73
|
+
let hasFeature = false;
|
|
74
|
+
let hasFix = false;
|
|
75
|
+
|
|
76
|
+
for (const commit of commits) {
|
|
77
|
+
const message = (commit.message || commit.commit?.message || '').toLowerCase();
|
|
78
|
+
const type = commit._type || '';
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
message.includes('breaking change') ||
|
|
82
|
+
message.includes('breaking:') ||
|
|
83
|
+
type === 'breaking'
|
|
84
|
+
) {
|
|
85
|
+
hasBreaking = true;
|
|
86
|
+
}
|
|
87
|
+
if (type === 'feat' || message.match(/^feat[(!:]/)) {
|
|
88
|
+
hasFeature = true;
|
|
89
|
+
}
|
|
90
|
+
if (type === 'fix' || message.match(/^fix[(!:]/)) {
|
|
91
|
+
hasFix = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let nextMajor = parsed.major;
|
|
96
|
+
let nextMinor = parsed.minor;
|
|
97
|
+
let nextPatch = parsed.patch;
|
|
98
|
+
|
|
99
|
+
if (hasBreaking) {
|
|
100
|
+
nextMajor += 1;
|
|
101
|
+
nextMinor = 0;
|
|
102
|
+
nextPatch = 0;
|
|
103
|
+
} else if (hasFeature) {
|
|
104
|
+
nextMinor += 1;
|
|
105
|
+
nextPatch = 0;
|
|
106
|
+
} else if (hasFix) {
|
|
107
|
+
nextPatch += 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nextVersion = `${nextMajor}.${nextMinor}.${nextPatch}`;
|
|
111
|
+
logger.info(`Next version determined: ${nextVersion} (breaking=${hasBreaking}, feature=${hasFeature}, fix=${hasFix})`);
|
|
112
|
+
return nextVersion;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a version tag indicates a prerelease.
|
|
117
|
+
*/
|
|
118
|
+
function isPrerelease(tag) {
|
|
119
|
+
if (!tag || typeof tag !== 'string') return false;
|
|
120
|
+
const cleaned = tag.replace(/^v/, '').trim();
|
|
121
|
+
return /[-.](alpha|beta|rc|pre|dev|canary|next|experimental|nightly)[-.]?\d*/i.test(cleaned);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
parseVersion,
|
|
126
|
+
compareVersions,
|
|
127
|
+
getNextVersion,
|
|
128
|
+
isPrerelease,
|
|
129
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Casual tone adapter.
|
|
5
|
+
* Developer-friendly, conversational, emoji-rich, approachable.
|
|
6
|
+
*/
|
|
7
|
+
class CasualTone {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.name = 'casual';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply casual tone to release data.
|
|
14
|
+
* @param {Object} data - ReleaseData object
|
|
15
|
+
* @returns {Object} Modified release data with casual tone
|
|
16
|
+
*/
|
|
17
|
+
apply(data) {
|
|
18
|
+
const result = { ...data };
|
|
19
|
+
|
|
20
|
+
// Transform summary
|
|
21
|
+
result.summary = this._casualSummary(data);
|
|
22
|
+
|
|
23
|
+
// Transform categories
|
|
24
|
+
if (data.categories) {
|
|
25
|
+
result.categories = {};
|
|
26
|
+
for (const [category, changes] of Object.entries(data.categories)) {
|
|
27
|
+
const newCategory = this._casualCategoryName(category);
|
|
28
|
+
result.categories[newCategory] = changes.map(c => ({
|
|
29
|
+
...c,
|
|
30
|
+
description: this._casualDescription(c.description, c.type),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Transform breaking changes
|
|
36
|
+
if (data.breaking) {
|
|
37
|
+
result.breaking = data.breaking.map(bc => ({
|
|
38
|
+
...bc,
|
|
39
|
+
description: this._casualBreaking(bc.description),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a casual summary.
|
|
48
|
+
*/
|
|
49
|
+
_casualSummary(data) {
|
|
50
|
+
const version = data.version || '0.0.0';
|
|
51
|
+
const parts = [`Here's what's new in v${version}!`];
|
|
52
|
+
|
|
53
|
+
const catCount = Object.keys(data.categories || {}).length;
|
|
54
|
+
if (catCount > 3) {
|
|
55
|
+
parts.push(` This release is packed with ${catCount} types of changes.`);
|
|
56
|
+
} else if (catCount > 0) {
|
|
57
|
+
parts.push(` We've got a bunch of goodies for you.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (data.breaking && data.breaking.length > 0) {
|
|
61
|
+
parts.push(` Heads up: there ${data.breaking.length === 1 ? 'is' : 'are'} ${data.breaking.length} breaking change${data.breaking.length > 1 ? 's' : ''}!`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (data.contributors && data.contributors.length > 0) {
|
|
65
|
+
parts.push(` Huge thanks to ${data.contributors.length} contributor${data.contributors.length > 1 ? 's' : ''}!`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parts.join('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Map category names to casual names with emojis.
|
|
73
|
+
*/
|
|
74
|
+
_casualCategoryName(category) {
|
|
75
|
+
const map = {
|
|
76
|
+
'๐ Features': ':rocket: Cool New Stuff',
|
|
77
|
+
'๐ Bug Fixes': ':bug: Squashed Bugs',
|
|
78
|
+
'๐ฅ Breaking Changes': ':boom: Heads Up - Breaking Changes!',
|
|
79
|
+
'โก Performance': ':zap: Speed Boosts',
|
|
80
|
+
'โป๏ธ Refactoring': ':recycle: Under the Hood',
|
|
81
|
+
'๐ Documentation': ':memo: Docs & Guides',
|
|
82
|
+
'๐จ Style': ':art: Pretty Code',
|
|
83
|
+
'๐งช Tests': ':test_tube: Test Improvements',
|
|
84
|
+
'๐ง Chore': ':wrench: Housekeeping',
|
|
85
|
+
'๐ Security': ':lock: Security Fixes',
|
|
86
|
+
'New Features': ':rocket: Cool New Stuff',
|
|
87
|
+
'Bug Fixes and Resolutions': ':bug: Squashed Bugs',
|
|
88
|
+
'Performance Improvements': ':zap: Speed Boosts',
|
|
89
|
+
};
|
|
90
|
+
return map[category] || `:sparkles: ${category}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Make a description more casual.
|
|
95
|
+
*/
|
|
96
|
+
_casualDescription(desc, type) {
|
|
97
|
+
if (!desc) return desc;
|
|
98
|
+
// Add exclamation for features
|
|
99
|
+
if (type === 'feat' && !desc.endsWith('!') && !desc.endsWith('.')) {
|
|
100
|
+
return desc + '!';
|
|
101
|
+
}
|
|
102
|
+
return desc;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Casual breaking change description.
|
|
107
|
+
*/
|
|
108
|
+
_casualBreaking(desc) {
|
|
109
|
+
if (!desc) return desc;
|
|
110
|
+
return `Watch out! ${desc}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { CasualTone };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Humorous tone adapter.
|
|
5
|
+
* Fun, witty descriptions with pop culture references.
|
|
6
|
+
* Memorable and shareable.
|
|
7
|
+
*/
|
|
8
|
+
class HumorousTone {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.name = 'humorous';
|
|
11
|
+
this._celebrations = [
|
|
12
|
+
'Hold onto your keyboards!',
|
|
13
|
+
'Drumroll please...',
|
|
14
|
+
'Spoiler alert: it\'s awesome.',
|
|
15
|
+
'Cue the confetti!',
|
|
16
|
+
'The release you\'ve been waiting for (probably).',
|
|
17
|
+
];
|
|
18
|
+
this._fixPhrases = [
|
|
19
|
+
'We fixed it. You\'re welcome.',
|
|
20
|
+
'The bug met its maker.',
|
|
21
|
+
'Squashed like the creepy-crawly it was.',
|
|
22
|
+
'Take that, bug!',
|
|
23
|
+
'Another one bites the dust.',
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply humorous tone to release data.
|
|
29
|
+
* @param {Object} data - ReleaseData object
|
|
30
|
+
* @returns {Object} Modified release data with humorous tone
|
|
31
|
+
*/
|
|
32
|
+
apply(data) {
|
|
33
|
+
const result = { ...data };
|
|
34
|
+
|
|
35
|
+
// Transform summary
|
|
36
|
+
result.summary = this._humorousSummary(data);
|
|
37
|
+
|
|
38
|
+
// Transform categories
|
|
39
|
+
if (data.categories) {
|
|
40
|
+
result.categories = {};
|
|
41
|
+
for (const [category, changes] of Object.entries(data.categories)) {
|
|
42
|
+
const newCategory = this._humorousCategoryName(category);
|
|
43
|
+
result.categories[newCategory] = changes.map((c, i) => ({
|
|
44
|
+
...c,
|
|
45
|
+
description: this._humorousDescription(c.description, c.type, i),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Transform breaking changes
|
|
51
|
+
if (data.breaking) {
|
|
52
|
+
result.breaking = data.breaking.map(bc => ({
|
|
53
|
+
...bc,
|
|
54
|
+
description: this._humorousBreaking(bc.description),
|
|
55
|
+
migration_guide: bc.migration_guide
|
|
56
|
+
? `Don't panic! ${bc.migration_guide}`
|
|
57
|
+
: 'Update your code. We believe in you.',
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a humorous summary.
|
|
66
|
+
*/
|
|
67
|
+
_humorousSummary(data) {
|
|
68
|
+
const version = data.version || '0.0.0';
|
|
69
|
+
const opener = this._celebrations[Math.floor(Math.random() * this._celebrations.length)];
|
|
70
|
+
|
|
71
|
+
const parts = [`${opener} v${version} has landed.`];
|
|
72
|
+
|
|
73
|
+
if (data.breaking && data.breaking.length > 0) {
|
|
74
|
+
parts.push(` Yes, there ${data.breaking.length === 1 ? 'is a' : 'are'} breaking change${data.breaking.length > 1 ? 's' : ''}. No pain, no gain, right?`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const catCount = Object.keys(data.categories || {}).length;
|
|
78
|
+
if (catCount > 0) {
|
|
79
|
+
parts.push(` ${catCount} categor${catCount !== 1 ? 'ies' : 'y'} of stuff that's different now.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (data.contributors && data.contributors.length > 0) {
|
|
83
|
+
if (data.contributors.length === 1) {
|
|
84
|
+
parts.push(` One brave soul made this happen.`);
|
|
85
|
+
} else {
|
|
86
|
+
parts.push(` ${data.contributors.length} amazing humans made this happen.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parts.join('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Map category names to humorous names.
|
|
95
|
+
*/
|
|
96
|
+
_humorousCategoryName(category) {
|
|
97
|
+
const map = {
|
|
98
|
+
'๐ Features': ':rocket: Things That Are New and Shiny',
|
|
99
|
+
'๐ Bug Fixes': ':bug: Bugs We Sent to Bug Heaven',
|
|
100
|
+
'๐ฅ Breaking Changes': ':boom: Things We Broke (Sorry, Not Sorry)',
|
|
101
|
+
'โก Performance': ':zap: We Made It Go Brrr',
|
|
102
|
+
'โป๏ธ Refactoring': ':recycle: Code We Rewrote for Fun',
|
|
103
|
+
'๐ Documentation': ':memo: Words About Code',
|
|
104
|
+
'๐จ Style': ':art: Making Code Pretty Again',
|
|
105
|
+
'๐งช Tests': ':test_tube: We Actually Test Things',
|
|
106
|
+
'๐ง Chore': ':wrench: Things Nobody Sees But Everyone Needs',
|
|
107
|
+
'๐ Security': ':lock: Fort Knox Mode',
|
|
108
|
+
'New Features': ':rocket: Things That Are New and Shiny',
|
|
109
|
+
'Bug Fixes and Resolutions': ':bug: Bugs We Sent to Bug Heaven',
|
|
110
|
+
'Performance Improvements': ':zap: We Made It Go Brrr',
|
|
111
|
+
'Code Quality Improvements': ':recycle: Code We Rewrote for Fun',
|
|
112
|
+
'Documentation Updates': ':memo: Words About Code',
|
|
113
|
+
'Test Coverage': ':test_tube: We Actually Test Things',
|
|
114
|
+
'Security Enhancements': ':lock: Fort Knox Mode',
|
|
115
|
+
};
|
|
116
|
+
return map[category] || `:sparkles: ${category}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Make a description more humorous.
|
|
121
|
+
*/
|
|
122
|
+
_humorousDescription(desc, type, index) {
|
|
123
|
+
if (!desc) return desc;
|
|
124
|
+
|
|
125
|
+
if (type === 'fix') {
|
|
126
|
+
const phrase = this._fixPhrases[index % this._fixPhrases.length];
|
|
127
|
+
return `${desc} - ${phrase}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (type === 'feat') {
|
|
131
|
+
const additions = [
|
|
132
|
+
' (you\'re gonna love this)',
|
|
133
|
+
' (finally!)',
|
|
134
|
+
' (yes, really)',
|
|
135
|
+
' (the people demanded it)',
|
|
136
|
+
' (you\'re welcome)',
|
|
137
|
+
];
|
|
138
|
+
return desc + additions[index % additions.length];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (type === 'perf') {
|
|
142
|
+
return `${desc} - it\'s fast now. Like, really fast.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return desc;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Humorous breaking change description.
|
|
150
|
+
*/
|
|
151
|
+
_humorousBreaking(desc) {
|
|
152
|
+
if (!desc) return desc;
|
|
153
|
+
const prefixes = [
|
|
154
|
+
'Plot twist:',
|
|
155
|
+
'In plot twist news:',
|
|
156
|
+
'Breaking news (literally):',
|
|
157
|
+
'Change of plans:',
|
|
158
|
+
];
|
|
159
|
+
const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
|
|
160
|
+
return `${prefix} ${desc}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { HumorousTone };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TONES, isValidTone } = require('./types');
|
|
4
|
+
const { ProfessionalTone } = require('./professional');
|
|
5
|
+
const { CasualTone } = require('./casual');
|
|
6
|
+
const { HumorousTone } = require('./humorous');
|
|
7
|
+
const { TechnicalTone } = require('./technical');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tone factory. Returns the appropriate tone adapter for a given tone name.
|
|
11
|
+
* @param {string} tone - Tone identifier ('professional', 'casual', 'humorous', 'technical')
|
|
12
|
+
* @returns {Object} Tone adapter instance with name and apply() method
|
|
13
|
+
* @throws {Error} If tone is not recognized
|
|
14
|
+
*/
|
|
15
|
+
function getTone(tone) {
|
|
16
|
+
switch (tone) {
|
|
17
|
+
case 'professional':
|
|
18
|
+
return new ProfessionalTone();
|
|
19
|
+
case 'casual':
|
|
20
|
+
return new CasualTone();
|
|
21
|
+
case 'humorous':
|
|
22
|
+
return new HumorousTone();
|
|
23
|
+
case 'technical':
|
|
24
|
+
return new TechnicalTone();
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(`Unknown tone: "${tone}". Valid tones: ${TONES.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
getTone,
|
|
32
|
+
TONES,
|
|
33
|
+
isValidTone,
|
|
34
|
+
ProfessionalTone,
|
|
35
|
+
CasualTone,
|
|
36
|
+
HumorousTone,
|
|
37
|
+
TechnicalTone,
|
|
38
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Professional tone adapter.
|
|
5
|
+
* Enterprise/corporate language with formal headers and structured sections.
|
|
6
|
+
*/
|
|
7
|
+
class ProfessionalTone {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.name = 'professional';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply professional tone to release data.
|
|
14
|
+
* @param {Object} data - ReleaseData object
|
|
15
|
+
* @returns {Object} Modified release data with professional tone
|
|
16
|
+
*/
|
|
17
|
+
apply(data) {
|
|
18
|
+
const result = { ...data };
|
|
19
|
+
|
|
20
|
+
// Transform summary
|
|
21
|
+
result.summary = this._professionalSummary(data);
|
|
22
|
+
|
|
23
|
+
// Transform categories
|
|
24
|
+
if (data.categories) {
|
|
25
|
+
result.categories = {};
|
|
26
|
+
for (const [category, changes] of Object.entries(data.categories)) {
|
|
27
|
+
const newCategory = this._professionalCategoryName(category);
|
|
28
|
+
result.categories[newCategory] = changes.map(c => ({
|
|
29
|
+
...c,
|
|
30
|
+
description: this._professionalDescription(c.description, c.type),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Transform breaking changes
|
|
36
|
+
if (data.breaking) {
|
|
37
|
+
result.breaking = data.breaking.map(bc => ({
|
|
38
|
+
...bc,
|
|
39
|
+
description: this._professionalBreaking(bc.description),
|
|
40
|
+
migration_guide: bc.migration_guide
|
|
41
|
+
? this._professionalMigration(bc.migration_guide)
|
|
42
|
+
: undefined,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a professional summary.
|
|
51
|
+
*/
|
|
52
|
+
_professionalSummary(data) {
|
|
53
|
+
const parts = [];
|
|
54
|
+
const version = data.version || '0.0.0';
|
|
55
|
+
|
|
56
|
+
if (data.breaking && data.breaking.length > 0) {
|
|
57
|
+
parts.push(`We are pleased to announce the release of version ${version}, which includes ${data.breaking.length} breaking change${data.breaking.length > 1 ? 's' : ''}`);
|
|
58
|
+
} else if (Object.keys(data.categories || {}).length > 0) {
|
|
59
|
+
const catCount = Object.keys(data.categories).length;
|
|
60
|
+
parts.push(`We are pleased to announce the release of version ${version}, comprising ${catCount} categor${catCount !== 1 ? 'ies' : 'y'} of improvements`);
|
|
61
|
+
} else {
|
|
62
|
+
parts.push(`We are pleased to announce the release of version ${version}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (data.contributors && data.contributors.length > 0) {
|
|
66
|
+
parts.push(`with contributions from ${data.contributors.length} developer${data.contributors.length > 1 ? 's' : ''}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
parts.push('.');
|
|
70
|
+
return parts.join(' ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map emoji category names to professional names.
|
|
75
|
+
*/
|
|
76
|
+
_professionalCategoryName(category) {
|
|
77
|
+
const map = {
|
|
78
|
+
'๐ Features': 'New Features',
|
|
79
|
+
'๐ Bug Fixes': 'Bug Fixes and Resolutions',
|
|
80
|
+
'๐ฅ Breaking Changes': 'Breaking Changes',
|
|
81
|
+
'โก Performance': 'Performance Improvements',
|
|
82
|
+
'โป๏ธ Refactoring': 'Code Quality Improvements',
|
|
83
|
+
'๐ Documentation': 'Documentation Updates',
|
|
84
|
+
'๐จ Style': 'Code Style and Formatting',
|
|
85
|
+
'๐งช Tests': 'Test Coverage',
|
|
86
|
+
'๐ง Chore': 'Maintenance and Infrastructure',
|
|
87
|
+
'๐ Security': 'Security Enhancements',
|
|
88
|
+
};
|
|
89
|
+
return map[category] || category.replace(/[^\w\s&]/g, '').trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Make a description more professional.
|
|
94
|
+
*/
|
|
95
|
+
_professionalDescription(desc, type) {
|
|
96
|
+
if (!desc) return desc;
|
|
97
|
+
// Capitalize first letter, ensure period at end if appropriate
|
|
98
|
+
let result = desc.charAt(0).toUpperCase() + desc.slice(1);
|
|
99
|
+
if (type === 'feat' && !result.startsWith('Implemented') && !result.startsWith('Added') && !result.startsWith('Introduced')) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
if (type === 'fix' && !result.startsWith('Resolved') && !result.startsWith('Fixed') && !result.startsWith('Addressed')) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Professional breaking change description.
|
|
110
|
+
*/
|
|
111
|
+
_professionalBreaking(desc) {
|
|
112
|
+
if (!desc) return desc;
|
|
113
|
+
return `This release introduces a change to ${desc.charAt(0).toLowerCase() + desc.slice(1)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Professional migration guide.
|
|
118
|
+
*/
|
|
119
|
+
_professionalMigration(guide) {
|
|
120
|
+
if (!guide) return guide;
|
|
121
|
+
return guide;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { ProfessionalTone };
|