github-reporadar 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.html CHANGED
@@ -16,7 +16,7 @@
16
16
  href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
17
17
  rel="stylesheet"
18
18
  />
19
- <script type="module" crossorigin src="/assets/index-7rO9STC2.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-Bb1rtvKk.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-B5UAXuco.css">
21
21
  </head>
22
22
  <body>
package/dist-cli/cli.js CHANGED
@@ -195,24 +195,41 @@ async function fetchLogin(token) {
195
195
  const { json } = await rest(token, "/user");
196
196
  return json.login;
197
197
  }
198
- async function listRecentRepos(token, since, includePrivate) {
199
- const repos = [];
198
+ async function listRecentRepos(token, since, includePrivate, orgs = []) {
200
199
  const visibility = "";
201
- let url = `/user/repos?sort=pushed&per_page=100&affiliation=owner${visibility}`;
202
- while (url) {
203
- const { json, next } = await rest(token, url);
204
- const page = json;
205
- repos.push(...page);
206
- if (page.length === 0 || new Date(page[page.length - 1].pushed_at) < since) break;
207
- url = next;
200
+ const urls = [
201
+ `/user/repos?sort=pushed&per_page=100&affiliation=owner${visibility}`,
202
+ // --orgで足した組織。トークンが見える範囲(type=all)をpushed降順で
203
+ ...orgs.map((o) => `/orgs/${encodeURIComponent(o)}/repos?sort=pushed&per_page=100&type=all`)
204
+ ];
205
+ const seen = /* @__PURE__ */ new Set();
206
+ const repos = [];
207
+ for (const start of urls) {
208
+ let url = start;
209
+ while (url) {
210
+ const { json, next } = await rest(token, url);
211
+ const page = json;
212
+ for (const r of page) {
213
+ if (!seen.has(r.full_name)) {
214
+ seen.add(r.full_name);
215
+ repos.push(r);
216
+ }
217
+ }
218
+ if (page.length === 0 || new Date(page[page.length - 1].pushed_at) < since) break;
219
+ url = next;
220
+ }
208
221
  }
209
222
  return repos.filter((r) => {
210
223
  return new Date(r.pushed_at) >= since;
211
224
  });
212
225
  }
213
- async function listCommits(token, repo, since) {
226
+ function commitsUrl(fullName, since, author) {
227
+ const base = `/repos/${fullName}/commits?since=${since.toISOString()}&per_page=100`;
228
+ return author ? `${base}&author=${encodeURIComponent(author)}` : base;
229
+ }
230
+ async function listCommits(token, repo, since, author) {
214
231
  const commits = [];
215
- let url = `/repos/${repo.full_name}/commits?since=${since.toISOString()}&per_page=100`;
232
+ let url = commitsUrl(repo.full_name, since, author);
216
233
  let pages = 0;
217
234
  while (url && pages < MAX_COMMIT_PAGES) {
218
235
  let result;
@@ -257,33 +274,44 @@ ${fields}
257
274
  })));
258
275
  return result;
259
276
  }
