co-maintainer 0.4.8 → 0.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,6 +53,7 @@ export async function parseArgs(args) {
53
53
  console.log(" --include-commit-history --include-how-repo-works");
54
54
  console.log(" --max-commits=N --max-pr-months=N --max-pull-request-change-lines=N --max-comment=N");
55
55
  console.log(" --only-request-changed-pr");
56
+ console.log(" --pr-state=open,closed,merged");
56
57
  console.log(" --auth=gh|pat --github-pat=... --ai=none|openrouter|hetzner --token=... --low-model=... --high-model=...");
57
58
  process.exit(0);
58
59
  }
@@ -111,6 +112,24 @@ export async function parseArgs(args) {
111
112
  : undefined;
112
113
  };
113
114
  const text = (name) => rest.find((item) => item.startsWith(`--${name}=`))?.slice(name.length + 3);
115
+ const prStateOption = (args, saved) => {
116
+ const prefix = "--pr-state=";
117
+ const raw = args
118
+ .find((arg) => arg.startsWith(prefix))
119
+ ?.slice(prefix.length);
120
+ if (raw === undefined)
121
+ return saved && saved.length > 0 ? saved : undefined;
122
+ const allowed = ["open", "closed", "merged"];
123
+ const parts = raw
124
+ .split(",")
125
+ .map((part) => part.trim())
126
+ .filter(Boolean);
127
+ if (parts.length === 0 ||
128
+ parts.some((part) => !allowed.includes(part))) {
129
+ die("pr-state must be a comma-separated list of: open, closed, merged");
130
+ }
131
+ return [...new Set(parts)];
132
+ };
114
133
  const choice = (name, allowed, fallback) => {
115
134
  const prefix = `--${name}=`;
116
135
  const raw = rest
@@ -130,6 +149,7 @@ export async function parseArgs(args) {
130
149
  "max-pr-months",
131
150
  "max-pull-request-change-lines",
132
151
  "max-comment",
152
+ "pr-state",
133
153
  "improve-matrix",
134
154
  "env",
135
155
  "gh-concurrent",
@@ -245,6 +265,7 @@ export async function parseArgs(args) {
245
265
  includeHowRepoWorks: enabled("include-how-repo-works"),
246
266
  onlyRequestChangedPr: rest.includes("--only-request-changed-pr") ||
247
267
  repoConfig.onlyRequestChangedPr === true,
268
+ prState: prStateOption(rest, repoConfig.prState),
248
269
  maxCommits: value("max-commits") ?? repoConfig.maxCommits ?? configDefault.maxCommits,
249
270
  maxPrMonths: value("max-pr-months") ??
250
271
  repoConfig.maxPrMonths ??
@@ -18,6 +18,7 @@ export type RepoConfig = {
18
18
  includeCommitHistory?: boolean;
19
19
  includeHowRepoWorks?: boolean;
20
20
  onlyRequestChangedPr?: boolean;
21
+ prState?: ("open" | "closed" | "merged")[];
21
22
  /** Five field cron in UTC, checked by `serve` to queue a remake. */
22
23
  remakeCron?: string;
23
24
  };
@@ -30,6 +30,36 @@ function listingCovers(cachedMonths, current) {
30
30
  return false;
31
31
  return cachedMonths >= current;
32
32
  }
33
+ function stateKey(states) {
34
+ if (!states || states.length === 0)
35
+ return "";
36
+ return [...states].sort().join(",");
37
+ }
38
+ /** `closed` is closed and not merged. GitHub's list has no merged state. */
39
+ function matchesPrState(pr, states) {
40
+ if (!states || states.length === 0)
41
+ return true;
42
+ const merged = Boolean(pr.merged_at);
43
+ const state = String(pr.state ?? "");
44
+ return states.some((wanted) => {
45
+ if (wanted === "merged")
46
+ return merged;
47
+ if (wanted === "open")
48
+ return state === "open";
49
+ return state === "closed" && !merged;
50
+ });
51
+ }
52
+ function listQueryState(states) {
53
+ if (!states || states.length === 0)
54
+ return "all";
55
+ const wantsOpen = states.includes("open");
56
+ const wantsClosed = states.includes("closed") || states.includes("merged");
57
+ if (wantsOpen && wantsClosed)
58
+ return "all";
59
+ if (wantsOpen)
60
+ return "open";
61
+ return "closed";
62
+ }
33
63
  async function loadListing(repo) {
34
64
  const raw = await cacheGet("pr-listing", repo);
35
65
  if (!raw)
@@ -39,20 +69,26 @@ async function loadListing(repo) {
39
69
  return undefined;
40
70
  return parsed;
41
71
  }
42
- async function saveListing(repo, maxPrMonths, items) {
43
- await cacheSet("pr-listing", repo, JSON.stringify({ maxPrMonths: maxPrMonths ?? 0, items }));
72
+ async function saveListing(repo, maxPrMonths, states, items) {
73
+ await cacheSet("pr-listing", repo, JSON.stringify({
74
+ maxPrMonths: maxPrMonths ?? 0,
75
+ states: stateKey(states),
76
+ items,
77
+ }));
44
78
  }
45
79
  async function listPullRequestPages(client, options, phase) {
46
80
  const cached = await loadListing(options.repo);
47
81
  const canCatchUp = cached !== undefined &&
48
- listingCovers(cached.maxPrMonths, options.maxPrMonths);
82
+ listingCovers(cached.maxPrMonths, options.maxPrMonths) &&
83
+ (cached.states ?? "") === stateKey(options.prState);
49
84
  const cachedByNumber = new Map((cached?.items ?? []).map((pr) => [Number(pr.number), pr]));
50
85
  const selected = [];
51
86
  const seen = new Set();
52
87
  const concurrency = Math.max(1, options.ghConcurrent);
53
- log("fetch", `pull request listing · concurrency=${concurrency}`);
88
+ const states = stateKey(options.prState) || "all";
89
+ log("fetch", `pull request listing · ${states} · concurrency=${concurrency}`);
54
90
  const fetchPage = async (page) => {
55
- const pageItems = await client.request(`repos/${options.repo}/pulls?state=all&sort=updated&direction=desc&per_page=100&page=${page}`);
91
+ const pageItems = await client.request(`repos/${options.repo}/pulls?state=${listQueryState(options.prState)}&sort=updated&direction=desc&per_page=100&page=${page}`);
56
92
  return Array.isArray(pageItems) ? pageItems : [];
57
93
  };
58
94
  const ingest = (page, pageItems) => {
@@ -72,6 +108,8 @@ async function listPullRequestPages(client, options, phase) {
72
108
  reason = "window reached";
73
109
  break;
74
110
  }
111
+ if (!matchesPrState(pr, options.prState))
112
+ continue;
75
113
  const number = Number(pr.number);
76
114
  selected.push(pr);
77
115
  seen.add(number);
@@ -111,6 +149,8 @@ async function listPullRequestPages(client, options, phase) {
111
149
  if (!withinPrWindow(String(pr.updated_at ?? ""), options.maxPrMonths)) {
112
150
  continue;
113
151
  }
152
+ if (!matchesPrState(pr, options.prState))
153
+ continue;
114
154
  selected.push(pr);
115
155
  seen.add(number);
116
156
  reused++;
@@ -119,7 +159,7 @@ async function listPullRequestPages(client, options, phase) {
119
159
  log("fetch", `pull request listing · reused ${reused} cached PRs · ${selected.length} total`);
120
160
  }
121
161
  }
122
- await saveListing(options.repo, options.maxPrMonths, selected);
162
+ await saveListing(options.repo, options.maxPrMonths, options.prState, selected);
123
163
  return selected;
124
164
  }
125
165
  async function mapPool(items, concurrency, fn) {
@@ -271,7 +311,8 @@ async function pullRequests(client, options, previous, phase, progress) {
271
311
  const discussionUnchanged = cached?.updatedAt === current.updatedAt;
272
312
  const decisionKnown = typeof cached?.changesRequested === "boolean";
273
313
  const diffUnchanged = cached?.headSha === current.headSha && Boolean(cached?.diff);
274
- let dropped = only && discussionUnchanged && cached?.changesRequested === false;
314
+ let droppedFromCache = only && discussionUnchanged && cached?.changesRequested === false;
315
+ let dropped = droppedFromCache;
275
316
  const loadComments = async () => {
276
317
  const comments = await client.pages(`repos/${options.repo}/issues/${number}/comments`);
277
318
  current.comments = comments
@@ -296,7 +337,9 @@ async function pullRequests(client, options, previous, phase, progress) {
296
337
  await loadReviews();
297
338
  }
298
339
  const discussionStatus = dropped
299
- ? "dropped"
340
+ ? droppedFromCache
341
+ ? "dropped cache"
342
+ : "dropped download"
300
343
  : discussionUnchanged
301
344
  ? "comments/reviews cache"
302
345
  : "download comments/reviews";
@@ -260,6 +260,7 @@ export async function runInitOrRemake(options) {
260
260
  includeCommitHistory: options.includeCommitHistory,
261
261
  includeHowRepoWorks: options.includeHowRepoWorks,
262
262
  onlyRequestChangedPr: options.onlyRequestChangedPr,
263
+ prState: options.prState,
263
264
  });
264
265
  });
265
266
  log("write", `${path} · ${result.changed.length
@@ -308,6 +309,7 @@ export function optionsFromConfig(repo, command) {
308
309
  includeCommitHistory: repoConfig.includeCommitHistory ?? true,
309
310
  includeHowRepoWorks: repoConfig.includeHowRepoWorks ?? true,
310
311
  onlyRequestChangedPr: repoConfig.onlyRequestChangedPr === true,
312
+ prState: repoConfig.prState,
311
313
  maxCommits: repoConfig.maxCommits ?? config.defaults?.maxCommits,
312
314
  maxPrMonths: repoConfig.maxPrMonths ?? config.defaults?.maxPrMonths,
313
315
  maxPullRequestChangeLines: repoConfig.maxPullRequestChangeLines ??
@@ -37,6 +37,8 @@ export type Options = {
37
37
  /** Keep only pull requests in the window that have at least one
38
38
  * `CHANGES_REQUESTED` review. Off by default, so the window is unchanged. */
39
39
  onlyRequestChangedPr: boolean;
40
+ /** Empty or omitted means every state. `closed` is closed and not merged. */
41
+ prState?: ("open" | "closed" | "merged")[];
40
42
  maxCommits?: number;
41
43
  maxPrMonths?: number;
42
44
  maxPullRequestChangeLines?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {