@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 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
@@ -31,5 +31,5 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "type": "module",
34
- "version": "2.0.1"
34
+ "version": "2.1.0"
35
35
  }
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.isArchived && !r.isFork);
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 ─────────────────────────────────────────────────────
@@ -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)</sub>`;
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(`![${svg.label}](${svg.path})`);
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(`![${svg.label}](${svg.path})`);
217
227
  }
218
228
 
219
- parts.push(attribution());
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
- // Render project tables grouped by category
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) && projects.length > 0) {
257
- parts.push(renderProjectTable(category, projects));
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[];