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/README.md +13 -1
- package/dist/assets/index-DmB2CQfF.js +90 -0
- package/dist/index.html +1 -1
- package/dist-cli/cli.js +83 -39
- package/package.json +1 -1
- package/dist/assets/index-BRwV22Gy.js +0 -90
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-
|
|
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.
|
|
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
|
|
754
|
+
const MAX_ROUNDS = 8;
|
|
738
755
|
const accountIdCache = /* @__PURE__ */ new Map();
|
|
739
756
|
async function fetchMyAccountId(cfg) {
|
|
740
|
-
const key2 = `${cfg.site}
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
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
|
|
764
|
-
const
|
|
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
|
|
770
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|