chapa-cli 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -16
- package/dist/index.js +138 -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,117 @@ 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
|
+
import { exec } from "child_process";
|
|
318
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
319
|
+
var MAX_POLL_ATTEMPTS = 150;
|
|
320
|
+
function openBrowser(url) {
|
|
321
|
+
const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
322
|
+
exec(cmd, () => {
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function sleep(ms) {
|
|
326
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
327
|
+
}
|
|
328
|
+
async function login(serverUrl) {
|
|
329
|
+
const baseUrl = serverUrl.replace(/\/+$/, "");
|
|
330
|
+
const sessionId = randomUUID();
|
|
331
|
+
const authorizeUrl = `${baseUrl}/cli/authorize?session=${sessionId}`;
|
|
332
|
+
console.log("Opening browser for authentication...");
|
|
333
|
+
console.log(`If your browser didn't open, visit:
|
|
334
|
+
${authorizeUrl}
|
|
335
|
+
`);
|
|
336
|
+
openBrowser(authorizeUrl);
|
|
337
|
+
console.log("Waiting for approval...");
|
|
338
|
+
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
|
|
339
|
+
await sleep(POLL_INTERVAL_MS);
|
|
340
|
+
let data = null;
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(
|
|
343
|
+
`${baseUrl}/api/cli/auth/poll?session=${sessionId}`
|
|
344
|
+
);
|
|
345
|
+
if (!res.ok) continue;
|
|
346
|
+
data = await res.json();
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (data?.status === "approved" && data.token && data.handle) {
|
|
351
|
+
saveConfig({
|
|
352
|
+
token: data.token,
|
|
353
|
+
handle: data.handle,
|
|
354
|
+
server: baseUrl
|
|
355
|
+
});
|
|
356
|
+
console.log(`
|
|
357
|
+
Logged in as ${data.handle}!`);
|
|
358
|
+
console.log("Credentials saved to ~/.chapa/credentials.json");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (data?.status === "expired") {
|
|
362
|
+
console.error("\nSession expired. Please try again.");
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
console.error("\nTimed out waiting for approval. Please try again.");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
274
370
|
// src/index.ts
|
|
275
|
-
var VERSION = true ? "0.
|
|
371
|
+
var VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
276
372
|
var HELP_TEXT = `chapa-cli v${VERSION}
|
|
277
373
|
|
|
278
374
|
Merge GitHub EMU (Enterprise Managed User) contributions into your Chapa badge.
|
|
279
375
|
|
|
280
|
-
|
|
281
|
-
chapa
|
|
376
|
+
Commands:
|
|
377
|
+
chapa login Authenticate with Chapa (opens browser)
|
|
378
|
+
chapa logout Clear stored credentials
|
|
379
|
+
chapa merge --emu-handle <emu> Merge EMU stats into your badge
|
|
282
380
|
|
|
283
381
|
Options:
|
|
284
|
-
--handle <handle>
|
|
285
|
-
--emu-handle <handle> Your EMU GitHub handle (required)
|
|
382
|
+
--emu-handle <handle> Your EMU GitHub handle (required for merge)
|
|
286
383
|
--emu-token <token> EMU GitHub token (or set GITHUB_EMU_TOKEN)
|
|
287
|
-
--
|
|
384
|
+
--handle <handle> Override personal handle (auto-detected from login)
|
|
385
|
+
--token <token> Override auth token (auto-detected from login)
|
|
288
386
|
--server <url> Chapa server URL (default: https://chapa.thecreativetoken.com)
|
|
289
387
|
--version, -v Show version number
|
|
290
388
|
--help, -h Show this help message
|
|
@@ -299,15 +397,33 @@ async function main() {
|
|
|
299
397
|
console.log(HELP_TEXT);
|
|
300
398
|
process.exit(0);
|
|
301
399
|
}
|
|
400
|
+
if (args.command === "login") {
|
|
401
|
+
await login(args.server);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (args.command === "logout") {
|
|
405
|
+
const removed = deleteConfig();
|
|
406
|
+
if (removed) {
|
|
407
|
+
console.log("Logged out. Credentials removed from ~/.chapa/credentials.json");
|
|
408
|
+
} else {
|
|
409
|
+
console.log("Not logged in (no credentials found).");
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
302
413
|
if (args.command !== "merge") {
|
|
303
|
-
console.error("Usage: chapa
|
|
414
|
+
console.error("Usage: chapa <login | logout | merge> [options]");
|
|
304
415
|
console.error("\nRun 'chapa --help' for more information.");
|
|
305
416
|
process.exit(1);
|
|
306
417
|
}
|
|
307
|
-
const
|
|
418
|
+
const config = loadConfig();
|
|
419
|
+
const handle = args.handle ?? config?.handle;
|
|
308
420
|
const emuHandle = args.emuHandle;
|
|
309
|
-
if (!
|
|
310
|
-
console.error("Error: --
|
|
421
|
+
if (!emuHandle) {
|
|
422
|
+
console.error("Error: --emu-handle is required.");
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
if (!handle) {
|
|
426
|
+
console.error("Error: No personal handle found. Run 'chapa login' first, or pass --handle.");
|
|
311
427
|
process.exit(1);
|
|
312
428
|
}
|
|
313
429
|
const emuToken = resolveToken(args.emuToken, "GITHUB_EMU_TOKEN");
|
|
@@ -315,9 +431,9 @@ async function main() {
|
|
|
315
431
|
console.error("Error: EMU token required. Use --emu-token or set GITHUB_EMU_TOKEN.");
|
|
316
432
|
process.exit(1);
|
|
317
433
|
}
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
320
|
-
console.error("Error:
|
|
434
|
+
const authToken = args.token ?? config?.token;
|
|
435
|
+
if (!authToken) {
|
|
436
|
+
console.error("Error: Not authenticated. Run 'chapa login' first, or pass --token.");
|
|
321
437
|
process.exit(1);
|
|
322
438
|
}
|
|
323
439
|
console.log(`Fetching stats for EMU account: ${emuHandle}...`);
|
|
@@ -327,13 +443,14 @@ async function main() {
|
|
|
327
443
|
process.exit(1);
|
|
328
444
|
}
|
|
329
445
|
console.log(`Found: ${emuStats.commitsTotal} commits, ${emuStats.prsMergedCount} PRs merged, ${emuStats.reviewsSubmittedCount} reviews`);
|
|
330
|
-
|
|
446
|
+
const serverUrl = args.server !== "https://chapa.thecreativetoken.com" ? args.server : config?.server ?? args.server;
|
|
447
|
+
console.log(`Uploading supplemental stats to ${serverUrl}...`);
|
|
331
448
|
const result = await uploadSupplementalStats({
|
|
332
449
|
targetHandle: handle,
|
|
333
450
|
sourceHandle: emuHandle,
|
|
334
451
|
stats: emuStats,
|
|
335
|
-
token:
|
|
336
|
-
serverUrl
|
|
452
|
+
token: authToken,
|
|
453
|
+
serverUrl
|
|
337
454
|
});
|
|
338
455
|
if (!result.success) {
|
|
339
456
|
console.error(`Error: ${result.error}`);
|