@ondrej-svec/hog 1.23.1 → 1.24.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.
@@ -11,16 +11,17 @@ var __export = (target, all) => {
11
11
  // src/github.ts
12
12
  import { execFile, execFileSync } from "child_process";
13
13
  import { promisify } from "util";
14
- function runGh(args) {
15
- return execFileSync("gh", args, { encoding: "utf-8", timeout: 3e4, stdio: "pipe" }).trim();
14
+ async function runGhAsync(args) {
15
+ const { stdout } = await execFileAsync("gh", args, { encoding: "utf-8", timeout: 3e4 });
16
+ return stdout.trim();
16
17
  }
17
- function runGhJson(args) {
18
- const output = runGh(args);
18
+ async function runGhJsonAsync(args) {
19
+ const output = await runGhAsync(args);
19
20
  return JSON.parse(output);
20
21
  }
21
- function runGhGraphQL(args) {
22
+ async function runGhGraphQLAsync(args) {
22
23
  try {
23
- return runGhJson(args);
24
+ return await runGhJsonAsync(args);
24
25
  } catch (err) {
25
26
  if (err && typeof err === "object" && "stdout" in err) {
26
27
  const stdout = err.stdout;
@@ -35,7 +36,26 @@ function runGhGraphQL(args) {
35
36
  throw err;
36
37
  }
37
38
  }
38
- function fetchRepoIssues(repo, options2 = {}) {
39
+ function parseFieldValues(fieldValues, statusKey) {
40
+ const result = {};
41
+ for (const fv of fieldValues) {
42
+ if (!fv) continue;
43
+ const fieldName = fv.field?.name ?? "";
44
+ if ("date" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {
45
+ result.targetDate = fv.date;
46
+ } else if ("name" in fv && fieldName === "Status" && fv.name) {
47
+ result[statusKey] = fv.name;
48
+ } else if (fieldName) {
49
+ const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
50
+ if (value != null) {
51
+ if (!result.customFields) result.customFields = {};
52
+ result.customFields[fieldName] = value;
53
+ }
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ async function fetchRepoIssuesAsync(repo, options2 = {}) {
39
59
  const { state = "open", limit = 100 } = options2;
40
60
  const args = [
41
61
  "issue",
@@ -52,9 +72,9 @@ function fetchRepoIssues(repo, options2 = {}) {
52
72
  if (options2.assignee) {
53
73
  args.push("--assignee", options2.assignee);
54
74
  }
55
- return runGhJson(args);
75
+ return runGhJsonAsync(args);
56
76
  }
57
- function fetchProjectEnrichment(repo, projectNumber) {
77
+ async function fetchProjectEnrichmentAsync(repo, projectNumber) {
58
78
  const [owner] = repo.split("/");
59
79
  if (!owner) return /* @__PURE__ */ new Map();
60
80
  const projectItemsFragment = `
@@ -116,29 +136,13 @@ function fetchProjectEnrichment(repo, projectNumber) {
116
136
  `projectNumber=${String(projectNumber)}`
117
137
  ];
118
138
  if (cursor) args.push("-f", `cursor=${cursor}`);
119
- const result = runGhGraphQL(args);
139
+ const result = await runGhGraphQLAsync(args);
120
140
  const ownerNode = result?.data?.organization ?? result?.data?.user;
121
141
  const page = ownerNode?.projectV2?.items;
122
142
  const nodes = page?.nodes ?? [];
123
143
  for (const item of nodes) {
124
144
  if (!item?.content?.number) continue;
125
- const enrichment = {};
126
- const fieldValues = item.fieldValues?.nodes ?? [];
127
- for (const fv of fieldValues) {
128
- if (!fv) continue;
129
- const fieldName = fv.field?.name ?? "";
130
- if ("date" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {
131
- enrichment.targetDate = fv.date;
132
- } else if ("name" in fv && fieldName === "Status" && fv.name) {
133
- enrichment.projectStatus = fv.name;
134
- } else if (fieldName) {
135
- const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
136
- if (value != null) {
137
- if (!enrichment.customFields) enrichment.customFields = {};
138
- enrichment.customFields[fieldName] = value;
139
- }
140
- }
141
- }
145
+ const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], "projectStatus");
142
146
  enrichMap.set(item.content.number, enrichment);
143
147
  }
144
148
  if (!page?.pageInfo?.hasNextPage) break;
@@ -149,7 +153,7 @@ function fetchProjectEnrichment(repo, projectNumber) {
149
153
  return /* @__PURE__ */ new Map();
150
154
  }
151
155
  }
152
- function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
156
+ async function fetchProjectStatusOptionsAsync(repo, projectNumber, _statusFieldId) {
153
157
  const [owner] = repo.split("/");
154
158
  if (!owner) return [];
155
159
  const statusFragment = `
@@ -171,7 +175,7 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
171
175
  }
172
176
  `;
173
177
  try {
174
- const result = runGhGraphQL([
178
+ const result = await runGhGraphQLAsync([
175
179
  "api",
176
180
  "graphql",
177
181
  "-f",
@@ -216,7 +220,8 @@ __export(fetch_exports, {
216
220
  fetchDashboard: () => fetchDashboard,
217
221
  fetchRecentActivity: () => fetchRecentActivity
218
222
  });
219
- import { execFileSync as execFileSync2 } from "child_process";
223
+ import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
224
+ import { promisify as promisify2 } from "util";
220
225
  function extractSlackUrl(body) {
221
226
  if (!body) return void 0;
222
227
  const match = body.match(SLACK_URL_RE);
@@ -233,6 +238,116 @@ function extractLinkedIssueNumbers(title, body) {
233
238
  if (!matches) return [];
234
239
  return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];
235
240
  }
241
+ function parseActivityOutput(output, shortName) {
242
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
243
+ const events = [];
244
+ for (const line of output.trim().split("\n")) {
245
+ if (!line.trim()) continue;
246
+ try {
247
+ const ev = JSON.parse(line);
248
+ const timestamp = new Date(ev.created_at);
249
+ if (timestamp.getTime() < cutoff) continue;
250
+ if (ev.type === "CreateEvent") {
251
+ if (ev.ref_type !== "branch" || !ev.ref) continue;
252
+ const issueNumbers = extractIssueNumbersFromBranch(ev.ref);
253
+ for (const num of issueNumbers) {
254
+ events.push({
255
+ type: "branch_created",
256
+ repoShortName: shortName,
257
+ issueNumber: num,
258
+ actor: ev.actor,
259
+ summary: `created branch ${ev.ref}`,
260
+ timestamp,
261
+ branchName: ev.ref
262
+ });
263
+ }
264
+ continue;
265
+ }
266
+ if (!ev.number) continue;
267
+ let eventType;
268
+ let summary;
269
+ let extras = {};
270
+ if (ev.type === "IssueCommentEvent") {
271
+ eventType = "comment";
272
+ const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
273
+ summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
274
+ } else if (ev.type === "IssuesEvent") {
275
+ switch (ev.action) {
276
+ case "opened":
277
+ eventType = "opened";
278
+ summary = `opened #${ev.number}: ${ev.title ?? ""}`;
279
+ break;
280
+ case "closed":
281
+ eventType = "closed";
282
+ summary = `closed #${ev.number}`;
283
+ break;
284
+ case "assigned":
285
+ eventType = "assignment";
286
+ summary = `assigned #${ev.number}`;
287
+ break;
288
+ case "labeled":
289
+ eventType = "labeled";
290
+ summary = `labeled #${ev.number}`;
291
+ break;
292
+ default:
293
+ continue;
294
+ }
295
+ } else if (ev.type === "PullRequestEvent") {
296
+ const prNumber = ev.number;
297
+ extras = { prNumber };
298
+ if (ev.action === "opened") {
299
+ eventType = "pr_opened";
300
+ summary = `opened PR #${prNumber}: ${ev.title ?? ""}`;
301
+ } else if (ev.action === "closed" && ev.merged) {
302
+ eventType = "pr_merged";
303
+ summary = `merged PR #${prNumber}: ${ev.title ?? ""}`;
304
+ } else if (ev.action === "closed") {
305
+ eventType = "pr_closed";
306
+ summary = `closed PR #${prNumber}`;
307
+ } else {
308
+ continue;
309
+ }
310
+ const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);
311
+ for (const issueNum of linkedIssues) {
312
+ events.push({
313
+ type: eventType,
314
+ repoShortName: shortName,
315
+ issueNumber: issueNum,
316
+ actor: ev.actor,
317
+ summary,
318
+ timestamp,
319
+ prNumber
320
+ });
321
+ }
322
+ if (linkedIssues.length === 0) {
323
+ events.push({
324
+ type: eventType,
325
+ repoShortName: shortName,
326
+ issueNumber: prNumber,
327
+ actor: ev.actor,
328
+ summary,
329
+ timestamp,
330
+ prNumber
331
+ });
332
+ }
333
+ continue;
334
+ } else {
335
+ continue;
336
+ }
337
+ events.push({
338
+ type: eventType,
339
+ repoShortName: shortName,
340
+ issueNumber: ev.number,
341
+ actor: ev.actor,
342
+ summary,
343
+ timestamp,
344
+ ...extras
345
+ });
346
+ } catch {
347
+ }
348
+ }
349
+ return events.slice(0, 15);
350
+ }
236
351
  function fetchRecentActivity(repoName, shortName) {
237
352
  try {
238
353
  const output = execFileSync2(
@@ -247,114 +362,61 @@ function fetchRecentActivity(repoName, shortName) {
247
362
  ],
248
363
  { encoding: "utf-8", timeout: 15e3, stdio: "pipe" }
249
364
  );
250
- const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
251
- const events = [];
252
- for (const line of output.trim().split("\n")) {
253
- if (!line.trim()) continue;
254
- try {
255
- const ev = JSON.parse(line);
256
- const timestamp = new Date(ev.created_at);
257
- if (timestamp.getTime() < cutoff) continue;
258
- if (ev.type === "CreateEvent") {
259
- if (ev.ref_type !== "branch" || !ev.ref) continue;
260
- const issueNumbers = extractIssueNumbersFromBranch(ev.ref);
261
- for (const num of issueNumbers) {
262
- events.push({
263
- type: "branch_created",
264
- repoShortName: shortName,
265
- issueNumber: num,
266
- actor: ev.actor,
267
- summary: `created branch ${ev.ref}`,
268
- timestamp,
269
- branchName: ev.ref
270
- });
271
- }
272
- continue;
273
- }
274
- if (!ev.number) continue;
275
- let eventType;
276
- let summary;
277
- let extras = {};
278
- if (ev.type === "IssueCommentEvent") {
279
- eventType = "comment";
280
- const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
281
- summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
282
- } else if (ev.type === "IssuesEvent") {
283
- switch (ev.action) {
284
- case "opened":
285
- eventType = "opened";
286
- summary = `opened #${ev.number}: ${ev.title ?? ""}`;
287
- break;
288
- case "closed":
289
- eventType = "closed";
290
- summary = `closed #${ev.number}`;
291
- break;
292
- case "assigned":
293
- eventType = "assignment";
294
- summary = `assigned #${ev.number}`;
295
- break;
296
- case "labeled":
297
- eventType = "labeled";
298
- summary = `labeled #${ev.number}`;
299
- break;
300
- default:
301
- continue;
302
- }
303
- } else if (ev.type === "PullRequestEvent") {
304
- const prNumber = ev.number;
305
- extras = { prNumber };
306
- if (ev.action === "opened") {
307
- eventType = "pr_opened";
308
- summary = `opened PR #${prNumber}: ${ev.title ?? ""}`;
309
- } else if (ev.action === "closed" && ev.merged) {
310
- eventType = "pr_merged";
311
- summary = `merged PR #${prNumber}: ${ev.title ?? ""}`;
312
- } else if (ev.action === "closed") {
313
- eventType = "pr_closed";
314
- summary = `closed PR #${prNumber}`;
315
- } else {
316
- continue;
317
- }
318
- const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);
319
- for (const issueNum of linkedIssues) {
320
- events.push({
321
- type: eventType,
322
- repoShortName: shortName,
323
- issueNumber: issueNum,
324
- actor: ev.actor,
325
- summary,
326
- timestamp,
327
- prNumber
328
- });
329
- }
330
- if (linkedIssues.length === 0) {
331
- events.push({
332
- type: eventType,
333
- repoShortName: shortName,
334
- issueNumber: prNumber,
335
- actor: ev.actor,
336
- summary,
337
- timestamp,
338
- prNumber
339
- });
340
- }
341
- continue;
342
- } else {
343
- continue;
344
- }
345
- events.push({
346
- type: eventType,
347
- repoShortName: shortName,
348
- issueNumber: ev.number,
349
- actor: ev.actor,
350
- summary,
351
- timestamp,
352
- ...extras
353
- });
354
- } catch {
355
- }
365
+ return parseActivityOutput(output, shortName);
366
+ } catch {
367
+ return [];
368
+ }
369
+ }
370
+ async function fetchRepoDataAsync(repo, assignee) {
371
+ try {
372
+ const fetchOpts = {};
373
+ if (assignee) fetchOpts.assignee = assignee;
374
+ const issues = await fetchRepoIssuesAsync(repo.name, fetchOpts);
375
+ let enrichedIssues = issues;
376
+ let statusOptions = [];
377
+ try {
378
+ const [enrichMap, opts] = await Promise.all([
379
+ fetchProjectEnrichmentAsync(repo.name, repo.projectNumber),
380
+ fetchProjectStatusOptionsAsync(repo.name, repo.projectNumber, repo.statusFieldId)
381
+ ]);
382
+ enrichedIssues = issues.map((issue) => {
383
+ const e = enrichMap.get(issue.number);
384
+ const slackUrl = extractSlackUrl(issue.body ?? "");
385
+ return {
386
+ ...issue,
387
+ ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
388
+ ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
389
+ ...e?.customFields !== void 0 ? { customFields: e.customFields } : {},
390
+ ...slackUrl ? { slackThreadUrl: slackUrl } : {}
391
+ };
392
+ });
393
+ statusOptions = opts;
394
+ } catch {
395
+ enrichedIssues = issues.map((issue) => {
396
+ const slackUrl = extractSlackUrl(issue.body ?? "");
397
+ return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;
398
+ });
356
399
  }
357
- return events.slice(0, 15);
400
+ return { repo, issues: enrichedIssues, statusOptions, error: null };
401
+ } catch (err) {
402
+ return { repo, issues: [], statusOptions: [], error: formatError(err) };
403
+ }
404
+ }
405
+ async function fetchRecentActivityAsync(repoName, shortName) {
406
+ try {
407
+ const { stdout } = await execFileAsync2(
408
+ "gh",
409
+ [
410
+ "api",
411
+ `repos/${repoName}/events`,
412
+ "-f",
413
+ "per_page=30",
414
+ "-q",
415
+ '.[] | select(.type == "IssuesEvent" or .type == "IssueCommentEvent" or .type == "PullRequestEvent" or .type == "CreateEvent") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}'
416
+ ],
417
+ { encoding: "utf-8", timeout: 15e3 }
418
+ );
419
+ return parseActivityOutput(stdout, shortName);
358
420
  } catch {
359
421
  return [];
360
422
  }
@@ -363,62 +425,25 @@ async function fetchDashboard(config2, options2 = {}) {
363
425
  const repos = options2.repoFilter ? config2.repos.filter(
364
426
  (r) => r.shortName === options2.repoFilter || r.name === options2.repoFilter
365
427
  ) : config2.repos;
366
- const repoData = repos.map((repo) => {
367
- try {
368
- const fetchOpts = {};
369
- if (options2.mineOnly) {
370
- fetchOpts.assignee = config2.board.assignee;
371
- }
372
- const issues = fetchRepoIssues(repo.name, fetchOpts);
373
- let enrichedIssues = issues;
374
- let statusOptions = [];
375
- try {
376
- const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);
377
- enrichedIssues = issues.map((issue) => {
378
- const e = enrichMap.get(issue.number);
379
- const slackUrl = extractSlackUrl(issue.body ?? "");
380
- return {
381
- ...issue,
382
- ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
383
- ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
384
- ...e?.customFields !== void 0 ? { customFields: e.customFields } : {},
385
- ...slackUrl ? { slackThreadUrl: slackUrl } : {}
386
- };
387
- });
388
- statusOptions = fetchProjectStatusOptions(
389
- repo.name,
390
- repo.projectNumber,
391
- repo.statusFieldId
392
- );
393
- } catch {
394
- enrichedIssues = issues.map((issue) => {
395
- const slackUrl = extractSlackUrl(issue.body ?? "");
396
- return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;
397
- });
398
- }
399
- return { repo, issues: enrichedIssues, statusOptions, error: null };
400
- } catch (err) {
401
- return { repo, issues: [], statusOptions: [], error: formatError(err) };
402
- }
403
- });
404
- const activity = [];
405
- for (const repo of repos) {
406
- const events = fetchRecentActivity(repo.name, repo.shortName);
407
- activity.push(...events);
408
- }
409
- activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
428
+ const assignee = options2.mineOnly ? config2.board.assignee : void 0;
429
+ const [repoData, ...activityResults] = await Promise.all([
430
+ Promise.all(repos.map((repo) => fetchRepoDataAsync(repo, assignee))),
431
+ ...repos.map((repo) => fetchRecentActivityAsync(repo.name, repo.shortName))
432
+ ]);
433
+ const activity = activityResults.flat().sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
410
434
  return {
411
435
  repos: repoData,
412
436
  activity: activity.slice(0, 15),
413
437
  fetchedAt: /* @__PURE__ */ new Date()
414
438
  };
415
439
  }
416
- var SLACK_URL_RE;
440
+ var execFileAsync2, SLACK_URL_RE;
417
441
  var init_fetch = __esm({
418
442
  "src/board/fetch.ts"() {
419
443
  "use strict";
420
444
  init_github();
421
445
  init_utils();
446
+ execFileAsync2 = promisify2(execFile2);
422
447
  SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
423
448
  }
424
449
  });