gh-statskit 0.0.0-alpha.1
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/LICENSE.md +21 -0
- package/README.md +93 -0
- package/bin/cli.mjs +3 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +299 -0
- package/dist/index.d.mts +155 -0
- package/dist/index.mjs +3 -0
- package/dist/rank-CFWMbXNE.mjs +61 -0
- package/package.json +80 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jing Haihan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# GitHub Statskit
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![JSDocs][jsdocs-src]][jsdocs-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
|
|
7
|
+
A CLI tool to export your GitHub stats and publish to a GitHub Gist. Automatically sync your contribution data to a Gist for easy sharing and tracking.
|
|
8
|
+
|
|
9
|
+
<p align='center'>
|
|
10
|
+
<img src='./assets/help.png' />
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Local usage
|
|
16
|
+
|
|
17
|
+
Run the tool locally to export your stats:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export GH_PAT=your_github_token
|
|
21
|
+
|
|
22
|
+
# Export to local file
|
|
23
|
+
npx gh-statskit
|
|
24
|
+
|
|
25
|
+
# Update to Gist
|
|
26
|
+
npx gh-statskit --gist-id id
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### GitHub CI usage
|
|
30
|
+
|
|
31
|
+
> [!IMPORTANT]
|
|
32
|
+
> Your Gist must already exist and contain a file with the specified filename (default: `github-stats.json`). You can customize the filename using the `--gist-filename` option.
|
|
33
|
+
|
|
34
|
+
**Set up GitHub Actions** to automatically sync your stats on a schedule:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
name: Upload GitHub Stats
|
|
38
|
+
|
|
39
|
+
on:
|
|
40
|
+
push:
|
|
41
|
+
branches: [main]
|
|
42
|
+
schedule:
|
|
43
|
+
- cron: '0 0 * * *'
|
|
44
|
+
workflow_dispatch:
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
sync:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
with:
|
|
52
|
+
fetch-depth: 0
|
|
53
|
+
|
|
54
|
+
- name: Set node
|
|
55
|
+
uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: lts/*
|
|
58
|
+
|
|
59
|
+
- name: Update to Gist
|
|
60
|
+
run: npx gh-statskit
|
|
61
|
+
env:
|
|
62
|
+
GH_PAT: ${{ secrets.GH_PAT }}
|
|
63
|
+
GIST_ID: ${{ secrets.GIST_ID }}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Configure secrets in your repository**:
|
|
67
|
+
- Go to your repository Settings > Secrets and variables > Actions
|
|
68
|
+
- Add `GH_PAT` as a repository secret (your GitHub Personal Access Token)
|
|
69
|
+
- Add `GIST_ID` as a repository secret (the ID of your Gist containing the configured filename, default: `github-stats.json`)
|
|
70
|
+
|
|
71
|
+
## Credits
|
|
72
|
+
|
|
73
|
+
This project is inspired by:
|
|
74
|
+
- [releases.antfu.me](https://github.com/antfu/releases.antfu.me) - @[Anthony Fu](https://github.com/antfu)
|
|
75
|
+
- [my-pull-requests](https://github.com/atinux/my-pull-requests) - @[Sébastien Chopin](https://github.com/atinux)
|
|
76
|
+
- [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) - @[anuraghazra](github-readme-stats)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[MIT](./LICENSE) License © [jinghaihan](https://github.com/jinghaihan)
|
|
81
|
+
|
|
82
|
+
<!-- Badges -->
|
|
83
|
+
|
|
84
|
+
[npm-version-src]: https://img.shields.io/npm/v/gh-statskit?style=flat&colorA=080f12&colorB=1fa669
|
|
85
|
+
[npm-version-href]: https://npmjs.com/package/gh-statskit
|
|
86
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/gh-statskit?style=flat&colorA=080f12&colorB=1fa669
|
|
87
|
+
[npm-downloads-href]: https://npmjs.com/package/gh-statskit
|
|
88
|
+
[bundle-src]: https://img.shields.io/bundlephobia/minzip/gh-statskit?style=flat&colorA=080f12&colorB=1fa669&label=minzip
|
|
89
|
+
[bundle-href]: https://bundlephobia.com/result?p=gh-statskit
|
|
90
|
+
[license-src]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat&colorA=080f12&colorB=1fa669
|
|
91
|
+
[license-href]: https://github.com/jinghaihan/gh-statskit/LICENSE
|
|
92
|
+
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
|
|
93
|
+
[jsdocs-href]: https://www.jsdocs.io/package/gh-statskit
|
package/bin/cli.mjs
ADDED
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { t as calculateRank } from "./rank-CFWMbXNE.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import c from "ansis";
|
|
5
|
+
import { cac } from "cac";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
import pRetry from "p-retry";
|
|
8
|
+
import { Octokit } from "@octokit/core";
|
|
9
|
+
import { writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "pathe";
|
|
11
|
+
|
|
12
|
+
//#region package.json
|
|
13
|
+
var name = "gh-statskit";
|
|
14
|
+
var version = "0.0.0-alpha.1";
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/constants.ts
|
|
18
|
+
const NAME = name;
|
|
19
|
+
const VERSION = version;
|
|
20
|
+
const DEFAULT_OPTIONS = {
|
|
21
|
+
apiVersion: "2022-11-28",
|
|
22
|
+
perPage: 50,
|
|
23
|
+
baseUrl: "github.com",
|
|
24
|
+
gistFilename: "github-stats.json",
|
|
25
|
+
yes: false
|
|
26
|
+
};
|
|
27
|
+
const GRAPHQL_REPOS_FIELD = `
|
|
28
|
+
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
|
29
|
+
totalCount
|
|
30
|
+
nodes {
|
|
31
|
+
name
|
|
32
|
+
stargazers {
|
|
33
|
+
totalCount
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
pageInfo {
|
|
37
|
+
hasNextPage
|
|
38
|
+
endCursor
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
const GRAPHQL_STATS_QUERY = `
|
|
43
|
+
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null) {
|
|
44
|
+
user(login: $login) {
|
|
45
|
+
name
|
|
46
|
+
login
|
|
47
|
+
commits: contributionsCollection (from: $startTime) {
|
|
48
|
+
totalCommitContributions,
|
|
49
|
+
}
|
|
50
|
+
reviews: contributionsCollection {
|
|
51
|
+
totalPullRequestReviewContributions
|
|
52
|
+
}
|
|
53
|
+
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
|
54
|
+
totalCount
|
|
55
|
+
}
|
|
56
|
+
pullRequests(first: 1) {
|
|
57
|
+
totalCount
|
|
58
|
+
}
|
|
59
|
+
mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
|
|
60
|
+
totalCount
|
|
61
|
+
}
|
|
62
|
+
openIssues: issues(states: OPEN) {
|
|
63
|
+
totalCount
|
|
64
|
+
}
|
|
65
|
+
closedIssues: issues(states: CLOSED) {
|
|
66
|
+
totalCount
|
|
67
|
+
}
|
|
68
|
+
followers {
|
|
69
|
+
totalCount
|
|
70
|
+
}
|
|
71
|
+
repositoryDiscussions @include(if: $includeDiscussions) {
|
|
72
|
+
totalCount
|
|
73
|
+
}
|
|
74
|
+
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
|
|
75
|
+
totalCount
|
|
76
|
+
}
|
|
77
|
+
${GRAPHQL_REPOS_FIELD}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/utils.ts
|
|
84
|
+
let octoKit;
|
|
85
|
+
function getOctoKit(token) {
|
|
86
|
+
if (!octoKit) octoKit = new Octokit({ auth: `Bearer ${token}` });
|
|
87
|
+
return octoKit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/git.ts
|
|
92
|
+
const RepoCache = /* @__PURE__ */ new Map();
|
|
93
|
+
async function readTokenFromGitHubCli() {
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execa("gh", ["auth", "token"]);
|
|
96
|
+
return stdout.trim();
|
|
97
|
+
} catch {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function getRepo(owner, name$1, token) {
|
|
102
|
+
if (RepoCache.has(`${owner}/${name$1}`)) return RepoCache.get(`${owner}/${name$1}`);
|
|
103
|
+
const { data } = await getOctoKit(token).request("GET /repos/{owner}/{name}", {
|
|
104
|
+
owner,
|
|
105
|
+
name: name$1
|
|
106
|
+
});
|
|
107
|
+
RepoCache.set(`${owner}/${name$1}`, data);
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
async function getUser(options) {
|
|
111
|
+
const octokit = getOctoKit(options.token);
|
|
112
|
+
const response = await pRetry(async () => {
|
|
113
|
+
return await octokit.request("GET /user");
|
|
114
|
+
}, { retries: 3 });
|
|
115
|
+
return {
|
|
116
|
+
name: response.data.name ?? response.data.login,
|
|
117
|
+
username: response.data.login,
|
|
118
|
+
avatar: response.data.avatar_url
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function getPullRequests(username, options) {
|
|
122
|
+
const octokit = getOctoKit(options.token);
|
|
123
|
+
const filteredPrs = (await pRetry(async () => {
|
|
124
|
+
return await octokit.request("GET /search/issues", {
|
|
125
|
+
q: `type:pr+author:"${username}"`,
|
|
126
|
+
per_page: options.perPage,
|
|
127
|
+
page: 1,
|
|
128
|
+
advanced_search: "true"
|
|
129
|
+
});
|
|
130
|
+
}, { retries: 3 })).data.items.filter((pr) => !(pr.state === "closed" && !pr.pull_request?.merged_at));
|
|
131
|
+
const prs = [];
|
|
132
|
+
for (const pr of filteredPrs) {
|
|
133
|
+
const [owner, name$1] = pr.repository_url.split("/").slice(-2);
|
|
134
|
+
const repo = await getRepo(owner, name$1, options.token);
|
|
135
|
+
prs.push({
|
|
136
|
+
repo: `${owner}/${name$1}`,
|
|
137
|
+
title: pr.title,
|
|
138
|
+
url: pr.html_url,
|
|
139
|
+
created_at: pr.created_at,
|
|
140
|
+
state: pr.pull_request?.merged_at ? "merged" : pr.draft ? "draft" : pr.state,
|
|
141
|
+
number: pr.number,
|
|
142
|
+
type: repo.owner.type,
|
|
143
|
+
stars: repo.stargazers_count
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return prs;
|
|
147
|
+
}
|
|
148
|
+
async function getGraphQLStats(user, options) {
|
|
149
|
+
const octokit = getOctoKit(options.token);
|
|
150
|
+
const variables = {
|
|
151
|
+
login: user.username,
|
|
152
|
+
after: null,
|
|
153
|
+
includeMergedPullRequests: true,
|
|
154
|
+
includeDiscussions: true,
|
|
155
|
+
includeDiscussionsAnswers: true
|
|
156
|
+
};
|
|
157
|
+
const { user: graphqlUser } = await pRetry(async () => {
|
|
158
|
+
return await octokit.graphql(GRAPHQL_STATS_QUERY, variables);
|
|
159
|
+
}, { retries: 3 });
|
|
160
|
+
return graphqlUser;
|
|
161
|
+
}
|
|
162
|
+
async function updateGist(data, options) {
|
|
163
|
+
const { gistId, gistFilename, token } = options;
|
|
164
|
+
const octokit = getOctoKit(token);
|
|
165
|
+
if (!(await pRetry(async () => {
|
|
166
|
+
return await octokit.request("GET /gists/{gist_id}", { gist_id: gistId });
|
|
167
|
+
}, { retries: 3 })).data.files?.[gistFilename]) throw new Error(`Gist does not contain ${gistFilename} file`);
|
|
168
|
+
const content = JSON.stringify(data, null, 2);
|
|
169
|
+
return (await pRetry(async () => {
|
|
170
|
+
return await octokit.request("PATCH /gists/{gist_id}", {
|
|
171
|
+
gist_id: gistId,
|
|
172
|
+
files: { [gistFilename]: { content } }
|
|
173
|
+
});
|
|
174
|
+
}, { retries: 3 })).data.html_url;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/config.ts
|
|
179
|
+
function normalizeConfig(options) {
|
|
180
|
+
if ("default" in options) options = options.default;
|
|
181
|
+
return options;
|
|
182
|
+
}
|
|
183
|
+
async function resolveConfig(options) {
|
|
184
|
+
const defaults = structuredClone(DEFAULT_OPTIONS);
|
|
185
|
+
options = normalizeConfig(options);
|
|
186
|
+
const merged = {
|
|
187
|
+
...defaults,
|
|
188
|
+
...options
|
|
189
|
+
};
|
|
190
|
+
merged.cwd = merged.cwd || process.cwd();
|
|
191
|
+
merged.token = merged.token || process.env.GH_PAT || process.env.GITHUB_TOKEN || await readTokenFromGitHubCli();
|
|
192
|
+
merged.gistId = merged.gistId || process.env.GIST_ID;
|
|
193
|
+
return merged;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/generator.ts
|
|
198
|
+
async function generate(data, options) {
|
|
199
|
+
const filepath = join(options.cwd, options.gistFilename);
|
|
200
|
+
await writeFile(filepath, JSON.stringify(data, null, 2));
|
|
201
|
+
if (!options.gistId) {
|
|
202
|
+
p.outro(`${c.green("✓")} ${c.dim("Local file:")} ${filepath}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const spinner = p.spinner();
|
|
206
|
+
try {
|
|
207
|
+
spinner.start("updating gist");
|
|
208
|
+
const gistUrl = await updateGist(data, options);
|
|
209
|
+
spinner.stop("gist updated successfully");
|
|
210
|
+
p.outro(`${c.green("✓")} ${c.dim("Gist URL:")} ${gistUrl}`);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
spinner.stop("failed to update gist");
|
|
213
|
+
p.outro(`${c.red("✗")} ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/cli.ts
|
|
219
|
+
try {
|
|
220
|
+
const cli = cac(NAME);
|
|
221
|
+
cli.command("", "Export GitHub stats and publish to a GitHub Gist").option("--cwd <path>", "Working directory").option("--token <token>", "GitHub token").option("--api-version <version>", "GitHub API version", { default: "2022-11-28" }).option("--per-page <count>", "GitHub API per page count", { default: 50 }).option("--base-url <url>", "GitHub base URL", { default: "github.com" }).option("--gist-id <id>", "GitHub Gist ID").option("--gist-filename <filename>", "GitHub Gist filename", { default: "github-stats.json" }).allowUnknownOptions().action(async (options) => {
|
|
222
|
+
p.intro(`${c.yellow`${NAME} `}${c.dim`v${VERSION}`}`);
|
|
223
|
+
const config = await resolveConfig(options);
|
|
224
|
+
const user = await fetchUser(config);
|
|
225
|
+
const stats = await fetchStats(user, await fetchPullRequests(user, config), config);
|
|
226
|
+
const rank = calculateRank({
|
|
227
|
+
commits: stats.commits,
|
|
228
|
+
prs: stats.pullRequest.totalCount,
|
|
229
|
+
issues: stats.issues.totalCount,
|
|
230
|
+
reviews: stats.reviews,
|
|
231
|
+
repos: stats.repositories.totalCount,
|
|
232
|
+
stars: stats.repositories.totalStargazers,
|
|
233
|
+
followers: stats.followers
|
|
234
|
+
});
|
|
235
|
+
await generate({
|
|
236
|
+
...stats,
|
|
237
|
+
rank
|
|
238
|
+
}, config);
|
|
239
|
+
});
|
|
240
|
+
cli.help();
|
|
241
|
+
cli.version(VERSION);
|
|
242
|
+
cli.parse();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
async function fetchUser(options) {
|
|
248
|
+
const spinner = p.spinner();
|
|
249
|
+
spinner.start("getting user information");
|
|
250
|
+
const user = await getUser(options);
|
|
251
|
+
spinner.stop("user information retrieved");
|
|
252
|
+
return user;
|
|
253
|
+
}
|
|
254
|
+
async function fetchPullRequests(user, options) {
|
|
255
|
+
const spinner = p.spinner();
|
|
256
|
+
spinner.start("getting pull requests");
|
|
257
|
+
const data = await getPullRequests(user.username, options);
|
|
258
|
+
spinner.stop(`pull requests retrieved: ${data.length}`);
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
async function fetchStats(user, pullRequests, options) {
|
|
262
|
+
const spinner = p.spinner();
|
|
263
|
+
spinner.start("getting stats");
|
|
264
|
+
const response = await getGraphQLStats(user, options);
|
|
265
|
+
const stats = {
|
|
266
|
+
user,
|
|
267
|
+
commits: response.commits.totalCommitContributions ?? 0,
|
|
268
|
+
reviews: response.reviews.totalPullRequestReviewContributions ?? 0,
|
|
269
|
+
repositoriesContributedTo: response.repositoriesContributedTo.totalCount,
|
|
270
|
+
pullRequest: {
|
|
271
|
+
totalCount: response.pullRequests.totalCount,
|
|
272
|
+
mergedCount: response.mergedPullRequests.totalCount,
|
|
273
|
+
data: pullRequests
|
|
274
|
+
},
|
|
275
|
+
issues: {
|
|
276
|
+
totalCount: response.openIssues.totalCount + response.closedIssues.totalCount,
|
|
277
|
+
openCount: response.openIssues.totalCount,
|
|
278
|
+
closedCount: response.closedIssues.totalCount
|
|
279
|
+
},
|
|
280
|
+
followers: response.followers.totalCount,
|
|
281
|
+
discussions: {
|
|
282
|
+
totalCount: response.repositoryDiscussions.totalCount,
|
|
283
|
+
commentsCount: response.repositoryDiscussionComments.totalCount
|
|
284
|
+
},
|
|
285
|
+
repositories: {
|
|
286
|
+
totalCount: response.repositories.totalCount,
|
|
287
|
+
totalStargazers: response.repositories.nodes.reduce((acc, repo) => acc + repo.stargazers.totalCount, 0),
|
|
288
|
+
data: response.repositories.nodes.map((node) => ({
|
|
289
|
+
name: node.name,
|
|
290
|
+
stargazers: node.stargazers.totalCount
|
|
291
|
+
}))
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
spinner.stop("stats retrieved");
|
|
295
|
+
return stats;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
export { };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//#region src/types/command.d.ts
|
|
2
|
+
interface CommandOptions {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
/**
|
|
5
|
+
* GitHub token
|
|
6
|
+
* https://github.com/settings/personal-access-tokens
|
|
7
|
+
*/
|
|
8
|
+
token?: string;
|
|
9
|
+
/**
|
|
10
|
+
* GitHub API version
|
|
11
|
+
*/
|
|
12
|
+
apiVersion?: string;
|
|
13
|
+
/**
|
|
14
|
+
* GitHub API per page count
|
|
15
|
+
*/
|
|
16
|
+
perPage?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Github base url
|
|
19
|
+
* @default github.com
|
|
20
|
+
*/
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* GitHub Gist ID
|
|
24
|
+
*/
|
|
25
|
+
gistId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* GitHub Gist filename
|
|
28
|
+
*/
|
|
29
|
+
gistFilename?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to skip the confirmation prompt
|
|
32
|
+
*/
|
|
33
|
+
yes?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface Options extends Required<CommandOptions> {}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/types/response.d.ts
|
|
38
|
+
interface GraphQLResponse {
|
|
39
|
+
user: GraphQLUser;
|
|
40
|
+
}
|
|
41
|
+
interface GraphQLUser {
|
|
42
|
+
name: string;
|
|
43
|
+
login: string;
|
|
44
|
+
commits: GraphQLContributions;
|
|
45
|
+
reviews: GraphQLContributions;
|
|
46
|
+
repositoriesContributedTo: GraphQLCountData;
|
|
47
|
+
pullRequests: GraphQLCountData;
|
|
48
|
+
mergedPullRequests: GraphQLCountData;
|
|
49
|
+
openIssues: GraphQLCountData;
|
|
50
|
+
closedIssues: GraphQLCountData;
|
|
51
|
+
followers: GraphQLCountData;
|
|
52
|
+
repositoryDiscussions: GraphQLCountData;
|
|
53
|
+
repositoryDiscussionComments: GraphQLCountData;
|
|
54
|
+
repositories: GraphQLRepositories;
|
|
55
|
+
}
|
|
56
|
+
interface GraphQLContributions {
|
|
57
|
+
totalCommitContributions?: number;
|
|
58
|
+
totalPullRequestReviewContributions?: number;
|
|
59
|
+
}
|
|
60
|
+
interface GraphQLCountData {
|
|
61
|
+
totalCount: number;
|
|
62
|
+
}
|
|
63
|
+
interface GraphQLRepositories {
|
|
64
|
+
totalCount: number;
|
|
65
|
+
nodes: GraphQLRepository[];
|
|
66
|
+
}
|
|
67
|
+
interface GraphQLRepository {
|
|
68
|
+
name: string;
|
|
69
|
+
stargazers: GraphQLStargazersData;
|
|
70
|
+
}
|
|
71
|
+
interface GraphQLStargazersData {
|
|
72
|
+
totalCount: number;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/types/stats.d.ts
|
|
76
|
+
interface GitHubStats {
|
|
77
|
+
user: GitHubUser;
|
|
78
|
+
commits: number;
|
|
79
|
+
reviews: number;
|
|
80
|
+
repositoriesContributedTo: number;
|
|
81
|
+
pullRequest: PullRequestStats;
|
|
82
|
+
issues: IssuesStats;
|
|
83
|
+
followers: number;
|
|
84
|
+
discussions: DiscussionsStats;
|
|
85
|
+
repositories: RepositoriesStats;
|
|
86
|
+
rank: RankStats;
|
|
87
|
+
}
|
|
88
|
+
interface GitHubUser {
|
|
89
|
+
name: string;
|
|
90
|
+
username: string;
|
|
91
|
+
avatar: string;
|
|
92
|
+
}
|
|
93
|
+
interface PullRequestStats {
|
|
94
|
+
totalCount: number;
|
|
95
|
+
mergedCount: number;
|
|
96
|
+
data: PullRequest[];
|
|
97
|
+
}
|
|
98
|
+
interface PullRequest {
|
|
99
|
+
repo: string;
|
|
100
|
+
title: string;
|
|
101
|
+
url: string;
|
|
102
|
+
created_at: string;
|
|
103
|
+
state: 'open' | 'closed' | 'merged' | 'draft';
|
|
104
|
+
number: number;
|
|
105
|
+
type: 'User' | 'Organization';
|
|
106
|
+
stars: number;
|
|
107
|
+
}
|
|
108
|
+
interface IssuesStats {
|
|
109
|
+
totalCount: number;
|
|
110
|
+
openCount: number;
|
|
111
|
+
closedCount: number;
|
|
112
|
+
}
|
|
113
|
+
interface DiscussionsStats {
|
|
114
|
+
totalCount: number;
|
|
115
|
+
commentsCount: number;
|
|
116
|
+
}
|
|
117
|
+
interface RepositoriesStats {
|
|
118
|
+
totalCount: number;
|
|
119
|
+
totalStargazers: number;
|
|
120
|
+
data: Repository[];
|
|
121
|
+
}
|
|
122
|
+
interface Repository {
|
|
123
|
+
name: string;
|
|
124
|
+
stargazers: number;
|
|
125
|
+
}
|
|
126
|
+
interface RankParams {
|
|
127
|
+
commits: number;
|
|
128
|
+
prs: number;
|
|
129
|
+
issues: number;
|
|
130
|
+
reviews: number;
|
|
131
|
+
repos: number;
|
|
132
|
+
stars: number;
|
|
133
|
+
followers: number;
|
|
134
|
+
}
|
|
135
|
+
interface RankStats {
|
|
136
|
+
level: string;
|
|
137
|
+
percentile: number;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/rank.d.ts
|
|
141
|
+
/**
|
|
142
|
+
* Calculates the users rank based on their GitHub statistics.
|
|
143
|
+
*/
|
|
144
|
+
declare function calculateRank({
|
|
145
|
+
commits,
|
|
146
|
+
prs,
|
|
147
|
+
issues,
|
|
148
|
+
reviews,
|
|
149
|
+
repos: _repos,
|
|
150
|
+
// unused
|
|
151
|
+
stars,
|
|
152
|
+
followers
|
|
153
|
+
}: RankParams): RankStats;
|
|
154
|
+
//#endregion
|
|
155
|
+
export { CommandOptions, DiscussionsStats, GitHubStats, GitHubUser, GraphQLContributions, GraphQLCountData, GraphQLRepositories, GraphQLRepository, GraphQLResponse, GraphQLStargazersData, GraphQLUser, IssuesStats, Options, PullRequest, PullRequestStats, RankParams, RankStats, RepositoriesStats, Repository, calculateRank };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/rank.ts
|
|
2
|
+
/**
|
|
3
|
+
* Calculates the exponential cdf.
|
|
4
|
+
*/
|
|
5
|
+
function exponential_cdf(x) {
|
|
6
|
+
return 1 - 2 ** -x;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Calculates the log normal cdf.
|
|
10
|
+
*/
|
|
11
|
+
function log_normal_cdf(x) {
|
|
12
|
+
return x / (1 + x);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Calculates the users rank based on their GitHub statistics.
|
|
16
|
+
*/
|
|
17
|
+
function calculateRank({ commits, prs, issues, reviews, repos: _repos, stars, followers }) {
|
|
18
|
+
const COMMITS_MEDIAN = 250;
|
|
19
|
+
const COMMITS_WEIGHT = 2;
|
|
20
|
+
const PRS_MEDIAN = 50;
|
|
21
|
+
const PRS_WEIGHT = 3;
|
|
22
|
+
const ISSUES_MEDIAN = 25;
|
|
23
|
+
const ISSUES_WEIGHT = 1;
|
|
24
|
+
const REVIEWS_MEDIAN = 2;
|
|
25
|
+
const REVIEWS_WEIGHT = 1;
|
|
26
|
+
const STARS_MEDIAN = 50;
|
|
27
|
+
const STARS_WEIGHT = 4;
|
|
28
|
+
const FOLLOWERS_MEDIAN = 10;
|
|
29
|
+
const FOLLOWERS_WEIGHT = 1;
|
|
30
|
+
const TOTAL_WEIGHT = COMMITS_WEIGHT + PRS_WEIGHT + ISSUES_WEIGHT + REVIEWS_WEIGHT + STARS_WEIGHT + FOLLOWERS_WEIGHT;
|
|
31
|
+
const THRESHOLDS = [
|
|
32
|
+
1,
|
|
33
|
+
12.5,
|
|
34
|
+
25,
|
|
35
|
+
37.5,
|
|
36
|
+
50,
|
|
37
|
+
62.5,
|
|
38
|
+
75,
|
|
39
|
+
87.5,
|
|
40
|
+
100
|
|
41
|
+
];
|
|
42
|
+
const LEVELS = [
|
|
43
|
+
"S",
|
|
44
|
+
"A+",
|
|
45
|
+
"A",
|
|
46
|
+
"A-",
|
|
47
|
+
"B+",
|
|
48
|
+
"B",
|
|
49
|
+
"B-",
|
|
50
|
+
"C+",
|
|
51
|
+
"C"
|
|
52
|
+
];
|
|
53
|
+
const rank = 1 - (COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) + PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) + ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) + REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) + STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) + FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) / TOTAL_WEIGHT;
|
|
54
|
+
return {
|
|
55
|
+
level: LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)],
|
|
56
|
+
percentile: rank * 100
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { calculateRank as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gh-statskit",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0-alpha.1",
|
|
5
|
+
"description": "A CLI to export GitHub stats and publish to a GitHub Gist.",
|
|
6
|
+
"author": "jinghaihan",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/jinghaihan/gh-statskit#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/jinghaihan/gh-statskit.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jinghaihan/gh-statskit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"github",
|
|
18
|
+
"stats",
|
|
19
|
+
"stars",
|
|
20
|
+
"commits",
|
|
21
|
+
"pull-requests",
|
|
22
|
+
"issues",
|
|
23
|
+
"contributions",
|
|
24
|
+
"gist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.mjs",
|
|
28
|
+
"./cli": "./dist/cli.mjs",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.mjs",
|
|
32
|
+
"module": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"bin": {
|
|
35
|
+
"gh-statskit": "./bin/cli.mjs"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin",
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^0.11.0",
|
|
43
|
+
"@octokit/core": "^7.0.6",
|
|
44
|
+
"ansis": "^4.2.0",
|
|
45
|
+
"cac": "^6.7.14",
|
|
46
|
+
"execa": "^9.6.1",
|
|
47
|
+
"p-retry": "^7.1.1",
|
|
48
|
+
"pathe": "^2.0.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@antfu/eslint-config": "^6.7.3",
|
|
52
|
+
"@types/node": "^25.0.3",
|
|
53
|
+
"bumpp": "^10.3.2",
|
|
54
|
+
"eslint": "^9.39.2",
|
|
55
|
+
"lint-staged": "^16.2.7",
|
|
56
|
+
"pncat": "^0.7.7",
|
|
57
|
+
"simple-git-hooks": "^2.13.1",
|
|
58
|
+
"taze": "^19.9.2",
|
|
59
|
+
"tsdown": "^0.18.4",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"vitest": "^4.0.16"
|
|
63
|
+
},
|
|
64
|
+
"simple-git-hooks": {
|
|
65
|
+
"pre-commit": "pnpm lint-staged"
|
|
66
|
+
},
|
|
67
|
+
"lint-staged": {
|
|
68
|
+
"*": "eslint --fix"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"start": "tsx ./src/cli.ts",
|
|
72
|
+
"build": "tsdown",
|
|
73
|
+
"deps": "taze major -I",
|
|
74
|
+
"lint": "eslint",
|
|
75
|
+
"typecheck": "tsc --noEmit",
|
|
76
|
+
"test": "vitest",
|
|
77
|
+
"release": "bumpp",
|
|
78
|
+
"bootstrap": "pnpm install"
|
|
79
|
+
}
|
|
80
|
+
}
|