@urmzd/github-insights 2.0.1 → 2.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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/api.ts +1 -1
- package/src/index.ts +6 -0
- package/src/metrics.ts +16 -2
- package/src/templates.test.ts +97 -0
- package/src/templates.ts +38 -11
- package/src/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 (2026-03-18)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- separate archived/legacy projects into dedicated sections across all templates ([6327c64](https://github.com/urmzd/github-insights/commit/6327c64e568b5f25affc7a2f2a90a01c49c37ee6))
|
|
8
|
+
|
|
9
|
+
[Full Changelog](https://github.com/urmzd/github-insights/compare/v2.0.1...v2.1.0)
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
## 2.0.1 (2026-03-17)
|
|
4
13
|
|
|
5
14
|
### Bug Fixes
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -86,7 +86,7 @@ export const fetchAllRepoData = async (
|
|
|
86
86
|
{ username },
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
-
return data.user.repositories.nodes.filter((r) => !r.
|
|
89
|
+
return data.user.repositories.nodes.filter((r) => !r.isFork);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
export const fetchManifestsForRepos = async (
|
package/src/index.ts
CHANGED
|
@@ -142,6 +142,7 @@ async function run(): Promise<void> {
|
|
|
142
142
|
active: activeProjects,
|
|
143
143
|
maintained: maintainedProjects,
|
|
144
144
|
inactive: inactiveProjects,
|
|
145
|
+
archived: archivedProjects,
|
|
145
146
|
} = splitProjectsByRecency(repos, contributionData, aiClassifications);
|
|
146
147
|
|
|
147
148
|
const sectionDefs = buildSections({
|
|
@@ -227,6 +228,7 @@ async function run(): Promise<void> {
|
|
|
227
228
|
...activeProjects,
|
|
228
229
|
...maintainedProjects,
|
|
229
230
|
...inactiveProjects,
|
|
231
|
+
...archivedProjects,
|
|
230
232
|
];
|
|
231
233
|
const categorizedProjects: Record<string, typeof allProjectItems> = {};
|
|
232
234
|
for (const project of allProjectItems) {
|
|
@@ -245,12 +247,14 @@ async function run(): Promise<void> {
|
|
|
245
247
|
title: userConfig.title,
|
|
246
248
|
bio: userConfig.bio,
|
|
247
249
|
preamble,
|
|
250
|
+
templateName,
|
|
248
251
|
svgs,
|
|
249
252
|
sectionSvgs,
|
|
250
253
|
profile: userProfile,
|
|
251
254
|
activeProjects,
|
|
252
255
|
maintainedProjects,
|
|
253
256
|
inactiveProjects,
|
|
257
|
+
archivedProjects,
|
|
254
258
|
allProjects: complexProjects,
|
|
255
259
|
categorizedProjects,
|
|
256
260
|
languages,
|
|
@@ -308,12 +312,14 @@ async function run(): Promise<void> {
|
|
|
308
312
|
title: userConfig.title,
|
|
309
313
|
bio: userConfig.bio,
|
|
310
314
|
preamble,
|
|
315
|
+
templateName: tplName,
|
|
311
316
|
svgs: previewSvgs,
|
|
312
317
|
sectionSvgs: previewSectionSvgs,
|
|
313
318
|
profile: userProfile,
|
|
314
319
|
activeProjects,
|
|
315
320
|
maintainedProjects,
|
|
316
321
|
inactiveProjects,
|
|
322
|
+
archivedProjects,
|
|
317
323
|
allProjects: complexProjects,
|
|
318
324
|
categorizedProjects,
|
|
319
325
|
languages,
|
package/src/metrics.ts
CHANGED
|
@@ -117,6 +117,7 @@ const toProjectItem = (repo: RepoNode): ProjectItem => ({
|
|
|
117
117
|
languageCount: repo.languages.edges.length,
|
|
118
118
|
codeSize: repo.diskUsage,
|
|
119
119
|
languages: repoLanguages(repo),
|
|
120
|
+
isArchived: repo.isArchived || undefined,
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
// ── Top Projects by Stars ───────────────────────────────────────────────────
|
|
@@ -190,6 +191,7 @@ export const splitProjectsByRecency = (
|
|
|
190
191
|
active: ProjectItem[];
|
|
191
192
|
maintained: ProjectItem[];
|
|
192
193
|
inactive: ProjectItem[];
|
|
194
|
+
archived: ProjectItem[];
|
|
193
195
|
} => {
|
|
194
196
|
const commitMap = new Map<string, number>();
|
|
195
197
|
for (const entry of contributionData.commitContributionsByRepository || []) {
|
|
@@ -206,8 +208,17 @@ export const splitProjectsByRecency = (
|
|
|
206
208
|
const activeRepos: RepoNode[] = [];
|
|
207
209
|
const maintainedRepos: RepoNode[] = [];
|
|
208
210
|
const inactiveRepos: RepoNode[] = [];
|
|
211
|
+
const archivedRepos: RepoNode[] = [];
|
|
209
212
|
|
|
210
213
|
for (const repo of repos) {
|
|
214
|
+
if (repo.isArchived) {
|
|
215
|
+
archivedRepos.push(repo);
|
|
216
|
+
console.info(
|
|
217
|
+
`[archived ] ${repo.name} (complexity=${complexityScore(repo).toFixed(1)})`,
|
|
218
|
+
);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
211
222
|
const commits = commitMap.get(repo.name) || 0;
|
|
212
223
|
const aiEntry = aiMap.get(repo.name);
|
|
213
224
|
const status = aiEntry?.status || heuristicStatus(commits, repo.createdAt);
|
|
@@ -227,7 +238,7 @@ export const splitProjectsByRecency = (
|
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
console.info(
|
|
230
|
-
`Split: ${activeRepos.length} active, ${maintainedRepos.length} maintained, ${inactiveRepos.length} inactive`,
|
|
241
|
+
`Split: ${activeRepos.length} active, ${maintainedRepos.length} maintained, ${inactiveRepos.length} inactive, ${archivedRepos.length} archived`,
|
|
231
242
|
);
|
|
232
243
|
|
|
233
244
|
const sortByComplexity = (a: RepoNode, b: RepoNode) =>
|
|
@@ -248,8 +259,11 @@ export const splitProjectsByRecency = (
|
|
|
248
259
|
const inactive: ProjectItem[] = inactiveRepos
|
|
249
260
|
.sort(sortByComplexity)
|
|
250
261
|
.map(toProjectItemWithSummary);
|
|
262
|
+
const archived: ProjectItem[] = archivedRepos
|
|
263
|
+
.sort(sortByComplexity)
|
|
264
|
+
.map(toProjectItemWithSummary);
|
|
251
265
|
|
|
252
|
-
return { active, maintained, inactive };
|
|
266
|
+
return { active, maintained, inactive, archived };
|
|
253
267
|
};
|
|
254
268
|
|
|
255
269
|
// ── Section definitions ─────────────────────────────────────────────────────
|
package/src/templates.test.ts
CHANGED
|
@@ -44,7 +44,9 @@ const makeContext = (
|
|
|
44
44
|
},
|
|
45
45
|
],
|
|
46
46
|
inactiveProjects: [],
|
|
47
|
+
archivedProjects: [],
|
|
47
48
|
allProjects: [],
|
|
49
|
+
templateName: "classic",
|
|
48
50
|
categorizedProjects: {
|
|
49
51
|
Applications: [
|
|
50
52
|
{
|
|
@@ -241,6 +243,28 @@ describe("classicTemplate", () => {
|
|
|
241
243
|
expect(output).toContain("/ˈʊrm.zəd/");
|
|
242
244
|
});
|
|
243
245
|
|
|
246
|
+
it("includes archived section for archived projects", () => {
|
|
247
|
+
const output = getTemplate("classic")(
|
|
248
|
+
makeContext({
|
|
249
|
+
archivedProjects: [
|
|
250
|
+
{
|
|
251
|
+
name: "old-project",
|
|
252
|
+
url: "https://github.com/urmzd/old-project",
|
|
253
|
+
description: "An archived project",
|
|
254
|
+
stars: 2,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
expect(output).toContain("## Archived");
|
|
260
|
+
expect(output).toContain("[old-project]");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("omits archived section when no archived projects", () => {
|
|
264
|
+
const output = getTemplate("classic")(makeContext());
|
|
265
|
+
expect(output).not.toContain("## Archived");
|
|
266
|
+
});
|
|
267
|
+
|
|
244
268
|
it("ends with trailing newline", () => {
|
|
245
269
|
const output = getTemplate("classic")(makeContext());
|
|
246
270
|
expect(output.endsWith("\n")).toBe(true);
|
|
@@ -307,6 +331,23 @@ describe("modernTemplate", () => {
|
|
|
307
331
|
expect(output).toContain("assets/insights/metrics-expertise.svg");
|
|
308
332
|
});
|
|
309
333
|
|
|
334
|
+
it("includes archived section separate from active/maintained", () => {
|
|
335
|
+
const output = getTemplate("modern")(
|
|
336
|
+
makeContext({
|
|
337
|
+
archivedProjects: [
|
|
338
|
+
{
|
|
339
|
+
name: "legacy-lib",
|
|
340
|
+
url: "https://github.com/urmzd/legacy-lib",
|
|
341
|
+
description: "A legacy library",
|
|
342
|
+
stars: 1,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
expect(output).toContain("## Archived");
|
|
348
|
+
expect(output).toContain("[legacy-lib]");
|
|
349
|
+
});
|
|
350
|
+
|
|
310
351
|
it("includes social badges", () => {
|
|
311
352
|
const output = getTemplate("modern")(makeContext());
|
|
312
353
|
expect(output).toContain("img.shields.io");
|
|
@@ -346,6 +387,23 @@ describe("minimalTemplate", () => {
|
|
|
346
387
|
expect(output).toContain("@urmzd/github-insights");
|
|
347
388
|
});
|
|
348
389
|
|
|
390
|
+
it("includes archived section for archived projects", () => {
|
|
391
|
+
const output = getTemplate("minimal")(
|
|
392
|
+
makeContext({
|
|
393
|
+
archivedProjects: [
|
|
394
|
+
{
|
|
395
|
+
name: "old-util",
|
|
396
|
+
url: "https://github.com/urmzd/old-util",
|
|
397
|
+
description: "A retired utility",
|
|
398
|
+
stars: 0,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
expect(output).toContain("## Archived");
|
|
404
|
+
expect(output).toContain("[old-util]");
|
|
405
|
+
});
|
|
406
|
+
|
|
349
407
|
it("ends with trailing newline", () => {
|
|
350
408
|
const output = getTemplate("minimal")(makeContext());
|
|
351
409
|
expect(output.endsWith("\n")).toBe(true);
|
|
@@ -405,6 +463,45 @@ describe("ecosystemTemplate", () => {
|
|
|
405
463
|
expect(output).toContain("@urmzd/github-insights");
|
|
406
464
|
});
|
|
407
465
|
|
|
466
|
+
it("separates archived projects from category tables", () => {
|
|
467
|
+
const output = getTemplate("ecosystem")(
|
|
468
|
+
makeContext({
|
|
469
|
+
archivedProjects: [
|
|
470
|
+
{
|
|
471
|
+
name: "old-app",
|
|
472
|
+
url: "https://github.com/urmzd/old-app",
|
|
473
|
+
description: "A retired application",
|
|
474
|
+
stars: 3,
|
|
475
|
+
category: "Applications",
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
categorizedProjects: {
|
|
479
|
+
Applications: [
|
|
480
|
+
{
|
|
481
|
+
name: "resume-generator",
|
|
482
|
+
url: "https://github.com/urmzd/resume-generator",
|
|
483
|
+
description: "CLI tool for professional resumes",
|
|
484
|
+
stars: 42,
|
|
485
|
+
category: "Applications",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "old-app",
|
|
489
|
+
url: "https://github.com/urmzd/old-app",
|
|
490
|
+
description: "A retired application",
|
|
491
|
+
stars: 3,
|
|
492
|
+
category: "Applications",
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
expect(output).toContain("### Archived");
|
|
499
|
+
expect(output).toContain("[old-app]");
|
|
500
|
+
// old-app should NOT appear in the Applications table
|
|
501
|
+
const appSection = output.split("### Applications")[1].split("###")[0];
|
|
502
|
+
expect(appSection).not.toContain("old-app");
|
|
503
|
+
});
|
|
504
|
+
|
|
408
505
|
it("ends with trailing newline", () => {
|
|
409
506
|
const output = getTemplate("ecosystem")(makeContext());
|
|
410
507
|
expect(output.endsWith("\n")).toBe(true);
|
package/src/templates.ts
CHANGED
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
10
10
|
|
|
11
|
-
function attribution(): string {
|
|
11
|
+
function attribution(templateName: string): string {
|
|
12
12
|
const now = new Date().toISOString().split("T")[0];
|
|
13
|
-
return `<sub>Last generated on ${now} using [@urmzd/github-insights](https://github.com/urmzd/github-insights)
|
|
13
|
+
return `<sub>Last generated on ${now} using [@urmzd/github-insights](https://github.com/urmzd/github-insights) · Template: \`${templateName}\`</sub>`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function extractFirstName(fullName: string): string {
|
|
@@ -131,11 +131,15 @@ function classicTemplate(ctx: TemplateContext): string {
|
|
|
131
131
|
parts.push(``);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
if (ctx.archivedProjects.length > 0) {
|
|
135
|
+
parts.push(renderProjectSection("Archived", ctx.archivedProjects));
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
if (ctx.bio) {
|
|
135
139
|
parts.push(`---\n\n<sub>${ctx.bio}</sub>`);
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
parts.push(attribution());
|
|
142
|
+
parts.push(attribution(ctx.templateName));
|
|
139
143
|
|
|
140
144
|
return `${parts.join("\n\n")}\n`;
|
|
141
145
|
}
|
|
@@ -173,6 +177,12 @@ function modernTemplate(ctx: TemplateContext): string {
|
|
|
173
177
|
);
|
|
174
178
|
if (inactiveSection) parts.push(inactiveSection);
|
|
175
179
|
|
|
180
|
+
const archivedSection = renderProjectSection(
|
|
181
|
+
"Archived",
|
|
182
|
+
ctx.archivedProjects,
|
|
183
|
+
);
|
|
184
|
+
if (archivedSection) parts.push(archivedSection);
|
|
185
|
+
|
|
176
186
|
// GitHub Stats section: pulse + calendar
|
|
177
187
|
const statsImages: string[] = [];
|
|
178
188
|
if (ctx.sectionSvgs.pulse) {
|
|
@@ -192,7 +202,7 @@ function modernTemplate(ctx: TemplateContext): string {
|
|
|
192
202
|
);
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
parts.push(attribution());
|
|
205
|
+
parts.push(attribution(ctx.templateName));
|
|
196
206
|
|
|
197
207
|
return `${parts.join("\n\n")}\n`;
|
|
198
208
|
}
|
|
@@ -216,7 +226,11 @@ function minimalTemplate(ctx: TemplateContext): string {
|
|
|
216
226
|
parts.push(``);
|
|
217
227
|
}
|
|
218
228
|
|
|
219
|
-
|
|
229
|
+
if (ctx.archivedProjects.length > 0) {
|
|
230
|
+
parts.push(renderProjectSection("Archived", ctx.archivedProjects));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
parts.push(attribution(ctx.templateName));
|
|
220
234
|
|
|
221
235
|
return `${parts.join("\n\n")}\n`;
|
|
222
236
|
}
|
|
@@ -243,21 +257,34 @@ function ecosystemTemplate(ctx: TemplateContext): string {
|
|
|
243
257
|
parts.push(ctx.socialBadges);
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
//
|
|
260
|
+
// Build a set of archived project names to filter them out of category tables
|
|
261
|
+
const archivedNames = new Set(ctx.archivedProjects.map((p) => p.name));
|
|
262
|
+
|
|
263
|
+
// Render project tables grouped by category (excluding archived)
|
|
247
264
|
for (const category of CATEGORY_ORDER) {
|
|
248
|
-
const projects = ctx.categorizedProjects[category]
|
|
265
|
+
const projects = ctx.categorizedProjects[category]?.filter(
|
|
266
|
+
(p) => !archivedNames.has(p.name),
|
|
267
|
+
);
|
|
249
268
|
if (projects && projects.length > 0) {
|
|
250
269
|
parts.push(renderProjectTable(category, projects));
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
// Render any uncategorized projects that don't match known categories
|
|
273
|
+
// Render any uncategorized projects that don't match known categories (excluding archived)
|
|
255
274
|
for (const [category, projects] of Object.entries(ctx.categorizedProjects)) {
|
|
256
|
-
if (!CATEGORY_ORDER.includes(category)
|
|
257
|
-
|
|
275
|
+
if (!CATEGORY_ORDER.includes(category)) {
|
|
276
|
+
const nonArchived = projects.filter((p) => !archivedNames.has(p.name));
|
|
277
|
+
if (nonArchived.length > 0) {
|
|
278
|
+
parts.push(renderProjectTable(category, nonArchived));
|
|
279
|
+
}
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
|
|
283
|
+
// Render all archived projects in one consolidated section
|
|
284
|
+
if (ctx.archivedProjects.length > 0) {
|
|
285
|
+
parts.push(renderProjectTable("Archived", ctx.archivedProjects));
|
|
286
|
+
}
|
|
287
|
+
|
|
261
288
|
// GitHub Stats section: pulse + calendar
|
|
262
289
|
const statsImages: string[] = [];
|
|
263
290
|
if (ctx.sectionSvgs.pulse) {
|
|
@@ -277,7 +304,7 @@ function ecosystemTemplate(ctx: TemplateContext): string {
|
|
|
277
304
|
);
|
|
278
305
|
}
|
|
279
306
|
|
|
280
|
-
parts.push(attribution());
|
|
307
|
+
parts.push(attribution(ctx.templateName));
|
|
281
308
|
|
|
282
309
|
return `${parts.join("\n\n")}\n`;
|
|
283
310
|
}
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface ProjectItem {
|
|
|
29
29
|
languages?: string[];
|
|
30
30
|
summary?: string;
|
|
31
31
|
category?: string;
|
|
32
|
+
isArchived?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// ── Bar chart generics ──────────────────────────────────────────────────────
|
|
@@ -217,12 +218,14 @@ export interface TemplateContext {
|
|
|
217
218
|
title?: string;
|
|
218
219
|
bio?: string;
|
|
219
220
|
preamble?: string;
|
|
221
|
+
templateName: TemplateName;
|
|
220
222
|
svgs: SvgEmbed[];
|
|
221
223
|
sectionSvgs: Record<string, string>;
|
|
222
224
|
profile: UserProfile;
|
|
223
225
|
activeProjects: ProjectItem[];
|
|
224
226
|
maintainedProjects: ProjectItem[];
|
|
225
227
|
inactiveProjects: ProjectItem[];
|
|
228
|
+
archivedProjects: ProjectItem[];
|
|
226
229
|
allProjects: ProjectItem[];
|
|
227
230
|
categorizedProjects: Record<string, ProjectItem[]>;
|
|
228
231
|
languages: LanguageItem[];
|