github-reporadar 0.6.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 +15 -2
- package/dist/assets/index-DQkwmLip.css +1 -0
- package/dist/assets/index-DmB2CQfF.js +90 -0
- package/dist/index.html +2 -2
- package/dist-cli/cli.js +95 -41
- package/package.json +1 -1
- package/dist/assets/index-B3RBEh9j.js +0 -90
- package/dist/assets/index-R2XOh3Es.css +0 -1
package/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
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-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DmB2CQfF.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DQkwmLip.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
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,42 +747,56 @@ 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;
|
|
755
|
+
const accountIdCache = /* @__PURE__ */ new Map();
|
|
738
756
|
async function fetchMyAccountId(cfg) {
|
|
757
|
+
const key2 = `${cfg.site}/${cfg.email}`;
|
|
758
|
+
const cached = accountIdCache.get(key2);
|
|
759
|
+
if (cached !== void 0) return cached;
|
|
739
760
|
const token = getAtlassianToken(cfg.email);
|
|
740
761
|
if (!token) return null;
|
|
741
762
|
try {
|
|
742
763
|
const res = await fetch(`https://${cfg.site}/wiki/rest/api/user/current`, { headers: basicAuthHeaders(cfg.email, token) });
|
|
743
764
|
if (!res.ok) return null;
|
|
744
765
|
const json = await res.json();
|
|
745
|
-
|
|
766
|
+
if (!json.accountId) return null;
|
|
767
|
+
accountIdCache.set(key2, json.accountId);
|
|
768
|
+
return json.accountId;
|
|
746
769
|
} catch {
|
|
747
770
|
return null;
|
|
748
771
|
}
|
|
749
772
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
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}`;
|
|
756
779
|
const out = [];
|
|
757
|
-
for (let
|
|
758
|
-
const
|
|
759
|
-
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 });
|
|
760
782
|
if (!res.ok)
|
|
761
783
|
throw new Error(`Confluence /content/search HTTP ${res.status}: ${(await res.text()).slice(0, 200)}${authHint(res.status, cfg.email)}`);
|
|
762
784
|
const json = await res.json();
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
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);
|
|
766
787
|
}
|
|
767
788
|
return out;
|
|
768
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
|
+
}
|
|
769
800
|
function buildSlackQuery(repo) {
|
|
770
801
|
return `"${repo}"`;
|
|
771
802
|
}
|
|
@@ -888,24 +919,22 @@ async function fetchMyMentions(max = COUNT) {
|
|
|
888
919
|
return [];
|
|
889
920
|
}
|
|
890
921
|
}
|
|
891
|
-
function diffWikiPages(prev, next, now,
|
|
922
|
+
function diffWikiPages(prev, next, now, opts = {}) {
|
|
892
923
|
if (prev === null) return [];
|
|
893
924
|
const before = new Map(prev.map((p) => [p.id, p]));
|
|
925
|
+
const since = opts.since ?? null;
|
|
894
926
|
const out = [];
|
|
927
|
+
const base = (page) => ({ at: now, repo: page.space, number: 0, title: page.title, url: page.url, source: "wiki" });
|
|
895
928
|
for (const page of next) {
|
|
896
929
|
const b = before.get(page.id);
|
|
897
|
-
if (!b
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
title: page.title,
|
|
906
|
-
url: page.url,
|
|
907
|
-
source: "wiki"
|
|
908
|
-
});
|
|
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;
|
|
936
|
+
if (page.byMe) continue;
|
|
937
|
+
out.push({ id: `wiki:${page.id}:${page.lastModified}`, kind: "wiki_updated", ...base(page) });
|
|
909
938
|
}
|
|
910
939
|
return out;
|
|
911
940
|
}
|
|
@@ -1052,6 +1081,7 @@ async function runSync(opts) {
|
|
|
1052
1081
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
1053
1082
|
const prevIssues = readJson("issues.json");
|
|
1054
1083
|
const prevRepos = readJson("repos.json");
|
|
1084
|
+
const prevSyncAt = readJson("meta.json")?.lastSyncAt ?? null;
|
|
1055
1085
|
const lastComments = await fetchLastComments(token, commentIncreased(prevIssues, issues), MAX_COMMENT_LOOKUPS);
|
|
1056
1086
|
const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), {
|
|
1057
1087
|
prevRepos: prevRepos ? new Set(prevRepos.map((r) => r.nameWithOwner)) : void 0,
|
|
@@ -1072,7 +1102,7 @@ async function runSync(opts) {
|
|
|
1072
1102
|
writeFileSync(join(opts.dataDir, "refs.json"), JSON.stringify(refStates, null, 2));
|
|
1073
1103
|
warnMissingAtlassianToken(opts, log);
|
|
1074
1104
|
await writeJira(opts, log);
|
|
1075
|
-
await writeConfluence(opts, repos, to.toISOString(), readJson, log);
|
|
1105
|
+
await writeConfluence(opts, repos, to.toISOString(), readJson, log, prevSyncAt);
|
|
1076
1106
|
await writeSlack(opts, to.toISOString(), readJson, log);
|
|
1077
1107
|
await writeSlackLinks(opts, issues, login, log);
|
|
1078
1108
|
const totalCommits = activity.reduce((sum, a) => sum + a.commits, 0);
|
|
@@ -1107,7 +1137,7 @@ async function runLightSync(opts, log, readJson) {
|
|
|
1107
1137
|
writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
|
1108
1138
|
if (meta) writeFileSync(join(opts.dataDir, "meta.json"), JSON.stringify({ ...meta, lastSyncAt: to.toISOString() }, null, 2));
|
|
1109
1139
|
await writeJira(opts, log);
|
|
1110
|
-
await writeConfluence(opts, repos, to.toISOString(), readJson, log);
|
|
1140
|
+
await writeConfluence(opts, repos, to.toISOString(), readJson, log, meta?.lastSyncAt ?? null);
|
|
1111
1141
|
await writeSlack(opts, to.toISOString(), readJson, log);
|
|
1112
1142
|
log(`更新: open ${issues.length}件${fresh.length > 0 ? ` / 受信箱に${fresh.length}件` : ""}`);
|
|
1113
1143
|
return { login, repos: repos.length, commits: 0, issues: issues.length, events: fresh.length };
|
|
@@ -1163,11 +1193,16 @@ function warnMissingAtlassianToken(opts, log) {
|
|
|
1163
1193
|
log(` security add-generic-password -s github-reporadar-atlassian -a "${cfg.email}" -w "<APIトークン>"`);
|
|
1164
1194
|
log(` トークンは https://id.atlassian.com/manage-profile/security/api-tokens で作れます`);
|
|
1165
1195
|
}
|
|
1166
|
-
async function writeConfluence(opts, repos, now, readJson, log) {
|
|
1196
|
+
async function writeConfluence(opts, repos, now, readJson, log, since) {
|
|
1167
1197
|
if (!opts.confluence) return;
|
|
1168
1198
|
try {
|
|
1169
1199
|
const pages = await fetchConfluencePages(opts.confluence, repos.map((r) => r.nameWithOwner));
|
|
1170
|
-
const
|
|
1200
|
+
const myAccountId = pages.length === 0 ? null : await fetchMyAccountId(opts.confluence);
|
|
1201
|
+
if (pages.length > 0 && myAccountId === null) {
|
|
1202
|
+
log(`Confluence: 自分のaccountIdを取れませんでした。誰が更新したかは「不明」として扱います`);
|
|
1203
|
+
}
|
|
1204
|
+
const mine = pages.filter((p) => p.mine);
|
|
1205
|
+
const drifts = driftingPages(mine, repos.map((r) => ({ nameWithOwner: r.nameWithOwner, pushedAt: r.pushedAt })));
|
|
1171
1206
|
writeFileSync(join(opts.dataDir, "confluence.json"), JSON.stringify(drifts, null, 2));
|
|
1172
1207
|
if (drifts.length > 0) log(`Confluence: 要確認${drifts.length}件`);
|
|
1173
1208
|
const refs = pages.map((p) => ({
|
|
@@ -1176,16 +1211,19 @@ async function writeConfluence(opts, repos, now, readJson, log) {
|
|
|
1176
1211
|
space: p.space,
|
|
1177
1212
|
url: p.url,
|
|
1178
1213
|
lastModified: p.lastModified,
|
|
1179
|
-
lastModifiedBy: p.lastModifiedBy
|
|
1214
|
+
lastModifiedBy: p.lastModifiedBy,
|
|
1215
|
+
byMe: myAccountId === null || p.lastModifiedBy === "" ? void 0 : p.lastModifiedBy === myAccountId,
|
|
1216
|
+
createdAt: p.createdAt,
|
|
1217
|
+
watched: p.watched
|
|
1180
1218
|
}));
|
|
1181
1219
|
const prevPages = readJson("confluence-pages.json");
|
|
1182
|
-
const
|
|
1183
|
-
const fresh = diffWikiPages(prevPages, refs, now, myAccountId);
|
|
1220
|
+
const fresh = diffWikiPages(prevPages, refs, now, { since });
|
|
1184
1221
|
writeFileSync(join(opts.dataDir, "confluence-pages.json"), JSON.stringify(refs, null, 2));
|
|
1185
1222
|
if (fresh.length > 0) {
|
|
1186
1223
|
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
1187
1224
|
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
1188
|
-
|
|
1225
|
+
const added = fresh.filter((e) => e.kind === "wiki_added").length;
|
|
1226
|
+
log(`Confluence: ${added > 0 ? `追加${added}件 / ` : ""}更新${fresh.length - added}件を受信箱へ`);
|
|
1189
1227
|
}
|
|
1190
1228
|
} catch (err) {
|
|
1191
1229
|
log(`Confluenceの取得に失敗: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1218,8 +1256,16 @@ const DEFAULTS = {
|
|
|
1218
1256
|
atlassianSite: null,
|
|
1219
1257
|
atlassianEmail: null,
|
|
1220
1258
|
jiraProjects: [],
|
|
1221
|
-
confluenceSpaces: []
|
|
1259
|
+
confluenceSpaces: [],
|
|
1260
|
+
confluenceFolders: []
|
|
1222
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
|
+
}
|
|
1223
1269
|
function intIn(flag, raw, min, max) {
|
|
1224
1270
|
if (raw === void 0 || raw.startsWith("--")) throw new RangeError(`${flag} には値が要ります`);
|
|
1225
1271
|
const n = Number(raw);
|
|
@@ -1229,7 +1275,7 @@ function intIn(flag, raw, min, max) {
|
|
|
1229
1275
|
return n;
|
|
1230
1276
|
}
|
|
1231
1277
|
function parseArgs(argv) {
|
|
1232
|
-
const out = { ...DEFAULTS, orgs: [], jiraProjects: [], confluenceSpaces: [] };
|
|
1278
|
+
const out = { ...DEFAULTS, orgs: [], jiraProjects: [], confluenceSpaces: [], confluenceFolders: [] };
|
|
1233
1279
|
let command = null;
|
|
1234
1280
|
for (let i = 0; i < argv.length; i++) {
|
|
1235
1281
|
const a = argv[i];
|
|
@@ -1280,6 +1326,13 @@ function parseArgs(argv) {
|
|
|
1280
1326
|
if (!out.confluenceSpaces.includes(v)) out.confluenceSpaces.push(v);
|
|
1281
1327
|
break;
|
|
1282
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
|
+
}
|
|
1283
1336
|
case "--jira-project": {
|
|
1284
1337
|
const v = argv[++i];
|
|
1285
1338
|
if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
|
|
@@ -1341,6 +1394,7 @@ const USAGE = [
|
|
|
1341
1394
|
row("--atlassian-site HOST", "Atlassianのサイト(example.atlassian.net)。--atlassian-emailとセット"),
|
|
1342
1395
|
row("--atlassian-email ADDR", "Atlassianのメールアドレス。トークンはキーチェーンから取る"),
|
|
1343
1396
|
row("--confluence-space KEY", "齟齬を見るスペースキー(繰り返し可)。省略すると自分が関わった全ページ"),
|
|
1397
|
+
row("--confluence-folder ID", "見張るフォルダ(繰り返し可)。IDでもフォルダのURLでも可。配下は他人が足した / 直したページも知らせる"),
|
|
1344
1398
|
row("--jira-project KEY", "取得するプロジェクトキー(繰り返し可)。省略すると全部"),
|
|
1345
1399
|
row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
|
|
1346
1400
|
row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
|
|
@@ -1505,7 +1559,7 @@ async function main() {
|
|
|
1505
1559
|
const site = args.atlassianSite;
|
|
1506
1560
|
const email = args.atlassianEmail;
|
|
1507
1561
|
const jira = site && email ? { site, email, projects: args.jiraProjects } : null;
|
|
1508
|
-
const confluence = site && email ? { site, email, spaces: args.confluenceSpaces } : null;
|
|
1562
|
+
const confluence = site && email ? { site, email, spaces: args.confluenceSpaces, folders: args.confluenceFolders } : null;
|
|
1509
1563
|
if (args.command === "sync") {
|
|
1510
1564
|
await runSync({ dataDir, days: args.days, orgs: args.orgs, jira, confluence });
|
|
1511
1565
|
return;
|