@intranefr/superbackend 1.6.7 → 1.7.8

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.
Files changed (119) hide show
  1. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
  2. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
  3. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
  4. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
  5. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
  6. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
  7. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
  8. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
  9. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
  10. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
  11. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
  12. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
  13. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
  14. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
  15. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
  16. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
  17. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
  18. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
  19. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
  20. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
  21. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
  22. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
  23. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
  24. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
  25. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
  26. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
  27. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
  28. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
  29. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
  30. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
  31. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
  32. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
  33. package/.beads/config.yaml +4 -0
  34. package/.beads/issues.jsonl +8 -0
  35. package/.beads/metadata.json +4 -0
  36. package/.env.example +8 -0
  37. package/autochangelog/.env.example +36 -0
  38. package/autochangelog/README.md +412 -0
  39. package/autochangelog/config/database.js +27 -0
  40. package/autochangelog/package.json +47 -0
  41. package/autochangelog/public/landing.html +581 -0
  42. package/autochangelog/server.js +104 -0
  43. package/autochangelog/src/app.js +181 -0
  44. package/autochangelog/src/config/database.js +26 -0
  45. package/autochangelog/src/controllers/auth.js +488 -0
  46. package/autochangelog/src/controllers/changelog.js +682 -0
  47. package/autochangelog/src/controllers/project.js +580 -0
  48. package/autochangelog/src/controllers/repository.js +780 -0
  49. package/autochangelog/src/middleware/auth.js +386 -0
  50. package/autochangelog/src/models/Changelog.js +443 -0
  51. package/autochangelog/src/models/Project.js +226 -0
  52. package/autochangelog/src/models/Repository.js +366 -0
  53. package/autochangelog/src/models/User.js +223 -0
  54. package/autochangelog/src/routes/auth.routes.js +32 -0
  55. package/autochangelog/src/routes/changelog.routes.js +42 -0
  56. package/autochangelog/src/routes/github-auth.routes.js +102 -0
  57. package/autochangelog/src/routes/project.routes.js +50 -0
  58. package/autochangelog/src/routes/repository.routes.js +54 -0
  59. package/autochangelog/src/services/changelog.js +722 -0
  60. package/autochangelog/src/services/github.js +243 -0
  61. package/autochangelog/utils/logger.js +77 -0
  62. package/autochangelog/views/404.ejs +18 -0
  63. package/autochangelog/views/dashboard.ejs +596 -0
  64. package/autochangelog/views/index.ejs +231 -0
  65. package/autochangelog/views/layouts/main.ejs +44 -0
  66. package/autochangelog/views/login.ejs +104 -0
  67. package/autochangelog/views/partials/footer.ejs +20 -0
  68. package/autochangelog/views/partials/navbar.ejs +51 -0
  69. package/autochangelog/views/register.ejs +109 -0
  70. package/autochangelog-cli/README.md +266 -0
  71. package/autochangelog-cli/bin/autochangelog +120 -0
  72. package/autochangelog-cli/package.json +46 -0
  73. package/autochangelog-cli/src/cli/commands/auth.js +291 -0
  74. package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
  75. package/autochangelog-cli/src/cli/commands/project.js +427 -0
  76. package/autochangelog-cli/src/cli/commands/repo.js +557 -0
  77. package/autochangelog-cli/src/cli/commands/stats.js +706 -0
  78. package/autochangelog-cli/src/cli/utils/config.js +277 -0
  79. package/autochangelog-cli/src/cli/utils/errors.js +307 -0
  80. package/autochangelog-cli/src/cli/utils/logger.js +75 -0
  81. package/autochangelog-cli/src/cli/utils/output.js +357 -0
  82. package/package.json +9 -3
  83. package/plugins/supercli/README.md +108 -0
  84. package/plugins/supercli/plugin.json +123 -0
  85. package/server.js +1 -1
  86. package/src/cli/api.js +380 -0
  87. package/src/cli/direct/agent-utils.js +61 -0
  88. package/src/cli/direct/cli-utils.js +112 -0
  89. package/src/cli/direct/data-seeding.js +307 -0
  90. package/src/cli/direct/db-admin.js +84 -0
  91. package/src/cli/direct/db-advanced.js +372 -0
  92. package/src/cli/direct/db-utils.js +558 -0
  93. package/src/cli/direct/help.js +195 -0
  94. package/src/cli/direct/migration.js +107 -0
  95. package/src/cli/direct/rbac-advanced.js +132 -0
  96. package/src/cli/direct/resources-additional.js +400 -0
  97. package/src/cli/direct/resources-cms-advanced.js +173 -0
  98. package/src/cli/direct/resources-cms.js +247 -0
  99. package/src/cli/direct/resources-core.js +253 -0
  100. package/src/cli/direct/resources-execution.js +367 -0
  101. package/src/cli/direct/resources-health.js +152 -0
  102. package/src/cli/direct/resources-integrations.js +182 -0
  103. package/src/cli/direct/resources-logs.js +204 -0
  104. package/src/cli/direct/resources-org-rbac.js +187 -0
  105. package/src/cli/direct/resources-system.js +236 -0
  106. package/src/cli/direct.js +556 -0
  107. package/src/controllers/admin.controller.js +4 -0
  108. package/src/controllers/auth.controller.js +148 -1
  109. package/src/controllers/waitingList.controller.js +130 -1
  110. package/src/models/RbacRole.js +1 -1
  111. package/src/models/User.js +39 -5
  112. package/src/routes/auth.routes.js +6 -0
  113. package/src/routes/waitingList.routes.js +12 -2
  114. package/src/routes/waitingListAdmin.routes.js +3 -0
  115. package/src/services/email.service.js +1 -0
  116. package/src/services/github.service.js +255 -0
  117. package/src/services/rateLimiter.service.js +29 -1
  118. package/src/services/waitingListJson.service.js +32 -3
  119. package/views/admin-waiting-list.ejs +386 -3
