github-reporadar 0.2.0 → 0.5.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/DESIGN.md +13 -4
- package/README.md +93 -0
- package/dist/assets/index-BR-EuuEr.css +1 -0
- package/dist/assets/index-qCZMarTQ.js +90 -0
- package/dist/index.html +2 -2
- package/dist-cli/cli.js +572 -10
- package/package.json +1 -1
- package/dist/assets/index-CwLJknHY.css +0 -1
- package/dist/assets/index-DwMah6Dr.js +0 -81
package/dist-cli/cli.js
CHANGED
|
@@ -271,7 +271,65 @@ function prMetaQuery(refs) {
|
|
|
271
271
|
${fields}
|
|
272
272
|
}`;
|
|
273
273
|
}
|
|
274
|
+
function refStateQuery(refs) {
|
|
275
|
+
const fields = refs.map((r, j) => {
|
|
276
|
+
const [owner, name] = r.repo.split("/");
|
|
277
|
+
return `r${j}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) { issueOrPullRequest(number: ${r.number}) { __typename ... on PullRequest { state } ... on Issue { state } } }`;
|
|
278
|
+
}).join("\n");
|
|
279
|
+
return `query {
|
|
280
|
+
${fields}
|
|
281
|
+
}`;
|
|
282
|
+
}
|
|
283
|
+
function parseRefState(node) {
|
|
284
|
+
const state = node?.state;
|
|
285
|
+
if (state === "MERGED") return "MERGED";
|
|
286
|
+
if (state === "CLOSED") return "CLOSED";
|
|
287
|
+
if (state === "OPEN") return "OPEN";
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function refsFromCommits(repos) {
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const repo of repos) {
|
|
294
|
+
for (const c of repo.lastCommits ?? []) {
|
|
295
|
+
for (const m of (c.message ?? "").matchAll(/(^|[\s(\[])#(\d+)\b/g)) {
|
|
296
|
+
const number = Number(m[2]);
|
|
297
|
+
const key2 = `${repo.nameWithOwner}#${number}`;
|
|
298
|
+
if (seen.has(key2)) continue;
|
|
299
|
+
seen.add(key2);
|
|
300
|
+
out.push({ repo: repo.nameWithOwner, number });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
274
306
|
const PR_META_CHUNK = 40;
|
|
307
|
+
async function fetchRefStates(token, refs, limit) {
|
|
308
|
+
const out = {};
|
|
309
|
+
const chunks = [];
|
|
310
|
+
for (let i = 0; i < refs.length; i += PR_META_CHUNK) chunks.push(refs.slice(i, i + PR_META_CHUNK));
|
|
311
|
+
await Promise.all(
|
|
312
|
+
chunks.map(
|
|
313
|
+
(chunk) => limit(async () => {
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch(`${API}/graphql`, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
318
|
+
body: JSON.stringify({ query: refStateQuery(chunk) })
|
|
319
|
+
});
|
|
320
|
+
if (!res.ok) return;
|
|
321
|
+
const json = await res.json();
|
|
322
|
+
chunk.forEach((r, j) => {
|
|
323
|
+
const state = parseRefState(json.data?.[`r${j}`]?.issueOrPullRequest ?? null);
|
|
324
|
+
if (state) out[`${r.repo}#${r.number}`] = state;
|
|
325
|
+
});
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
275
333
|
async function fetchPrMeta(token, refs, limit) {
|
|
276
334
|
const out = /* @__PURE__ */ new Map();
|
|
277
335
|
const chunks = [];
|
|
@@ -330,6 +388,32 @@ function issuesUrls(fullName, login) {
|
|
|
330
388
|
const me = encodeURIComponent(login);
|
|
331
389
|
return [`${base}&creator=${me}`, `${base}&assignee=${me}`];
|
|
332
390
|
}
|
|
391
|
+
function lastCommentUrl(key2) {
|
|
392
|
+
const m = /^(.+)#(\d+)$/.exec(key2);
|
|
393
|
+
if (!m) return null;
|
|
394
|
+
return `/repos/${m[1]}/issues/${m[2]}/comments?per_page=1&sort=created&direction=desc`;
|
|
395
|
+
}
|
|
396
|
+
function parseLastComment(json) {
|
|
397
|
+
if (!Array.isArray(json) || json.length === 0) return null;
|
|
398
|
+
const c = json[json.length - 1];
|
|
399
|
+
const author = c?.user?.login;
|
|
400
|
+
if (!author) return null;
|
|
401
|
+
return { author, at: c.created_at ?? "" };
|
|
402
|
+
}
|
|
403
|
+
async function fetchLastComments(token, keys, max) {
|
|
404
|
+
const out = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const key2 of keys.slice(0, max)) {
|
|
406
|
+
const url = lastCommentUrl(key2);
|
|
407
|
+
if (!url) continue;
|
|
408
|
+
try {
|
|
409
|
+
const { json } = await rest(token, url);
|
|
410
|
+
const last = parseLastComment(json);
|
|
411
|
+
if (last) out.set(key2, last);
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
333
417
|
async function listOpenIssues(token, repo, login) {
|
|
334
418
|
const items = [];
|
|
335
419
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -487,15 +571,314 @@ async function fetchActivity(token, from, opts) {
|
|
|
487
571
|
});
|
|
488
572
|
return { login, repos, activity, issues };
|
|
489
573
|
}
|
|
574
|
+
function buildJql(projects = []) {
|
|
575
|
+
const scope = projects.length > 0 ? `project in (${projects.join(",")}) AND ` : "";
|
|
576
|
+
return `${scope}(assignee = currentUser() OR reporter = currentUser()) AND statusCategory != Done ORDER BY updated DESC`;
|
|
577
|
+
}
|
|
578
|
+
function mapIssue(raw, site, myAccountId) {
|
|
579
|
+
const f = raw.fields;
|
|
580
|
+
return {
|
|
581
|
+
key: raw.key,
|
|
582
|
+
title: f.summary,
|
|
583
|
+
status: f.status.name,
|
|
584
|
+
statusCategory: f.status.statusCategory.key,
|
|
585
|
+
priority: f.priority?.name ?? null,
|
|
586
|
+
type: f.issuetype?.name ?? null,
|
|
587
|
+
project: f.project.key,
|
|
588
|
+
projectName: f.project.name,
|
|
589
|
+
updatedAt: f.updated,
|
|
590
|
+
url: `https://${site}/browse/${raw.key}`,
|
|
591
|
+
isMine: f.assignee?.accountId === myAccountId,
|
|
592
|
+
isSubtask: f.issuetype?.subtask ?? false
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function getJiraToken(email) {
|
|
596
|
+
try {
|
|
597
|
+
return execFileSync("security", ["find-generic-password", "-s", "github-reporadar-jira", "-a", email, "-w"], {
|
|
598
|
+
encoding: "utf8"
|
|
599
|
+
}).trim();
|
|
600
|
+
} catch {
|
|
601
|
+
return process.env.RR_JIRA_TOKEN?.trim() || null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const FIELDS = ["summary", "updated", "status", "priority", "issuetype", "project", "assignee", "reporter"];
|
|
605
|
+
const MAX = 100;
|
|
606
|
+
async function fetchJiraIssues(cfg) {
|
|
607
|
+
if (!cfg?.site || !cfg.email) return [];
|
|
608
|
+
const token = getJiraToken(cfg.email);
|
|
609
|
+
if (!token) return [];
|
|
610
|
+
const auth = Buffer.from(`${cfg.email}:${token}`).toString("base64");
|
|
611
|
+
const headers = { authorization: `Basic ${auth}`, accept: "application/json", "content-type": "application/json" };
|
|
612
|
+
const me = await fetch(`https://${cfg.site}/rest/api/3/myself`, { headers });
|
|
613
|
+
if (!me.ok) throw new Error(`Jira /myself HTTP ${me.status}`);
|
|
614
|
+
const { accountId } = await me.json();
|
|
615
|
+
const res = await fetch(`https://${cfg.site}/rest/api/3/search/jql`, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers,
|
|
618
|
+
body: JSON.stringify({ jql: buildJql(cfg.projects ?? []), fields: FIELDS, maxResults: MAX })
|
|
619
|
+
});
|
|
620
|
+
if (!res.ok) throw new Error(`Jira /search HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
621
|
+
const json = await res.json();
|
|
622
|
+
return (json.issues ?? []).map((i) => mapIssue(i, cfg.site, accountId));
|
|
623
|
+
}
|
|
624
|
+
const SERVICES = ["github-reporadar-atlassian", "github-reporadar-jira"];
|
|
625
|
+
const ENV_KEYS = ["RR_ATLASSIAN_TOKEN", "RR_JIRA_TOKEN"];
|
|
626
|
+
function getAtlassianToken(email) {
|
|
627
|
+
for (const service of SERVICES) {
|
|
628
|
+
try {
|
|
629
|
+
const out = execFileSync("security", ["find-generic-password", "-s", service, "-a", email, "-w"], {
|
|
630
|
+
encoding: "utf8",
|
|
631
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
632
|
+
}).trim();
|
|
633
|
+
if (out) return out;
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const key2 of ENV_KEYS) {
|
|
638
|
+
const v = process.env[key2]?.trim();
|
|
639
|
+
if (v) return v;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function basicAuthHeaders(email, token) {
|
|
644
|
+
const auth = Buffer.from(`${email}:${token}`).toString("base64");
|
|
645
|
+
return { authorization: `Basic ${auth}`, accept: "application/json", "content-type": "application/json" };
|
|
646
|
+
}
|
|
647
|
+
function buildCql(spaces = []) {
|
|
648
|
+
const scope = spaces.length > 0 ? `space in (${spaces.join(",")}) AND ` : "";
|
|
649
|
+
return `${scope}contributor = currentUser() AND type = page ORDER BY lastmodified DESC`;
|
|
650
|
+
}
|
|
651
|
+
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
|
|
652
|
+
const decodeEntities = (t) => t.replace(/&([a-z]+|#\d+);/gi, (m, name) => ENTITIES[name.toLowerCase()] ?? m);
|
|
653
|
+
function stripMarkup(source) {
|
|
654
|
+
if (!source) return "";
|
|
655
|
+
return decodeEntities(
|
|
656
|
+
source.replace(/<!\[CDATA\[/g, "").replace(/\]\]>/g, "").replace(/<[^>]*>/g, " ")
|
|
657
|
+
).replace(/\s+/g, " ").trim();
|
|
658
|
+
}
|
|
659
|
+
const ATTR_URL = /\b(?:href|ri:value)="([^"]*)"/gi;
|
|
660
|
+
function linkTargets(source) {
|
|
661
|
+
if (!source) return "";
|
|
662
|
+
const out = [];
|
|
663
|
+
for (const m of source.matchAll(ATTR_URL)) out.push(decodeEntities(m[1]));
|
|
664
|
+
return out.join(" ");
|
|
665
|
+
}
|
|
666
|
+
const BARE_MIN_WORD = 8;
|
|
667
|
+
const looksLikeRepoName = (bare) => /[-_.\d]/.test(bare) || bare.length >= BARE_MIN_WORD;
|
|
668
|
+
const NAME_CHAR = "[A-Za-z0-9_-]";
|
|
669
|
+
const BEFORE_FULL = "(^|[^A-Za-z0-9_-])";
|
|
670
|
+
const BEFORE_BARE = "(^|[^A-Za-z0-9_/-])";
|
|
671
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
672
|
+
function extractRepoRefs(text, knownRepos) {
|
|
673
|
+
if (!text) return [];
|
|
674
|
+
const count = /* @__PURE__ */ new Map();
|
|
675
|
+
for (const full of knownRepos) {
|
|
676
|
+
const bare = full.slice(full.indexOf("/") + 1).toLowerCase();
|
|
677
|
+
count.set(bare, (count.get(bare) ?? 0) + 1);
|
|
678
|
+
}
|
|
679
|
+
const found = [];
|
|
680
|
+
for (const full of knownRepos) {
|
|
681
|
+
const bare = full.slice(full.indexOf("/") + 1);
|
|
682
|
+
const fullHit = new RegExp(`${BEFORE_FULL}${escapeRe(full)}(?!${NAME_CHAR})`, "i").test(text);
|
|
683
|
+
const bareHit = looksLikeRepoName(bare) && count.get(bare.toLowerCase()) === 1 && new RegExp(`${BEFORE_BARE}${escapeRe(bare)}(?!${NAME_CHAR})`, "i").test(text);
|
|
684
|
+
if (fullHit || bareHit) found.push(full);
|
|
685
|
+
}
|
|
686
|
+
return found;
|
|
687
|
+
}
|
|
688
|
+
function mapPage(raw, site, knownRepos) {
|
|
689
|
+
const body = raw.body?.storage?.value;
|
|
690
|
+
const text = `${raw.title} ${linkTargets(body)} ${stripMarkup(body)}`;
|
|
691
|
+
const webui = raw._links?.webui ?? "";
|
|
692
|
+
return {
|
|
693
|
+
id: raw.id,
|
|
694
|
+
title: raw.title,
|
|
695
|
+
space: raw.space?.key ?? "",
|
|
696
|
+
spaceName: raw.space?.name ?? "",
|
|
697
|
+
url: webui ? `https://${site}/wiki${webui}` : `https://${site}/wiki/pages/viewpage.action?pageId=${raw.id}`,
|
|
698
|
+
lastModified: raw.version?.when ?? "",
|
|
699
|
+
lastModifiedBy: raw.version?.by?.accountId ?? "",
|
|
700
|
+
repos: extractRepoRefs(text, knownRepos)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const DAY = 24 * 60 * 60 * 1e3;
|
|
704
|
+
function driftingPages(pages, repos, minDays = 1) {
|
|
705
|
+
const pushedAt = new Map(repos.map((r) => [r.nameWithOwner, r.pushedAt]));
|
|
706
|
+
const out = [];
|
|
707
|
+
for (const page of pages) {
|
|
708
|
+
const modified = Date.parse(page.lastModified);
|
|
709
|
+
if (!Number.isFinite(modified)) continue;
|
|
710
|
+
for (const repo of page.repos) {
|
|
711
|
+
const push = pushedAt.get(repo);
|
|
712
|
+
const pushed = push ? Date.parse(push) : NaN;
|
|
713
|
+
if (!Number.isFinite(pushed)) continue;
|
|
714
|
+
const daysBehind = Math.floor((pushed - modified) / DAY);
|
|
715
|
+
if (daysBehind < minDays) continue;
|
|
716
|
+
out.push({
|
|
717
|
+
pageId: page.id,
|
|
718
|
+
title: page.title,
|
|
719
|
+
space: page.space,
|
|
720
|
+
url: page.url,
|
|
721
|
+
lastModified: page.lastModified,
|
|
722
|
+
repo,
|
|
723
|
+
pushedAt: push,
|
|
724
|
+
daysBehind
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return out.sort((a, b) => b.daysBehind - a.daysBehind || a.title.localeCompare(b.title));
|
|
729
|
+
}
|
|
730
|
+
const EXPAND = "space,version,body.storage";
|
|
731
|
+
const PAGE_SIZE = 50;
|
|
732
|
+
const MAX_TOTAL = 200;
|
|
733
|
+
async function fetchMyAccountId(cfg) {
|
|
734
|
+
const token = getAtlassianToken(cfg.email);
|
|
735
|
+
if (!token) return null;
|
|
736
|
+
try {
|
|
737
|
+
const res = await fetch(`https://${cfg.site}/wiki/rest/api/user/current`, { headers: basicAuthHeaders(cfg.email, token) });
|
|
738
|
+
if (!res.ok) return null;
|
|
739
|
+
const json = await res.json();
|
|
740
|
+
return json.accountId ?? null;
|
|
741
|
+
} catch {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async function fetchConfluencePages(cfg, knownRepos) {
|
|
746
|
+
if (!cfg?.site || !cfg.email) return [];
|
|
747
|
+
const token = getAtlassianToken(cfg.email);
|
|
748
|
+
if (!token) return [];
|
|
749
|
+
const headers = basicAuthHeaders(cfg.email, token);
|
|
750
|
+
const cql = buildCql(cfg.spaces ?? []);
|
|
751
|
+
const out = [];
|
|
752
|
+
for (let start = 0; start < MAX_TOTAL; start += PAGE_SIZE) {
|
|
753
|
+
const params = new URLSearchParams({ cql, expand: EXPAND, limit: String(PAGE_SIZE), start: String(start) });
|
|
754
|
+
const res = await fetch(`https://${cfg.site}/wiki/rest/api/content/search?${params}`, { headers });
|
|
755
|
+
if (!res.ok) throw new Error(`Confluence /content/search HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
756
|
+
const json = await res.json();
|
|
757
|
+
const batch = json.results ?? [];
|
|
758
|
+
for (const raw of batch) if (raw?.id) out.push(mapPage(raw, cfg.site, knownRepos));
|
|
759
|
+
if (batch.length < PAGE_SIZE) break;
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
function channelName(channel) {
|
|
764
|
+
if (!channel) return "";
|
|
765
|
+
if (channel.is_mpim) return "グループDM";
|
|
766
|
+
if (channel.is_im) return "DM";
|
|
767
|
+
return channel.name ?? "";
|
|
768
|
+
}
|
|
769
|
+
function getSlackToken() {
|
|
770
|
+
try {
|
|
771
|
+
const out = execFileSync("security", ["find-generic-password", "-s", "github-reporadar-slack", "-a", "slack", "-w"], {
|
|
772
|
+
encoding: "utf8",
|
|
773
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
774
|
+
}).trim();
|
|
775
|
+
if (out) return out;
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
return process.env.RR_SLACK_TOKEN?.trim() || null;
|
|
779
|
+
}
|
|
780
|
+
async function fetchMyUserId(token) {
|
|
781
|
+
try {
|
|
782
|
+
const res = await fetch("https://slack.com/api/auth.test", {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/x-www-form-urlencoded" }
|
|
785
|
+
});
|
|
786
|
+
if (!res.ok) return null;
|
|
787
|
+
const json = await res.json();
|
|
788
|
+
return json.ok ? json.user_id ?? null : null;
|
|
789
|
+
} catch {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function mentionQuery(userId) {
|
|
794
|
+
return `<@${userId}>`;
|
|
795
|
+
}
|
|
796
|
+
function mapMention(match) {
|
|
797
|
+
const permalink = match.permalink ?? "";
|
|
798
|
+
if (!permalink) return null;
|
|
799
|
+
return {
|
|
800
|
+
channel: channelName(match.channel),
|
|
801
|
+
permalink,
|
|
802
|
+
at: new Date(Number(match.ts ?? 0) * 1e3).toISOString(),
|
|
803
|
+
from: match.username ?? ""
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const COUNT = 20;
|
|
807
|
+
async function fetchMyMentions(max = COUNT) {
|
|
808
|
+
const token = getSlackToken();
|
|
809
|
+
if (!token) return [];
|
|
810
|
+
const userId = await fetchMyUserId(token);
|
|
811
|
+
if (!userId) return [];
|
|
812
|
+
const body = new URLSearchParams({ query: mentionQuery(userId), count: String(max), sort: "timestamp" });
|
|
813
|
+
try {
|
|
814
|
+
const res = await fetch("https://slack.com/api/search.messages", {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/x-www-form-urlencoded" },
|
|
817
|
+
body: body.toString()
|
|
818
|
+
});
|
|
819
|
+
if (!res.ok) return [];
|
|
820
|
+
const json = await res.json();
|
|
821
|
+
if (!json.ok) return [];
|
|
822
|
+
const out = [];
|
|
823
|
+
for (const match of json.messages?.matches ?? []) {
|
|
824
|
+
const m = mapMention(match);
|
|
825
|
+
if (m) out.push(m);
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
} catch {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function diffWikiPages(prev, next, now, myAccountId) {
|
|
833
|
+
if (prev === null) return [];
|
|
834
|
+
const before = new Map(prev.map((p) => [p.id, p]));
|
|
835
|
+
const out = [];
|
|
836
|
+
for (const page of next) {
|
|
837
|
+
const b = before.get(page.id);
|
|
838
|
+
if (!b || !page.lastModified || page.lastModified <= b.lastModified) continue;
|
|
839
|
+
if (myAccountId && page.lastModifiedBy === myAccountId) continue;
|
|
840
|
+
out.push({
|
|
841
|
+
id: `wiki:${page.id}:${page.lastModified}`,
|
|
842
|
+
at: now,
|
|
843
|
+
kind: "wiki_updated",
|
|
844
|
+
repo: page.space,
|
|
845
|
+
number: 0,
|
|
846
|
+
title: page.title,
|
|
847
|
+
url: page.url,
|
|
848
|
+
source: "wiki"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
490
853
|
const same = (a, b) => a != null && a.toLowerCase() === b.toLowerCase();
|
|
491
854
|
const key = (i) => `${i.repo}#${i.number}`;
|
|
855
|
+
function commentIncreased(prev, next) {
|
|
856
|
+
if (!prev) return [];
|
|
857
|
+
const before = new Map(prev.map((i) => [key(i), i.comments]));
|
|
858
|
+
const out = [];
|
|
859
|
+
for (const i of next) {
|
|
860
|
+
const b = before.get(key(i));
|
|
861
|
+
if (b !== void 0 && i.comments > b) out.push(key(i));
|
|
862
|
+
}
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
492
865
|
function diffEvents(prev, next, login, now, opts = {}) {
|
|
493
866
|
if (!login || prev === null) return [];
|
|
494
867
|
const before = new Map(prev.filter((i) => i.type === "pr").map((i) => [key(i), i]));
|
|
868
|
+
const beforeAll = new Map(prev.map((i) => [key(i), i]));
|
|
495
869
|
const knownRepo = (repo) => !opts.prevRepos || opts.prevRepos.has(repo);
|
|
496
870
|
const repoStillHere = (repo) => !opts.nextRepos || opts.nextRepos.has(repo);
|
|
497
871
|
const out = [];
|
|
498
872
|
const base = (p) => ({ at: now, repo: p.repo, number: p.number, title: p.title, url: p.url });
|
|
873
|
+
for (const p of next) {
|
|
874
|
+
const b = beforeAll.get(key(p));
|
|
875
|
+
if (!b || !knownRepo(p.repo) || p.comments <= b.comments) continue;
|
|
876
|
+
const last = opts.lastComments?.get(key(p));
|
|
877
|
+
if (!last || same(last.author, login)) continue;
|
|
878
|
+
if (same(p.author, login)) {
|
|
879
|
+
out.push({ id: `${key(p)}:comment:${p.comments}`, kind: "commented", ...base(p), actor: last.author });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
499
882
|
for (const p of next) {
|
|
500
883
|
if (p.type !== "pr") continue;
|
|
501
884
|
const b = before.get(key(p));
|
|
@@ -504,6 +887,21 @@ function diffEvents(prev, next, login, now, opts = {}) {
|
|
|
504
887
|
const wasReviewer = (b?.reviewers ?? []).some((r) => same(r, login));
|
|
505
888
|
const isReviewer = (p.reviewers ?? []).some((r) => same(r, login));
|
|
506
889
|
if (!mine && isReviewer && !wasReviewer) out.push({ id: `${key(p)}:requested`, kind: "review_requested", ...base(p), actor: p.author ?? void 0 });
|
|
890
|
+
const iReviewed = same(p.lastReview?.author, login) || same(b?.lastReview?.author, login);
|
|
891
|
+
if (!mine && b && (isReviewer || iReviewed) && !(isReviewer && !wasReviewer)) {
|
|
892
|
+
const lastComment = opts.lastComments?.get(key(p));
|
|
893
|
+
const newComment = p.comments > b.comments && lastComment && !same(lastComment.author, login);
|
|
894
|
+
const newPush = p.lastCommitAt && b.lastCommitAt && p.lastCommitAt > b.lastCommitAt;
|
|
895
|
+
if (newComment || newPush) {
|
|
896
|
+
const stamp = newPush ? p.lastCommitAt : `c${p.comments}`;
|
|
897
|
+
out.push({
|
|
898
|
+
id: `${key(p)}:review-update:${stamp}`,
|
|
899
|
+
kind: "review_update",
|
|
900
|
+
...base(p),
|
|
901
|
+
actor: (newComment ? lastComment?.author : p.author) ?? void 0
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
507
905
|
if (!mine) continue;
|
|
508
906
|
const prevCount = b?.reviewCount ?? 0;
|
|
509
907
|
const count = p.reviewCount ?? 0;
|
|
@@ -527,8 +925,30 @@ function mergeEvents(existing, fresh, max = 200) {
|
|
|
527
925
|
const added = fresh.filter((e) => !seen.has(e.id));
|
|
528
926
|
return [...added, ...existing].sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0).slice(0, max);
|
|
529
927
|
}
|
|
928
|
+
function diffSlackMentions(prev, next, now) {
|
|
929
|
+
if (prev === null) return [];
|
|
930
|
+
const before = new Set(prev.map((m) => m.permalink));
|
|
931
|
+
const out = [];
|
|
932
|
+
for (const m of next) {
|
|
933
|
+
if (!m.permalink || before.has(m.permalink)) continue;
|
|
934
|
+
out.push({
|
|
935
|
+
id: `slack:${m.permalink}`,
|
|
936
|
+
at: now,
|
|
937
|
+
kind: "slack_mention",
|
|
938
|
+
repo: m.channel,
|
|
939
|
+
number: 0,
|
|
940
|
+
title: "",
|
|
941
|
+
url: m.permalink,
|
|
942
|
+
actor: m.from || void 0,
|
|
943
|
+
source: "slack"
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
530
948
|
const REPO_CACHE_DIR = join(import.meta.dirname, "..", "public", "data");
|
|
531
949
|
const DEFAULT_WINDOW_DAYS = 90;
|
|
950
|
+
const MAX_COMMENT_LOOKUPS = 15;
|
|
951
|
+
const REF_STATE_CONCURRENCY = 4;
|
|
532
952
|
async function runSync(opts) {
|
|
533
953
|
const windowDays = opts.days ?? DEFAULT_WINDOW_DAYS;
|
|
534
954
|
const log = opts.log ?? ((line) => console.log(line));
|
|
@@ -555,9 +975,11 @@ async function runSync(opts) {
|
|
|
555
975
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
556
976
|
const prevIssues = readJson("issues.json");
|
|
557
977
|
const prevRepos = readJson("repos.json");
|
|
978
|
+
const lastComments = await fetchLastComments(token, commentIncreased(prevIssues, issues), MAX_COMMENT_LOOKUPS);
|
|
558
979
|
const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), {
|
|
559
980
|
prevRepos: prevRepos ? new Set(prevRepos.map((r) => r.nameWithOwner)) : void 0,
|
|
560
|
-
nextRepos: new Set(repos.map((r) => r.nameWithOwner))
|
|
981
|
+
nextRepos: new Set(repos.map((r) => r.nameWithOwner)),
|
|
982
|
+
lastComments
|
|
561
983
|
});
|
|
562
984
|
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
563
985
|
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
@@ -567,6 +989,12 @@ async function runSync(opts) {
|
|
|
567
989
|
writeFileSync(join(opts.dataDir, "activity.json"), JSON.stringify(activity, null, 2));
|
|
568
990
|
writeFileSync(join(opts.dataDir, "issues.json"), JSON.stringify(issues, null, 2));
|
|
569
991
|
writeFileSync(join(opts.dataDir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
992
|
+
const refs = refsFromCommits(repos);
|
|
993
|
+
const refStates = await fetchRefStates(token, refs, createLimiter(REF_STATE_CONCURRENCY));
|
|
994
|
+
writeFileSync(join(opts.dataDir, "refs.json"), JSON.stringify(refStates, null, 2));
|
|
995
|
+
await writeJira(opts, log);
|
|
996
|
+
await writeConfluence(opts, repos, to.toISOString(), readJson, log);
|
|
997
|
+
await writeSlack(opts, to.toISOString(), readJson, log);
|
|
570
998
|
const totalCommits = activity.reduce((sum, a) => sum + a.commits, 0);
|
|
571
999
|
log(`OK: ${repos.length} repos / ${totalCommits} commits / ${issues.length} open issues+PRs → ${opts.dataDir}`);
|
|
572
1000
|
return { login, repos: repos.length, commits: totalCommits, issues: issues.length, events: fresh.length };
|
|
@@ -585,7 +1013,8 @@ async function runLightSync(opts, log, readJson) {
|
|
|
585
1013
|
const to = /* @__PURE__ */ new Date();
|
|
586
1014
|
const prevIssues = readJson("issues.json");
|
|
587
1015
|
const repoSet = new Set(names);
|
|
588
|
-
const
|
|
1016
|
+
const lastComments = await fetchLastComments(token, commentIncreased(prevIssues, issues), MAX_COMMENT_LOOKUPS);
|
|
1017
|
+
const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), { prevRepos: repoSet, nextRepos: repoSet, lastComments });
|
|
589
1018
|
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
590
1019
|
const repos = prevRepos.map((r) => {
|
|
591
1020
|
const c = counts.get(r.nameWithOwner);
|
|
@@ -596,9 +1025,66 @@ async function runLightSync(opts, log, readJson) {
|
|
|
596
1025
|
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
597
1026
|
writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
|
598
1027
|
if (meta) writeFileSync(join(opts.dataDir, "meta.json"), JSON.stringify({ ...meta, lastSyncAt: to.toISOString() }, null, 2));
|
|
1028
|
+
await writeJira(opts, log);
|
|
1029
|
+
await writeConfluence(opts, repos, to.toISOString(), readJson, log);
|
|
1030
|
+
await writeSlack(opts, to.toISOString(), readJson, log);
|
|
599
1031
|
log(`更新: open ${issues.length}件${fresh.length > 0 ? ` / 受信箱に${fresh.length}件` : ""}`);
|
|
600
1032
|
return { login, repos: repos.length, commits: 0, issues: issues.length, events: fresh.length };
|
|
601
1033
|
}
|
|
1034
|
+
async function writeJira(opts, log) {
|
|
1035
|
+
if (!opts.jira) return;
|
|
1036
|
+
try {
|
|
1037
|
+
const issues = await fetchJiraIssues(opts.jira);
|
|
1038
|
+
writeFileSync(join(opts.dataDir, "jira.json"), JSON.stringify(issues, null, 2));
|
|
1039
|
+
if (issues.length > 0) log(`Jira: 未完了${issues.length}件`);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
log(`Jiraの取得に失敗: ${err instanceof Error ? err.message : String(err)}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function writeSlack(opts, now, readJson, log) {
|
|
1045
|
+
try {
|
|
1046
|
+
const mentions = await fetchMyMentions();
|
|
1047
|
+
if (mentions.length === 0) return;
|
|
1048
|
+
const prev = readJson("slack-mentions.json");
|
|
1049
|
+
const fresh = diffSlackMentions(prev, mentions, now);
|
|
1050
|
+
writeFileSync(join(opts.dataDir, "slack-mentions.json"), JSON.stringify(mentions, null, 2));
|
|
1051
|
+
if (fresh.length > 0) {
|
|
1052
|
+
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
1053
|
+
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
1054
|
+
log(`Slack: メンション${fresh.length}件を受信箱へ`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
log(`Slackの取得に失敗: ${err instanceof Error ? err.message : String(err)}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function writeConfluence(opts, repos, now, readJson, log) {
|
|
1061
|
+
if (!opts.confluence) return;
|
|
1062
|
+
try {
|
|
1063
|
+
const pages = await fetchConfluencePages(opts.confluence, repos.map((r) => r.nameWithOwner));
|
|
1064
|
+
const drifts = driftingPages(pages, repos.map((r) => ({ nameWithOwner: r.nameWithOwner, pushedAt: r.pushedAt })));
|
|
1065
|
+
writeFileSync(join(opts.dataDir, "confluence.json"), JSON.stringify(drifts, null, 2));
|
|
1066
|
+
if (drifts.length > 0) log(`Confluence: 要確認${drifts.length}件`);
|
|
1067
|
+
const refs = pages.map((p) => ({
|
|
1068
|
+
id: p.id,
|
|
1069
|
+
title: p.title,
|
|
1070
|
+
space: p.space,
|
|
1071
|
+
url: p.url,
|
|
1072
|
+
lastModified: p.lastModified,
|
|
1073
|
+
lastModifiedBy: p.lastModifiedBy
|
|
1074
|
+
}));
|
|
1075
|
+
const prevPages = readJson("confluence-pages.json");
|
|
1076
|
+
const myAccountId = prevPages === null ? null : await fetchMyAccountId(opts.confluence);
|
|
1077
|
+
const fresh = diffWikiPages(prevPages, refs, now, myAccountId);
|
|
1078
|
+
writeFileSync(join(opts.dataDir, "confluence-pages.json"), JSON.stringify(refs, null, 2));
|
|
1079
|
+
if (fresh.length > 0) {
|
|
1080
|
+
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
1081
|
+
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
1082
|
+
log(`Confluence: 更新${fresh.length}件を受信箱へ`);
|
|
1083
|
+
}
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
log(`Confluenceの取得に失敗: ${err instanceof Error ? err.message : String(err)}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
602
1088
|
const isMain = process.argv[1]?.endsWith("sync.ts") || process.argv[1]?.endsWith("sync.js");
|
|
603
1089
|
if (isMain) {
|
|
604
1090
|
const daysArg = process.argv.indexOf("--days");
|
|
@@ -622,7 +1108,11 @@ const DEFAULTS = {
|
|
|
622
1108
|
sort: "active",
|
|
623
1109
|
limit: 15,
|
|
624
1110
|
orgs: [],
|
|
625
|
-
watchSeconds: null
|
|
1111
|
+
watchSeconds: null,
|
|
1112
|
+
atlassianSite: null,
|
|
1113
|
+
atlassianEmail: null,
|
|
1114
|
+
jiraProjects: [],
|
|
1115
|
+
confluenceSpaces: []
|
|
626
1116
|
};
|
|
627
1117
|
function intIn(flag, raw, min, max) {
|
|
628
1118
|
if (raw === void 0 || raw.startsWith("--")) throw new RangeError(`${flag} には値が要ります`);
|
|
@@ -633,7 +1123,7 @@ function intIn(flag, raw, min, max) {
|
|
|
633
1123
|
return n;
|
|
634
1124
|
}
|
|
635
1125
|
function parseArgs(argv) {
|
|
636
|
-
const out = { ...DEFAULTS, orgs: [] };
|
|
1126
|
+
const out = { ...DEFAULTS, orgs: [], jiraProjects: [], confluenceSpaces: [] };
|
|
637
1127
|
let command = null;
|
|
638
1128
|
for (let i = 0; i < argv.length; i++) {
|
|
639
1129
|
const a = argv[i];
|
|
@@ -666,6 +1156,31 @@ function parseArgs(argv) {
|
|
|
666
1156
|
out.dataDir = v;
|
|
667
1157
|
break;
|
|
668
1158
|
}
|
|
1159
|
+
// --jira-*はConfluenceを足す前からある名前。同じ資格情報なので受け続ける
|
|
1160
|
+
case "--atlassian-site":
|
|
1161
|
+
case "--atlassian-email":
|
|
1162
|
+
case "--jira-site":
|
|
1163
|
+
case "--jira-email": {
|
|
1164
|
+
const v = argv[++i];
|
|
1165
|
+
if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
|
|
1166
|
+
if (a.endsWith("-site")) out.atlassianSite = v;
|
|
1167
|
+
else out.atlassianEmail = v;
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
case "--confluence-space": {
|
|
1171
|
+
const v = argv[++i];
|
|
1172
|
+
if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
|
|
1173
|
+
if (!/^[A-Za-z0-9_~]+$/.test(v)) throw new RangeError(`--confluence-spaceはスペースキー(英数字)で指定してください(受け取った値: ${v})`);
|
|
1174
|
+
if (!out.confluenceSpaces.includes(v)) out.confluenceSpaces.push(v);
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
case "--jira-project": {
|
|
1178
|
+
const v = argv[++i];
|
|
1179
|
+
if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
|
|
1180
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(v)) throw new RangeError(`--jira-projectはプロジェクトキー(英大文字と数字)で指定してください(受け取った値: ${v})`);
|
|
1181
|
+
if (!out.jiraProjects.includes(v)) out.jiraProjects.push(v);
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
669
1184
|
case "--watch": {
|
|
670
1185
|
const v = argv[++i];
|
|
671
1186
|
const m = v && !v.startsWith("--") ? /^(\d+)(s|m|h)$/.exec(v) : null;
|
|
@@ -717,6 +1232,10 @@ const USAGE = [
|
|
|
717
1232
|
row("--no-open", "ブラウザを開かない"),
|
|
718
1233
|
row("--no-sync", "同期せずに手元のデータで開く"),
|
|
719
1234
|
row("--watch 1m", "開いたまま、この間隔でopen issue/PRを取り直す(30秒以上)。受信箱に自分に起きたことが溜まる"),
|
|
1235
|
+
row("--atlassian-site HOST", "Atlassianのサイト(example.atlassian.net)。--atlassian-emailとセット"),
|
|
1236
|
+
row("--atlassian-email ADDR", "Atlassianのメールアドレス。トークンはキーチェーンから取る"),
|
|
1237
|
+
row("--confluence-space KEY", "齟齬を見るスペースキー(繰り返し可)。省略すると自分が関わった全ページ"),
|
|
1238
|
+
row("--jira-project KEY", "取得するプロジェクトキー(繰り返し可)。省略すると全部"),
|
|
720
1239
|
row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
|
|
721
1240
|
row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
|
|
722
1241
|
row("--sort KEY", "brief の並び: active | recent | stale"),
|
|
@@ -732,8 +1251,10 @@ function defaultDataDir(env, home) {
|
|
|
732
1251
|
return join(base, "github-reporadar", "data");
|
|
733
1252
|
}
|
|
734
1253
|
const HOST = "127.0.0.1";
|
|
735
|
-
const DATA_FILES = /* @__PURE__ */ new Set(["repos.json", "activity.json", "issues.json", "meta.json", "events.json"]);
|
|
1254
|
+
const DATA_FILES = /* @__PURE__ */ new Set(["repos.json", "activity.json", "issues.json", "meta.json", "events.json", "jira.json", "confluence.json", "confluence-pages.json", "slack-mentions.json", "refs.json"]);
|
|
736
1255
|
const PORT_TRIES = 20;
|
|
1256
|
+
const COACH_LOG = "coach-log.md";
|
|
1257
|
+
const COACH_LOG_MAX = 1024 * 1024;
|
|
737
1258
|
const TYPES = {
|
|
738
1259
|
".html": "text/html; charset=utf-8",
|
|
739
1260
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -776,6 +1297,7 @@ function resolveRequest(rawPath, dist, dataDir) {
|
|
|
776
1297
|
const file = insideRoot(dataDir, name);
|
|
777
1298
|
return file ? { file, fallback: false } : null;
|
|
778
1299
|
}
|
|
1300
|
+
if (pathname.startsWith("/api/")) return null;
|
|
779
1301
|
const candidate = insideRoot(dist, pathname);
|
|
780
1302
|
if (candidate && isFile(candidate)) return { file: candidate, fallback: false };
|
|
781
1303
|
return { file: join(dist, "index.html"), fallback: true };
|
|
@@ -798,6 +1320,29 @@ function listen(server, port) {
|
|
|
798
1320
|
}
|
|
799
1321
|
async function startServer(opts) {
|
|
800
1322
|
const server = createServer((req, res) => {
|
|
1323
|
+
if (req.method === "POST" && (req.url ?? "").split("?")[0] === "/api/coach-log") {
|
|
1324
|
+
let body = "";
|
|
1325
|
+
let over = false;
|
|
1326
|
+
req.on("data", (chunk) => {
|
|
1327
|
+
if (over) return;
|
|
1328
|
+
body += chunk.toString("utf8");
|
|
1329
|
+
if (body.length > COACH_LOG_MAX) {
|
|
1330
|
+
over = true;
|
|
1331
|
+
body = "";
|
|
1332
|
+
res.writeHead(413).end();
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
req.on("end", () => {
|
|
1336
|
+
if (over) return;
|
|
1337
|
+
try {
|
|
1338
|
+
writeFileSync(join(opts.dataDir, COACH_LOG), body);
|
|
1339
|
+
res.writeHead(204).end();
|
|
1340
|
+
} catch {
|
|
1341
|
+
res.writeHead(500).end();
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
801
1346
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
802
1347
|
res.writeHead(405).end();
|
|
803
1348
|
return;
|
|
@@ -851,17 +1396,26 @@ async function main() {
|
|
|
851
1396
|
return;
|
|
852
1397
|
}
|
|
853
1398
|
const dataDir = args.dataDir ?? defaultDataDir(process.env, homedir());
|
|
854
|
-
|
|
855
|
-
|
|
1399
|
+
const site = args.atlassianSite;
|
|
1400
|
+
const email = args.atlassianEmail;
|
|
1401
|
+
const jira = site && email ? { site, email, projects: args.jiraProjects } : null;
|
|
1402
|
+
const confluence = site && email ? { site, email, spaces: args.confluenceSpaces } : null;
|
|
1403
|
+
if (args.command === "sync") {
|
|
1404
|
+
await runSync({ dataDir, days: args.days, orgs: args.orgs, jira, confluence });
|
|
1405
|
+
return;
|
|
856
1406
|
}
|
|
857
|
-
if (args.command === "sync") return;
|
|
858
1407
|
if (args.command === "brief") {
|
|
859
1408
|
runBrief({ dataDir, days: args.days, sort: args.sort, limit: args.limit, includePrivate: args.includePrivate });
|
|
860
1409
|
return;
|
|
861
1410
|
}
|
|
862
|
-
|
|
863
|
-
|
|
1411
|
+
const hasData = existsSync(join(dataDir, "meta.json"));
|
|
1412
|
+
if (!hasData) {
|
|
1413
|
+
if (!args.sync) {
|
|
1414
|
+
throw new Error(`データがありません: ${dataDir}
|
|
864
1415
|
先に \`github-reporadar sync\`を実行するか、--no-syncを外してください`);
|
|
1416
|
+
}
|
|
1417
|
+
console.log("はじめての同期です。1分ほどかかります");
|
|
1418
|
+
await runSync({ dataDir, days: args.days, orgs: args.orgs, jira, confluence });
|
|
865
1419
|
}
|
|
866
1420
|
const dist = distDir();
|
|
867
1421
|
if (!existsSync(join(dist, "index.html"))) {
|
|
@@ -873,6 +1427,12 @@ async function main() {
|
|
|
873
1427
|
console.log(`GitHub RepoRadar: ${url} (データ: ${dataDir})`);
|
|
874
1428
|
console.log("止めるにはCtrl+C");
|
|
875
1429
|
if (args.open) openBrowser(url);
|
|
1430
|
+
if (hasData && args.sync) {
|
|
1431
|
+
console.log("裏で同期しています");
|
|
1432
|
+
runSync({ dataDir, days: args.days, orgs: args.orgs, jira, confluence, log: (line) => console.log(`[sync] ${line}`) }).catch(
|
|
1433
|
+
(err) => console.error(`同期に失敗: ${err instanceof Error ? err.message : String(err)}`)
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
876
1436
|
let timer = null;
|
|
877
1437
|
if (args.watchSeconds) {
|
|
878
1438
|
const FULL_EVERY_MS = 30 * 60 * 1e3;
|
|
@@ -891,6 +1451,8 @@ async function main() {
|
|
|
891
1451
|
dataDir,
|
|
892
1452
|
days: args.days,
|
|
893
1453
|
orgs: args.orgs,
|
|
1454
|
+
jira,
|
|
1455
|
+
confluence,
|
|
894
1456
|
mode: full ? "full" : "light",
|
|
895
1457
|
log: (line) => console.log(`[watch] ${line}`)
|
|
896
1458
|
}).catch((err) => console.error(`[watch] 同期に失敗: ${err instanceof Error ? err.message : String(err)}`)).finally(() => {
|