github-portfolio-analyzer 1.4.1 → 1.4.2

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
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.4.2] - 2026-04-03
8
+
9
+ ### Fixed
10
+ - `normalizeRepository` now explicitly coerces `private` to boolean, preventing `undefined` from propagating through the pipeline and causing private repos to be treated as public.
11
+ - `buildReportModel` always includes `private` in output (was omitted when falsy), ensuring the worker always receives an explicit value.
12
+
13
+ ### Added
14
+ - Language breakdown via GitHub `/languages` API endpoint. Each repo now has a `languages` field (`Record<string, number>`) with byte counts per language, enabling multi-language badge display in the portfolio frontend.
15
+
7
16
  ## [1.4.1] - 2026-04-03
8
17
 
9
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-portfolio-analyzer",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "commands": [
5
5
  { "id": "analyze" },
6
6
  { "id": "ingest-ideas" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-portfolio-analyzer",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "CLI tool to analyze GitHub repos and portfolio ideas",
5
5
  "type": "module",
6
6
  "bin": {
@@ -203,7 +203,8 @@
203
203
  "priorityBand",
204
204
  "priorityOverrides",
205
205
  "priorityWhy",
206
- "nextAction"
206
+ "nextAction",
207
+ "private"
207
208
  ],
208
209
  "properties": {
209
210
  "slug": { "type": "string" },
@@ -243,6 +244,10 @@
243
244
  },
244
245
  "nextAction": { "type": "string" },
245
246
  "language": { "type": "string" },
247
+ "languages": {
248
+ "type": "object",
249
+ "additionalProperties": { "type": "number" }
250
+ },
246
251
  "topics": {
247
252
  "type": "array",
248
253
  "items": { "type": "string" }
@@ -205,13 +205,16 @@ export function buildReportModel(portfolioData, inventoryData = null, options =
205
205
  nextAction: String(item.nextAction ?? '').trim(),
206
206
  // presentation fields — passed directly from portfolio item
207
207
  ...(item.language != null ? { language: item.language } : {}),
208
+ ...(typeof item.languages === 'object' && item.languages !== null && !Array.isArray(item.languages) && Object.keys(item.languages).length > 0
209
+ ? { languages: item.languages }
210
+ : {}),
208
211
  ...(Array.isArray(item.topics) && item.topics.length > 0 ? { topics: item.topics } : {}),
209
212
  ...(!isPrivate && item.htmlUrl != null ? { htmlUrl: item.htmlUrl } : {}),
210
213
  ...(!isPrivate && item.homepage != null ? { homepage: item.homepage } : {}),
211
214
  ...(item.category != null ? { category: item.category } : {}),
212
215
  ...(item.fork != null ? { fork: Boolean(item.fork) } : {}),
213
216
  ...(item.forkType != null ? { forkType: item.forkType } : {}),
214
- ...(item.private != null ? { private: Boolean(item.private) } : {}),
217
+ private: Boolean(item.private ?? false),
215
218
  ...(item.publicAlias != null ? { publicAlias: item.publicAlias } : {}),
216
219
  ...(!isPrivate && item.description != null ? { description: item.description } : {}),
217
220
  ...(isPrivate && item.description != null ? { _description: item.description } : {})
@@ -73,6 +73,27 @@ export async function fetchAllRepositories(client, asOfDate = new Date().toISOSt
73
73
  );
74
74
  }
75
75
 
76
+ for (let index = 0; index < repositories.length; index += 5) {
77
+ const batch = repositories.slice(index, index + 5);
78
+ await Promise.all(
79
+ batch.map(async (repository) => {
80
+ const ownerLogin = repository.owner?.login ?? repository.ownerLogin ?? '';
81
+ if (!ownerLogin || !repository.name) {
82
+ repository.languages = {};
83
+ return;
84
+ }
85
+
86
+ try {
87
+ repository.languages = await client.request(
88
+ `/repos/${encodeURIComponent(ownerLogin)}/${encodeURIComponent(repository.name)}/languages`
89
+ );
90
+ } catch {
91
+ repository.languages = {};
92
+ }
93
+ })
94
+ );
95
+ }
96
+
76
97
  return repositories;
77
98
  }
78
99
 
@@ -81,9 +102,9 @@ export function normalizeRepository(repo) {
81
102
  id: repo.id,
82
103
  nodeId: repo.node_id,
83
104
  name: repo.name,
84
- ownerLogin: repo.owner?.login ?? '',
105
+ ownerLogin: repo.owner?.login ?? repo.ownerLogin ?? '',
85
106
  fullName: repo.full_name,
86
- private: repo.private,
107
+ private: Boolean(repo.private),
87
108
  archived: repo.archived,
88
109
  fork: repo.fork,
89
110
  forkType: repo.forkType ?? null,
@@ -91,6 +112,10 @@ export function normalizeRepository(repo) {
91
112
  htmlUrl: repo.html_url,
92
113
  description: repo.description,
93
114
  language: repo.language,
115
+ languages:
116
+ typeof repo.languages === 'object' && repo.languages !== null && !Array.isArray(repo.languages)
117
+ ? repo.languages
118
+ : {},
94
119
  homepage: typeof repo.homepage === 'string' && repo.homepage.trim().length > 0
95
120
  ? repo.homepage.trim()
96
121
  : null,