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.
Files changed (3) hide show
  1. package/README.md +27 -16
  2. package/dist/index.js +138 -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,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.1.4" : "0.0.0-dev";
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
- Usage:
281
- chapa merge --handle <personal> --emu-handle <emu> [options]
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> Your personal GitHub handle (required)
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
- --token <token> Personal GitHub token (or set GITHUB_TOKEN)
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 merge --handle <personal> --emu-handle <emu> [--emu-token <token>] [--token <token>] [--server <url>]");
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 handle = args.handle;
418
+ const config = loadConfig();
419
+ const handle = args.handle ?? config?.handle;
308
420
  const emuHandle = args.emuHandle;
309
- if (!handle || !emuHandle) {
310
- console.error("Error: --handle and --emu-handle are required.");
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 personalToken = resolveToken(args.token, "GITHUB_TOKEN");
319
- if (!personalToken) {
320
- console.error("Error: Personal token required. Use --token or set GITHUB_TOKEN.");
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
- console.log(`Uploading supplemental stats to ${args.server}...`);
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: personalToken,
336
- serverUrl: args.server
452
+ token: authToken,
453
+ serverUrl
337
454
  });
338
455
  if (!result.success) {
339
456
  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.0",
4
4
  "description": "Merge GitHub EMU contributions into your Chapa developer impact badge",
5
5
  "type": "module",
6
6
  "bin": {