github-reporadar 0.7.0 → 0.8.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/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-BRwV22Gy.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DmB2CQfF.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DQkwmLip.css">
21
21
  </head>
22
22
  <body>
package/dist-cli/cli.js CHANGED
@@ -653,6 +653,10 @@ function buildCql(spaces = []) {
653
653
  const scope = spaces.length > 0 ? `space in (${spaces.join(",")}) AND ` : "";
654
654
  return `${scope}contributor = currentUser() AND type = page ORDER BY lastmodified DESC`;
655
655
  }
656
+ function buildFolderCql(folders) {
657
+ if (folders.length === 0) return "";
658
+ return `ancestor in (${folders.join(",")}) AND type = page ORDER BY lastmodified DESC`;
659
+ }
656
660
  const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
657
661
  const decodeEntities = (t) => t.replace(/&([a-z]+|#\d+);/gi, (m, name) => ENTITIES[name.toLowerCase()] ?? m);
658
662
  function stripMarkup(source) {
@@ -690,7 +694,7 @@ function extractRepoRefs(text, knownRepos) {
690
694
  }
691
695
  return found;
692
696
  }
693
- function mapPage(raw, site, knownRepos) {
697
+ function mapPage(raw, site, knownRepos, origin = { mine: true, watched: false }) {
694
698
  const body = raw.body?.storage?.value;
695
699
  const text = `${raw.title} ${linkTargets(body)} ${stripMarkup(body)}`;
696
700
  const webui = raw._links?.webui ?? "";
@@ -702,9 +706,22 @@ function mapPage(raw, site, knownRepos) {
702
706
  url: webui ? `https://${site}/wiki${webui}` : `https://${site}/wiki/pages/viewpage.action?pageId=${raw.id}`,
703
707
  lastModified: raw.version?.when ?? "",
704
708
  lastModifiedBy: raw.version?.by?.accountId ?? "",
709
+ createdAt: raw.history?.createdDate ?? "",
710
+ mine: origin.mine,
711
+ watched: origin.watched,
705
712
  repos: extractRepoRefs(text, knownRepos)
706
713
  };
707
714
  }
715
+ function mergePages(groups) {
716
+ const byId = /* @__PURE__ */ new Map();
717
+ for (const group of groups) {
718
+ for (const page of group) {
719
+ const seen = byId.get(page.id);
720
+ byId.set(page.id, seen ? { ...seen, mine: seen.mine || page.mine, watched: seen.watched || page.watched } : page);
721
+ }
722
+ }
723
+ return [...byId.values()];
724
+ }
708
725
  const DAY = 24 * 60 * 60 * 1e3;
709
726
  function driftingPages(pages, repos, minDays = 1) {
710
727
  const pushedAt = new Map(repos.map((r) => [r.nameWithOwner, r.pushedAt]));
@@ -730,14 +747,14 @@ function driftingPages(pages, repos, minDays = 1) {
730
747
  });
731
748
  }
732
749
  }
733
- return out.sort((a, b) => b.daysBehind - a.daysBehind || a.title.localeCompare(b.title));
750
+ return out.sort((a, b) => a.lastModified < b.lastModified ? 1 : a.lastModified > b.lastModified ? -1 : a.title.localeCompare(b.title));
734
751
  }
735
- const EXPAND = "space,version,body.storage";
752
+ const EXPAND = "space,version,history,body.storage";
736
753
  const PAGE_SIZE = 50;
737
- const MAX_TOTAL = 200;
754
+ const MAX_ROUNDS = 8;
738
755
  const accountIdCache = /* @__PURE__ */ new Map();
739
756
  async function fetchMyAccountId(cfg) {
740
- const key2 = `${cfg.site}\0${cfg.email}`;
757
+ const key2 = `${cfg.site}/${cfg.email}`;
741
758
  const cached = accountIdCache.get(key2);
742
759
  if (cached !== void 0) return cached;
743
760
  const token = getAtlassianToken(cfg.email);
@@ -753,25 +770,33 @@ async function fetchMyAccountId(cfg) {
753
770
  return null;
754
771
  }
755
772
  }