260
- async function listOpenIssues(token, repo) {
277
+ function issuesUrls(fullName, login) {
278
+ const base = `/repos/${fullName}/issues?state=open&per_page=100`;
279
+ if (!login) return [base];
280
+ const me = encodeURIComponent(login);
281
+ return [`${base}&creator=${me}`, `${base}&assignee=${me}`];
282
+ }
283
+ async function listOpenIssues(token, repo, login) {
261
284
  const items = [];
262
- let url = `/repos/${repo.full_name}/issues?state=open&per_page=100`;
263
- let pages = 0;
264
- while (url && pages < 5) {
265
- let result;
266
- try {
267
- result = await rest(token, url);
268
- } catch (err) {
269
- if (err instanceof Error && err.message.includes("HTTP 409")) return [];
270
- throw err;
271
- }
272
- for (const it of result.json) {
273
- items.push({
274
- repo: repo.full_name,
275
- type: it.pull_request ? "pr" : "issue",
276
- number: it.number,
277
- title: it.title,
278
- state: it.state,
279
- labels: it.labels.map((l) => l.name),
280
- updatedAt: it.updated_at,
281
- url: it.html_url,
282
- comments: it.comments
283
- });
285
+ const seen = /* @__PURE__ */ new Set();
286
+ for (const start of issuesUrls(repo.full_name, login)) {
287
+ let url = start;
288
+ let pages = 0;
289
+ while (url && pages < 5) {
290
+ let result;
291
+ try {
292
+ result = await rest(token, url);
293
+ } catch (err) {
294
+ if (err instanceof Error && err.message.includes("HTTP 409")) return [];
295
+ throw err;
296
+ }
297
+ for (const it of result.json) {
298
+ if (seen.has(it.number)) continue;
299
+ seen.add(it.number);
300
+ items.push({
301
+ repo: repo.full_name,
302
+ type: it.pull_request ? "pr" : "issue",
303
+ number: it.number,
304
+ title: it.title,
305
+ state: it.state,
306
+ labels: it.labels.map((l) => l.name),
307
+ updatedAt: it.updated_at,
308
+ url: it.html_url,
309
+ comments: it.comments
310
+ });
311
+ }
312
+ url = result.next;
313
+ pages++;
284
314
  }
285
- url = result.next;
286
- pages++;
287
315
  }
288
316
  return items;
289
317
  }
@@ -312,8 +340,9 @@ async function fetchActivity(token, from, opts) {
312
340
  const limit = createLimiter(CONCURRENCY);
313
341
  const [login, recentRepos] = await Promise.all([
314
342
  fetchLogin(token),
315
- listRecentRepos(token, from)
343
+ listRecentRepos(token, from, opts.includePrivate, opts.orgs ?? [])
316
344
  ]);
345
+ const mineOnly = (r) => r.owner.login.toLowerCase() === login.toLowerCase() ? null : login;
317
346
  const openCounts = await fetchOpenCounts(token, recentRepos.map((r) => r.full_name), limit);
318
347
  const issueRepos = recentRepos.filter((r) => {
319
348
  const c = openCounts.get(r.full_name);
@@ -322,7 +351,7 @@ async function fetchActivity(token, from, opts) {
322
351
  const progress = createProgress(recentRepos.length, issueRepos.length);
323
352
  const commitsPromise = Promise.all(
324
353
  recentRepos.map(
325
- (r) => limit(() => listCommits(token, r, from)).then((commits) => {
354
+ (r) => limit(() => listCommits(token, r, from, mineOnly(r))).then((commits) => {
326
355
  progress.repoDone();
327
356
  return commits;
328
357
  })
@@ -330,7 +359,7 @@ async function fetchActivity(token, from, opts) {
330
359
  );
331
360
  const issuesPromise = Promise.all(
332
361
  issueRepos.map(
333
- (r) => limit(() => listOpenIssues(token, r)).then((items) => {
362
+ (r) => limit(() => listOpenIssues(token, r, mineOnly(r))).then((items) => {
334
363
  progress.issueDone();
335
364
  return items;
336
365
  })
@@ -382,9 +411,11 @@ async function runSync(opts) {
382
411
  const from = new Date(to.getTime() - windowDays * 24 * 60 * 60 * 1e3);
383
412
  log(`GitHub RepoRadar sync: ${from.toISOString().slice(0, 10)} 〜 ${to.toISOString().slice(0, 10)} (${windowDays}日)`);
384
413
  const token = getToken();
385
- const { login, repos, activity, issues } = await fetchActivity(token, from);
414
+ const orgs = opts.orgs ?? [];
415
+ if (orgs.length > 0) log(`組織も取得: ${orgs.join(", ")}(自分がauthorのコミットだけ数える)`);
416
+ const { login, repos, activity, issues } = await fetchActivity(token, from, { includePrivate: true, orgs });
386
417
  mkdirSync(opts.dataDir, { recursive: true });
387
- const meta = { lastSyncAt: to.toISOString(), login, windowDays, includesPrivate: true };
418
+ const meta = { lastSyncAt: to.toISOString(), login, windowDays, includesPrivate: true, ...orgs.length > 0 ? { orgs } : {} };
388
419
  writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
389
420
  writeFileSync(join(opts.dataDir, "activity.json"), JSON.stringify(activity, null, 2));
390
421
  writeFileSync(join(opts.dataDir, "issues.json"), JSON.stringify(issues, null, 2));
@@ -414,7 +445,8 @@ const DEFAULTS = {
414
445
  dataDir: null,
415
446
  includePrivate: false,
416
447
  sort: "active",
417
- limit: 15
448
+ limit: 15,
449
+ orgs: []
418
450
  };
419
451
  function intIn(flag, raw, min, max) {
420
452
  if (raw === void 0 || raw.startsWith("--")) throw new RangeError(`${flag} には値が要ります`);
@@ -425,7 +457,7 @@ function intIn(flag, raw, min, max) {
425
457
  return n;
426
458
  }
427
459
  function parseArgs(argv) {
428
- const out = { ...DEFAULTS };
460
+ const out = { ...DEFAULTS, orgs: [] };
429
461
  let command = null;
430
462
  for (let i = 0; i < argv.length; i++) {
431
463
  const a = argv[i];
@@ -458,6 +490,14 @@ function parseArgs(argv) {
458
490
  out.dataDir = v;
459
491
  break;
460
492
  }
493
+ case "--org": {
494
+ const v = argv[++i];
495
+ if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
496
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(v)) throw new RangeError(`--orgはGitHubの組織名(英数字とハイフン)で指定してください(受け取った値: ${v})`);
497
+ if (out.orgs.includes(v)) throw new RangeError(`--org ${v}が重複しています`);
498
+ out.orgs.push(v);
499
+ break;
500
+ }
461
501
  case "--sort": {
462
502
  const v = argv[++i];
463
503
  if (!SORTS.includes(v)) throw new RangeError(`--sortは${SORTS.join(" | ")} のどれか(受け取った値: ${v})`);
@@ -477,26 +517,30 @@ function parseArgs(argv) {
477
517
  }
478
518
  return out;
479
519
  }
480
- const USAGE = `github-reporadar 自分のGitHub活動を一晩ぶん眺める計器盤
481
-
482
- 使い方:
483
- npx github-reporadar同期してからローカルで開く(既定)
484
- npx github-reporadar sync同期だけ
485
- npx github-reporadar brief端末に要約を出す
486
-
487
- フラグ:
488
- --days N取得する期間(1〜365、既定90)。briefは7 | 30 | 90
489
- --port N待ち受けポート(既定5177。使用中なら次を探す)
490
- --no-openブラウザを開かない
491
- --no-sync同期せずに手元のデータで開く
492
- --data-dir DIRデータの置き場(既定: $XDG_CACHE_HOMEか~/.cacheのgithub-reporadar/data)
493
- --sort KEY briefの並び: active | recent | stale
494
- --limit N briefの件数(既定15)
495
- --private briefにprivate repoも出す
496
-
497
- トークンはgh CLI(gh auth token)から実行時に取り、どこにも保存しません。
498
- 待ち受けは127.0.0.1だけです。データにはprivate repo名とコミット文が含まれます。
499
- `;
520
+ const row = (left, right) => ` ${left.padEnd(34)}${right}`;
521
+ const USAGE = [
522
+ "github-reporadar — 自分のGitHub活動を一晩ぶん眺める計器盤",
523
+ "",
524
+ "使い方:",
525
+ row("npx github-reporadar", "同期してからローカルで開く(既定)"),
526
+ row("npx github-reporadar sync", "同期だけ"),
527
+ row("npx github-reporadar brief", "端末に要約を出す"),
528
+ "",
529
+ "フラグ:",
530
+ row("--days N", "取得する期間(1〜365、既定90)。brief は 7 | 30 | 90"),
531
+ row("--port N", "待ち受けポート(既定5177。使用中なら次を探す)"),
532
+ row("--no-open", "ブラウザを開かない"),
533
+ row("--no-sync", "同期せずに手元のデータで開く"),
534
+ row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
535
+ row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
536
+ row("--sort KEY", "brief の並び: active | recent | stale"),
537
+ row("--limit N", "brief の件数(既定15)"),
538
+ row("--private", "brief に private repo も出す"),
539
+ "",
540
+ "トークンは gh CLI(gh auth token)から実行時に取り、どこにも保存しません。",
541
+ "待ち受けは 127.0.0.1 だけです。データには private repo 名とコミット文が含まれます。",
542
+ ""
543
+ ].join("\n");
500
544
  function defaultDataDir(env, home) {
501
545
  const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME !== "" ? env.XDG_CACHE_HOME : join(home, ".cache");
502
546
  return join(base, "github-reporadar", "data");
@@ -622,7 +666,7 @@ async function main() {
622
666
  }
623
667
  const dataDir = args.dataDir ?? defaultDataDir(process.env, homedir());
624
668
  if (args.command === "sync" || args.command === "serve" && args.sync) {
625
- await runSync({ dataDir, days: args.days });
669
+ await runSync({ dataDir, days: args.days, orgs: args.orgs });
626
670
  }
627
671
  if (args.command === "sync") return;
628
672
  if (args.command === "brief") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-reporadar",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "自分のGitHub活動を一晩ぶん眺める計器盤。npx github-reporadarでローカルに開く",
5
5
  "license": "MIT",
6
6
  "bin": {