@@ -0,0 +1,722 @@
1
+ const simpleGit = require('simple-git');
2
+ const { parse } = require('conventional-commits-parser');
3
+ const logger = require('../../utils/logger');
4
+ const Project = require('../models/Project');
5
+ const Repository = require('../models/Repository');
6
+ const Changelog = require('../models/Changelog');
7
+ const GitHubService = require('./github');
8
+
9
+ class ChangelogService {
10
+ constructor() {
11
+ this.git = simpleGit();
12
+ }
13
+
14
+ // Generate changelog for a specific month and year
15
+ async generateChangelog(projectId, repositoryId, month, year, options = {}) {
16
+ const { format = 'markdown', template = 'default', includeCommitsWithoutPR = true } = options;
17
+
18
+ try {
19
+ // Get project and repository
20
+ const [project, repository] = await Promise.all([
21
+ Project.findById(projectId),
22
+ Repository.findById(repositoryId),
23
+ ]);
24
+
25
+ if (!project || !repository) {
26
+ throw new Error('Project or repository not found');
27
+ }
28
+
29
+ // Check if changelog already exists
30
+ const existingChangelog = await Changelog.findOne({
31
+ projectId,
32
+ repositoryId,
33
+ month,
34
+ year,
35
+ });
36
+
37
+ if (existingChangelog) {
38
+ throw new Error('Changelog already exists for this period');
39
+ }
40
+
41
+ // Create changelog record with pending status
42
+ const changelog = new Changelog({
43
+ projectId,
44
+ repositoryId,
45
+ title: `${project.name} - ${this.getMonthName(month)} ${year}`,
46
+ description: `Changelog for ${this.getMonthName(month)} ${year}`,
47
+ month,
48
+ year,
49
+ format,
50
+ template,
51
+ status: 'generating',
52
+ generatedBy: repository.ownerId,
53
+ });
54
+
55
+ await changelog.save();
56
+
57
+ // Generate changelog content
58
+ const startTime = Date.now();
59
+ const content = await this.generateContent(repository, month, year, { format, template, includeCommitsWithoutPR });
60
+ const generationTime = Date.now() - startTime;
61
+
62
+ // Update changelog with content and completion status
63
+ changelog.content = content;
64
+ changelog.status = 'completed';
65
+ changelog.generationTime = generationTime;
66
+ changelog.generatedAt = new Date();
67
+
68
+ await changelog.save();
69
+
70
+ // Update repository metadata
71
+ await this.updateRepositoryMetadata(repository, month, year);
72
+
73
+ logger.info(`Changelog generated successfully: ${changelog.title}`);
74
+ return changelog;
75
+ } catch (error) {
76
+ logger.error('Changelog generation error:', error);
77
+
78
+ // Update changelog status to failed if it was created
79
+ if (changelog) {
80
+ changelog.status = 'failed';
81
+ changelog.error = error.message;
82
+ await changelog.save();
83
+ }
84
+
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ // Generate changelog content
90
+ async generateContent(repository, month, year, options = {}) {
91
+ const { format = 'markdown', template = 'default', includeCommitsWithoutPR = true } = options;
92
+
93
+ try {
94
+ // Get commit history for the specified month
95
+ const commits = await this.getCommitsForMonth(repository, month, year);
96
+
97
+ // Parse commits and categorize them
98
+ const parsedCommits = await this.parseCommits(commits);
99
+ const categorizedCommits = this.categorizeCommits(parsedCommits);
100
+
101
+ // Get additional context (PRs, issues, etc.)
102
+ const context = await this.getCommitContext(repository, commits, { includeCommitsWithoutPR });
103
+
104
+ // Generate content based on format and template
105
+ switch (format) {
106
+ case 'markdown':
107
+ return this.generateMarkdownContent(repository, month, year, categorizedCommits, context, template);
108
+ case 'html':
109
+ return this.generateHTMLContent(repository, month, year, categorizedCommits, context, template);
110
+ case 'json':
111
+ return this.generateJSONContent(repository, month, year, categorizedCommits, context);
112
+ default:
113
+ return this.generateMarkdownContent(repository, month, year, categorizedCommits, context, template);
114
+ }
115
+ } catch (error) {
116
+ logger.error('Content generation error:', error);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ // Get commits for a specific month
122
+ async getCommitsForMonth(repository, month, year) {
123
+ try {
124
+ const startDate = new Date(year, parseInt(month) - 1, 1);
125
+ const endDate = new Date(year, parseInt(month), 0, 23, 59, 59, 999);
126
+
127
+ // Use GitHub API to get commits
128
+ const commits = await GitHubService.getCommits(repository.githubAccessToken,
129
+ repository.githubRepoOwner,
130
+ repository.githubRepoName, {
131
+ since: startDate.toISOString(),
132
+ until: endDate.toISOString(),
133
+ sha: repository.defaultBranch,
134
+ });
135
+
136
+ return commits;
137
+ } catch (error) {
138
+ logger.error('Get commits error:', error);
139
+ throw new Error('Failed to fetch commits from GitHub');
140
+ }
141
+ }
142
+
143
+ // Parse commits using conventional commits parser
144
+ async parseCommits(commits) {
145
+ const parsedCommits = [];
146
+
147
+ for (const commit of commits) {
148
+ try {
149
+ const parsed = parse(commit.commit.message);
150
+ parsedCommits.push({
151
+ sha: commit.sha,
152
+ message: commit.commit.message,
153
+ author: {
154
+ name: commit.commit.author.name,
155
+ email: commit.commit.author.email,
156
+ },
157
+ date: commit.commit.author.date,
158
+ parsed,
159
+ });
160
+ } catch (error) {
161
+ // If parsing fails, include the commit as-is
162
+ parsedCommits.push({
163
+ sha: commit.sha,
164
+ message: commit.commit.message,
165
+ author: {
166
+ name: commit.commit.author.name,
167
+ email: commit.commit.author.email,
168
+ },
169
+ date: commit.commit.author.date,
170
+ parsed: null,
171
+ });
172
+ }
173
+ }
174
+
175
+ return parsedCommits;
176
+ }
177
+
178
+ // Categorize commits by type
179
+ categorizeCommits(parsedCommits) {
180
+ const categories = {
181
+ 'feat': [],
182
+ 'fix': [],
183
+ 'docs': [],
184
+ 'style': [],
185
+ 'refactor': [],
186
+ 'test': [],
187
+ 'chore': [],
188
+ 'perf': [],
189
+ 'ci': [],
190
+ 'deps': [],
191
+ 'other': [],
192
+ };
193
+
194
+ const authors = new Map();
195
+
196
+ for (const commit of parsedCommits) {
197
+ const type = commit.parsed?.type || 'other';
198
+ const category = this.mapCommitType(type);
199
+
200
+ categories[category].push({
201
+ sha: commit.sha,
202
+ message: commit.message,
203
+ author: commit.author,
204
+ date: commit.date,
205
+ parsed: commit.parsed,
206
+ });
207
+
208
+ // Track authors
209
+ const authorKey = `${commit.author.name} <${commit.author.email}>`;
210
+ if (!authors.has(authorKey)) {
211
+ authors.set(authorKey, {
212
+ name: commit.author.name,
213
+ email: commit.author.email,
214
+ commits: 0,
215
+ });
216
+ }
217
+ authors.get(authorKey).commits++;
218
+ }
219
+
220
+ // Convert authors map to array and sort by commit count
221
+ const sortedAuthors = Array.from(authors.values())
222
+ .sort((a, b) => b.commits - a.commits);
223
+
224
+ return {
225
+ categories,
226
+ authors: sortedAuthors,
227
+ totalCommits: parsedCommits.length,
228
+ };
229
+ }
230
+
231
+ // Map commit types to standard categories
232
+ mapCommitType(type) {
233
+ const typeMap = {
234
+ 'feat': 'feat',
235
+ 'feature': 'feat',
236
+ 'enhancement': 'feat',
237
+ 'fix': 'fix',
238
+ 'bugfix': 'fix',
239
+ 'bug': 'fix',
240
+ 'docs': 'docs',
241
+ 'documentation': 'docs',
242
+ 'style': 'style',
243
+ 'formatting': 'style',
244
+ 'refactor': 'refactor',
245
+ 'refactoring': 'refactor',
246
+ 'test': 'test',
247
+ 'testing': 'test',
248
+ 'chore': 'chore',
249
+ 'maintenance': 'chore',
250
+ 'perf': 'perf',
251
+ 'performance': 'perf',
252
+ 'ci': 'ci',
253
+ 'continuous-integration': 'ci',
254
+ 'deps': 'deps',
255
+ 'dependencies': 'deps',
256
+ };
257
+
258
+ return typeMap[type] || 'other';
259
+ }
260
+
261
+ // Get additional context for commits (PRs, issues, etc.)
262
+ async getCommitContext(repository, commits, options = {}) {
263
+ const { includeCommitsWithoutPR = true } = options;
264
+
265
+ try {
266
+ const context = {
267
+ pullRequests: [],
268
+ issues: [],
269
+ releases: [],
270
+ };
271
+
272
+ // Get pull requests for the repository
273
+ const prs = await GitHubService.getPullRequests(repository.githubAccessToken,
274
+ repository.githubRepoOwner,
275
+ repository.githubRepoName, {
276
+ state: 'closed',
277
+ sort: 'updated',
278
+ direction: 'desc',
279
+ });
280
+
281
+ // Filter PRs that were merged in the specified time period
282
+ const commitShas = new Set(commits.map(c => c.sha));
283
+
284
+ for (const pr of prs) {
285
+ if (pr.merged_at) {
286
+ const mergedDate = new Date(pr.merged_at);
287
+ const prDate = new Date(pr.created_at);
288
+
289
+ // Check if PR was merged or created in our target period
290
+ if (this.isInSameMonth(mergedDate) || this.isInSameMonth(prDate)) {
291
+ context.pullRequests.push({
292
+ number: pr.number,
293
+ title: pr.title,
294
+ body: pr.body,
295
+ author: pr.user.login,
296
+ mergedAt: pr.merged_at,
297
+ createdAt: pr.created_at,
298
+ url: pr.html_url,
299
+ commits: pr.commits,
300
+ additions: pr.additions,
301
+ deletions: pr.deletions,
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ // Get issues
308
+ const issues = await GitHubService.getIssues(repository.githubAccessToken,
309
+ repository.githubRepoOwner,
310
+ repository.githubRepoName, {
311
+ state: 'closed',
312
+ sort: 'updated',
313
+ direction: 'desc',
314
+ });
315
+
316
+ for (const issue of issues) {
317
+ if (issue.pull_request) continue; // Skip PRs
318
+
319
+ const issueDate = new Date(issue.closed_at || issue.created_at);
320
+ if (this.isInSameMonth(issueDate)) {
321
+ context.issues.push({
322
+ number: issue.number,
323
+ title: issue.title,
324
+ body: issue.body,
325
+ author: issue.user.login,
326
+ closedAt: issue.closed_at,
327
+ createdAt: issue.created_at,
328
+ url: issue.html_url,
329
+ labels: issue.labels.map(l => l.name),
330
+ });
331
+ }
332
+ }
333
+
334
+ // Get releases
335
+ const releases = await GitHubService.getReleases(repository.githubAccessToken,
336
+ repository.githubRepoOwner,
337
+ repository.githubRepoName);
338
+
339
+ for (const release of releases) {
340
+ const releaseDate = new Date(release.published_at);
341
+ if (this.isInSameMonth(releaseDate)) {
342
+ context.releases.push({
343
+ tagName: release.tag_name,
344
+ name: release.name,
345
+ body: release.body,
346
+ author: release.author.login,
347
+ publishedAt: release.published_at,
348
+ url: release.html_url,
349
+ assets: release.assets,
350
+ });
351
+ }
352
+ }
353
+
354
+ return context;
355
+ } catch (error) {
356
+ logger.error('Get commit context error:', error);
357
+ return {
358
+ pullRequests: [],
359
+ issues: [],
360
+ releases: [],
361
+ };
362
+ }
363
+ }
364
+
365
+ // Check if a date is in the same month as the target month/year
366
+ isInSameMonth(date, targetMonth, targetYear) {
367
+ return date.getMonth() + 1 === parseInt(targetMonth) && date.getFullYear() === parseInt(targetYear);
368
+ }
369
+
370
+ // Generate markdown content
371
+ generateMarkdownContent(repository, month, year, categorizedCommits, context, template) {
372
+ const monthName = this.getMonthName(month);
373
+ const dateRange = this.getDateRange(month, year);
374
+
375
+ let content = `# ${repository.name} - ${monthName} ${year}\n\n`;
376
+ content += `**Date Range:** ${dateRange}\n`;
377
+ content += `**Repository:** [${repository.githubRepoFullName}](${repository.githubRepoUrl})\n\n`;
378
+
379
+ // Summary
380
+ content += `## Summary\n\n`;
381
+ content += `- **Total Commits:** ${categorizedCommits.totalCommits}\n`;
382
+ content += `- **Contributors:** ${categorizedCommits.authors.length}\n`;
383
+ content += `- **Pull Requests:** ${context.pullRequests.length}\n`;
384
+ content += `- **Issues Closed:** ${context.issues.length}\n\n`;
385
+
386
+ // Releases
387
+ if (context.releases.length > 0) {
388
+ content += `## Releases\n\n`;
389
+ for (const release of context.releases) {
390
+ content += `- **[${release.tagName}](${release.url})** - ${release.name}\n`;
391
+ if (release.body) {
392
+ content += ` ${release.body.split('\n')[0]}\n`;
393
+ }
394
+ content += `\n`;
395
+ }
396
+ }
397
+
398
+ // Changes by Category
399
+ content += `## Changes by Category\n\n`;
400
+
401
+ const categoryOrder = [
402
+ 'feat', 'fix', 'perf', 'refactor', 'test',
403
+ 'docs', 'style', 'ci', 'deps', 'chore', 'other'
404
+ ];
405
+
406
+ for (const category of categoryOrder) {
407
+ const commits = categorizedCommits.categories[category];
408
+ if (commits.length > 0) {
409
+ content += `### ${this.getCategoryDisplayName(category)}\n\n`;
410
+
411
+ for (const commit of commits) {
412
+ const message = this.formatCommitMessage(commit.message);
413
+ content += `- ${message} (${commit.author.name})\n`;
414
+ }
415
+ content += `\n`;
416
+ }
417
+ }
418
+
419
+ // Pull Requests
420
+ if (context.pullRequests.length > 0) {
421
+ content += `## Pull Requests\n\n`;
422
+ for (const pr of context.pullRequests) {
423
+ content += `- **[#${pr.number}](${pr.url})** ${pr.title} by ${pr.author}\n`;
424
+ }
425
+ content += `\n`;
426
+ }
427
+
428
+ // Issues
429
+ if (context.issues.length > 0) {
430
+ content += `## Issues Closed\n\n`;
431
+ for (const issue of context.issues) {
432
+ content += `- **[#${issue.number}](${issue.url})** ${issue.title} by ${issue.author}\n`;
433
+ }
434
+ content += `\n`;
435
+ }
436
+
437
+ // Contributors
438
+ if (categorizedCommits.authors.length > 0) {
439
+ content += `## Contributors\n\n`;
440
+ for (const author of categorizedCommits.authors) {
441
+ content += `- **${author.name}** (${author.email}) - ${author.commits} commits\n`;
442
+ }
443
+ content += `\n`;
444
+ }
445
+
446
+ content += `---\n`;
447
+ content += `Generated by [AutoChangelog](https://autochangelog.com) on ${new Date().toISOString()}\n`;
448
+
449
+ return content;
450
+ }
451
+
452
+ // Generate HTML content
453
+ generateHTMLContent(repository, month, year, categorizedCommits, context, template) {
454
+ const monthName = this.getMonthName(month);
455
+ const dateRange = this.getDateRange(month, year);
456
+
457
+ let content = `<!DOCTYPE html>
458
+ <html>
459
+ <head>
460
+ <meta charset="utf-8">
461
+ <title>${repository.name} - ${monthName} ${year}</title>
462
+ <style>
463
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }
464
+ h1, h2, h3 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
465
+ h1 { font-size: 2.5em; }
466
+ h2 { font-size: 1.8em; margin-top: 30px; }
467
+ h3 { font-size: 1.4em; margin-top: 25px; }
468
+ .summary { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; }
469
+ .category { margin: 20px 0; }
470
+ .category h3 { display: inline-block; background: #3498db; color: white; padding: 5px 10px; border-radius: 3px; }
471
+ ul { padding-left: 20px; }
472
+ li { margin: 5px 0; }
473
+ .commit { font-family: monospace; font-size: 0.9em; color: #666; }
474
+ .author { font-weight: bold; color: #2c3e50; }
475
+ a { color: #3498db; text-decoration: none; }
476
+ a:hover { text-decoration: underline; }
477
+ .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; font-size: 0.9em; color: #666; }
478
+ </style>
479
+ </head>
480
+ <body>
481
+ <h1>${repository.name} - ${monthName} ${year}</h1>
482
+ <p><strong>Date Range:</strong> ${dateRange}</p>
483
+ <p><strong>Repository:</strong> <a href="${repository.githubRepoUrl}" target="_blank">${repository.githubRepoFullName}</a></p>
484
+
485
+ <div class="summary">
486
+ <h2>Summary</h2>
487
+ <ul>
488
+ <li><strong>Total Commits:</strong> ${categorizedCommits.totalCommits}</li>
489
+ <li><strong>Contributors:</strong> ${categorizedCommits.authors.length}</li>
490
+ <li><strong>Pull Requests:</strong> ${context.pullRequests.length}</li>
491
+ <li><strong>Issues Closed:</strong> ${context.issues.length}</li>
492
+ </ul>
493
+ </div>`;
494
+
495
+ // Releases
496
+ if (context.releases.length > 0) {
497
+ content += `<div class="category">
498
+ <h2>Releases</h2>
499
+ <ul>`;
500
+ for (const release of context.releases) {
501
+ content += `<li><strong><a href="${release.url}" target="_blank">${release.tagName}</a></strong> - ${release.name}`;
502
+ if (release.body) {
503
+ content += `<br><span class="commit">${release.body.split('\n')[0]}</span>`;
504
+ }
505
+ content += `</li>`;
506
+ }
507
+ content += `</ul></div>`;
508
+ }
509
+
510
+ // Changes by Category
511
+ content += `<div class="category">
512
+ <h2>Changes by Category</h2>`;
513
+
514
+ const categoryOrder = [
515
+ 'feat', 'fix', 'perf', 'refactor', 'test',
516
+ 'docs', 'style', 'ci', 'deps', 'chore', 'other'
517
+ ];
518
+
519
+ for (const category of categoryOrder) {
520
+ const commits = categorizedCommits.categories[category];
521
+ if (commits.length > 0) {
522
+ content += `<div class="category">
523
+ <h3>${this.getCategoryDisplayName(category)}</h3>
524
+ <ul>`;
525
+ for (const commit of commits) {
526
+ const message = this.formatCommitMessage(commit.message);
527
+ content += `<li><span class="commit">${message}</span> <span class="author">(${commit.author.name})</span></li>`;
528
+ }
529
+ content += `</ul></div>`;
530
+ }
531
+ }
532
+
533
+ content += `</div>`;
534
+
535
+ // Pull Requests
536
+ if (context.pullRequests.length > 0) {
537
+ content += `<div class="category">
538
+ <h2>Pull Requests</h2>
539
+ <ul>`;
540
+ for (const pr of context.pullRequests) {
541
+ content += `<li><strong><a href="${pr.url}" target="_blank">#${pr.number}</a></strong> ${pr.title} by ${pr.author}</li>`;
542
+ }
543
+ content += `</ul></div>`;
544
+ }
545
+
546
+ // Issues
547
+ if (context.issues.length > 0) {
548
+ content += `<div class="category">
549
+ <h2>Issues Closed</h2>
550
+ <ul>`;
551
+ for (const issue of context.issues) {
552
+ content += `<li><strong><a href="${issue.url}" target="_blank">#${issue.number}</a></strong> ${issue.title} by ${issue.author}</li>`;
553
+ }
554
+ content += `</ul></div>`;
555
+ }
556
+
557
+ // Contributors
558
+ if (categorizedCommits.authors.length > 0) {
559
+ content += `<div class="category">
560
+ <h2>Contributors</h2>
561
+ <ul>`;
562
+ for (const author of categorizedCommits.authors) {
563
+ content += `<li><span class="author">${author.name}</span> (${author.email}) - ${author.commits} commits</li>`;
564
+ }
565
+ content += `</ul></div>`;
566
+ }
567
+
568
+ content += `<div class="footer">
569
+ <p>Generated by <a href="https://autochangelog.com" target="_blank">AutoChangelog</a> on ${new Date().toISOString()}</p>
570
+ </div>
571
+ </body>
572
+ </html>`;
573
+
574
+ return content;
575
+ }
576
+
577
+ // Generate JSON content
578
+ generateJSONContent(repository, month, year, categorizedCommits, context) {
579
+ const data = {
580
+ repository: {
581
+ name: repository.name,
582
+ fullName: repository.githubRepoFullName,
583
+ url: repository.githubRepoUrl,
584
+ },
585
+ period: {
586
+ month: parseInt(month),
587
+ year: parseInt(year),
588
+ monthName: this.getMonthName(month),
589
+ },
590
+ summary: {
591
+ totalCommits: categorizedCommits.totalCommits,
592
+ contributors: categorizedCommits.authors.length,
593
+ pullRequests: context.pullRequests.length,
594
+ issuesClosed: context.issues.length,
595
+ },
596
+ categories: categorizedCommits.categories,
597
+ authors: categorizedCommits.authors,
598
+ pullRequests: context.pullRequests,
599
+ issues: context.issues,
600
+ releases: context.releases,
601
+ generatedAt: new Date().toISOString(),
602
+ generator: 'AutoChangelog',
603
+ };
604
+
605
+ return JSON.stringify(data, null, 2);
606
+ }
607
+
608
+ // Helper methods
609
+ getMonthName(month) {
610
+ const months = [
611
+ 'January', 'February', 'March', 'April', 'May', 'June',
612
+ 'July', 'August', 'September', 'October', 'November', 'December'
613
+ ];
614
+ return months[parseInt(month) - 1] || 'Unknown';
615
+ }
616
+
617
+ getDateRange(month, year) {
618
+ const startDate = new Date(year, parseInt(month) - 1, 1);
619
+ const endDate = new Date(year, parseInt(month), 0);
620
+ return `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
621
+ }
622
+
623
+ getCategoryDisplayName(category) {
624
+ const displayNames = {
625
+ 'feat': 'Features',
626
+ 'fix': 'Bug Fixes',
627
+ 'perf': 'Performance',
628
+ 'refactor': 'Refactoring',
629
+ 'test': 'Tests',
630
+ 'docs': 'Documentation',
631
+ 'style': 'Code Style',
632
+ 'ci': 'CI/CD',
633
+ 'deps': 'Dependencies',
634
+ 'chore': 'Maintenance',
635
+ 'other': 'Other Changes',
636
+ };
637
+ return displayNames[category] || category;
638
+ }
639
+
640
+ formatCommitMessage(message) {
641
+ // Remove multiple newlines and trim
642
+ return message.split('\n')[0].trim();
643
+ }
644
+
645
+ // Update repository metadata after changelog generation
646
+ async updateRepositoryMetadata(repository, month, year) {
647
+ try {
648
+ // Update last changelog generated date
649
+ await Repository.findByIdAndUpdate(repository._id, {
650
+ $set: {
651
+ 'metadata.lastChangelogGenerated': new Date(),
652
+ },
653
+ });
654
+ } catch (error) {
655
+ logger.error('Update repository metadata error:', error);
656
+ }
657
+ }
658
+
659
+ // Get available months for a repository
660
+ async getAvailableMonths(repositoryId) {
661
+ try {
662
+ const repository = await Repository.findById(repositoryId);
663
+ if (!repository) {
664
+ throw new Error('Repository not found');
665
+ }
666
+
667
+ // Get commit history
668
+ const commits = await this.getCommitHistory(repository);
669
+
670
+ // Get already generated changelogs
671
+ const generatedChangelogs = await Changelog.find({
672
+ repositoryId,
673
+ status: 'completed',
674
+ }).select('month year').lean();
675
+
676
+ const generatedMonths = new Set(
677
+ generatedChangelogs.map(c => `${c.year}-${c.month}`)
678
+ );
679
+
680
+ // Extract unique months from commits
681
+ const availableMonths = new Set();
682
+
683
+ commits.forEach(commit => {
684
+ const date = new Date(commit.commit.author.date);
685
+ const year = date.getFullYear();
686
+ const month = String(date.getMonth() + 1).padStart(2, '0');
687
+ const key = `${year}-${month}`;
688
+ availableMonths.add(key);
689
+ });
690
+
691
+ // Return months that have commits but no generated changelog
692
+ const result = Array.from(availableMonths)
693
+ .filter(month => !generatedMonths.has(month))
694
+ .sort()
695
+ .reverse();
696
+
697
+ return result;
698
+ } catch (error) {
699
+ logger.error('Get available months error:', error);
700
+ throw error;
701
+ }
702
+ }
703
+
704
+ // Get commit history for a repository
705
+ async getCommitHistory(repository) {
706
+ try {
707
+ const commits = await GitHubService.getCommits(repository.githubAccessToken,
708
+ repository.githubRepoOwner,
709
+ repository.githubRepoName, {
710
+ sha: repository.defaultBranch,
711
+ per_page: 1000,
712
+ });
713
+
714
+ return commits;
715
+ } catch (error) {
716
+ logger.error('Get commit history error:', error);
717
+ throw new Error('Failed to fetch commit history');
718
+ }
719
+ }
720
+ }
721
+
722
+ module.exports = new ChangelogService();