chapa-cli 0.1.0 → 0.1.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/dist/index.js +307 -41
- package/package.json +3 -1
- package/dist/auth.d.ts +0 -7
- package/dist/auth.js +0 -14
- package/dist/auth.test.d.ts +0 -1
- package/dist/auth.test.js +0 -30
- package/dist/cli.d.ts +0 -9
- package/dist/cli.js +0 -28
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -43
- package/dist/fetch-emu.d.ts +0 -2
- package/dist/fetch-emu.js +0 -157
- package/dist/fetch-emu.test.d.ts +0 -1
- package/dist/fetch-emu.test.js +0 -164
- package/dist/index.d.ts +0 -2
- package/dist/upload.d.ts +0 -14
- package/dist/upload.js +0 -32
- package/dist/upload.test.d.ts +0 -1
- package/dist/upload.test.js +0 -114
package/dist/index.js
CHANGED
|
@@ -1,50 +1,316 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs as nodeParseArgs } from "util";
|
|
5
|
+
var DEFAULT_SERVER = "https://chapa.thecreativetoken.com";
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
8
|
+
const command = positional === "merge" ? "merge" : null;
|
|
9
|
+
const flagArgs = argv.filter((a) => a !== positional || a.startsWith("--"));
|
|
10
|
+
const { values } = nodeParseArgs({
|
|
11
|
+
args: flagArgs,
|
|
12
|
+
options: {
|
|
13
|
+
handle: { type: "string" },
|
|
14
|
+
"emu-handle": { type: "string" },
|
|
15
|
+
"emu-token": { type: "string" },
|
|
16
|
+
token: { type: "string" },
|
|
17
|
+
server: { type: "string", default: DEFAULT_SERVER },
|
|
18
|
+
version: { type: "boolean", short: "v", default: false },
|
|
19
|
+
help: { type: "boolean", short: "h", default: false }
|
|
20
|
+
},
|
|
21
|
+
strict: false
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
command,
|
|
25
|
+
handle: values.handle,
|
|
26
|
+
emuHandle: values["emu-handle"],
|
|
27
|
+
emuToken: values["emu-token"],
|
|
28
|
+
token: values.token,
|
|
29
|
+
server: values.server ?? DEFAULT_SERVER,
|
|
30
|
+
version: values.version ?? false,
|
|
31
|
+
help: values.help ?? false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/auth.ts
|
|
36
|
+
function resolveToken(flag, envVar) {
|
|
37
|
+
if (flag) return flag;
|
|
38
|
+
const env = process.env[envVar]?.trim();
|
|
39
|
+
if (env) return env;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ../shared/src/github-query.ts
|
|
44
|
+
var CONTRIBUTION_QUERY = `
|
|
45
|
+
query($login: String!, $since: DateTime!, $until: DateTime!, $historySince: GitTimestamp!, $historyUntil: GitTimestamp!) {
|
|
46
|
+
user(login: $login) {
|
|
47
|
+
login
|
|
48
|
+
name
|
|
49
|
+
avatarUrl
|
|
50
|
+
contributionsCollection(from: $since, to: $until) {
|
|
51
|
+
contributionCalendar {
|
|
52
|
+
totalContributions
|
|
53
|
+
weeks {
|
|
54
|
+
contributionDays {
|
|
55
|
+
date
|
|
56
|
+
contributionCount
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
pullRequestContributions(first: 100) {
|
|
61
|
+
totalCount
|
|
62
|
+
nodes {
|
|
63
|
+
pullRequest {
|
|
64
|
+
additions
|
|
65
|
+
deletions
|
|
66
|
+
changedFiles
|
|
67
|
+
merged
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
pullRequestReviewContributions(first: 1) {
|
|
72
|
+
totalCount
|
|
73
|
+
}
|
|
74
|
+
issueContributions(first: 1) {
|
|
75
|
+
totalCount
|
|
76
|
+
}
|
|
15
77
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
78
|
+
repositories(first: 20, ownerAffiliations: [OWNER, COLLABORATOR], orderBy: {field: PUSHED_AT, direction: DESC}) {
|
|
79
|
+
totalCount
|
|
80
|
+
nodes {
|
|
81
|
+
nameWithOwner
|
|
82
|
+
defaultBranchRef {
|
|
83
|
+
target {
|
|
84
|
+
... on Commit {
|
|
85
|
+
history(since: $historySince, until: $historyUntil) {
|
|
86
|
+
totalCount
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
21
92
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
// ../shared/src/scoring.ts
|
|
98
|
+
function computePrWeight(pr) {
|
|
99
|
+
const w = 0.5 + 0.25 * Math.log(1 + pr.changedFiles) + 0.25 * Math.log(1 + pr.additions + pr.deletions);
|
|
100
|
+
return Math.min(w, 3);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ../shared/src/stats-aggregation.ts
|
|
104
|
+
function buildStats90dFromRaw(raw) {
|
|
105
|
+
const heatmapData = [];
|
|
106
|
+
for (const week of raw.contributionCalendar.weeks) {
|
|
107
|
+
for (const day of week.contributionDays) {
|
|
108
|
+
heatmapData.push({ date: day.date, count: day.contributionCount });
|
|
26
109
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
}
|
|
111
|
+
const activeDays = heatmapData.filter((d) => d.count > 0).length;
|
|
112
|
+
const commitsTotal = raw.contributionCalendar.totalContributions;
|
|
113
|
+
const mergedPRs = raw.pullRequests.nodes.filter((pr) => pr.merged);
|
|
114
|
+
const prsMergedCount = mergedPRs.length;
|
|
115
|
+
const prsMergedWeight = Math.min(
|
|
116
|
+
mergedPRs.reduce((sum, pr) => sum + computePrWeight(pr), 0),
|
|
117
|
+
40
|
|
118
|
+
);
|
|
119
|
+
const linesAdded = mergedPRs.reduce((sum, pr) => sum + pr.additions, 0);
|
|
120
|
+
const linesDeleted = mergedPRs.reduce((sum, pr) => sum + pr.deletions, 0);
|
|
121
|
+
const reviewsSubmittedCount = raw.reviews.totalCount;
|
|
122
|
+
const issuesClosedCount = raw.issues.totalCount;
|
|
123
|
+
const repoCommits = raw.repositories.nodes.map((r) => ({
|
|
124
|
+
name: r.nameWithOwner,
|
|
125
|
+
commits: r.defaultBranchRef?.target?.history?.totalCount ?? 0
|
|
126
|
+
})).filter((r) => r.commits > 0);
|
|
127
|
+
const reposContributed = repoCommits.length;
|
|
128
|
+
const totalRepoCommits = repoCommits.reduce((s, r) => s + r.commits, 0);
|
|
129
|
+
const topRepoShare = totalRepoCommits > 0 ? Math.max(...repoCommits.map((r) => r.commits)) / totalRepoCommits : 0;
|
|
130
|
+
const maxDailyCount = Math.max(...heatmapData.map((d) => d.count), 0);
|
|
131
|
+
const maxCommitsIn10Min = maxDailyCount >= 30 ? maxDailyCount : 0;
|
|
132
|
+
return {
|
|
133
|
+
handle: raw.login,
|
|
134
|
+
displayName: raw.name ?? void 0,
|
|
135
|
+
avatarUrl: raw.avatarUrl,
|
|
136
|
+
commitsTotal,
|
|
137
|
+
activeDays,
|
|
138
|
+
prsMergedCount,
|
|
139
|
+
prsMergedWeight,
|
|
140
|
+
reviewsSubmittedCount,
|
|
141
|
+
issuesClosedCount,
|
|
142
|
+
linesAdded,
|
|
143
|
+
linesDeleted,
|
|
144
|
+
reposContributed,
|
|
145
|
+
topRepoShare,
|
|
146
|
+
maxCommitsIn10Min,
|
|
147
|
+
heatmapData,
|
|
148
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/fetch-emu.ts
|
|
153
|
+
async function fetchEmuStats(login, emuToken) {
|
|
154
|
+
const now = /* @__PURE__ */ new Date();
|
|
155
|
+
const since = new Date(now);
|
|
156
|
+
since.setDate(since.getDate() - 90);
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch("https://api.github.com/graphql", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
Accept: "application/json",
|
|
163
|
+
Authorization: `Bearer ${emuToken}`
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
query: CONTRIBUTION_QUERY,
|
|
167
|
+
variables: {
|
|
168
|
+
login,
|
|
169
|
+
since: since.toISOString(),
|
|
170
|
+
until: now.toISOString(),
|
|
171
|
+
historySince: since.toISOString(),
|
|
172
|
+
historyUntil: now.toISOString()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const body = await res.text().catch(() => "(unreadable)");
|
|
178
|
+
console.error(`[cli] GraphQL HTTP ${res.status}: ${body}`);
|
|
179
|
+
return null;
|
|
33
180
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
181
|
+
const json = await res.json();
|
|
182
|
+
if (!json.data?.user) return null;
|
|
183
|
+
const user = json.data.user;
|
|
184
|
+
const cc = user.contributionsCollection;
|
|
185
|
+
const prNodes = cc.pullRequestContributions.nodes.filter(
|
|
186
|
+
(n) => n != null && n.pullRequest != null
|
|
187
|
+
).map(
|
|
188
|
+
(n) => n.pullRequest
|
|
189
|
+
);
|
|
190
|
+
const raw = {
|
|
191
|
+
login: user.login,
|
|
192
|
+
name: user.name,
|
|
193
|
+
avatarUrl: user.avatarUrl,
|
|
194
|
+
contributionCalendar: cc.contributionCalendar,
|
|
195
|
+
pullRequests: {
|
|
196
|
+
totalCount: cc.pullRequestContributions.totalCount,
|
|
197
|
+
nodes: prNodes
|
|
198
|
+
},
|
|
199
|
+
reviews: { totalCount: cc.pullRequestReviewContributions.totalCount },
|
|
200
|
+
issues: { totalCount: cc.issueContributions.totalCount },
|
|
201
|
+
repositories: {
|
|
202
|
+
totalCount: user.repositories.totalCount,
|
|
203
|
+
nodes: user.repositories.nodes
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
return buildStats90dFromRaw(raw);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error(`[cli] fetch error:`, err);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/upload.ts
|
|
214
|
+
async function uploadSupplementalStats(opts) {
|
|
215
|
+
const baseUrl = opts.serverUrl.replace(/\/+$/, "");
|
|
216
|
+
const url = `${baseUrl}/api/supplemental`;
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(url, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: {
|
|
221
|
+
"Content-Type": "application/json",
|
|
222
|
+
Authorization: `Bearer ${opts.token}`
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
targetHandle: opts.targetHandle,
|
|
226
|
+
sourceHandle: opts.sourceHandle,
|
|
227
|
+
stats: opts.stats
|
|
228
|
+
})
|
|
43
229
|
});
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const body = await res.json().catch(() => ({}));
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: `Server returned ${res.status}: ${body.error ?? "Unknown error"}`
|
|
235
|
+
};
|
|
47
236
|
}
|
|
48
|
-
|
|
237
|
+
return { success: true };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `Upload failed: ${err.message}`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/index.ts
|
|
247
|
+
var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
248
|
+
var HELP_TEXT = `chapa-cli v${VERSION}
|
|
249
|
+
|
|
250
|
+
Merge GitHub EMU (Enterprise Managed User) contributions into your Chapa badge.
|
|
251
|
+
|
|
252
|
+
Usage:
|
|
253
|
+
chapa merge --handle <personal> --emu-handle <emu> [options]
|
|
254
|
+
|
|
255
|
+
Options:
|
|
256
|
+
--handle <handle> Your personal GitHub handle (required)
|
|
257
|
+
--emu-handle <handle> Your EMU GitHub handle (required)
|
|
258
|
+
--emu-token <token> EMU GitHub token (or set GITHUB_EMU_TOKEN)
|
|
259
|
+
--token <token> Personal GitHub token (or set GITHUB_TOKEN)
|
|
260
|
+
--server <url> Chapa server URL (default: https://chapa.thecreativetoken.com)
|
|
261
|
+
--version, -v Show version number
|
|
262
|
+
--help, -h Show this help message
|
|
263
|
+
`;
|
|
264
|
+
async function main() {
|
|
265
|
+
const args = parseArgs(process.argv.slice(2));
|
|
266
|
+
if (args.version) {
|
|
267
|
+
console.log(VERSION);
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
if (args.help) {
|
|
271
|
+
console.log(HELP_TEXT);
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
if (args.command !== "merge") {
|
|
275
|
+
console.error("Usage: chapa merge --handle <personal> --emu-handle <emu> [--emu-token <token>] [--token <token>] [--server <url>]");
|
|
276
|
+
console.error("\nRun 'chapa --help' for more information.");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
const handle = args.handle;
|
|
280
|
+
const emuHandle = args.emuHandle;
|
|
281
|
+
if (!handle || !emuHandle) {
|
|
282
|
+
console.error("Error: --handle and --emu-handle are required.");
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const emuToken = resolveToken(args.emuToken, "GITHUB_EMU_TOKEN");
|
|
286
|
+
if (!emuToken) {
|
|
287
|
+
console.error("Error: EMU token required. Use --emu-token, set GITHUB_EMU_TOKEN, or ensure `gh auth token` works.");
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
const personalToken = resolveToken(args.token, "GITHUB_TOKEN");
|
|
291
|
+
if (!personalToken) {
|
|
292
|
+
console.error("Error: Personal token required. Use --token, set GITHUB_TOKEN, or ensure `gh auth token` works.");
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
console.log(`Fetching stats for EMU account: ${emuHandle}...`);
|
|
296
|
+
const emuStats = await fetchEmuStats(emuHandle, emuToken);
|
|
297
|
+
if (!emuStats) {
|
|
298
|
+
console.error("Error: Failed to fetch EMU stats. Check your EMU token and handle.");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
console.log(`Found: ${emuStats.commitsTotal} commits, ${emuStats.prsMergedCount} PRs merged, ${emuStats.reviewsSubmittedCount} reviews`);
|
|
302
|
+
console.log(`Uploading supplemental stats to ${args.server}...`);
|
|
303
|
+
const result = await uploadSupplementalStats({
|
|
304
|
+
targetHandle: handle,
|
|
305
|
+
sourceHandle: emuHandle,
|
|
306
|
+
stats: emuStats,
|
|
307
|
+
token: personalToken,
|
|
308
|
+
serverUrl: args.server
|
|
309
|
+
});
|
|
310
|
+
if (!result.success) {
|
|
311
|
+
console.error(`Error: ${result.error}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
console.log("Success! Supplemental stats uploaded. Your badge will reflect combined data on next refresh.");
|
|
49
315
|
}
|
|
50
316
|
main();
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chapa-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Merge GitHub EMU contributions into your Chapa developer impact badge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"chapa-cli": "dist/index.js",
|
|
7
8
|
"chapa": "dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsup",
|
|
15
|
+
"prepublishOnly": "tsup",
|
|
14
16
|
"typecheck": "tsc --noEmit"
|
|
15
17
|
},
|
|
16
18
|
"keywords": [
|
package/dist/auth.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token resolution for CLI.
|
|
3
|
-
*
|
|
4
|
-
* Priority: explicit flag → environment variable → null
|
|
5
|
-
* (gh auth token fallback is handled at the CLI orchestration level)
|
|
6
|
-
*/
|
|
7
|
-
export declare function resolveToken(flag: string | undefined, envVar: string): string | null;
|
package/dist/auth.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token resolution for CLI.
|
|
3
|
-
*
|
|
4
|
-
* Priority: explicit flag → environment variable → null
|
|
5
|
-
* (gh auth token fallback is handled at the CLI orchestration level)
|
|
6
|
-
*/
|
|
7
|
-
export function resolveToken(flag, envVar) {
|
|
8
|
-
if (flag)
|
|
9
|
-
return flag;
|
|
10
|
-
const env = process.env[envVar]?.trim();
|
|
11
|
-
if (env)
|
|
12
|
-
return env;
|
|
13
|
-
return null;
|
|
14
|
-
}
|
package/dist/auth.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/auth.test.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { resolveToken } from "./auth";
|
|
3
|
-
describe("resolveToken", () => {
|
|
4
|
-
const originalEnv = process.env;
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
process.env = { ...originalEnv };
|
|
7
|
-
});
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
process.env = originalEnv;
|
|
10
|
-
});
|
|
11
|
-
it("returns explicit flag value when provided", () => {
|
|
12
|
-
const token = resolveToken("ghp_explicit", "GITHUB_TOKEN");
|
|
13
|
-
expect(token).toBe("ghp_explicit");
|
|
14
|
-
});
|
|
15
|
-
it("falls back to environment variable when flag is undefined", () => {
|
|
16
|
-
process.env.GITHUB_TOKEN = "ghp_from_env";
|
|
17
|
-
const token = resolveToken(undefined, "GITHUB_TOKEN");
|
|
18
|
-
expect(token).toBe("ghp_from_env");
|
|
19
|
-
});
|
|
20
|
-
it("trims whitespace from environment variable", () => {
|
|
21
|
-
process.env.GITHUB_TOKEN = " ghp_padded \n";
|
|
22
|
-
const token = resolveToken(undefined, "GITHUB_TOKEN");
|
|
23
|
-
expect(token).toBe("ghp_padded");
|
|
24
|
-
});
|
|
25
|
-
it("returns null when no flag and no env var", () => {
|
|
26
|
-
delete process.env.GITHUB_TOKEN;
|
|
27
|
-
const token = resolveToken(undefined, "GITHUB_TOKEN");
|
|
28
|
-
expect(token).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
});
|
package/dist/cli.d.ts
DELETED
package/dist/cli.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { parseArgs as nodeParseArgs } from "node:util";
|
|
2
|
-
const DEFAULT_SERVER = "https://chapa.thecreativetoken.com";
|
|
3
|
-
export function parseArgs(argv) {
|
|
4
|
-
// Extract positional command before flags
|
|
5
|
-
const positional = argv.find((a) => !a.startsWith("--"));
|
|
6
|
-
const command = positional === "merge" ? "merge" : null;
|
|
7
|
-
// Remove positional from argv for nodeParseArgs
|
|
8
|
-
const flagArgs = argv.filter((a) => a !== positional || a.startsWith("--"));
|
|
9
|
-
const { values } = nodeParseArgs({
|
|
10
|
-
args: flagArgs,
|
|
11
|
-
options: {
|
|
12
|
-
handle: { type: "string" },
|
|
13
|
-
"emu-handle": { type: "string" },
|
|
14
|
-
"emu-token": { type: "string" },
|
|
15
|
-
token: { type: "string" },
|
|
16
|
-
server: { type: "string", default: DEFAULT_SERVER },
|
|
17
|
-
},
|
|
18
|
-
strict: false,
|
|
19
|
-
});
|
|
20
|
-
return {
|
|
21
|
-
command,
|
|
22
|
-
handle: values.handle,
|
|
23
|
-
emuHandle: values["emu-handle"],
|
|
24
|
-
emuToken: values["emu-token"],
|
|
25
|
-
token: values.token,
|
|
26
|
-
server: values.server ?? DEFAULT_SERVER,
|
|
27
|
-
};
|
|
28
|
-
}
|
package/dist/cli.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/cli.test.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseArgs } from "./cli";
|
|
3
|
-
describe("parseArgs", () => {
|
|
4
|
-
it("parses all required flags", () => {
|
|
5
|
-
const args = parseArgs([
|
|
6
|
-
"merge",
|
|
7
|
-
"--handle", "juan294",
|
|
8
|
-
"--emu-handle", "Juan_corp",
|
|
9
|
-
]);
|
|
10
|
-
expect(args.command).toBe("merge");
|
|
11
|
-
expect(args.handle).toBe("juan294");
|
|
12
|
-
expect(args.emuHandle).toBe("Juan_corp");
|
|
13
|
-
});
|
|
14
|
-
it("parses optional flags", () => {
|
|
15
|
-
const args = parseArgs([
|
|
16
|
-
"merge",
|
|
17
|
-
"--handle", "juan294",
|
|
18
|
-
"--emu-handle", "Juan_corp",
|
|
19
|
-
"--emu-token", "ghp_emu",
|
|
20
|
-
"--token", "gho_personal",
|
|
21
|
-
"--server", "http://localhost:3001",
|
|
22
|
-
]);
|
|
23
|
-
expect(args.emuToken).toBe("ghp_emu");
|
|
24
|
-
expect(args.token).toBe("gho_personal");
|
|
25
|
-
expect(args.server).toBe("http://localhost:3001");
|
|
26
|
-
});
|
|
27
|
-
it("uses default server URL when not provided", () => {
|
|
28
|
-
const args = parseArgs([
|
|
29
|
-
"merge",
|
|
30
|
-
"--handle", "juan294",
|
|
31
|
-
"--emu-handle", "Juan_corp",
|
|
32
|
-
]);
|
|
33
|
-
expect(args.server).toBe("https://chapa.thecreativetoken.com");
|
|
34
|
-
});
|
|
35
|
-
it("returns null command when no positional arg", () => {
|
|
36
|
-
const args = parseArgs(["--handle", "juan294"]);
|
|
37
|
-
expect(args.command).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
it("returns null command for unknown commands", () => {
|
|
40
|
-
const args = parseArgs(["unknown", "--handle", "juan294"]);
|
|
41
|
-
expect(args.command).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
});
|
package/dist/fetch-emu.d.ts
DELETED
package/dist/fetch-emu.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// GraphQL query (duplicated from apps/web/lib/github/queries.ts)
|
|
3
|
-
// This is ~stable code; sharing via a common package is a future refactor.
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
const CONTRIBUTION_QUERY = `
|
|
6
|
-
query($login: String!, $since: DateTime!, $until: DateTime!, $historySince: GitTimestamp!, $historyUntil: GitTimestamp!) {
|
|
7
|
-
user(login: $login) {
|
|
8
|
-
login
|
|
9
|
-
name
|
|
10
|
-
avatarUrl
|
|
11
|
-
contributionsCollection(from: $since, to: $until) {
|
|
12
|
-
contributionCalendar {
|
|
13
|
-
totalContributions
|
|
14
|
-
weeks {
|
|
15
|
-
contributionDays {
|
|
16
|
-
date
|
|
17
|
-
contributionCount
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
pullRequestContributions(first: 100) {
|
|
22
|
-
totalCount
|
|
23
|
-
nodes {
|
|
24
|
-
pullRequest {
|
|
25
|
-
additions
|
|
26
|
-
deletions
|
|
27
|
-
changedFiles
|
|
28
|
-
merged
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
pullRequestReviewContributions(first: 1) {
|
|
33
|
-
totalCount
|
|
34
|
-
}
|
|
35
|
-
issueContributions(first: 1) {
|
|
36
|
-
totalCount
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
repositories(first: 20, ownerAffiliations: [OWNER, COLLABORATOR], orderBy: {field: PUSHED_AT, direction: DESC}) {
|
|
40
|
-
totalCount
|
|
41
|
-
nodes {
|
|
42
|
-
nameWithOwner
|
|
43
|
-
defaultBranchRef {
|
|
44
|
-
target {
|
|
45
|
-
... on Commit {
|
|
46
|
-
history(since: $historySince, until: $historyUntil) {
|
|
47
|
-
totalCount
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
`;
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// PR weight formula (duplicated from apps/web/lib/github/stats90d.ts)
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
function computePrWeight(pr) {
|
|
61
|
-
const w = 0.5 +
|
|
62
|
-
0.25 * Math.log(1 + pr.changedFiles) +
|
|
63
|
-
0.25 * Math.log(1 + pr.additions + pr.deletions);
|
|
64
|
-
return Math.min(w, 3.0);
|
|
65
|
-
}
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Fetch EMU stats via GraphQL (requires EMU token with auth)
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
export async function fetchEmuStats(login, emuToken) {
|
|
70
|
-
const now = new Date();
|
|
71
|
-
const since = new Date(now);
|
|
72
|
-
since.setDate(since.getDate() - 90);
|
|
73
|
-
try {
|
|
74
|
-
const res = await fetch("https://api.github.com/graphql", {
|
|
75
|
-
method: "POST",
|
|
76
|
-
headers: {
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
Accept: "application/json",
|
|
79
|
-
Authorization: `Bearer ${emuToken}`,
|
|
80
|
-
},
|
|
81
|
-
body: JSON.stringify({
|
|
82
|
-
query: CONTRIBUTION_QUERY,
|
|
83
|
-
variables: {
|
|
84
|
-
login,
|
|
85
|
-
since: since.toISOString(),
|
|
86
|
-
until: now.toISOString(),
|
|
87
|
-
historySince: since.toISOString(),
|
|
88
|
-
historyUntil: now.toISOString(),
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
});
|
|
92
|
-
if (!res.ok) {
|
|
93
|
-
const body = await res.text().catch(() => "(unreadable)");
|
|
94
|
-
console.error(`[cli] GraphQL HTTP ${res.status}: ${body}`);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const json = await res.json();
|
|
98
|
-
if (!json.data?.user)
|
|
99
|
-
return null;
|
|
100
|
-
const user = json.data.user;
|
|
101
|
-
const cc = user.contributionsCollection;
|
|
102
|
-
// Heatmap
|
|
103
|
-
const heatmapData = [];
|
|
104
|
-
for (const week of cc.contributionCalendar.weeks) {
|
|
105
|
-
for (const day of week.contributionDays) {
|
|
106
|
-
heatmapData.push({ date: day.date, count: day.contributionCount });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const activeDays = heatmapData.filter((d) => d.count > 0).length;
|
|
110
|
-
// PRs
|
|
111
|
-
const prNodes = cc.pullRequestContributions.nodes
|
|
112
|
-
.filter((n) => n != null && n.pullRequest != null)
|
|
113
|
-
.map((n) => n.pullRequest);
|
|
114
|
-
const mergedPRs = prNodes.filter((pr) => pr.merged);
|
|
115
|
-
const prsMergedCount = mergedPRs.length;
|
|
116
|
-
const prsMergedWeight = Math.min(mergedPRs.reduce((sum, pr) => sum + computePrWeight(pr), 0), 40);
|
|
117
|
-
const linesAdded = mergedPRs.reduce((sum, pr) => sum + pr.additions, 0);
|
|
118
|
-
const linesDeleted = mergedPRs.reduce((sum, pr) => sum + pr.deletions, 0);
|
|
119
|
-
// Repos
|
|
120
|
-
const repoCommits = user.repositories.nodes
|
|
121
|
-
.map((r) => ({
|
|
122
|
-
name: r.nameWithOwner,
|
|
123
|
-
commits: r.defaultBranchRef?.target?.history?.totalCount ?? 0,
|
|
124
|
-
}))
|
|
125
|
-
.filter((r) => r.commits > 0);
|
|
126
|
-
const reposContributed = repoCommits.length;
|
|
127
|
-
const totalRepoCommits = repoCommits.reduce((s, r) => s + r.commits, 0);
|
|
128
|
-
const topRepoShare = totalRepoCommits > 0
|
|
129
|
-
? Math.max(...repoCommits.map((r) => r.commits)) / totalRepoCommits
|
|
130
|
-
: 0;
|
|
131
|
-
// Max daily spike approximation
|
|
132
|
-
const maxDailyCount = Math.max(...heatmapData.map((d) => d.count), 0);
|
|
133
|
-
const maxCommitsIn10Min = maxDailyCount >= 30 ? maxDailyCount : 0;
|
|
134
|
-
return {
|
|
135
|
-
handle: user.login,
|
|
136
|
-
displayName: user.name ?? undefined,
|
|
137
|
-
avatarUrl: user.avatarUrl,
|
|
138
|
-
commitsTotal: cc.contributionCalendar.totalContributions,
|
|
139
|
-
activeDays,
|
|
140
|
-
prsMergedCount,
|
|
141
|
-
prsMergedWeight,
|
|
142
|
-
reviewsSubmittedCount: cc.pullRequestReviewContributions.totalCount,
|
|
143
|
-
issuesClosedCount: cc.issueContributions.totalCount,
|
|
144
|
-
linesAdded,
|
|
145
|
-
linesDeleted,
|
|
146
|
-
reposContributed,
|
|
147
|
-
topRepoShare,
|
|
148
|
-
maxCommitsIn10Min,
|
|
149
|
-
heatmapData,
|
|
150
|
-
fetchedAt: new Date().toISOString(),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
console.error(`[cli] fetch error:`, err);
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
package/dist/fetch-emu.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/fetch-emu.test.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { fetchEmuStats } from "./fetch-emu";
|
|
3
|
-
// Mock global fetch
|
|
4
|
-
const mockFetch = vi.fn();
|
|
5
|
-
describe("fetchEmuStats", () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
8
|
-
});
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.restoreAllMocks();
|
|
11
|
-
});
|
|
12
|
-
it("returns Stats90d on successful GraphQL response", async () => {
|
|
13
|
-
mockFetch.mockResolvedValue({
|
|
14
|
-
ok: true,
|
|
15
|
-
json: async () => ({
|
|
16
|
-
data: {
|
|
17
|
-
user: {
|
|
18
|
-
login: "Juan_corp",
|
|
19
|
-
name: "Juan Corp",
|
|
20
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
21
|
-
contributionsCollection: {
|
|
22
|
-
contributionCalendar: {
|
|
23
|
-
totalContributions: 42,
|
|
24
|
-
weeks: [
|
|
25
|
-
{
|
|
26
|
-
contributionDays: [
|
|
27
|
-
{ date: "2025-01-01", contributionCount: 5 },
|
|
28
|
-
{ date: "2025-01-02", contributionCount: 0 },
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
pullRequestContributions: {
|
|
34
|
-
totalCount: 3,
|
|
35
|
-
nodes: [
|
|
36
|
-
{
|
|
37
|
-
pullRequest: {
|
|
38
|
-
additions: 100,
|
|
39
|
-
deletions: 50,
|
|
40
|
-
changedFiles: 3,
|
|
41
|
-
merged: true,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
pullRequest: {
|
|
46
|
-
additions: 20,
|
|
47
|
-
deletions: 5,
|
|
48
|
-
changedFiles: 1,
|
|
49
|
-
merged: false,
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
pullRequestReviewContributions: { totalCount: 5 },
|
|
55
|
-
issueContributions: { totalCount: 2 },
|
|
56
|
-
},
|
|
57
|
-
repositories: {
|
|
58
|
-
totalCount: 3,
|
|
59
|
-
nodes: [
|
|
60
|
-
{
|
|
61
|
-
nameWithOwner: "org/repo1",
|
|
62
|
-
defaultBranchRef: {
|
|
63
|
-
target: { history: { totalCount: 30 } },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
nameWithOwner: "org/repo2",
|
|
68
|
-
defaultBranchRef: {
|
|
69
|
-
target: { history: { totalCount: 10 } },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
});
|
|
78
|
-
const result = await fetchEmuStats("Juan_corp", "ghp_emu_token");
|
|
79
|
-
expect(result).not.toBeNull();
|
|
80
|
-
expect(result.handle).toBe("Juan_corp");
|
|
81
|
-
expect(result.commitsTotal).toBe(42);
|
|
82
|
-
expect(result.prsMergedCount).toBe(1); // only merged
|
|
83
|
-
expect(result.reviewsSubmittedCount).toBe(5);
|
|
84
|
-
expect(result.issuesClosedCount).toBe(2);
|
|
85
|
-
expect(result.reposContributed).toBe(2);
|
|
86
|
-
expect(result.heatmapData).toHaveLength(2);
|
|
87
|
-
expect(result.activeDays).toBe(1); // only Jan 1 has count > 0
|
|
88
|
-
});
|
|
89
|
-
it("sends Authorization header with EMU token", async () => {
|
|
90
|
-
mockFetch.mockResolvedValue({
|
|
91
|
-
ok: true,
|
|
92
|
-
json: async () => ({ data: { user: null } }),
|
|
93
|
-
});
|
|
94
|
-
await fetchEmuStats("corp_user", "ghp_test_token");
|
|
95
|
-
expect(mockFetch).toHaveBeenCalledWith("https://api.github.com/graphql", expect.objectContaining({
|
|
96
|
-
headers: expect.objectContaining({
|
|
97
|
-
Authorization: "Bearer ghp_test_token",
|
|
98
|
-
}),
|
|
99
|
-
}));
|
|
100
|
-
});
|
|
101
|
-
it("returns null when API returns HTTP error", async () => {
|
|
102
|
-
mockFetch.mockResolvedValue({
|
|
103
|
-
ok: false,
|
|
104
|
-
status: 401,
|
|
105
|
-
text: async () => "Unauthorized",
|
|
106
|
-
});
|
|
107
|
-
const result = await fetchEmuStats("corp_user", "bad_token");
|
|
108
|
-
expect(result).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
it("returns null when user is not found", async () => {
|
|
111
|
-
mockFetch.mockResolvedValue({
|
|
112
|
-
ok: true,
|
|
113
|
-
json: async () => ({ data: { user: null } }),
|
|
114
|
-
});
|
|
115
|
-
const result = await fetchEmuStats("nonexistent_user", "ghp_token");
|
|
116
|
-
expect(result).toBeNull();
|
|
117
|
-
});
|
|
118
|
-
it("returns null when fetch throws", async () => {
|
|
119
|
-
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
120
|
-
const result = await fetchEmuStats("corp_user", "ghp_token");
|
|
121
|
-
expect(result).toBeNull();
|
|
122
|
-
});
|
|
123
|
-
it("filters out null PR nodes", async () => {
|
|
124
|
-
mockFetch.mockResolvedValue({
|
|
125
|
-
ok: true,
|
|
126
|
-
json: async () => ({
|
|
127
|
-
data: {
|
|
128
|
-
user: {
|
|
129
|
-
login: "corp_user",
|
|
130
|
-
name: null,
|
|
131
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
132
|
-
contributionsCollection: {
|
|
133
|
-
contributionCalendar: {
|
|
134
|
-
totalContributions: 10,
|
|
135
|
-
weeks: [],
|
|
136
|
-
},
|
|
137
|
-
pullRequestContributions: {
|
|
138
|
-
totalCount: 2,
|
|
139
|
-
nodes: [
|
|
140
|
-
null,
|
|
141
|
-
{ pullRequest: null },
|
|
142
|
-
{
|
|
143
|
-
pullRequest: {
|
|
144
|
-
additions: 10,
|
|
145
|
-
deletions: 5,
|
|
146
|
-
changedFiles: 1,
|
|
147
|
-
merged: true,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
},
|
|
152
|
-
pullRequestReviewContributions: { totalCount: 0 },
|
|
153
|
-
issueContributions: { totalCount: 0 },
|
|
154
|
-
},
|
|
155
|
-
repositories: { totalCount: 0, nodes: [] },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
});
|
|
160
|
-
const result = await fetchEmuStats("corp_user", "ghp_token");
|
|
161
|
-
expect(result).not.toBeNull();
|
|
162
|
-
expect(result.prsMergedCount).toBe(1);
|
|
163
|
-
});
|
|
164
|
-
});
|
package/dist/index.d.ts
DELETED
package/dist/upload.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Stats90d } from "@chapa/shared";
|
|
2
|
-
interface UploadOptions {
|
|
3
|
-
targetHandle: string;
|
|
4
|
-
sourceHandle: string;
|
|
5
|
-
stats: Stats90d;
|
|
6
|
-
token: string;
|
|
7
|
-
serverUrl: string;
|
|
8
|
-
}
|
|
9
|
-
interface UploadResult {
|
|
10
|
-
success: boolean;
|
|
11
|
-
error?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function uploadSupplementalStats(opts: UploadOptions): Promise<UploadResult>;
|
|
14
|
-
export {};
|
package/dist/upload.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export async function uploadSupplementalStats(opts) {
|
|
2
|
-
const baseUrl = opts.serverUrl.replace(/\/+$/, "");
|
|
3
|
-
const url = `${baseUrl}/api/supplemental`;
|
|
4
|
-
try {
|
|
5
|
-
const res = await fetch(url, {
|
|
6
|
-
method: "POST",
|
|
7
|
-
headers: {
|
|
8
|
-
"Content-Type": "application/json",
|
|
9
|
-
Authorization: `Bearer ${opts.token}`,
|
|
10
|
-
},
|
|
11
|
-
body: JSON.stringify({
|
|
12
|
-
targetHandle: opts.targetHandle,
|
|
13
|
-
sourceHandle: opts.sourceHandle,
|
|
14
|
-
stats: opts.stats,
|
|
15
|
-
}),
|
|
16
|
-
});
|
|
17
|
-
if (!res.ok) {
|
|
18
|
-
const body = await res.json().catch(() => ({}));
|
|
19
|
-
return {
|
|
20
|
-
success: false,
|
|
21
|
-
error: `Server returned ${res.status}: ${body.error ?? "Unknown error"}`,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
return { success: true };
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
return {
|
|
28
|
-
success: false,
|
|
29
|
-
error: `Upload failed: ${err.message}`,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
package/dist/upload.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/upload.test.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { uploadSupplementalStats } from "./upload";
|
|
3
|
-
const mockFetch = vi.fn();
|
|
4
|
-
function makeStats() {
|
|
5
|
-
return {
|
|
6
|
-
handle: "corp_user",
|
|
7
|
-
commitsTotal: 30,
|
|
8
|
-
activeDays: 10,
|
|
9
|
-
prsMergedCount: 3,
|
|
10
|
-
prsMergedWeight: 5,
|
|
11
|
-
reviewsSubmittedCount: 2,
|
|
12
|
-
issuesClosedCount: 1,
|
|
13
|
-
linesAdded: 500,
|
|
14
|
-
linesDeleted: 200,
|
|
15
|
-
reposContributed: 2,
|
|
16
|
-
topRepoShare: 0.6,
|
|
17
|
-
maxCommitsIn10Min: 3,
|
|
18
|
-
heatmapData: [],
|
|
19
|
-
fetchedAt: new Date().toISOString(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
describe("uploadSupplementalStats", () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
25
|
-
});
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
vi.restoreAllMocks();
|
|
28
|
-
});
|
|
29
|
-
it("sends POST with correct body and auth header", async () => {
|
|
30
|
-
mockFetch.mockResolvedValue({
|
|
31
|
-
ok: true,
|
|
32
|
-
status: 200,
|
|
33
|
-
json: async () => ({ success: true }),
|
|
34
|
-
});
|
|
35
|
-
const result = await uploadSupplementalStats({
|
|
36
|
-
targetHandle: "juan294",
|
|
37
|
-
sourceHandle: "corp_user",
|
|
38
|
-
stats: makeStats(),
|
|
39
|
-
token: "gho_personal",
|
|
40
|
-
serverUrl: "https://chapa.thecreativetoken.com",
|
|
41
|
-
});
|
|
42
|
-
expect(result.success).toBe(true);
|
|
43
|
-
expect(mockFetch).toHaveBeenCalledWith("https://chapa.thecreativetoken.com/api/supplemental", expect.objectContaining({
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: expect.objectContaining({
|
|
46
|
-
Authorization: "Bearer gho_personal",
|
|
47
|
-
"Content-Type": "application/json",
|
|
48
|
-
}),
|
|
49
|
-
}));
|
|
50
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
51
|
-
expect(body.targetHandle).toBe("juan294");
|
|
52
|
-
expect(body.sourceHandle).toBe("corp_user");
|
|
53
|
-
expect(body.stats).toEqual(expect.objectContaining({ handle: "corp_user", commitsTotal: 30 }));
|
|
54
|
-
});
|
|
55
|
-
it("returns error message on 401", async () => {
|
|
56
|
-
mockFetch.mockResolvedValue({
|
|
57
|
-
ok: false,
|
|
58
|
-
status: 401,
|
|
59
|
-
json: async () => ({ error: "Invalid token" }),
|
|
60
|
-
});
|
|
61
|
-
const result = await uploadSupplementalStats({
|
|
62
|
-
targetHandle: "juan294",
|
|
63
|
-
sourceHandle: "corp_user",
|
|
64
|
-
stats: makeStats(),
|
|
65
|
-
token: "bad_token",
|
|
66
|
-
serverUrl: "https://chapa.thecreativetoken.com",
|
|
67
|
-
});
|
|
68
|
-
expect(result.success).toBe(false);
|
|
69
|
-
expect(result.error).toContain("401");
|
|
70
|
-
});
|
|
71
|
-
it("returns error message on 403", async () => {
|
|
72
|
-
mockFetch.mockResolvedValue({
|
|
73
|
-
ok: false,
|
|
74
|
-
status: 403,
|
|
75
|
-
json: async () => ({ error: "Handle mismatch" }),
|
|
76
|
-
});
|
|
77
|
-
const result = await uploadSupplementalStats({
|
|
78
|
-
targetHandle: "juan294",
|
|
79
|
-
sourceHandle: "corp_user",
|
|
80
|
-
stats: makeStats(),
|
|
81
|
-
token: "wrong_user_token",
|
|
82
|
-
serverUrl: "https://chapa.thecreativetoken.com",
|
|
83
|
-
});
|
|
84
|
-
expect(result.success).toBe(false);
|
|
85
|
-
expect(result.error).toContain("403");
|
|
86
|
-
});
|
|
87
|
-
it("returns error on network failure", async () => {
|
|
88
|
-
mockFetch.mockRejectedValue(new Error("Connection refused"));
|
|
89
|
-
const result = await uploadSupplementalStats({
|
|
90
|
-
targetHandle: "juan294",
|
|
91
|
-
sourceHandle: "corp_user",
|
|
92
|
-
stats: makeStats(),
|
|
93
|
-
token: "gho_personal",
|
|
94
|
-
serverUrl: "https://chapa.thecreativetoken.com",
|
|
95
|
-
});
|
|
96
|
-
expect(result.success).toBe(false);
|
|
97
|
-
expect(result.error).toContain("Connection refused");
|
|
98
|
-
});
|
|
99
|
-
it("strips trailing slash from server URL", async () => {
|
|
100
|
-
mockFetch.mockResolvedValue({
|
|
101
|
-
ok: true,
|
|
102
|
-
status: 200,
|
|
103
|
-
json: async () => ({ success: true }),
|
|
104
|
-
});
|
|
105
|
-
await uploadSupplementalStats({
|
|
106
|
-
targetHandle: "juan294",
|
|
107
|
-
sourceHandle: "corp_user",
|
|
108
|
-
stats: makeStats(),
|
|
109
|
-
token: "gho_personal",
|
|
110
|
-
serverUrl: "https://chapa.thecreativetoken.com/",
|
|
111
|
-
});
|
|
112
|
-
expect(mockFetch).toHaveBeenCalledWith("https://chapa.thecreativetoken.com/api/supplemental", expect.anything());
|
|
113
|
-
});
|
|
114
|
-
});
|