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.
Files changed (3) hide show
  1. package/README.md +27 -16
  2. package/dist/index.js +133 -21
  3. 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
- ## Usage
7
+ ## Quick start
8
8
 
9
9
  ```bash
10
- npx chapa-cli merge \
11
- --handle your-personal-handle \
12
- --emu-handle your-emu-handle
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
- ## Options
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
- | `--token <token>` | Personal GitHub token | No* |
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
- *Tokens are resolved in order: flag > environment variable (`GITHUB_TOKEN` / `GITHUB_EMU_TOKEN`).
39
+ *EMU token is resolved from: flag > `GITHUB_EMU_TOKEN` env var.
28
40
 
29
- ## Token setup
41
+ ## EMU token setup
30
42
 
31
- Set tokens via environment variables to avoid passing them on every run:
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. Fetches your last 12 months of activity from the EMU account via GitHub GraphQL API
43
- 2. Uploads the stats (commits, PRs merged, reviews) to the Chapa server
44
- 3. Your Chapa badge recalculates on next refresh, combining personal + EMU contributions
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 === "merge" ? "merge" : null;
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(login, emuToken) {
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 ${login}:`, json.errors);
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.4" : "0.0.0-dev";
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
- Usage:
281
- chapa merge --handle <personal> --emu-handle <emu> [options]
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> Your personal GitHub handle (required)
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
- --token <token> Personal GitHub token (or set GITHUB_TOKEN)
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 merge --handle <personal> --emu-handle <emu> [--emu-token <token>] [--token <token>] [--server <url>]");
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 handle = args.handle;
413
+ const config = loadConfig();
414
+ const handle = args.handle ?? config?.handle;
308
415
  const emuHandle = args.emuHandle;
309
- if (!handle || !emuHandle) {
310
- console.error("Error: --handle and --emu-handle are required.");
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 personalToken = resolveToken(args.token, "GITHUB_TOKEN");
319
- if (!personalToken) {
320
- console.error("Error: Personal token required. Use --token or set GITHUB_TOKEN.");
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
- console.log(`Uploading supplemental stats to ${args.server}...`);
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: personalToken,
336
- serverUrl: args.server
447
+ token: authToken,
448
+ serverUrl
337
449
  });
338
450
  if (!result.success) {
339
451
  console.error(`Error: ${result.error}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapa-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Merge GitHub EMU contributions into your Chapa developer impact badge",
5
5
  "type": "module",
6
6
  "bin": {