standup-mr 0.1.2 → 0.2.1
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/CHANGELOG.md +94 -0
- package/README.md +112 -23
- package/dist/chunk-DL2P3XR6.js +1119 -0
- package/dist/cli.js +10 -13
- package/dist/index.d.ts +138 -17
- package/dist/index.js +45 -5
- package/dist/mcp/server.js +610 -82
- package/mcp/README.md +28 -6
- package/package.json +29 -7
- package/skills/standup/SKILL.md +73 -26
- package/dist/chunk-SKCJT2AY.js +0 -551
package/dist/mcp/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { pathToFileURL } from "url";
|
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
9
|
|
|
10
10
|
// src/config/config.constants.ts
|
|
11
|
-
var
|
|
11
|
+
var CLI_TIMEOUT_MS = 15e3;
|
|
12
12
|
|
|
13
13
|
// src/config/config.ts
|
|
14
14
|
var ConfigError = class extends Error {
|
|
@@ -17,51 +17,61 @@ var ConfigError = class extends Error {
|
|
|
17
17
|
this.name = "ConfigError";
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
var GITLAB_LABELS = {
|
|
21
|
+
name: "GitLab",
|
|
22
|
+
cli: "glab",
|
|
23
|
+
envHost: "GITLAB_HOST",
|
|
24
|
+
envToken: "GITLAB_TOKEN",
|
|
25
|
+
login: (host) => `glab auth login --hostname ${host}`
|
|
26
|
+
};
|
|
27
|
+
var GITHUB_LABELS = {
|
|
28
|
+
name: "GitHub",
|
|
29
|
+
cli: "gh",
|
|
30
|
+
envHost: "GITHUB_HOST",
|
|
31
|
+
envToken: "GITHUB_TOKEN",
|
|
32
|
+
login: (host) => `gh auth login --hostname ${host}`
|
|
33
|
+
};
|
|
34
|
+
function resolveHost(cliHost, envHost, hostList, labels = GITLAB_LABELS) {
|
|
21
35
|
if (cliHost) return cliHost;
|
|
22
36
|
if (envHost) return envHost;
|
|
23
|
-
const hosts =
|
|
37
|
+
const hosts = hostList ?? [];
|
|
24
38
|
if (hosts.length === 1) return hosts[0];
|
|
25
39
|
if (hosts.length === 0) {
|
|
26
40
|
throw new ConfigError(
|
|
27
|
-
|
|
41
|
+
`No ${labels.name} host configured. Pass --host, set ${labels.envHost}, or log in with \`${labels.cli} auth login\`.`
|
|
28
42
|
);
|
|
29
43
|
}
|
|
30
44
|
throw new ConfigError(
|
|
31
|
-
`Multiple
|
|
45
|
+
`Multiple ${labels.name} hosts found (${[...hosts].sort().join(", ")}). Pick one with --host or ${labels.envHost}.`
|
|
32
46
|
);
|
|
33
47
|
}
|
|
34
|
-
function resolveToken(host, cliToken, envToken,
|
|
48
|
+
function resolveToken(host, cliToken, envToken, lookup, labels = GITLAB_LABELS) {
|
|
35
49
|
if (cliToken) return cliToken;
|
|
36
50
|
if (envToken) return envToken;
|
|
37
|
-
if (
|
|
38
|
-
const token =
|
|
51
|
+
if (lookup) {
|
|
52
|
+
const token = lookup(host);
|
|
39
53
|
if (token) return token;
|
|
40
54
|
}
|
|
41
55
|
throw new ConfigError(
|
|
42
|
-
`No token for ${host}. Pass --token, set
|
|
56
|
+
`No token for ${host}. Pass --token, set ${labels.envToken}, or run \`${labels.login(host)}\`.`
|
|
43
57
|
);
|
|
44
58
|
}
|
|
45
|
-
function
|
|
59
|
+
function cliCapture(cli, args) {
|
|
46
60
|
try {
|
|
47
|
-
return execFileSync(
|
|
61
|
+
return execFileSync(cli, args, {
|
|
48
62
|
encoding: "utf8",
|
|
49
|
-
timeout:
|
|
63
|
+
timeout: CLI_TIMEOUT_MS,
|
|
50
64
|
stdio: ["ignore", "pipe", "ignore"]
|
|
51
65
|
}).trim();
|
|
52
66
|
} catch {
|
|
53
67
|
return "";
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
|
-
function
|
|
57
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map((match) => match[1]);
|
|
58
|
-
return [...new Set(hosts)].sort();
|
|
59
|
-
}
|
|
60
|
-
function glabStatus() {
|
|
70
|
+
function cliStatus(cli) {
|
|
61
71
|
try {
|
|
62
|
-
return execFileSync(
|
|
72
|
+
return execFileSync(cli, ["auth", "status"], {
|
|
63
73
|
encoding: "utf8",
|
|
64
|
-
timeout:
|
|
74
|
+
timeout: CLI_TIMEOUT_MS,
|
|
65
75
|
stdio: ["ignore", "pipe", "pipe"]
|
|
66
76
|
});
|
|
67
77
|
} catch (error) {
|
|
@@ -69,11 +79,21 @@ function glabStatus() {
|
|
|
69
79
|
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
82
|
+
function parseLoggedInHosts(statusOutput) {
|
|
83
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map((match) => match[1]);
|
|
84
|
+
return [...new Set(hosts)].sort();
|
|
85
|
+
}
|
|
72
86
|
function glabHosts() {
|
|
73
|
-
return
|
|
87
|
+
return parseLoggedInHosts(cliStatus("glab"));
|
|
74
88
|
}
|
|
75
89
|
function glabToken(host) {
|
|
76
|
-
return glab
|
|
90
|
+
return cliCapture("glab", ["config", "get", "token", "--host", host]);
|
|
91
|
+
}
|
|
92
|
+
function ghHosts() {
|
|
93
|
+
return parseLoggedInHosts(cliStatus("gh"));
|
|
94
|
+
}
|
|
95
|
+
function ghToken(host) {
|
|
96
|
+
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
// src/buckets/buckets.constants.ts
|
|
@@ -123,6 +143,12 @@ function isoDay(day) {
|
|
|
123
143
|
const date = String(day.getDate()).padStart(2, "0");
|
|
124
144
|
return `${year}-${month}-${date}`;
|
|
125
145
|
}
|
|
146
|
+
function localAt(iso) {
|
|
147
|
+
const at = new Date(iso);
|
|
148
|
+
const hours = String(at.getHours()).padStart(2, "0");
|
|
149
|
+
const minutes = String(at.getMinutes()).padStart(2, "0");
|
|
150
|
+
return `${isoDay(at)}T${hours}:${minutes}`;
|
|
151
|
+
}
|
|
126
152
|
function label(day, lang = "en") {
|
|
127
153
|
const key = DAYS[lang] ? lang : "en";
|
|
128
154
|
const weekday = DAYS[key][day.getDay()];
|
|
@@ -130,21 +156,27 @@ function label(day, lang = "en") {
|
|
|
130
156
|
const date = day.getDate();
|
|
131
157
|
return key === "tr" ? `${date} ${month} ${weekday}` : `${weekday}, ${date} ${month}`;
|
|
132
158
|
}
|
|
133
|
-
function
|
|
159
|
+
function isWeekday(day) {
|
|
160
|
+
const weekday = (/* @__PURE__ */ new Date(`${day}T00:00:00`)).getDay();
|
|
161
|
+
return weekday >= 1 && weekday <= 5;
|
|
162
|
+
}
|
|
163
|
+
function previousActiveDays(eventDates, today) {
|
|
134
164
|
const cutoff = isoDay(today);
|
|
135
|
-
const past = [...eventDates].filter((
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const past = [...eventDates].filter((day) => day < cutoff).sort();
|
|
166
|
+
if (past.length === 0) return [];
|
|
167
|
+
const anchor = past.filter(isWeekday).pop() ?? past[0];
|
|
168
|
+
return past.filter((day) => day >= anchor).map((day) => ({
|
|
169
|
+
date: day,
|
|
170
|
+
gapDays: Math.round(
|
|
171
|
+
(Date.parse(`${cutoff}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / MS_PER_DAY
|
|
172
|
+
)
|
|
173
|
+
}));
|
|
142
174
|
}
|
|
143
175
|
|
|
144
176
|
// src/buckets/buckets.ts
|
|
145
177
|
function classify(mr, today, staleDays = STALE_DAYS) {
|
|
146
178
|
if (mr.draft) return "draft";
|
|
147
|
-
if (mr.pipeline === "failed" || mr.unresolved > 0) return "blocked";
|
|
179
|
+
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0) return "blocked";
|
|
148
180
|
const age = Math.round(
|
|
149
181
|
(Date.parse(`${isoDay(today)}T00:00:00Z`) - Date.parse(`${mr.updated}T00:00:00Z`)) / MS_PER_DAY
|
|
150
182
|
);
|
|
@@ -163,6 +195,11 @@ var SECTION = /section_(start|end):\d+:\S*/g;
|
|
|
163
195
|
var SIGNAL = /(\berror\b|Error:|fatal:|npm ERR!|\bfailed\b)/i;
|
|
164
196
|
var NOISE = /^(Cleaning up|Job failed: exit status|ERROR: Job failed|Uploading artifacts|Job succeeded)/i;
|
|
165
197
|
var MAX_LINE = 200;
|
|
198
|
+
var TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/;
|
|
199
|
+
var ERROR_MARKER = /^##\[error\]\s*/;
|
|
200
|
+
var GROUP_START = /^##\[group\]/;
|
|
201
|
+
var GROUP_END = /^##\[endgroup\]/;
|
|
202
|
+
var RUN_GROUP = /^##\[group\]Run\s/;
|
|
166
203
|
|
|
167
204
|
// src/trace/trace.ts
|
|
168
205
|
function extractErrors(rawTrace, limit = 8) {
|
|
@@ -170,9 +207,21 @@ function extractErrors(rawTrace, limit = 8) {
|
|
|
170
207
|
const cleaned = rawTrace.replace(ANSI, "").replace(SECTION, "");
|
|
171
208
|
const seen = /* @__PURE__ */ new Set();
|
|
172
209
|
const hits = [];
|
|
210
|
+
let suppressing = false;
|
|
173
211
|
for (const rawLine of cleaned.split("\n")) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
212
|
+
const stamped = rawLine.trim().replace(TIMESTAMP, "");
|
|
213
|
+
if (GROUP_START.test(stamped)) {
|
|
214
|
+
suppressing = RUN_GROUP.test(stamped);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (GROUP_END.test(stamped)) {
|
|
218
|
+
suppressing = false;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (suppressing) continue;
|
|
222
|
+
if (!stamped || NOISE.test(stamped) || !SIGNAL.test(stamped)) continue;
|
|
223
|
+
const line = stamped.replace(ERROR_MARKER, "").trim();
|
|
224
|
+
if (!line) continue;
|
|
176
225
|
const clipped = line.slice(0, MAX_LINE);
|
|
177
226
|
if (seen.has(clipped)) continue;
|
|
178
227
|
seen.add(clipped);
|
|
@@ -181,64 +230,465 @@ function extractErrors(rawTrace, limit = 8) {
|
|
|
181
230
|
return hits.slice(-limit);
|
|
182
231
|
}
|
|
183
232
|
|
|
184
|
-
// src/providers/
|
|
233
|
+
// src/providers/base/http.ts
|
|
234
|
+
function buildUrl(api, path, params) {
|
|
235
|
+
const base = `${api}/${path}`;
|
|
236
|
+
if (!params || Object.keys(params).length === 0) return base;
|
|
237
|
+
const query = new URLSearchParams();
|
|
238
|
+
for (const [key, value] of Object.entries(params)) {
|
|
239
|
+
query.set(key, String(value));
|
|
240
|
+
}
|
|
241
|
+
return `${base}?${query.toString()}`;
|
|
242
|
+
}
|
|
243
|
+
var ApiError = class extends Error {
|
|
244
|
+
constructor(message) {
|
|
245
|
+
super(message);
|
|
246
|
+
this.name = "ApiError";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function remaining(response) {
|
|
250
|
+
return response.headers.get("x-ratelimit-remaining") ?? response.headers.get("ratelimit-remaining");
|
|
251
|
+
}
|
|
252
|
+
function resetAt(response) {
|
|
253
|
+
const reset = response.headers.get("x-ratelimit-reset") ?? response.headers.get("ratelimit-reset");
|
|
254
|
+
if (!reset) return "";
|
|
255
|
+
const seconds = Number(reset);
|
|
256
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "";
|
|
257
|
+
return `; resets at ${new Date(seconds * 1e3).toISOString()}`;
|
|
258
|
+
}
|
|
259
|
+
function retryAfter(response) {
|
|
260
|
+
const value = response.headers.get("retry-after");
|
|
261
|
+
if (!value) return "";
|
|
262
|
+
const seconds = Number(value);
|
|
263
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return `; retry after ${value}`;
|
|
264
|
+
return `; retry after ${seconds}s`;
|
|
265
|
+
}
|
|
266
|
+
function assertUsable(response, host) {
|
|
267
|
+
if (response.ok || response.status === 404) return;
|
|
268
|
+
if (response.status === 403 || response.status === 429) {
|
|
269
|
+
if (remaining(response) === "0") {
|
|
270
|
+
throw new ApiError(`${host} rate limit reached${resetAt(response)}.`);
|
|
271
|
+
}
|
|
272
|
+
const retry = retryAfter(response);
|
|
273
|
+
if (retry) throw new ApiError(`${host} rate limit reached${retry}.`);
|
|
274
|
+
}
|
|
275
|
+
if (response.status === 401) {
|
|
276
|
+
throw new ApiError(
|
|
277
|
+
`${host} rejected the token (401). Check the token and its scopes.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (response.status === 403) {
|
|
281
|
+
throw new ApiError(
|
|
282
|
+
`${host} refused the request (403). The token has no access to that resource.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
throw new ApiError(`${host} returned ${response.status}.`);
|
|
286
|
+
}
|
|
287
|
+
function unreachable(host, cause) {
|
|
288
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
289
|
+
return new ApiError(`Could not reach ${host}: ${detail}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/providers/github/github.constants.ts
|
|
185
293
|
var PAGE_SIZE = 100;
|
|
186
294
|
var FRESH_REVIEW_DAYS = 7;
|
|
295
|
+
var API_VERSION = "2022-11-28";
|
|
296
|
+
var DOT_COM = "github.com";
|
|
297
|
+
var SEARCH_CAP = 3;
|
|
298
|
+
var EVENT_FEED_CAP = 300;
|
|
299
|
+
var EVENTS_PAGE_CAP = EVENT_FEED_CAP / PAGE_SIZE;
|
|
300
|
+
var FAILED_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
301
|
+
"failure",
|
|
302
|
+
"timed_out",
|
|
303
|
+
"startup_failure",
|
|
304
|
+
"action_required"
|
|
305
|
+
]);
|
|
306
|
+
var EVENT_ACTIONS = {
|
|
307
|
+
PushEvent: "pushed to",
|
|
308
|
+
PullRequestReviewEvent: "reviewed",
|
|
309
|
+
PullRequestReviewCommentEvent: "commented on",
|
|
310
|
+
IssueCommentEvent: "commented on",
|
|
311
|
+
CreateEvent: "created",
|
|
312
|
+
DeleteEvent: "deleted"
|
|
313
|
+
};
|
|
314
|
+
var EVENT_TARGET_TYPES = {
|
|
315
|
+
PullRequestEvent: "MergeRequest",
|
|
316
|
+
PullRequestReviewEvent: "MergeRequest",
|
|
317
|
+
PullRequestReviewCommentEvent: "MergeRequest",
|
|
318
|
+
IssueCommentEvent: "Note"
|
|
319
|
+
};
|
|
187
320
|
|
|
188
|
-
// src/providers/
|
|
189
|
-
|
|
321
|
+
// src/providers/github/github.map.ts
|
|
322
|
+
function repoFromUrl(repositoryUrl) {
|
|
323
|
+
const match = /\/repos\/([^/]+\/[^/]+)/.exec(repositoryUrl);
|
|
324
|
+
return match ? match[1] : "";
|
|
325
|
+
}
|
|
326
|
+
function mapEvent(raw) {
|
|
327
|
+
const payload = raw.payload ?? {};
|
|
328
|
+
const type = raw.type ?? "";
|
|
329
|
+
const commits = payload.commits ?? [];
|
|
330
|
+
const last = commits[commits.length - 1];
|
|
331
|
+
return {
|
|
332
|
+
at: localAt(raw.created_at),
|
|
333
|
+
action: type === "PullRequestEvent" ? String(payload.action ?? "updated") : EVENT_ACTIONS[type] ?? type.replace(/Event$/, "").toLowerCase(),
|
|
334
|
+
project: raw.repo?.name ?? "",
|
|
335
|
+
targetType: EVENT_TARGET_TYPES[type] ?? "",
|
|
336
|
+
title: payload.pull_request?.title ?? payload.issue?.title ?? "",
|
|
337
|
+
branch: String(payload.ref ?? "").replace(/^refs\/heads\//, ""),
|
|
338
|
+
commits: Number(payload.size ?? 0),
|
|
339
|
+
commitTitle: last ? String(last.message ?? "").split("\n")[0] : ""
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/providers/github/github.state.ts
|
|
344
|
+
function normalizeChecks(runs) {
|
|
345
|
+
if (runs.length === 0) return { pipeline: null, pipelineId: null };
|
|
346
|
+
const failed = runs.find(
|
|
347
|
+
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
348
|
+
);
|
|
349
|
+
if (failed) return { pipeline: "failed", pipelineId: failed.id };
|
|
350
|
+
if (runs.some((run) => run.status !== "completed")) {
|
|
351
|
+
return { pipeline: "running", pipelineId: null };
|
|
352
|
+
}
|
|
353
|
+
if (runs.some((run) => run.conclusion === "cancelled")) {
|
|
354
|
+
return { pipeline: "canceled", pipelineId: null };
|
|
355
|
+
}
|
|
356
|
+
return { pipeline: "success", pipelineId: null };
|
|
357
|
+
}
|
|
358
|
+
function latestStateByReviewer(reviews) {
|
|
359
|
+
const latest = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const review of reviews) {
|
|
361
|
+
const login = review.user?.login;
|
|
362
|
+
if (!login) continue;
|
|
363
|
+
if (review.state === "COMMENTED" || review.state === "PENDING") continue;
|
|
364
|
+
latest.set(login, review.state);
|
|
365
|
+
}
|
|
366
|
+
return latest;
|
|
367
|
+
}
|
|
368
|
+
function countChangesRequested(reviews) {
|
|
369
|
+
return [...latestStateByReviewer(reviews).values()].filter(
|
|
370
|
+
(state) => state === "CHANGES_REQUESTED"
|
|
371
|
+
).length;
|
|
372
|
+
}
|
|
373
|
+
function approvedBy(reviews, login) {
|
|
374
|
+
return latestStateByReviewer(reviews).get(login) === "APPROVED";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/providers/github/github.ts
|
|
378
|
+
var GitHubProvider = class {
|
|
379
|
+
kind = "github";
|
|
190
380
|
host;
|
|
191
381
|
api;
|
|
192
382
|
token;
|
|
193
383
|
fetchImpl;
|
|
384
|
+
identity = null;
|
|
194
385
|
constructor(host, token, fetchImpl = fetch) {
|
|
195
386
|
this.host = host;
|
|
387
|
+
this.api = host === DOT_COM ? "https://api.github.com" : `https://${host}/api/v3`;
|
|
196
388
|
this.token = token;
|
|
197
|
-
this.api = `https://${host}/api/v4`;
|
|
198
389
|
this.fetchImpl = fetchImpl;
|
|
199
390
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
391
|
+
headers() {
|
|
392
|
+
return {
|
|
393
|
+
authorization: `Bearer ${this.token}`,
|
|
394
|
+
accept: "application/vnd.github+json",
|
|
395
|
+
"x-github-api-version": API_VERSION
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async send(url, init) {
|
|
399
|
+
try {
|
|
400
|
+
return await this.fetchImpl(url, { headers: this.headers(), ...init });
|
|
401
|
+
} catch (cause) {
|
|
402
|
+
throw unreachable(this.host, cause);
|
|
206
403
|
}
|
|
207
|
-
return `${base}?${query.toString()}`;
|
|
208
404
|
}
|
|
209
405
|
async getJson(path, params) {
|
|
406
|
+
const response = await this.send(buildUrl(this.api, path, params));
|
|
407
|
+
assertUsable(response, this.host);
|
|
408
|
+
if (!response.ok) return null;
|
|
210
409
|
try {
|
|
211
|
-
const response = await this.fetchImpl(this.url(path, params), {
|
|
212
|
-
headers: { "PRIVATE-TOKEN": this.token }
|
|
213
|
-
});
|
|
214
|
-
if (!response.ok) return null;
|
|
215
410
|
return await response.json();
|
|
216
|
-
} catch {
|
|
217
|
-
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
throw unreachable(this.host, cause);
|
|
218
413
|
}
|
|
219
414
|
}
|
|
220
|
-
async
|
|
415
|
+
async getPaged(path, params = {}, cap = 5) {
|
|
416
|
+
const rows = [];
|
|
417
|
+
for (let page = 1; page <= cap; page += 1) {
|
|
418
|
+
const chunk = await this.getJson(path, {
|
|
419
|
+
...params,
|
|
420
|
+
per_page: PAGE_SIZE,
|
|
421
|
+
page
|
|
422
|
+
});
|
|
423
|
+
if (!chunk || chunk.length === 0) break;
|
|
424
|
+
rows.push(...chunk);
|
|
425
|
+
if (chunk.length < PAGE_SIZE) break;
|
|
426
|
+
}
|
|
427
|
+
return rows;
|
|
428
|
+
}
|
|
429
|
+
async getSearch(query, cap = 3) {
|
|
430
|
+
const rows = [];
|
|
431
|
+
for (let page = 1; page <= cap; page += 1) {
|
|
432
|
+
const chunk = await this.getJson("search/issues", {
|
|
433
|
+
q: query,
|
|
434
|
+
per_page: PAGE_SIZE,
|
|
435
|
+
page
|
|
436
|
+
});
|
|
437
|
+
const items = chunk?.items ?? [];
|
|
438
|
+
if (items.length === 0) break;
|
|
439
|
+
rows.push(...items);
|
|
440
|
+
if (items.length < PAGE_SIZE) break;
|
|
441
|
+
}
|
|
442
|
+
return rows;
|
|
443
|
+
}
|
|
444
|
+
async getLogText(path) {
|
|
445
|
+
const first = await this.send(buildUrl(this.api, path), { redirect: "manual" });
|
|
446
|
+
if (first.status === 404) return "";
|
|
447
|
+
const location = first.headers.get("location");
|
|
448
|
+
if (!location) {
|
|
449
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400) return "";
|
|
450
|
+
assertUsable(first, this.host);
|
|
451
|
+
return first.ok ? await first.text() : "";
|
|
452
|
+
}
|
|
453
|
+
let blob;
|
|
454
|
+
try {
|
|
455
|
+
blob = await this.fetchImpl(location);
|
|
456
|
+
} catch (cause) {
|
|
457
|
+
throw unreachable(this.host, cause);
|
|
458
|
+
}
|
|
459
|
+
if (!blob.ok) return "";
|
|
460
|
+
return await blob.text();
|
|
461
|
+
}
|
|
462
|
+
async getIdentity() {
|
|
463
|
+
if (this.identity) return this.identity;
|
|
464
|
+
const me = await this.getJson("user");
|
|
465
|
+
if (!me) {
|
|
466
|
+
throw new ApiError(
|
|
467
|
+
`Could not read the ${this.host} user. Check the host, the token, and network access.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
this.identity = { id: me.id, username: me.login };
|
|
471
|
+
return this.identity;
|
|
472
|
+
}
|
|
473
|
+
async getEvents(since) {
|
|
474
|
+
const login = (await this.getIdentity()).username;
|
|
475
|
+
const raw = await this.getPaged(
|
|
476
|
+
`users/${encodeURIComponent(login)}/events`,
|
|
477
|
+
{},
|
|
478
|
+
EVENTS_PAGE_CAP
|
|
479
|
+
);
|
|
480
|
+
if (raw.length === 0) {
|
|
481
|
+
process.stderr.write(
|
|
482
|
+
`Warning: the ${this.host} events feed returned nothing. Private activity is only visible to a token that belongs to the same account.
|
|
483
|
+
`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const floor = isoDay(since);
|
|
487
|
+
return raw.map(mapEvent).filter((event) => event.at.slice(0, 10) >= floor).sort((a, b) => a.at.localeCompare(b.at));
|
|
488
|
+
}
|
|
489
|
+
async shapePr(item) {
|
|
490
|
+
const project = repoFromUrl(item.repository_url);
|
|
491
|
+
const iid = item.number;
|
|
492
|
+
const pull = await this.getJson(`repos/${project}/pulls/${iid}`);
|
|
493
|
+
if (!pull) return null;
|
|
494
|
+
const sha = pull.head?.sha ?? "";
|
|
495
|
+
const [checks, reviews] = await Promise.all([
|
|
496
|
+
sha ? this.getJson(
|
|
497
|
+
`repos/${project}/commits/${sha}/check-runs`,
|
|
498
|
+
{ per_page: PAGE_SIZE }
|
|
499
|
+
) : Promise.resolve(null),
|
|
500
|
+
this.getPaged(`repos/${project}/pulls/${iid}/reviews`, {}, 2)
|
|
501
|
+
]);
|
|
502
|
+
const { pipeline, pipelineId } = normalizeChecks(checks?.check_runs ?? []);
|
|
503
|
+
return {
|
|
504
|
+
provider: "github",
|
|
505
|
+
project,
|
|
506
|
+
projectId: pull.base?.repo?.id ?? 0,
|
|
507
|
+
iid,
|
|
508
|
+
title: pull.title,
|
|
509
|
+
draft: pull.draft,
|
|
510
|
+
branch: pull.head?.ref ?? "",
|
|
511
|
+
target: pull.base?.ref ?? "",
|
|
512
|
+
updated: localAt(pull.updated_at).slice(0, 10),
|
|
513
|
+
url: pull.html_url,
|
|
514
|
+
mergeStatus: pull.mergeable_state ?? null,
|
|
515
|
+
pipeline,
|
|
516
|
+
pipelineId,
|
|
517
|
+
unresolved: countChangesRequested(reviews),
|
|
518
|
+
pipelineMissing: false,
|
|
519
|
+
bucket: "ready"
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async getMyMrs(today) {
|
|
523
|
+
const login = (await this.getIdentity()).username;
|
|
524
|
+
const items = await this.getSearch(
|
|
525
|
+
`is:pr is:open author:${login} archived:false`,
|
|
526
|
+
SEARCH_CAP
|
|
527
|
+
);
|
|
528
|
+
const shaped = await Promise.all(items.map((item) => this.shapePr(item)));
|
|
529
|
+
const rows = shaped.filter((row) => row !== null);
|
|
530
|
+
markMissingPipelines(rows);
|
|
531
|
+
for (const row of rows) {
|
|
532
|
+
row.bucket = classify(row, today);
|
|
533
|
+
}
|
|
534
|
+
return rows;
|
|
535
|
+
}
|
|
536
|
+
async getReviews(identity, today) {
|
|
537
|
+
const items = await this.getSearch(
|
|
538
|
+
`is:pr is:open review-requested:${identity.username}`,
|
|
539
|
+
SEARCH_CAP
|
|
540
|
+
);
|
|
541
|
+
const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY));
|
|
542
|
+
const rows = await Promise.all(
|
|
543
|
+
items.map(async (item) => {
|
|
544
|
+
const project = repoFromUrl(item.repository_url);
|
|
545
|
+
const reviews = await this.getPaged(
|
|
546
|
+
`repos/${project}/pulls/${item.number}/reviews`,
|
|
547
|
+
{},
|
|
548
|
+
2
|
|
549
|
+
);
|
|
550
|
+
const updated = localAt(item.updated_at).slice(0, 10);
|
|
551
|
+
return {
|
|
552
|
+
provider: "github",
|
|
553
|
+
project,
|
|
554
|
+
iid: item.number,
|
|
555
|
+
title: item.title,
|
|
556
|
+
author: item.user?.login ?? "",
|
|
557
|
+
updated,
|
|
558
|
+
draft: item.draft ?? false,
|
|
559
|
+
url: item.html_url,
|
|
560
|
+
fresh: updated >= cutoff,
|
|
561
|
+
approvedByMe: approvedBy(reviews, identity.username)
|
|
562
|
+
};
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
566
|
+
}
|
|
567
|
+
async actionsBlocker(mr, sha) {
|
|
568
|
+
const runs = await this.getJson(
|
|
569
|
+
`repos/${mr.project}/actions/runs`,
|
|
570
|
+
{ head_sha: sha, per_page: 10 }
|
|
571
|
+
);
|
|
572
|
+
const run = (runs?.workflow_runs ?? []).find(
|
|
573
|
+
(row) => row.conclusion !== null && FAILED_CONCLUSIONS.has(row.conclusion)
|
|
574
|
+
);
|
|
575
|
+
if (!run) return null;
|
|
576
|
+
const jobs = await this.getJson(
|
|
577
|
+
`repos/${mr.project}/actions/runs/${run.id}/jobs`,
|
|
578
|
+
{ per_page: PAGE_SIZE }
|
|
579
|
+
);
|
|
580
|
+
const job = (jobs?.jobs ?? []).find(
|
|
581
|
+
(row) => row.conclusion !== null && FAILED_CONCLUSIONS.has(row.conclusion)
|
|
582
|
+
);
|
|
583
|
+
if (!job) return null;
|
|
584
|
+
const log = await this.getLogText(
|
|
585
|
+
`repos/${mr.project}/actions/jobs/${job.id}/logs`
|
|
586
|
+
);
|
|
587
|
+
return {
|
|
588
|
+
provider: "github",
|
|
589
|
+
project: mr.project,
|
|
590
|
+
mr: mr.iid,
|
|
591
|
+
title: mr.title,
|
|
592
|
+
job: job.name,
|
|
593
|
+
stage: run.name ?? "",
|
|
594
|
+
url: mr.url,
|
|
595
|
+
errors: extractErrors(log)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async checkRunBlocker(mr, sha) {
|
|
599
|
+
const checks = await this.getJson(
|
|
600
|
+
`repos/${mr.project}/commits/${sha}/check-runs`,
|
|
601
|
+
{ per_page: PAGE_SIZE }
|
|
602
|
+
);
|
|
603
|
+
const failed = (checks?.check_runs ?? []).find(
|
|
604
|
+
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
605
|
+
);
|
|
606
|
+
if (!failed) return null;
|
|
607
|
+
const summary = [failed.output?.summary ?? "", failed.output?.text ?? ""].filter(Boolean).join("\n");
|
|
608
|
+
const errors = extractErrors(summary);
|
|
609
|
+
if (errors.length === 0) return null;
|
|
610
|
+
return {
|
|
611
|
+
provider: "github",
|
|
612
|
+
project: mr.project,
|
|
613
|
+
mr: mr.iid,
|
|
614
|
+
title: mr.title,
|
|
615
|
+
job: failed.name,
|
|
616
|
+
stage: failed.app?.slug ?? "check",
|
|
617
|
+
url: mr.url,
|
|
618
|
+
errors
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async diagnose(mr) {
|
|
622
|
+
const pull = await this.getJson(
|
|
623
|
+
`repos/${mr.project}/pulls/${mr.iid}`
|
|
624
|
+
);
|
|
625
|
+
const sha = pull?.head?.sha;
|
|
626
|
+
if (!sha) return null;
|
|
627
|
+
const actions = await this.actionsBlocker(mr, sha);
|
|
628
|
+
if (actions?.errors.length) return actions;
|
|
629
|
+
return await this.checkRunBlocker(mr, sha) ?? actions;
|
|
630
|
+
}
|
|
631
|
+
async getBlockers(mrs) {
|
|
632
|
+
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
633
|
+
const diagnosed = await Promise.all(red.map((mr) => this.diagnose(mr)));
|
|
634
|
+
return diagnosed.filter((row) => row !== null);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/providers/gitlab/gitlab.constants.ts
|
|
639
|
+
var PAGE_SIZE2 = 100;
|
|
640
|
+
var FRESH_REVIEW_DAYS2 = 7;
|
|
641
|
+
|
|
642
|
+
// src/providers/gitlab/gitlab.ts
|
|
643
|
+
var GitLabProvider = class {
|
|
644
|
+
kind = "gitlab";
|
|
645
|
+
host;
|
|
646
|
+
api;
|
|
647
|
+
token;
|
|
648
|
+
fetchImpl;
|
|
649
|
+
constructor(host, token, fetchImpl = fetch) {
|
|
650
|
+
this.host = host;
|
|
651
|
+
this.token = token;
|
|
652
|
+
this.api = `https://${host}/api/v4`;
|
|
653
|
+
this.fetchImpl = fetchImpl;
|
|
654
|
+
}
|
|
655
|
+
async send(url) {
|
|
656
|
+
let response;
|
|
221
657
|
try {
|
|
222
|
-
|
|
658
|
+
response = await this.fetchImpl(url, {
|
|
223
659
|
headers: { "PRIVATE-TOKEN": this.token }
|
|
224
660
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} catch {
|
|
228
|
-
return "";
|
|
661
|
+
} catch (cause) {
|
|
662
|
+
throw unreachable(this.host, cause);
|
|
229
663
|
}
|
|
664
|
+
assertUsable(response, this.host);
|
|
665
|
+
return response;
|
|
666
|
+
}
|
|
667
|
+
async getJson(path, params) {
|
|
668
|
+
const response = await this.send(buildUrl(this.api, path, params));
|
|
669
|
+
if (!response.ok) return null;
|
|
670
|
+
try {
|
|
671
|
+
return await response.json();
|
|
672
|
+
} catch (cause) {
|
|
673
|
+
throw unreachable(this.host, cause);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async getText(path) {
|
|
677
|
+
const response = await this.send(buildUrl(this.api, path));
|
|
678
|
+
if (!response.ok) return "";
|
|
679
|
+
return await response.text();
|
|
230
680
|
}
|
|
231
681
|
async getPaged(path, params = {}, cap = 5) {
|
|
232
682
|
const rows = [];
|
|
233
683
|
for (let page = 1; page <= cap; page += 1) {
|
|
234
684
|
const chunk = await this.getJson(path, {
|
|
235
685
|
...params,
|
|
236
|
-
per_page:
|
|
686
|
+
per_page: PAGE_SIZE2,
|
|
237
687
|
page
|
|
238
688
|
});
|
|
239
689
|
if (!chunk || chunk.length === 0) break;
|
|
240
690
|
rows.push(...chunk);
|
|
241
|
-
if (chunk.length <
|
|
691
|
+
if (chunk.length < PAGE_SIZE2) break;
|
|
242
692
|
}
|
|
243
693
|
return rows;
|
|
244
694
|
}
|
|
@@ -284,7 +734,7 @@ var GitLabProvider = class {
|
|
|
284
734
|
async countUnresolved(projectId, iid) {
|
|
285
735
|
const discussions = await this.getJson(
|
|
286
736
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
287
|
-
{ per_page:
|
|
737
|
+
{ per_page: PAGE_SIZE2 }
|
|
288
738
|
);
|
|
289
739
|
if (!discussions) return 0;
|
|
290
740
|
return discussions.filter((discussion) => {
|
|
@@ -304,6 +754,7 @@ var GitLabProvider = class {
|
|
|
304
754
|
]);
|
|
305
755
|
const latest = pipelines?.[0];
|
|
306
756
|
return {
|
|
757
|
+
provider: "gitlab",
|
|
307
758
|
project: String(mr.references.full).split("!")[0],
|
|
308
759
|
projectId,
|
|
309
760
|
iid,
|
|
@@ -338,15 +789,16 @@ var GitLabProvider = class {
|
|
|
338
789
|
if (!approvals) return false;
|
|
339
790
|
return (approvals.approved_by ?? []).some((entry2) => entry2.user?.id === uid);
|
|
340
791
|
}
|
|
341
|
-
async getReviews(
|
|
792
|
+
async getReviews(identity, today) {
|
|
342
793
|
const raw = await this.getPaged("merge_requests", {
|
|
343
794
|
scope: "all",
|
|
344
795
|
state: "opened",
|
|
345
|
-
reviewer_id:
|
|
796
|
+
reviewer_id: identity.id
|
|
346
797
|
});
|
|
347
|
-
const cutoff = isoDay(new Date(today.getTime() -
|
|
798
|
+
const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY));
|
|
348
799
|
const rows = await Promise.all(
|
|
349
800
|
raw.map(async (mr) => ({
|
|
801
|
+
provider: "gitlab",
|
|
350
802
|
project: String(mr.references.full).split("!")[0],
|
|
351
803
|
iid: mr.iid,
|
|
352
804
|
title: mr.title,
|
|
@@ -355,7 +807,7 @@ var GitLabProvider = class {
|
|
|
355
807
|
draft: mr.draft,
|
|
356
808
|
url: mr.web_url,
|
|
357
809
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
358
|
-
approvedByMe: await this.approvedByMe(mr.project_id, mr.iid,
|
|
810
|
+
approvedByMe: await this.approvedByMe(mr.project_id, mr.iid, identity.id)
|
|
359
811
|
}))
|
|
360
812
|
);
|
|
361
813
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -366,7 +818,7 @@ var GitLabProvider = class {
|
|
|
366
818
|
red.map(async (mr) => {
|
|
367
819
|
const jobs = await this.getJson(
|
|
368
820
|
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
369
|
-
{ per_page:
|
|
821
|
+
{ per_page: PAGE_SIZE2 }
|
|
370
822
|
) ?? [];
|
|
371
823
|
const job = jobs.find((j) => j.status === "failed");
|
|
372
824
|
if (!job) return null;
|
|
@@ -374,6 +826,7 @@ var GitLabProvider = class {
|
|
|
374
826
|
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
375
827
|
);
|
|
376
828
|
return {
|
|
829
|
+
provider: "gitlab",
|
|
377
830
|
project: mr.project,
|
|
378
831
|
mr: mr.iid,
|
|
379
832
|
title: mr.title,
|
|
@@ -388,6 +841,86 @@ var GitLabProvider = class {
|
|
|
388
841
|
}
|
|
389
842
|
};
|
|
390
843
|
|
|
844
|
+
// src/providers/select.ts
|
|
845
|
+
var DEFAULT_PROBE = { gitlab: glabHosts, github: ghHosts };
|
|
846
|
+
function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
847
|
+
if (cliHost) return cliHost;
|
|
848
|
+
if (envHost) return envHost;
|
|
849
|
+
const found = probe();
|
|
850
|
+
return resolveHost(void 0, void 0, found.length > 0 ? found : fallback, labels);
|
|
851
|
+
}
|
|
852
|
+
function chooseKind(options = {}) {
|
|
853
|
+
const env = options.env ?? process.env;
|
|
854
|
+
const probe = options.probe ?? DEFAULT_PROBE;
|
|
855
|
+
if (options.provider) {
|
|
856
|
+
if (options.provider === "github" || options.provider === "gitlab") {
|
|
857
|
+
return options.provider;
|
|
858
|
+
}
|
|
859
|
+
throw new ConfigError(
|
|
860
|
+
`Unknown provider "${options.provider}" for --provider. Use github or gitlab.`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (options.host) {
|
|
864
|
+
const host = options.host.toLowerCase();
|
|
865
|
+
if (host === DOT_COM || host.includes("github")) return "github";
|
|
866
|
+
if (host === "gitlab.com" || host.includes("gitlab")) return "gitlab";
|
|
867
|
+
}
|
|
868
|
+
if (env.STANDUP_PROVIDER) {
|
|
869
|
+
if (env.STANDUP_PROVIDER === "github" || env.STANDUP_PROVIDER === "gitlab") {
|
|
870
|
+
return env.STANDUP_PROVIDER;
|
|
871
|
+
}
|
|
872
|
+
throw new ConfigError(
|
|
873
|
+
`Unknown provider "${env.STANDUP_PROVIDER}" for STANDUP_PROVIDER. Use github or gitlab.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const hasGitHubEnv = Boolean(env.GITHUB_HOST || env.GITHUB_TOKEN);
|
|
877
|
+
const hasGitLabEnv = Boolean(env.GITLAB_HOST || env.GITLAB_TOKEN);
|
|
878
|
+
if (hasGitHubEnv !== hasGitLabEnv) return hasGitHubEnv ? "github" : "gitlab";
|
|
879
|
+
if (hasGitHubEnv && hasGitLabEnv) {
|
|
880
|
+
const ghHost = Boolean(env.GITHUB_HOST);
|
|
881
|
+
const glabHost = Boolean(env.GITLAB_HOST);
|
|
882
|
+
if (ghHost !== glabHost) return ghHost ? "github" : "gitlab";
|
|
883
|
+
throw new ConfigError(
|
|
884
|
+
"Both GITHUB_* and GITLAB_* are configured. Pass --provider github or --provider gitlab."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const ghLoggedIn = probe.github().length > 0;
|
|
888
|
+
const glabLoggedIn = probe.gitlab().length > 0;
|
|
889
|
+
if (ghLoggedIn !== glabLoggedIn) return ghLoggedIn ? "github" : "gitlab";
|
|
890
|
+
if (ghLoggedIn && glabLoggedIn) {
|
|
891
|
+
throw new ConfigError(
|
|
892
|
+
"Both gh and glab are authenticated. Pass --provider github or --provider gitlab."
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
throw new ConfigError(
|
|
896
|
+
"No provider configured. Pass --provider with --host and --token, set GITHUB_* or GITLAB_* environment variables, or log in with gh or glab."
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
function connect(options = {}) {
|
|
900
|
+
const env = options.env ?? process.env;
|
|
901
|
+
const probe = options.probe ?? DEFAULT_PROBE;
|
|
902
|
+
if (chooseKind(options) === "github") {
|
|
903
|
+
const host2 = hostFrom(options.host, env.GITHUB_HOST, probe.github, [DOT_COM], GITHUB_LABELS);
|
|
904
|
+
const token2 = resolveToken(
|
|
905
|
+
host2,
|
|
906
|
+
options.token,
|
|
907
|
+
env.GITHUB_TOKEN,
|
|
908
|
+
ghToken,
|
|
909
|
+
GITHUB_LABELS
|
|
910
|
+
);
|
|
911
|
+
return new GitHubProvider(host2, token2);
|
|
912
|
+
}
|
|
913
|
+
const host = hostFrom(options.host, env.GITLAB_HOST, probe.gitlab, [], GITLAB_LABELS);
|
|
914
|
+
const token = resolveToken(
|
|
915
|
+
host,
|
|
916
|
+
options.token,
|
|
917
|
+
env.GITLAB_TOKEN,
|
|
918
|
+
glabToken,
|
|
919
|
+
GITLAB_LABELS
|
|
920
|
+
);
|
|
921
|
+
return new GitLabProvider(host, token);
|
|
922
|
+
}
|
|
923
|
+
|
|
391
924
|
// src/report/report.constants.ts
|
|
392
925
|
var LOOKBACK_DAYS = 21;
|
|
393
926
|
|
|
@@ -396,27 +929,27 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
396
929
|
const identity = await provider.getIdentity();
|
|
397
930
|
const since = new Date(today.getTime() - lookbackDays * MS_PER_DAY);
|
|
398
931
|
const events = await provider.getEvents(since);
|
|
399
|
-
const
|
|
932
|
+
const days = previousActiveDays(
|
|
400
933
|
new Set(events.map((e) => e.at.slice(0, 10))),
|
|
401
934
|
today
|
|
402
935
|
);
|
|
403
|
-
const
|
|
936
|
+
const previousDays = days.map(({ date, gapDays }) => ({
|
|
937
|
+
date,
|
|
938
|
+
label: label(/* @__PURE__ */ new Date(`${date}T00:00:00`), lang),
|
|
939
|
+
gapDays,
|
|
940
|
+
events: events.filter((e) => e.at.slice(0, 10) === date)
|
|
941
|
+
}));
|
|
404
942
|
const todayEvents = events.filter((e) => e.at.slice(0, 10) === isoDay(today));
|
|
405
943
|
const myMrs = await provider.getMyMrs(today);
|
|
406
944
|
const [reviews, blockers] = await Promise.all([
|
|
407
|
-
provider.getReviews(identity
|
|
945
|
+
provider.getReviews(identity, today),
|
|
408
946
|
provider.getBlockers(myMrs)
|
|
409
947
|
]);
|
|
410
948
|
return {
|
|
949
|
+
provider: provider.kind,
|
|
411
950
|
user: identity.username,
|
|
412
951
|
today: { date: isoDay(today), label: label(today, lang) },
|
|
413
|
-
|
|
414
|
-
date: previousDate,
|
|
415
|
-
label: previousDate ? label(/* @__PURE__ */ new Date(`${previousDate}T00:00:00`), lang) : null,
|
|
416
|
-
gapDays,
|
|
417
|
-
eventCount: previousEvents.length
|
|
418
|
-
},
|
|
419
|
-
previousEvents,
|
|
952
|
+
previousDays,
|
|
420
953
|
todayEvents,
|
|
421
954
|
myMrs,
|
|
422
955
|
reviews,
|
|
@@ -426,22 +959,17 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
426
959
|
}
|
|
427
960
|
|
|
428
961
|
// mcp/server.ts
|
|
429
|
-
function resolveProvider(options) {
|
|
430
|
-
if (options.provider) return options.provider;
|
|
431
|
-
const host = resolveHost(options.host, process.env.GITLAB_HOST, glabHosts());
|
|
432
|
-
const token = resolveToken(host, options.token, process.env.GITLAB_TOKEN, glabToken);
|
|
433
|
-
return new GitLabProvider(host, token);
|
|
434
|
-
}
|
|
435
962
|
async function collect(options = {}) {
|
|
436
|
-
|
|
963
|
+
const provider = options.providerImpl ?? connect({ provider: options.provider, host: options.host, token: options.token });
|
|
964
|
+
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
437
965
|
}
|
|
438
966
|
async function main() {
|
|
439
967
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
440
968
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
441
|
-
const server = new McpServer({ name: "standup-mr", version: "0.1
|
|
969
|
+
const server = new McpServer({ name: "standup-mr", version: "0.2.1" });
|
|
442
970
|
server.tool(
|
|
443
971
|
"get_standup_data",
|
|
444
|
-
"Collect merge-request-based standup data from GitLab. Returns the previous working day activity, open merge requests bucketed by state (ready / blocked / draft / stale), pending reviews, and the error lines from any failed
|
|
972
|
+
"Collect merge-request-based standup data from GitLab or GitHub. Returns the previous working day activity, open merge requests or pull requests bucketed by state (ready / blocked / draft / stale), pending reviews, and the error lines from any failed pipeline or check.",
|
|
445
973
|
{},
|
|
446
974
|
async () => {
|
|
447
975
|
const report = await collect();
|