@felix-ayush/openhearth 2.1.0 → 2.2.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/README.md +31 -16
- package/dist/cli.js +180 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,9 +37,12 @@ openhearth audit USERNAME --month 2026-07
|
|
|
37
37
|
# Full audit — PRs + issues + reviews
|
|
38
38
|
openhearth audit Ayush7614 --month 2026-07
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Ranked report: repos likely hidden from your activity sidebar
|
|
41
41
|
openhearth hidden Ayush7614 --month 2026-07
|
|
42
42
|
|
|
43
|
+
# Check auth + Search API quota
|
|
44
|
+
openhearth doctor
|
|
45
|
+
|
|
43
46
|
# Export for spreadsheets or dashboards
|
|
44
47
|
openhearth audit Ayush7614 --month 2026-07 --json report.json
|
|
45
48
|
openhearth audit Ayush7614 --month 2026-07 --csv report.csv
|
|
@@ -50,7 +53,8 @@ openhearth audit Ayush7614 --month 2026-07 --csv report.csv
|
|
|
50
53
|
| Command | Description |
|
|
51
54
|
|---------|-------------|
|
|
52
55
|
| `openhearth audit <user>` | Full PR + issue + review audit (default) |
|
|
53
|
-
| `openhearth hidden <user>` |
|
|
56
|
+
| `openhearth hidden <user>` | Ranked likely-hidden repos vs activity sidebar |
|
|
57
|
+
| `openhearth doctor` | Auth + GitHub rate-limit status |
|
|
54
58
|
| `--month YYYY-MM` | Audit a calendar month |
|
|
55
59
|
| `--from YYYY-MM-DD` | Custom range start |
|
|
56
60
|
| `--to YYYY-MM-DD` | Custom range end |
|
|
@@ -59,19 +63,21 @@ openhearth audit Ayush7614 --month 2026-07 --csv report.csv
|
|
|
59
63
|
| `--json [file]` | Export JSON (`stdout` if no file) |
|
|
60
64
|
| `--csv [file]` | Export CSV |
|
|
61
65
|
| `--quiet` | Minimal terminal output |
|
|
66
|
+
| `-V, --version` | Print CLI version |
|
|
62
67
|
|
|
63
68
|
## What makes OpenHearth different
|
|
64
69
|
|
|
65
|
-
- **
|
|
70
|
+
- **Ranked hidden repos** — lower-activity repos past the ~25 sidebar cap, least activity first
|
|
71
|
+
- **Clear rate-limit errors** — tells you to set `GITHUB_TOKEN` (or when quota resets)
|
|
72
|
+
- **Doctor** — `openhearth doctor` checks auth and Search/Core API limits
|
|
73
|
+
- **GitHub Action** — monthly/on-demand audits with JSON/CSV artifacts
|
|
66
74
|
- **Full Search API pagination** — fetches all results, not just the first page
|
|
67
|
-
- **Auto date-splitting** — when a month exceeds GitHub’s 1000-result search cap
|
|
68
|
-
- **One-command full audit** — PRs, issues, and reviews together
|
|
69
|
-
- **Insights** — merge rate, busiest day, top repositories
|
|
75
|
+
- **Auto date-splitting** — when a month exceeds GitHub’s 1000-result search cap
|
|
70
76
|
- **Export** — JSON and CSV for your own tooling
|
|
71
77
|
|
|
72
78
|
## Authentication (recommended)
|
|
73
79
|
|
|
74
|
-
Without a token, the unauthenticated Search API is limited (~
|
|
80
|
+
Without a token, the unauthenticated Search API is limited (~60 requests/hour).
|
|
75
81
|
|
|
76
82
|
```bash
|
|
77
83
|
export GITHUB_TOKEN=ghp_your_token_here
|
|
@@ -80,6 +86,16 @@ openhearth audit USERNAME --month 2026-07
|
|
|
80
86
|
|
|
81
87
|
A classic PAT with **public read** access is enough. The token is sent only to `api.github.com`.
|
|
82
88
|
|
|
89
|
+
If you hit a limit, the CLI prints a fix hint — or run `openhearth doctor`.
|
|
90
|
+
|
|
91
|
+
## GitHub Action
|
|
92
|
+
|
|
93
|
+
This repository includes `.github/workflows/audit.yml`:
|
|
94
|
+
|
|
95
|
+
- **workflow_dispatch** — pick username + month
|
|
96
|
+
- **schedule** — 1st of each month (previous calendar month)
|
|
97
|
+
- Uploads JSON/CSV artifacts from `audit` and `hidden`
|
|
98
|
+
|
|
83
99
|
## Example output
|
|
84
100
|
|
|
85
101
|
```
|
|
@@ -87,18 +103,17 @@ A classic PAT with **public read** access is enough. The token is sent only to `
|
|
|
87
103
|
|
|
88
104
|
Summary · @Ayush7614 · 2026-07
|
|
89
105
|
|
|
90
|
-
Total contributions
|
|
91
|
-
Unique repositories
|
|
92
|
-
PR merge rate
|
|
93
|
-
By kind PRs 394 · Issues 12 · Reviews 6
|
|
106
|
+
Total contributions 494
|
|
107
|
+
Unique repositories 78
|
|
108
|
+
PR merge rate 51%
|
|
94
109
|
|
|
95
110
|
⚠ Hidden by activity feed
|
|
96
|
-
Feed shows ~25 repos; Search API found
|
|
97
|
-
~
|
|
111
|
+
Feed shows ~25 busiest repos; Search API found 78.
|
|
112
|
+
~53 lower-activity repositories are likely truncated.
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
·
|
|
101
|
-
·
|
|
114
|
+
Likely hidden repositories · least activity first
|
|
115
|
+
· small-org/side-project 1
|
|
116
|
+
· another/low-activity 2
|
|
102
117
|
...
|
|
103
118
|
```
|
|
104
119
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
4
5
|
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
5
8
|
|
|
6
9
|
// ../core/dist/github.js
|
|
7
10
|
var ACTIVITY_FEED_REPO_CAP = 25;
|
|
@@ -10,7 +13,12 @@ function setAuthToken(token) {
|
|
|
10
13
|
authToken = token.trim();
|
|
11
14
|
}
|
|
12
15
|
function getAuthToken() {
|
|
13
|
-
|
|
16
|
+
if (authToken)
|
|
17
|
+
return authToken;
|
|
18
|
+
if (typeof process !== "undefined" && process.env) {
|
|
19
|
+
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
14
22
|
}
|
|
15
23
|
var lastRateLimit = {
|
|
16
24
|
remaining: -1,
|
|
@@ -36,6 +44,22 @@ var GitHubApiError = class extends Error {
|
|
|
36
44
|
this.status = status;
|
|
37
45
|
}
|
|
38
46
|
};
|
|
47
|
+
function formatRateLimitHint() {
|
|
48
|
+
const hasToken = Boolean(getAuthToken());
|
|
49
|
+
const { remaining, limit, reset } = lastRateLimit;
|
|
50
|
+
const lines = [];
|
|
51
|
+
if (!hasToken) {
|
|
52
|
+
lines.push("Unauthenticated Search API is limited (~60 requests/hour).", "Set GITHUB_TOKEN or pass --token for ~5,000 requests/hour.", "Create a token: https://github.com/settings/tokens", "Then: export GITHUB_TOKEN=YOUR_TOKEN");
|
|
53
|
+
} else if (reset > 0) {
|
|
54
|
+
const resetAt = new Date(reset * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
55
|
+
const quota = remaining >= 0 && limit > 0 ? `${remaining}/${limit} remaining` : "quota exhausted";
|
|
56
|
+
lines.push(`Authenticated rate limit: ${quota}. Resets at ${resetAt}.`);
|
|
57
|
+
lines.push("Wait for the reset, or retry with openhearth doctor to check status.");
|
|
58
|
+
} else {
|
|
59
|
+
lines.push("Set GITHUB_TOKEN or pass --token, then retry.");
|
|
60
|
+
}
|
|
61
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
62
|
+
}
|
|
39
63
|
async function githubFetch(url) {
|
|
40
64
|
const headers = {
|
|
41
65
|
Accept: "application/vnd.github+json",
|
|
@@ -49,8 +73,10 @@ async function githubFetch(url) {
|
|
|
49
73
|
updateRateLimit(res);
|
|
50
74
|
if (res.status === 403 || res.status === 429) {
|
|
51
75
|
const body = await res.json().catch(() => ({}));
|
|
52
|
-
const
|
|
53
|
-
throw new GitHubApiError(
|
|
76
|
+
const apiMsg = body.message ?? "GitHub rate limit exceeded.";
|
|
77
|
+
throw new GitHubApiError(`${apiMsg}
|
|
78
|
+
|
|
79
|
+
${formatRateLimitHint()}`, res.status);
|
|
54
80
|
}
|
|
55
81
|
if (!res.ok) {
|
|
56
82
|
const body = await res.json().catch(() => ({}));
|
|
@@ -59,6 +85,23 @@ async function githubFetch(url) {
|
|
|
59
85
|
}
|
|
60
86
|
return res;
|
|
61
87
|
}
|
|
88
|
+
async function checkRateLimit() {
|
|
89
|
+
const res = await githubFetch("https://api.github.com/rate_limit");
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
return {
|
|
92
|
+
authenticated: Boolean(getAuthToken()),
|
|
93
|
+
core: {
|
|
94
|
+
remaining: data.resources.core.remaining,
|
|
95
|
+
limit: data.resources.core.limit,
|
|
96
|
+
reset: data.resources.core.reset
|
|
97
|
+
},
|
|
98
|
+
search: {
|
|
99
|
+
remaining: data.resources.search.remaining,
|
|
100
|
+
limit: data.resources.search.limit,
|
|
101
|
+
reset: data.resources.search.reset
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
62
105
|
async function searchIssuesPage(query, page) {
|
|
63
106
|
const params = new URLSearchParams({
|
|
64
107
|
q: query,
|
|
@@ -259,12 +302,22 @@ function buildInsights(pr, issue, review) {
|
|
|
259
302
|
const uniqueRepos = repoTotals.size;
|
|
260
303
|
const reposHiddenByFeed = Math.max(0, uniqueRepos - ACTIVITY_FEED_REPO_CAP);
|
|
261
304
|
const reposVisibleOnFeed = Math.min(uniqueRepos, ACTIVITY_FEED_REPO_CAP);
|
|
262
|
-
const
|
|
305
|
+
const ranked = [...repoTotals.entries()].sort((a, b) => {
|
|
306
|
+
if (b[1] !== a[1])
|
|
307
|
+
return b[1] - a[1];
|
|
308
|
+
return a[0].localeCompare(b[0]);
|
|
309
|
+
});
|
|
310
|
+
const topRepos = ranked.slice(0, 10).map(([repo, count]) => ({ repo, count }));
|
|
311
|
+
const likelyHiddenRepos = ranked.slice(ACTIVITY_FEED_REPO_CAP).map(([repo, count]) => ({ repo, count })).sort((a, b) => {
|
|
312
|
+
if (a.count !== b.count)
|
|
313
|
+
return a.count - b.count;
|
|
314
|
+
return a.repo.localeCompare(b.repo);
|
|
315
|
+
});
|
|
263
316
|
const merged = pr.items.filter((i) => i.state === "merged").length;
|
|
264
317
|
const mergeRate = pr.total > 0 ? Math.round(merged / pr.total * 100) : 0;
|
|
265
318
|
let feedTruncationNote = "Activity feed likely shows all repos for this range.";
|
|
266
319
|
if (reposHiddenByFeed > 0) {
|
|
267
|
-
feedTruncationNote = `GitHub's activity sidebar typically lists ~${ACTIVITY_FEED_REPO_CAP} repos before "N repositories not shown". Search API found ${uniqueRepos} repos \u2014 about ${reposHiddenByFeed}
|
|
320
|
+
feedTruncationNote = `GitHub's activity sidebar typically lists ~${ACTIVITY_FEED_REPO_CAP} busiest repos before "N repositories not shown". Search API found ${uniqueRepos} repos \u2014 about ${reposHiddenByFeed} lower-activity repos are likely hidden. Listed below as \u201Clikely hidden\u201D (least activity first).`;
|
|
268
321
|
}
|
|
269
322
|
return {
|
|
270
323
|
totalContributions: pr.total + issue.total + review.total,
|
|
@@ -273,6 +326,7 @@ function buildInsights(pr, issue, review) {
|
|
|
273
326
|
reposHiddenByFeed,
|
|
274
327
|
feedTruncationNote,
|
|
275
328
|
topRepos,
|
|
329
|
+
likelyHiddenRepos,
|
|
276
330
|
busiestDay: busiestDay(allItems),
|
|
277
331
|
mergeRate,
|
|
278
332
|
byKind: { pr: pr.total, issue: issue.total, review: review.total }
|
|
@@ -395,22 +449,37 @@ function printInsights(insights, rangeLabel2) {
|
|
|
395
449
|
console.log(amber(" \u26A0 Hidden by activity feed"));
|
|
396
450
|
console.log(
|
|
397
451
|
dim(
|
|
398
|
-
` Feed shows ~${insights.reposVisibleOnFeed} repos; Search API found ${insights.uniqueRepos}.`
|
|
452
|
+
` Feed shows ~${insights.reposVisibleOnFeed} busiest repos; Search API found ${insights.uniqueRepos}.`
|
|
399
453
|
)
|
|
400
454
|
);
|
|
401
455
|
console.log(
|
|
402
|
-
dim(` ~${insights.reposHiddenByFeed} repositories
|
|
456
|
+
dim(` ~${insights.reposHiddenByFeed} lower-activity repositories are likely truncated.`)
|
|
403
457
|
);
|
|
404
458
|
console.log("");
|
|
405
459
|
}
|
|
406
460
|
if (insights.topRepos.length > 0) {
|
|
407
|
-
console.log(bold(" Top repositories"));
|
|
461
|
+
console.log(bold(" Top repositories") + dim(" \xB7 likely visible on feed"));
|
|
408
462
|
for (const { repo, count } of insights.topRepos.slice(0, 8)) {
|
|
409
463
|
console.log(` ${dim("\xB7")} ${repo} ${dim(String(count))}`);
|
|
410
464
|
}
|
|
411
465
|
console.log("");
|
|
412
466
|
}
|
|
413
467
|
}
|
|
468
|
+
function printLikelyHidden(insights, limit = 20) {
|
|
469
|
+
if (insights.likelyHiddenRepos.length === 0) return;
|
|
470
|
+
console.log(
|
|
471
|
+
bold(" Likely hidden repositories") + dim(` \xB7 ${insights.likelyHiddenRepos.length} past the ~${insights.reposVisibleOnFeed} sidebar cap`)
|
|
472
|
+
);
|
|
473
|
+
console.log(dim(" Ranked least activity first (most likely truncated)."));
|
|
474
|
+
console.log("");
|
|
475
|
+
for (const { repo, count } of insights.likelyHiddenRepos.slice(0, limit)) {
|
|
476
|
+
console.log(` ${dim("\xB7")} ${repo} ${dim(String(count))}`);
|
|
477
|
+
}
|
|
478
|
+
if (insights.likelyHiddenRepos.length > limit) {
|
|
479
|
+
console.log(dim(` \u2026 and ${insights.likelyHiddenRepos.length - limit} more`));
|
|
480
|
+
}
|
|
481
|
+
console.log("");
|
|
482
|
+
}
|
|
414
483
|
function printAuditSection(title, result) {
|
|
415
484
|
console.log(bold(` ${title}`) + dim(` \xB7 ${result.total} across ${result.repos.length} repos`));
|
|
416
485
|
if (result.repos.length === 0) {
|
|
@@ -433,6 +502,7 @@ function printAuditSection(title, result) {
|
|
|
433
502
|
}
|
|
434
503
|
function printFullReport(result, rangeLabel2) {
|
|
435
504
|
printInsights(result.insights, `@${result.username} \xB7 ${rangeLabel2}`);
|
|
505
|
+
printLikelyHidden(result.insights, 12);
|
|
436
506
|
printAuditSection("Pull requests", result.pullRequests);
|
|
437
507
|
printAuditSection("Issues", result.issues);
|
|
438
508
|
printAuditSection("Reviews", result.reviews);
|
|
@@ -440,15 +510,58 @@ function printFullReport(result, rangeLabel2) {
|
|
|
440
510
|
console.log("");
|
|
441
511
|
}
|
|
442
512
|
function printError(message) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
`));
|
|
513
|
+
const lines = message.split("\n");
|
|
514
|
+
console.error("");
|
|
515
|
+
console.error(red(` Error: ${lines[0]}`));
|
|
516
|
+
for (const line of lines.slice(1)) {
|
|
517
|
+
console.error(line.length ? dim(` ${line}`) : "");
|
|
518
|
+
}
|
|
519
|
+
console.error("");
|
|
446
520
|
}
|
|
447
521
|
function printProgress(message) {
|
|
448
522
|
console.error(dim(` ${message}`));
|
|
449
523
|
}
|
|
524
|
+
function formatReset(reset) {
|
|
525
|
+
if (!reset) return "unknown";
|
|
526
|
+
return new Date(reset * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
527
|
+
}
|
|
528
|
+
function printDoctor(report) {
|
|
529
|
+
printBanner();
|
|
530
|
+
console.log(bold(" Doctor") + dim(" \xB7 environment check"));
|
|
531
|
+
console.log("");
|
|
532
|
+
console.log(` CLI version ${report.version}`);
|
|
533
|
+
console.log(` Node.js ${report.node}`);
|
|
534
|
+
console.log(
|
|
535
|
+
` Auth ${report.authenticated ? green(`yes (${report.tokenSource})`) : amber("no \u2014 unauthenticated")}`
|
|
536
|
+
);
|
|
537
|
+
console.log("");
|
|
538
|
+
console.log(bold(" Rate limits"));
|
|
539
|
+
console.log(
|
|
540
|
+
` Core API ${report.core.remaining}/${report.core.limit} reset ${formatReset(report.core.reset)}`
|
|
541
|
+
);
|
|
542
|
+
console.log(
|
|
543
|
+
` Search API ${report.search.remaining}/${report.search.limit} reset ${formatReset(report.search.reset)}`
|
|
544
|
+
);
|
|
545
|
+
console.log("");
|
|
546
|
+
if (!report.authenticated) {
|
|
547
|
+
console.log(amber(" Tip: export GITHUB_TOKEN=\u2026 before running audits."));
|
|
548
|
+
console.log(dim(" https://github.com/settings/tokens"));
|
|
549
|
+
console.log("");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
450
552
|
|
|
451
553
|
// src/cli.ts
|
|
554
|
+
function cliVersion() {
|
|
555
|
+
const injected = true ? "2.2.0" : "";
|
|
556
|
+
if (injected) return injected;
|
|
557
|
+
try {
|
|
558
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
559
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
560
|
+
return pkg.version;
|
|
561
|
+
} catch {
|
|
562
|
+
return "0.0.0";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
452
565
|
function usage() {
|
|
453
566
|
return `
|
|
454
567
|
${APP_NAME} \u2014 audit GitHub contributions the activity feed hides
|
|
@@ -456,10 +569,13 @@ ${APP_NAME} \u2014 audit GitHub contributions the activity feed hides
|
|
|
456
569
|
Usage:
|
|
457
570
|
openhearth audit <username> [options]
|
|
458
571
|
openhearth hidden <username> [options]
|
|
572
|
+
openhearth doctor
|
|
573
|
+
openhearth --version
|
|
459
574
|
|
|
460
575
|
Commands:
|
|
461
576
|
audit Full PR + issue + review audit (default)
|
|
462
|
-
hidden
|
|
577
|
+
hidden Hidden-repo report vs activity sidebar (ranked)
|
|
578
|
+
doctor Check auth + GitHub rate-limit status
|
|
463
579
|
|
|
464
580
|
Options:
|
|
465
581
|
--month YYYY-MM Audit a calendar month (e.g. 2026-07)
|
|
@@ -470,16 +586,18 @@ Options:
|
|
|
470
586
|
--json [file] Export JSON (stdout if no file)
|
|
471
587
|
--csv [file] Export CSV (stdout if no file)
|
|
472
588
|
--quiet Minimal output
|
|
589
|
+
-V, --version Print CLI version
|
|
473
590
|
-h, --help Show help
|
|
474
591
|
|
|
475
592
|
Examples:
|
|
476
593
|
npx @felix-ayush/openhearth audit Ayush7614 --month 2026-07
|
|
477
594
|
npx @felix-ayush/openhearth hidden Ayush7614 --month 2026-07
|
|
595
|
+
npx @felix-ayush/openhearth doctor
|
|
478
596
|
npx @felix-ayush/openhearth audit torvalds --from 2026-01-01 --to 2026-01-31 --json report.json
|
|
479
597
|
|
|
480
598
|
Unique features:
|
|
481
599
|
\xB7 Finds ALL repos via Search API (not truncated like github.com activity)
|
|
482
|
-
\xB7
|
|
600
|
+
\xB7 Ranks likely-hidden repos (lowest activity past the ~25 sidebar cap)
|
|
483
601
|
\xB7 Auto-splits date ranges past GitHub's 1000-result search cap
|
|
484
602
|
\xB7 Full audit: PRs + issues + reviews in one run
|
|
485
603
|
`;
|
|
@@ -491,7 +609,9 @@ function parseArgs(argv) {
|
|
|
491
609
|
kind: "all",
|
|
492
610
|
quiet: false,
|
|
493
611
|
hidden: false,
|
|
494
|
-
help: false
|
|
612
|
+
help: false,
|
|
613
|
+
version: false,
|
|
614
|
+
doctor: false
|
|
495
615
|
};
|
|
496
616
|
const positional = [];
|
|
497
617
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -500,6 +620,10 @@ function parseArgs(argv) {
|
|
|
500
620
|
opts.help = true;
|
|
501
621
|
continue;
|
|
502
622
|
}
|
|
623
|
+
if (arg === "-V" || arg === "--version") {
|
|
624
|
+
opts.version = true;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
503
627
|
if (arg === "--quiet") {
|
|
504
628
|
opts.quiet = true;
|
|
505
629
|
continue;
|
|
@@ -536,10 +660,11 @@ function parseArgs(argv) {
|
|
|
536
660
|
positional.push(arg);
|
|
537
661
|
}
|
|
538
662
|
}
|
|
539
|
-
if (positional[0] === "audit" || positional[0] === "hidden") {
|
|
663
|
+
if (positional[0] === "audit" || positional[0] === "hidden" || positional[0] === "doctor") {
|
|
540
664
|
opts.command = positional[0];
|
|
541
665
|
opts.username = (positional[1] ?? "").replace(/^@/, "");
|
|
542
666
|
if (opts.command === "hidden") opts.hidden = true;
|
|
667
|
+
if (opts.command === "doctor") opts.doctor = true;
|
|
543
668
|
} else {
|
|
544
669
|
opts.username = (positional[0] ?? "").replace(/^@/, "");
|
|
545
670
|
}
|
|
@@ -569,13 +694,46 @@ function writeOutput(content, path) {
|
|
|
569
694
|
writeFileSync(path, content, "utf8");
|
|
570
695
|
console.error(` Wrote ${path}`);
|
|
571
696
|
}
|
|
697
|
+
function tokenSourceLabel() {
|
|
698
|
+
if (process.env.GITHUB_TOKEN) return "GITHUB_TOKEN";
|
|
699
|
+
if (process.env.GH_TOKEN) return "GH_TOKEN";
|
|
700
|
+
return "--token";
|
|
701
|
+
}
|
|
702
|
+
async function runDoctor() {
|
|
703
|
+
const status = await checkRateLimit();
|
|
704
|
+
printDoctor({
|
|
705
|
+
version: cliVersion(),
|
|
706
|
+
node: process.version,
|
|
707
|
+
authenticated: status.authenticated,
|
|
708
|
+
tokenSource: status.authenticated ? tokenSourceLabel() : "none",
|
|
709
|
+
core: status.core,
|
|
710
|
+
search: status.search
|
|
711
|
+
});
|
|
712
|
+
}
|
|
572
713
|
async function main() {
|
|
573
714
|
const opts = parseArgs(process.argv.slice(2));
|
|
574
|
-
if (opts.
|
|
715
|
+
if (opts.version) {
|
|
716
|
+
console.log(cliVersion());
|
|
717
|
+
process.exit(0);
|
|
718
|
+
}
|
|
719
|
+
if (opts.help) {
|
|
575
720
|
console.log(usage());
|
|
576
|
-
process.exit(
|
|
721
|
+
process.exit(0);
|
|
577
722
|
}
|
|
578
723
|
if (opts.token) setAuthToken(opts.token);
|
|
724
|
+
if (opts.doctor || opts.command === "doctor") {
|
|
725
|
+
try {
|
|
726
|
+
await runDoctor();
|
|
727
|
+
} catch (err) {
|
|
728
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (!opts.username) {
|
|
734
|
+
console.log(usage());
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
579
737
|
const range = resolveRange(opts);
|
|
580
738
|
const label = rangeLabel(range, opts.month);
|
|
581
739
|
const onProgress = opts.quiet ? void 0 : printProgress;
|
|
@@ -585,10 +743,12 @@ async function main() {
|
|
|
585
743
|
const full = await runFullAudit(opts.username, range, runAudit, onProgress);
|
|
586
744
|
if (!opts.quiet) {
|
|
587
745
|
printInsights(full.insights, `@${opts.username} \xB7 ${label}`);
|
|
746
|
+
printLikelyHidden(full.insights);
|
|
588
747
|
console.log(` ${full.insights.feedTruncationNote}
|
|
589
748
|
`);
|
|
590
749
|
}
|
|
591
750
|
if (opts.json) writeOutput(fullAuditToJson(full), opts.json);
|
|
751
|
+
if (opts.csv) writeOutput(fullAuditToCsv(full), opts.csv);
|
|
592
752
|
return;
|
|
593
753
|
}
|
|
594
754
|
if (opts.kind === "all") {
|
|
@@ -611,6 +771,9 @@ async function main() {
|
|
|
611
771
|
}
|
|
612
772
|
} catch (err) {
|
|
613
773
|
printError(err instanceof Error ? err.message : String(err));
|
|
774
|
+
if (!getAuthToken()) {
|
|
775
|
+
printProgress("Hint: run `openhearth doctor` after setting GITHUB_TOKEN.");
|
|
776
|
+
}
|
|
614
777
|
process.exit(1);
|
|
615
778
|
}
|
|
616
779
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@felix-ayush/openhearth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "CLI to audit every GitHub PR, issue, and review — find repos hidden from your activity feed (\"N repositories not shown\")",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"node": ">=20"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@felix-ayush/openhearth-core": "
|
|
57
|
+
"@felix-ayush/openhearth-core": "*",
|
|
58
58
|
"@types/node": "^22.13.10",
|
|
59
59
|
"esbuild": "^0.25.0",
|
|
60
60
|
"typescript": "^5.8.2"
|