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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ import '../dist/cli.mjs'
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 { };
@@ -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,3 @@
1
+ import { t as calculateRank } from "./rank-CFWMbXNE.mjs";
2
+
3
+ export { calculateRank };
@@ -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
+ }