756
- async function fetchConfluencePages(cfg, knownRepos) {
757
- if (!cfg?.site || !cfg.email) return [];
758
- const token = getAtlassianToken(cfg.email);
759
- if (!token) return [];
760
- const headers = basicAuthHeaders(cfg.email, token);
761
- const cql = buildCql(cfg.spaces ?? []);
773
+ function nextPath(link) {
774
+ return link && link.startsWith("/rest/") ? link : null;
775
+ }
776
+ async function searchByCql(cfg, cql, headers, knownRepos, origin) {
777
+ const params = new URLSearchParams({ cql, expand: EXPAND, limit: String(PAGE_SIZE) });
778
+ let path = `/rest/api/content/search?${params}`;
762
779
  const out = [];
763
- for (let start = 0; start < MAX_TOTAL; start += PAGE_SIZE) {
764
- const params = new URLSearchParams({ cql, expand: EXPAND, limit: String(PAGE_SIZE), start: String(start) });
765
- const res = await fetch(`https://${cfg.site}/wiki/rest/api/content/search?${params}`, { headers });
780
+ for (let round = 0; round < MAX_ROUNDS && path; round++) {
781
+ const res = await fetch(`https://${cfg.site}/wiki${path}`, { headers });
766
782
  if (!res.ok)
767
783
  throw new Error(`Confluence /content/search HTTP ${res.status}: ${(await res.text()).slice(0, 200)}${authHint(res.status, cfg.email)}`);
768
784
  const json = await res.json();
769
- const batch = json.results ?? [];
770
- for (const raw of batch) if (raw?.id) out.push(mapPage(raw, cfg.site, knownRepos));
771
- if (batch.length < PAGE_SIZE) break;
785
+ for (const raw of json.results ?? []) if (raw?.id) out.push(mapPage(raw, cfg.site, knownRepos, origin));
786
+ path = nextPath(json._links?.next);
772
787
  }
773
788
  return out;
774
789
  }
790
+ async function fetchConfluencePages(cfg, knownRepos) {
791
+ if (!cfg?.site || !cfg.email) return [];
792
+ const token = getAtlassianToken(cfg.email);
793
+ if (!token) return [];
794
+ const headers = basicAuthHeaders(cfg.email, token);
795
+ const groups = [await searchByCql(cfg, buildCql(cfg.spaces ?? []), headers, knownRepos, { mine: true, watched: false })];
796
+ const folderCql = buildFolderCql(cfg.folders ?? []);
797
+ if (folderCql) groups.push(await searchByCql(cfg, folderCql, headers, knownRepos, { mine: false, watched: true }));
798
+ return mergePages(groups);
799
+ }
775
800
  function buildSlackQuery(repo) {
776
801
  return `"${repo}"`;
777
802
  }
@@ -894,24 +919,22 @@ async function fetchMyMentions(max = COUNT) {
894
919
  return [];
895
920
  }
896
921
  }
897
- function diffWikiPages(prev, next, now) {
922
+ function diffWikiPages(prev, next, now, opts = {}) {
898
923
  if (prev === null) return [];
899
924
  const before = new Map(prev.map((p) => [p.id, p]));
925
+ const since = opts.since ?? null;
900
926
  const out = [];
927
+ const base = (page) => ({ at: now, repo: page.space, number: 0, title: page.title, url: page.url, source: "wiki" });
901
928
  for (const page of next) {
902
929
  const b = before.get(page.id);
903
- if (!b || !page.lastModified || page.lastModified <= b.lastModified) continue;
930
+ if (!b) {
931
+ if (!since || !page.createdAt || page.createdAt <= since || page.byMe) continue;
932
+ out.push({ id: `wiki:${page.id}:added`, kind: "wiki_added", ...base(page) });
933
+ continue;
934
+ }
935
+ if (!page.lastModified || page.lastModified <= b.lastModified) continue;
904
936
  if (page.byMe) continue;
905
- out.push({
906
- id: `wiki:${page.id}:${page.lastModified}`,
907
- at: now,
908
- kind: "wiki_updated",
909
- repo: page.space,
910
- number: 0,
911
- title: page.title,
912
- url: page.url,
913
- source: "wiki"
914
- });
937
+ out.push({ id: `wiki:${page.id}:${page.lastModified}`, kind: "wiki_updated", ...base(page) });
915
938
  }
916
939
  return out;
917
940
  }
@@ -1058,6 +1081,7 @@ async function runSync(opts) {
1058
1081
  mkdirSync(opts.dataDir, { recursive: true });
1059
1082
  const prevIssues = readJson("issues.json");
1060
1083
  const prevRepos = readJson("repos.json");
1084
+ const prevSyncAt = readJson("meta.json")?.lastSyncAt ?? null;
1061
1085
  const lastComments = await fetchLastComments(token, commentIncreased(prevIssues, issues), MAX_COMMENT_LOOKUPS);
1062
1086
  const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), {
1063
1087
  prevRepos: prevRepos ? new Set(prevRepos.map((r) => r.nameWithOwner)) : void 0,
@@ -1078,7 +1102,7 @@ async function runSync(opts) {
1078
1102
  writeFileSync(join(opts.dataDir, "refs.json"), JSON.stringify(refStates, null, 2));
1079
1103
  warnMissingAtlassianToken(opts, log);
1080
1104
  await writeJira(opts, log);
1081
- await writeConfluence(opts, repos, to.toISOString(), readJson, log);
1105
+ await writeConfluence(opts, repos, to.toISOString(), readJson, log, prevSyncAt);
1082
1106
  await writeSlack(opts, to.toISOString(), readJson, log);
1083
1107
  await writeSlackLinks(opts, issues, login, log);
1084
1108
  const totalCommits = activity.reduce((sum, a) => sum + a.commits, 0);
@@ -1113,7 +1137,7 @@ async function runLightSync(opts, log, readJson) {
1113
1137
  writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
1114
1138
  if (meta) writeFileSync(join(opts.dataDir, "meta.json"), JSON.stringify({ ...meta, lastSyncAt: to.toISOString() }, null, 2));
1115
1139
  await writeJira(opts, log);
1116
- await writeConfluence(opts, repos, to.toISOString(), readJson, log);
1140
+ await writeConfluence(opts, repos, to.toISOString(), readJson, log, meta?.lastSyncAt ?? null);
1117
1141
  await writeSlack(opts, to.toISOString(), readJson, log);
1118
1142
  log(`更新: open ${issues.length}件${fresh.length > 0 ? ` / 受信箱に${fresh.length}件` : ""}`);
1119
1143
  return { login, repos: repos.length, commits: 0, issues: issues.length, events: fresh.length };
@@ -1169,7 +1193,7 @@ function warnMissingAtlassianToken(opts, log) {
1169
1193
  log(` security add-generic-password -s github-reporadar-atlassian -a "${cfg.email}" -w "<APIトークン>"`);
1170
1194
  log(` トークンは https://id.atlassian.com/manage-profile/security/api-tokens で作れます`);
1171
1195
  }
1172
- async function writeConfluence(opts, repos, now, readJson, log) {
1196
+ async function writeConfluence(opts, repos, now, readJson, log, since) {
1173
1197
  if (!opts.confluence) return;
1174
1198
  try {
1175
1199
  const pages = await fetchConfluencePages(opts.confluence, repos.map((r) => r.nameWithOwner));
@@ -1177,7 +1201,8 @@ async function writeConfluence(opts, repos, now, readJson, log) {
1177
1201
  if (pages.length > 0 && myAccountId === null) {
1178
1202
  log(`Confluence: 自分のaccountIdを取れませんでした。誰が更新したかは「不明」として扱います`);
1179
1203
  }
1180
- const drifts = driftingPages(pages, repos.map((r) => ({ nameWithOwner: r.nameWithOwner, pushedAt: r.pushedAt })));
1204
+ const mine = pages.filter((p) => p.mine);
1205
+ const drifts = driftingPages(mine, repos.map((r) => ({ nameWithOwner: r.nameWithOwner, pushedAt: r.pushedAt })));
1181
1206
  writeFileSync(join(opts.dataDir, "confluence.json"), JSON.stringify(drifts, null, 2));
1182
1207
  if (drifts.length > 0) log(`Confluence: 要確認${drifts.length}件`);
1183
1208
  const refs = pages.map((p) => ({
@@ -1187,15 +1212,18 @@ async function writeConfluence(opts, repos, now, readJson, log) {
1187
1212
  url: p.url,
1188
1213
  lastModified: p.lastModified,
1189
1214
  lastModifiedBy: p.lastModifiedBy,
1190
- byMe: myAccountId === null || p.lastModifiedBy === "" ? void 0 : p.lastModifiedBy === myAccountId
1215
+ byMe: myAccountId === null || p.lastModifiedBy === "" ? void 0 : p.lastModifiedBy === myAccountId,
1216
+ createdAt: p.createdAt,
1217
+ watched: p.watched
1191
1218
  }));
1192
1219
  const prevPages = readJson("confluence-pages.json");
1193
- const fresh = diffWikiPages(prevPages, refs, now);
1220
+ const fresh = diffWikiPages(prevPages, refs, now, { since });
1194
1221
  writeFileSync(join(opts.dataDir, "confluence-pages.json"), JSON.stringify(refs, null, 2));
1195
1222
  if (fresh.length > 0) {
1196
1223
  const events = mergeEvents(readJson("events.json") ?? [], fresh);
1197
1224
  writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
1198
- log(`Confluence: 更新${fresh.length}件を受信箱へ`);
1225
+ const added = fresh.filter((e) => e.kind === "wiki_added").length;
1226
+ log(`Confluence: ${added > 0 ? `追加${added}件 / ` : ""}更新${fresh.length - added}件を受信箱へ`);
1199
1227
  }
1200
1228
  } catch (err) {
1201
1229
  log(`Confluenceの取得に失敗: ${err instanceof Error ? err.message : String(err)}`);
@@ -1228,8 +1256,16 @@ const DEFAULTS = {
1228
1256
  atlassianSite: null,
1229
1257
  atlassianEmail: null,
1230
1258
  jiraProjects: [],
1231
- confluenceSpaces: []
1259
+ confluenceSpaces: [],
1260
+ confluenceFolders: []
1232
1261
  };
1262
+ function parseFolderId(raw) {
1263
+ const trimmed = raw.trim();
1264
+ if (/^\d+$/.test(trimmed)) return trimmed;
1265
+ const m = /\/folder\/(\d+)/.exec(trimmed);
1266
+ if (m) return m[1];
1267
+ throw new RangeError(`--confluence-folderはフォルダのID(数字)か、フォルダのURLで指定してください(受け取った値: ${raw})`);
1268
+ }
1233
1269
  function intIn(flag, raw, min, max) {
1234
1270
  if (raw === void 0 || raw.startsWith("--")) throw new RangeError(`${flag} には値が要ります`);
1235
1271
  const n = Number(raw);
@@ -1239,7 +1275,7 @@ function intIn(flag, raw, min, max) {
1239
1275
  return n;
1240
1276
  }
1241
1277
  function parseArgs(argv) {
1242
- const out = { ...DEFAULTS, orgs: [], jiraProjects: [], confluenceSpaces: [] };
1278
+ const out = { ...DEFAULTS, orgs: [], jiraProjects: [], confluenceSpaces: [], confluenceFolders: [] };
1243
1279
  let command = null;
1244
1280
  for (let i = 0; i < argv.length; i++) {
1245
1281
  const a = argv[i];
@@ -1290,6 +1326,13 @@ function parseArgs(argv) {
1290
1326
  if (!out.confluenceSpaces.includes(v)) out.confluenceSpaces.push(v);
1291
1327
  break;
1292
1328
  }
1329
+ case "--confluence-folder": {
1330
+ const v = argv[++i];
1331
+ if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
1332
+ const id = parseFolderId(v);
1333
+ if (!out.confluenceFolders.includes(id)) out.confluenceFolders.push(id);
1334
+ break;
1335
+ }
1293
1336
  case "--jira-project": {
1294
1337
  const v = argv[++i];
1295
1338
  if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
@@ -1351,6 +1394,7 @@ const USAGE = [
1351
1394
  row("--atlassian-site HOST", "Atlassianのサイト(example.atlassian.net)。--atlassian-emailとセット"),
1352
1395
  row("--atlassian-email ADDR", "Atlassianのメールアドレス。トークンはキーチェーンから取る"),
1353
1396
  row("--confluence-space KEY", "齟齬を見るスペースキー(繰り返し可)。省略すると自分が関わった全ページ"),
1397
+ row("--confluence-folder ID", "見張るフォルダ(繰り返し可)。IDでもフォルダのURLでも可。配下は他人が足した / 直したページも知らせる"),
1354
1398
  row("--jira-project KEY", "取得するプロジェクトキー(繰り返し可)。省略すると全部"),
1355
1399
  row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
1356
1400
  row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
@@ -1515,7 +1559,7 @@ async function main() {
1515
1559
  const site = args.atlassianSite;
1516
1560
  const email = args.atlassianEmail;
1517
1561
  const jira = site && email ? { site, email, projects: args.jiraProjects } : null;
1518
- const confluence = site && email ? { site, email, spaces: args.confluenceSpaces } : null;
1562
+ const confluence = site && email ? { site, email, spaces: args.confluenceSpaces, folders: args.confluenceFolders } : null;
1519
1563
  if (args.command === "sync") {
1520
1564
  await runSync({ dataDir, days: args.days, orgs: args.orgs, jira, confluence });
1521
1565
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-reporadar",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "自分のGitHub活動を一晩ぶん眺める計器盤。npx github-reporadarでローカルに開く",
5
5
  "license": "MIT",
6
6
  "bin": {