chapa-cli 0.1.4 → 0.2.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/README.md +27 -16
- package/dist/index.js +133 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,44 +4,55 @@ Merge GitHub EMU (Enterprise Managed User) contributions into your [Chapa](https
|
|
|
4
4
|
|
|
5
5
|
If your employer uses GitHub EMU accounts, your work contributions are invisible on your personal profile. This CLI fetches your EMU stats and uploads them to Chapa so your badge reflects your full impact.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Quick start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# 1. Authenticate with Chapa (opens browser)
|
|
11
|
+
npx chapa-cli login
|
|
12
|
+
|
|
13
|
+
# 2. Merge your EMU stats
|
|
14
|
+
npx chapa-cli merge --emu-handle your-emu-handle
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
That's it! Your personal handle and auth token are auto-detected from the login step.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
| Command | Description |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| `chapa login` | Authenticate via browser (like `npm login`) |
|
|
24
|
+
| `chapa logout` | Clear stored credentials |
|
|
25
|
+
| `chapa merge` | Fetch EMU stats and upload to Chapa |
|
|
26
|
+
|
|
27
|
+
## Options (for merge)
|
|
16
28
|
|
|
17
29
|
| Flag | Description | Required |
|
|
18
30
|
|------|-------------|----------|
|
|
19
|
-
| `--handle <handle>` | Your personal GitHub handle | Yes |
|
|
20
31
|
| `--emu-handle <handle>` | Your EMU GitHub handle | Yes |
|
|
21
32
|
| `--emu-token <token>` | EMU GitHub token | No* |
|
|
22
|
-
| `--
|
|
33
|
+
| `--handle <handle>` | Override personal handle | No |
|
|
34
|
+
| `--token <token>` | Override auth token | No |
|
|
23
35
|
| `--server <url>` | Chapa server URL | No |
|
|
24
36
|
| `--version`, `-v` | Show version number | |
|
|
25
37
|
| `--help`, `-h` | Show help message | |
|
|
26
38
|
|
|
27
|
-
*
|
|
39
|
+
*EMU token is resolved from: flag > `GITHUB_EMU_TOKEN` env var.
|
|
28
40
|
|
|
29
|
-
##
|
|
41
|
+
## EMU token setup
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
You need one token from your EMU (work) GitHub account with `read:user` scope:
|
|
32
44
|
|
|
33
45
|
```bash
|
|
34
|
-
export GITHUB_TOKEN=ghp_your_personal_token
|
|
35
46
|
export GITHUB_EMU_TOKEN=ghp_your_emu_token
|
|
36
47
|
```
|
|
37
48
|
|
|
38
|
-
Tokens need the `read:user` scope at minimum.
|
|
39
|
-
|
|
40
49
|
## How it works
|
|
41
50
|
|
|
42
|
-
1.
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
51
|
+
1. `chapa login` opens your browser for OAuth authentication with Chapa
|
|
52
|
+
2. After approval, a signed CLI token is saved to `~/.chapa/credentials.json`
|
|
53
|
+
3. `chapa merge` fetches your last 12 months of EMU activity via GitHub GraphQL API
|
|
54
|
+
4. Stats (commits, PRs merged, reviews) are uploaded to the Chapa server
|
|
55
|
+
5. Your badge recalculates on next refresh, combining personal + EMU contributions
|
|
45
56
|
|
|
46
57
|
## License
|
|
47
58
|
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs as nodeParseArgs } from "util";
|
|
5
5
|
var DEFAULT_SERVER = "https://chapa.thecreativetoken.com";
|
|
6
|
+
var VALID_COMMANDS = ["merge", "login", "logout"];
|
|
6
7
|
function parseArgs(argv) {
|
|
7
|
-
const positional = argv.find((a) => !a.startsWith("--"));
|
|
8
|
-
const command = positional
|
|
8
|
+
const positional = argv.find((a) => !a.startsWith("--") && !a.startsWith("-"));
|
|
9
|
+
const command = VALID_COMMANDS.includes(positional) ? positional : null;
|
|
9
10
|
const flagArgs = argv.filter((a) => a !== positional || a.startsWith("--"));
|
|
10
11
|
const { values } = nodeParseArgs({
|
|
11
12
|
args: flagArgs,
|
|
@@ -172,7 +173,7 @@ function buildStatsFromRaw(raw) {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// src/fetch-emu.ts
|
|
175
|
-
async function fetchEmuStats(
|
|
176
|
+
async function fetchEmuStats(login2, emuToken) {
|
|
176
177
|
const now = /* @__PURE__ */ new Date();
|
|
177
178
|
const since = new Date(now);
|
|
178
179
|
since.setDate(since.getDate() - SCORING_WINDOW_DAYS);
|
|
@@ -187,7 +188,7 @@ async function fetchEmuStats(login, emuToken) {
|
|
|
187
188
|
body: JSON.stringify({
|
|
188
189
|
query: CONTRIBUTION_QUERY,
|
|
189
190
|
variables: {
|
|
190
|
-
login,
|
|
191
|
+
login: login2,
|
|
191
192
|
since: since.toISOString(),
|
|
192
193
|
until: now.toISOString(),
|
|
193
194
|
historySince: since.toISOString(),
|
|
@@ -202,7 +203,7 @@ async function fetchEmuStats(login, emuToken) {
|
|
|
202
203
|
}
|
|
203
204
|
const json = await res.json();
|
|
204
205
|
if (json.errors) {
|
|
205
|
-
console.error(`[cli] GraphQL errors for ${
|
|
206
|
+
console.error(`[cli] GraphQL errors for ${login2}:`, json.errors);
|
|
206
207
|
}
|
|
207
208
|
if (!json.data?.user) return null;
|
|
208
209
|
const user = json.data.user;
|
|
@@ -271,20 +272,112 @@ async function uploadSupplementalStats(opts) {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
// src/config.ts
|
|
276
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
277
|
+
import { join } from "path";
|
|
278
|
+
import { homedir } from "os";
|
|
279
|
+
function configDir() {
|
|
280
|
+
return join(homedir(), ".chapa");
|
|
281
|
+
}
|
|
282
|
+
function configPath() {
|
|
283
|
+
return join(configDir(), "credentials.json");
|
|
284
|
+
}
|
|
285
|
+
function loadConfig() {
|
|
286
|
+
const path = configPath();
|
|
287
|
+
if (!existsSync(path)) return null;
|
|
288
|
+
try {
|
|
289
|
+
const raw = readFileSync(path, "utf8");
|
|
290
|
+
const data = JSON.parse(raw);
|
|
291
|
+
if (data.token && data.handle && data.server) {
|
|
292
|
+
return data;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function saveConfig(config) {
|
|
300
|
+
const dir = configDir();
|
|
301
|
+
if (!existsSync(dir)) {
|
|
302
|
+
mkdirSync(dir, { mode: 448, recursive: true });
|
|
303
|
+
}
|
|
304
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", {
|
|
305
|
+
mode: 384
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function deleteConfig() {
|
|
309
|
+
const path = configPath();
|
|
310
|
+
if (!existsSync(path)) return false;
|
|
311
|
+
unlinkSync(path);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/login.ts
|
|
316
|
+
import { randomUUID } from "crypto";
|
|
317
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
318
|
+
var MAX_POLL_ATTEMPTS = 150;
|
|
319
|
+
function sleep(ms) {
|
|
320
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
321
|
+
}
|
|
322
|
+
async function login(serverUrl) {
|
|
323
|
+
const baseUrl = serverUrl.replace(/\/+$/, "");
|
|
324
|
+
const sessionId = randomUUID();
|
|
325
|
+
const authorizeUrl = `${baseUrl}/cli/authorize?session=${sessionId}`;
|
|
326
|
+
console.log("\nOpen this URL in a browser where your personal GitHub account is logged in:");
|
|
327
|
+
console.log(`
|
|
328
|
+
${authorizeUrl}
|
|
329
|
+
`);
|
|
330
|
+
console.log("Tip: If your default browser has your work (EMU) account,");
|
|
331
|
+
console.log(" use a different browser or an incognito/private window.\n");
|
|
332
|
+
console.log("Waiting for approval...");
|
|
333
|
+
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
|
|
334
|
+
await sleep(POLL_INTERVAL_MS);
|
|
335
|
+
let data = null;
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch(
|
|
338
|
+
`${baseUrl}/api/cli/auth/poll?session=${sessionId}`
|
|
339
|
+
);
|
|
340
|
+
if (!res.ok) continue;
|
|
341
|
+
data = await res.json();
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (data?.status === "approved" && data.token && data.handle) {
|
|
346
|
+
saveConfig({
|
|
347
|
+
token: data.token,
|
|
348
|
+
handle: data.handle,
|
|
349
|
+
server: baseUrl
|
|
350
|
+
});
|
|
351
|
+
console.log(`
|
|
352
|
+
Logged in as ${data.handle}!`);
|
|
353
|
+
console.log("Credentials saved to ~/.chapa/credentials.json");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (data?.status === "expired") {
|
|
357
|
+
console.error("\nSession expired. Please try again.");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
console.error("\nTimed out waiting for approval. Please try again.");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
274
365
|
// src/index.ts
|
|
275
|
-
var VERSION = true ? "0.1
|
|
366
|
+
var VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
276
367
|
var HELP_TEXT = `chapa-cli v${VERSION}
|
|
277
368
|
|
|
278
369
|
Merge GitHub EMU (Enterprise Managed User) contributions into your Chapa badge.
|
|
279
370
|
|
|
280
|
-
|
|
281
|
-
chapa
|
|
371
|
+
Commands:
|
|
372
|
+
chapa login Authenticate with Chapa (opens browser)
|
|
373
|
+
chapa logout Clear stored credentials
|
|
374
|
+
chapa merge --emu-handle <emu> Merge EMU stats into your badge
|
|
282
375
|
|
|
283
376
|
Options:
|
|
284
|
-
--handle <handle>
|
|
285
|
-
--emu-handle <handle> Your EMU GitHub handle (required)
|
|
377
|
+
--emu-handle <handle> Your EMU GitHub handle (required for merge)
|
|
286
378
|
--emu-token <token> EMU GitHub token (or set GITHUB_EMU_TOKEN)
|
|
287
|
-
--
|
|
379
|
+
--handle <handle> Override personal handle (auto-detected from login)
|
|
380
|
+
--token <token> Override auth token (auto-detected from login)
|
|
288
381
|
--server <url> Chapa server URL (default: https://chapa.thecreativetoken.com)
|
|
289
382
|
--version, -v Show version number
|
|
290
383
|
--help, -h Show this help message
|
|
@@ -299,15 +392,33 @@ async function main() {
|
|
|
299
392
|
console.log(HELP_TEXT);
|
|
300
393
|
process.exit(0);
|
|
301
394
|
}
|
|
395
|
+
if (args.command === "login") {
|
|
396
|
+
await login(args.server);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (args.command === "logout") {
|
|
400
|
+
const removed = deleteConfig();
|
|
401
|
+
if (removed) {
|
|
402
|
+
console.log("Logged out. Credentials removed from ~/.chapa/credentials.json");
|
|
403
|
+
} else {
|
|
404
|
+
console.log("Not logged in (no credentials found).");
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
302
408
|
if (args.command !== "merge") {
|
|
303
|
-
console.error("Usage: chapa
|
|
409
|
+
console.error("Usage: chapa <login | logout | merge> [options]");
|
|
304
410
|
console.error("\nRun 'chapa --help' for more information.");
|
|
305
411
|
process.exit(1);
|
|
306
412
|
}
|
|
307
|
-
const
|
|
413
|
+
const config = loadConfig();
|
|
414
|
+
const handle = args.handle ?? config?.handle;
|
|
308
415
|
const emuHandle = args.emuHandle;
|
|
309
|
-
if (!
|
|
310
|
-
console.error("Error: --
|
|
416
|
+
if (!emuHandle) {
|
|
417
|
+
console.error("Error: --emu-handle is required.");
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
if (!handle) {
|
|
421
|
+
console.error("Error: No personal handle found. Run 'chapa login' first, or pass --handle.");
|
|
311
422
|
process.exit(1);
|
|
312
423
|
}
|
|
313
424
|
const emuToken = resolveToken(args.emuToken, "GITHUB_EMU_TOKEN");
|
|
@@ -315,9 +426,9 @@ async function main() {
|
|
|
315
426
|
console.error("Error: EMU token required. Use --emu-token or set GITHUB_EMU_TOKEN.");
|
|
316
427
|
process.exit(1);
|
|
317
428
|
}
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
320
|
-
console.error("Error:
|
|
429
|
+
const authToken = args.token ?? config?.token;
|
|
430
|
+
if (!authToken) {
|
|
431
|
+
console.error("Error: Not authenticated. Run 'chapa login' first, or pass --token.");
|
|
321
432
|
process.exit(1);
|
|
322
433
|
}
|
|
323
434
|
console.log(`Fetching stats for EMU account: ${emuHandle}...`);
|
|
@@ -327,13 +438,14 @@ async function main() {
|
|
|
327
438
|
process.exit(1);
|
|
328
439
|
}
|
|
329
440
|
console.log(`Found: ${emuStats.commitsTotal} commits, ${emuStats.prsMergedCount} PRs merged, ${emuStats.reviewsSubmittedCount} reviews`);
|
|
330
|
-
|
|
441
|
+
const serverUrl = args.server !== "https://chapa.thecreativetoken.com" ? args.server : config?.server ?? args.server;
|
|
442
|
+
console.log(`Uploading supplemental stats to ${serverUrl}...`);
|
|
331
443
|
const result = await uploadSupplementalStats({
|
|
332
444
|
targetHandle: handle,
|
|
333
445
|
sourceHandle: emuHandle,
|
|
334
446
|
stats: emuStats,
|
|
335
|
-
token:
|
|
336
|
-
serverUrl
|
|
447
|
+
token: authToken,
|
|
448
|
+
serverUrl
|
|
337
449
|
});
|
|
338
450
|
if (!result.success) {
|
|
339
451
|
console.error(`Error: ${result.error}`);
|