@rmdes/indiekit-endpoint-github 1.0.4 → 1.0.5

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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import express from "express";
2
2
 
3
3
  import { activityController } from "./lib/controllers/activity.js";
4
+ import { changelogController } from "./lib/controllers/changelog.js";
4
5
  import { commitsController } from "./lib/controllers/commits.js";
5
6
  import { contributionsController } from "./lib/controllers/contributions.js";
6
7
  import { dashboardController } from "./lib/controllers/dashboard.js";
@@ -85,6 +86,7 @@ export default class GitHubEndpoint {
85
86
  publicRouter.get("/api/contributions", contributionsController.api);
86
87
  publicRouter.get("/api/activity", activityController.api);
87
88
  publicRouter.get("/api/featured", featuredController.api);
89
+ publicRouter.get("/api/changelog", changelogController.api);
88
90
 
89
91
  return publicRouter;
90
92
  }
@@ -0,0 +1,166 @@
1
+ import { GitHubClient } from "../github-client.js";
2
+
3
+ /**
4
+ * Categorize a repo by its name prefix
5
+ * @param {string} name - Repository name
6
+ * @returns {string} - Category key
7
+ */
8
+ function categorizeRepo(name) {
9
+ if (name === "indiekit") return "core";
10
+ if (name === "indiekit-cloudron" || name === "indiekit-deploy")
11
+ return "deployment";
12
+ if (name.includes("theme")) return "theme";
13
+ if (name.startsWith("indiekit-endpoint-")) return "endpoints";
14
+ if (name.startsWith("indiekit-syndicator-")) return "syndicators";
15
+ if (name.startsWith("indiekit-post-type-")) return "post-types";
16
+ if (name.startsWith("indiekit-preset-")) return "presets";
17
+ return "other";
18
+ }
19
+
20
+ const CATEGORY_LABELS = {
21
+ core: "Core",
22
+ deployment: "Deployment",
23
+ theme: "Theme",
24
+ endpoints: "Endpoints",
25
+ syndicators: "Syndicators",
26
+ "post-types": "Post Types",
27
+ presets: "Presets",
28
+ other: "Other",
29
+ };
30
+
31
+ /**
32
+ * Split an array into chunks of a given size
33
+ * @param {Array} array
34
+ * @param {number} size
35
+ * @returns {Array<Array>}
36
+ */
37
+ function chunks(array, size) {
38
+ const result = [];
39
+ for (let i = 0; i < array.length; i += size) {
40
+ result.push(array.slice(i, i + size));
41
+ }
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Changelog API controller
47
+ * @type {import("express").RequestHandler}
48
+ */
49
+ export const changelogController = {
50
+ async api(request, response, next) {
51
+ try {
52
+ const { username, token, cacheTtl } =
53
+ request.app.locals.application.githubConfig;
54
+
55
+ if (!username) {
56
+ return response.status(400).json({ error: "No username configured" });
57
+ }
58
+
59
+ const client = new GitHubClient({ token, cacheTtl });
60
+
61
+ // Parse days param — default 30, "all" for unlimited
62
+ const daysParam = request.query.days || "30";
63
+ let sinceDate = null;
64
+ let daysValue = daysParam;
65
+
66
+ if (daysParam !== "all") {
67
+ const days = Number.parseInt(daysParam, 10);
68
+ if (Number.isNaN(days) || days < 1) {
69
+ return response
70
+ .status(400)
71
+ .json({ error: "Invalid days parameter" });
72
+ }
73
+ daysValue = days;
74
+ const since = new Date();
75
+ since.setDate(since.getDate() - days);
76
+ sinceDate = since.toISOString();
77
+ }
78
+
79
+ // Fetch all user repos (100 to cover all indiekit repos)
80
+ const repos = await client.getUserRepos(username, 100, "pushed");
81
+
82
+ if (!Array.isArray(repos)) {
83
+ return response.json({
84
+ commits: [],
85
+ categories: {},
86
+ totalCommits: 0,
87
+ days: daysValue,
88
+ generatedAt: new Date().toISOString(),
89
+ });
90
+ }
91
+
92
+ // Filter to indiekit repos only
93
+ const indiekitRepos = repos.filter((repo) =>
94
+ repo.name.includes("indiekit"),
95
+ );
96
+
97
+ // Build categories map from discovered repos
98
+ const categories = {};
99
+ for (const repo of indiekitRepos) {
100
+ const cat = categorizeRepo(repo.name);
101
+ if (!categories[cat]) {
102
+ categories[cat] = {
103
+ label: CATEGORY_LABELS[cat] || cat,
104
+ repos: [],
105
+ };
106
+ }
107
+ categories[cat].repos.push(repo.name);
108
+ }
109
+
110
+ // Fetch commits in batches of 5 to avoid secondary rate limits
111
+ const allCommits = [];
112
+ for (const batch of chunks(indiekitRepos, 5)) {
113
+ const results = await Promise.all(
114
+ batch.map(async (repo) => {
115
+ try {
116
+ const commits = await client.getRepoCommits(
117
+ repo.owner.login,
118
+ repo.name,
119
+ 20,
120
+ sinceDate,
121
+ );
122
+
123
+ if (!Array.isArray(commits)) return [];
124
+
125
+ // Filter out merge commits and map to response shape
126
+ return commits
127
+ .filter((c) => (c.parents?.length || 0) <= 1)
128
+ .map((c) => ({
129
+ sha: c.sha.slice(0, 7),
130
+ fullSha: c.sha,
131
+ title: c.commit.message.split("\n")[0],
132
+ body: c.commit.message.split("\n").slice(1).join("\n").trim(),
133
+ url: c.html_url,
134
+ date:
135
+ c.commit.author?.date || c.commit.committer?.date || null,
136
+ author: c.commit.author?.name || "Unknown",
137
+ repo: repo.full_name,
138
+ repoName: repo.name,
139
+ repoUrl: repo.html_url,
140
+ category: categorizeRepo(repo.name),
141
+ }));
142
+ } catch {
143
+ return [];
144
+ }
145
+ }),
146
+ );
147
+ allCommits.push(...results.flat());
148
+ }
149
+
150
+ // Sort by date descending
151
+ allCommits.sort(
152
+ (a, b) => new Date(b.date || 0) - new Date(a.date || 0),
153
+ );
154
+
155
+ response.json({
156
+ commits: allCommits,
157
+ categories,
158
+ totalCommits: allCommits.length,
159
+ days: daysValue,
160
+ generatedAt: new Date().toISOString(),
161
+ });
162
+ } catch (error) {
163
+ next(error);
164
+ }
165
+ },
166
+ };
@@ -71,10 +71,15 @@ export class GitHubClient {
71
71
  * @param {string} owner - Repository owner
72
72
  * @param {string} repo - Repository name
73
73
  * @param {number} [limit] - Number of commits to fetch
74
+ * @param {string} [since] - ISO 8601 date string to filter commits after
74
75
  * @returns {Promise<Array>} - Repository commits
75
76
  */
76
- async getRepoCommits(owner, repo, limit = 10) {
77
- return this.fetch(`/repos/${owner}/${repo}/commits?per_page=${limit}`);
77
+ async getRepoCommits(owner, repo, limit = 10, since = null) {
78
+ let endpoint = `/repos/${owner}/${repo}/commits?per_page=${limit}`;
79
+ if (since) {
80
+ endpoint += `&since=${since}`;
81
+ }
82
+ return this.fetch(endpoint);
78
83
  }
79
84
 
80
85
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-github",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "GitHub activity endpoint for Indiekit. Display commits, stars, contributions, and featured repositories.",
5
5
  "keywords": [
6
6
  "indiekit",