@felix-ayush/openhearth 2.0.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/dist/cli.js +177 -0
- package/dist/format.js +69 -0
- package/package.json +47 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { APP_NAME, auditToCsv, auditToJson, fullAuditToCsv, fullAuditToJson, parseMonth, runAudit, runFullAudit, setAuthToken, } from "@felix-ayush/openhearth-core";
|
|
4
|
+
import { printAuditSection, printBanner, printError, printFullReport, printInsights, printProgress, } from "./format.js";
|
|
5
|
+
function usage() {
|
|
6
|
+
return `
|
|
7
|
+
${APP_NAME} — audit GitHub contributions the activity feed hides
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
openhearth audit <username> [options]
|
|
11
|
+
openhearth hidden <username> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
audit Full PR + issue + review audit (default)
|
|
15
|
+
hidden Quick report: repos hidden from the activity sidebar
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--month YYYY-MM Audit a calendar month (e.g. 2026-07)
|
|
19
|
+
--from YYYY-MM-DD Custom range start
|
|
20
|
+
--to YYYY-MM-DD Custom range end
|
|
21
|
+
--kind pr|issue|review|all Default: all
|
|
22
|
+
--token TOKEN GitHub PAT (or set GITHUB_TOKEN)
|
|
23
|
+
--json [file] Export JSON (stdout if no file)
|
|
24
|
+
--csv [file] Export CSV (stdout if no file)
|
|
25
|
+
--quiet Minimal output
|
|
26
|
+
-h, --help Show help
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
npx @felix-ayush/openhearth audit Ayush7614 --month 2026-07
|
|
30
|
+
npx @felix-ayush/openhearth hidden Ayush7614 --month 2026-07
|
|
31
|
+
npx @felix-ayush/openhearth audit torvalds --from 2026-01-01 --to 2026-01-31 --json report.json
|
|
32
|
+
|
|
33
|
+
Unique features:
|
|
34
|
+
· Finds ALL repos via Search API (not truncated like github.com activity)
|
|
35
|
+
· Reports how many repos the profile sidebar likely hides
|
|
36
|
+
· Auto-splits date ranges past GitHub's 1000-result search cap
|
|
37
|
+
· Full audit: PRs + issues + reviews in one run
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const opts = {
|
|
42
|
+
command: "audit",
|
|
43
|
+
username: "",
|
|
44
|
+
kind: "all",
|
|
45
|
+
quiet: false,
|
|
46
|
+
hidden: false,
|
|
47
|
+
help: false,
|
|
48
|
+
};
|
|
49
|
+
const positional = [];
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg === "-h" || arg === "--help") {
|
|
53
|
+
opts.help = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === "--quiet") {
|
|
57
|
+
opts.quiet = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--month" && argv[i + 1]) {
|
|
61
|
+
opts.month = argv[++i];
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--from" && argv[i + 1]) {
|
|
65
|
+
opts.from = argv[++i];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === "--to" && argv[i + 1]) {
|
|
69
|
+
opts.to = argv[++i];
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--token" && argv[i + 1]) {
|
|
73
|
+
opts.token = argv[++i];
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--kind" && argv[i + 1]) {
|
|
77
|
+
opts.kind = argv[++i];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--json") {
|
|
81
|
+
opts.json = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : "-";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--csv") {
|
|
85
|
+
opts.csv = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : "-";
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!arg.startsWith("-")) {
|
|
89
|
+
positional.push(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (positional[0] === "audit" || positional[0] === "hidden") {
|
|
93
|
+
opts.command = positional[0];
|
|
94
|
+
opts.username = (positional[1] ?? "").replace(/^@/, "");
|
|
95
|
+
if (opts.command === "hidden")
|
|
96
|
+
opts.hidden = true;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
opts.username = (positional[0] ?? "").replace(/^@/, "");
|
|
100
|
+
}
|
|
101
|
+
return opts;
|
|
102
|
+
}
|
|
103
|
+
function resolveRange(opts) {
|
|
104
|
+
if (opts.month) {
|
|
105
|
+
return parseMonth(opts.month);
|
|
106
|
+
}
|
|
107
|
+
if (opts.from && opts.to) {
|
|
108
|
+
return { from: opts.from, to: opts.to };
|
|
109
|
+
}
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const year = now.getFullYear();
|
|
112
|
+
const month = now.getMonth() + 1;
|
|
113
|
+
return parseMonth(`${year}-${String(month).padStart(2, "0")}`);
|
|
114
|
+
}
|
|
115
|
+
function rangeLabel(range, month) {
|
|
116
|
+
if (month)
|
|
117
|
+
return month;
|
|
118
|
+
return `${range.from} → ${range.to}`;
|
|
119
|
+
}
|
|
120
|
+
function writeOutput(content, path) {
|
|
121
|
+
if (!path || path === "-") {
|
|
122
|
+
console.log(content);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
writeFileSync(path, content, "utf8");
|
|
126
|
+
console.error(` Wrote ${path}`);
|
|
127
|
+
}
|
|
128
|
+
async function main() {
|
|
129
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
130
|
+
if (opts.help || !opts.username) {
|
|
131
|
+
console.log(usage());
|
|
132
|
+
process.exit(opts.help ? 0 : 1);
|
|
133
|
+
}
|
|
134
|
+
if (opts.token)
|
|
135
|
+
setAuthToken(opts.token);
|
|
136
|
+
const range = resolveRange(opts);
|
|
137
|
+
const label = rangeLabel(range, opts.month);
|
|
138
|
+
const onProgress = opts.quiet ? undefined : printProgress;
|
|
139
|
+
if (!opts.quiet)
|
|
140
|
+
printBanner();
|
|
141
|
+
try {
|
|
142
|
+
if (opts.hidden || opts.command === "hidden") {
|
|
143
|
+
const full = await runFullAudit(opts.username, range, runAudit, onProgress);
|
|
144
|
+
if (!opts.quiet) {
|
|
145
|
+
printInsights(full.insights, `@${opts.username} · ${label}`);
|
|
146
|
+
console.log(` ${full.insights.feedTruncationNote}\n`);
|
|
147
|
+
}
|
|
148
|
+
if (opts.json)
|
|
149
|
+
writeOutput(fullAuditToJson(full), opts.json);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (opts.kind === "all") {
|
|
153
|
+
const full = await runFullAudit(opts.username, range, runAudit, onProgress);
|
|
154
|
+
if (opts.json)
|
|
155
|
+
writeOutput(fullAuditToJson(full), opts.json);
|
|
156
|
+
if (opts.csv)
|
|
157
|
+
writeOutput(fullAuditToCsv(full), opts.csv);
|
|
158
|
+
if (!opts.quiet && !opts.json && !opts.csv) {
|
|
159
|
+
printFullReport(full, label);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const result = await runAudit(opts.username, opts.kind, range, onProgress);
|
|
164
|
+
if (opts.json)
|
|
165
|
+
writeOutput(auditToJson(result), opts.json);
|
|
166
|
+
if (opts.csv)
|
|
167
|
+
writeOutput(auditToCsv(result), opts.csv);
|
|
168
|
+
if (!opts.quiet && !opts.json && !opts.csv) {
|
|
169
|
+
printAuditSection(opts.kind === "pr" ? "Pull requests" : opts.kind === "issue" ? "Issues" : "Reviews", result);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
main();
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
2
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
3
|
+
const amber = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
4
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
5
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
6
|
+
export function printBanner() {
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log(bold(" OpenHearth") + dim(" · contribution audit"));
|
|
9
|
+
console.log("");
|
|
10
|
+
}
|
|
11
|
+
export function printInsights(insights, rangeLabel) {
|
|
12
|
+
console.log(bold(" Summary") + dim(` · ${rangeLabel}`));
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log(` Total contributions ${bold(String(insights.totalContributions))}`);
|
|
15
|
+
console.log(` Unique repositories ${bold(String(insights.uniqueRepos))}`);
|
|
16
|
+
console.log(` PR merge rate ${bold(`${insights.mergeRate}%`)}`);
|
|
17
|
+
console.log(` By kind PRs ${insights.byKind.pr} · Issues ${insights.byKind.issue} · Reviews ${insights.byKind.review}`);
|
|
18
|
+
if (insights.busiestDay) {
|
|
19
|
+
console.log(` Busiest day ${insights.busiestDay}`);
|
|
20
|
+
}
|
|
21
|
+
console.log("");
|
|
22
|
+
if (insights.reposHiddenByFeed > 0) {
|
|
23
|
+
console.log(amber(" ⚠ Hidden by activity feed"));
|
|
24
|
+
console.log(dim(` Feed shows ~${insights.reposVisibleOnFeed} repos; Search API found ${insights.uniqueRepos}.`));
|
|
25
|
+
console.log(dim(` ~${insights.reposHiddenByFeed} repositories may not appear on your profile sidebar.`));
|
|
26
|
+
console.log("");
|
|
27
|
+
}
|
|
28
|
+
if (insights.topRepos.length > 0) {
|
|
29
|
+
console.log(bold(" Top repositories"));
|
|
30
|
+
for (const { repo, count } of insights.topRepos.slice(0, 8)) {
|
|
31
|
+
console.log(` ${dim("·")} ${repo} ${dim(String(count))}`);
|
|
32
|
+
}
|
|
33
|
+
console.log("");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function printAuditSection(title, result) {
|
|
37
|
+
console.log(bold(` ${title}`) + dim(` · ${result.total} across ${result.repos.length} repos`));
|
|
38
|
+
if (result.repos.length === 0) {
|
|
39
|
+
console.log(dim(" (none)"));
|
|
40
|
+
console.log("");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const repo of result.repos.slice(0, 6)) {
|
|
44
|
+
const parts = [
|
|
45
|
+
repo.merged ? green(`${repo.merged} merged`) : "",
|
|
46
|
+
repo.open ? `${repo.open} open` : "",
|
|
47
|
+
repo.closed ? dim(`${repo.closed} closed`) : "",
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
console.log(` ${dim("▸")} ${repo.repo} ${dim(parts.join(" · "))}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.repos.length > 6) {
|
|
52
|
+
console.log(dim(` … and ${result.repos.length - 6} more repositories`));
|
|
53
|
+
}
|
|
54
|
+
console.log("");
|
|
55
|
+
}
|
|
56
|
+
export function printFullReport(result, rangeLabel) {
|
|
57
|
+
printInsights(result.insights, `@${result.username} · ${rangeLabel}`);
|
|
58
|
+
printAuditSection("Pull requests", result.pullRequests);
|
|
59
|
+
printAuditSection("Issues", result.issues);
|
|
60
|
+
printAuditSection("Reviews", result.reviews);
|
|
61
|
+
console.log(dim(` ${result.insights.feedTruncationNote}`));
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
export function printError(message) {
|
|
65
|
+
console.error(red(`\n Error: ${message}\n`));
|
|
66
|
+
}
|
|
67
|
+
export function printProgress(message) {
|
|
68
|
+
console.error(dim(` ${message}`));
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@felix-ayush/openhearth",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI to audit every GitHub PR, issue, and review — including what the activity feed hides",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openhearth": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"github",
|
|
18
|
+
"contributions",
|
|
19
|
+
"pull-requests",
|
|
20
|
+
"open-source",
|
|
21
|
+
"audit",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Ayush Kumar",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/Ayush7614/OpenHearth.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://ayush7614.github.io/OpenHearth/",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/Ayush7614/OpenHearth/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@felix-ayush/openhearth-core": "2.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.13.10",
|
|
45
|
+
"typescript": "^5.8.2"
|
|
46
|
+
}
|
|
47
|
+
}
|