@smwb/onreel-git-mcp 0.2.1 → 0.4.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.
@@ -2,6 +2,10 @@ import {
2
2
  defaultSecretSink,
3
3
  redactSecretText
4
4
  } from "../chunk-L7CPE2SO.js";
5
+ import {
6
+ issueLinkSchema,
7
+ timestampSchema
8
+ } from "../chunk-4ZGRXIVA.js";
5
9
 
6
10
  // src/tracker/tracker.const.ts
7
11
  var TRACKER_TIMEOUT_MS = 15e3;
@@ -9,6 +13,13 @@ var TRACKER_PAGE_LIMIT_DEFAULT = 50;
9
13
  var TRACKER_ERROR_BODY_MAX = 300;
10
14
  var TRACKER_SECRETS_FILE = "tracker-secrets.json";
11
15
  var TRACKER_LOCAL_AUTHOR = "owner";
16
+ var ISSUE_FILTERS = ["status", "labels", "assignee", "author", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"];
17
+ var ISSUE_SORT_FIELDS = ["created", "updated", "priority", "title", "comments"];
18
+ var COMMENT_FILTERS = ["updatedAfter"];
19
+ var LEGACY_ISSUE_CAPABILITIES = {
20
+ list: { filters: ["status"], clientSide: ["status"], sorts: [] },
21
+ comments: { filters: [], clientSide: [], order: ["asc"] }
22
+ };
12
23
 
13
24
  // src/tracker/tracker.lib.ts
14
25
  var TrackerError = class extends Error {
@@ -50,6 +61,89 @@ function nameOf(table, category) {
50
61
  function priorityName(table, priority) {
51
62
  return Object.entries(table).find(([, value]) => value === priority)?.[0];
52
63
  }
64
+ function usedFilters(query, names) {
65
+ return names.filter((name) => {
66
+ const value = query[name];
67
+ return value !== void 0 && !(Array.isArray(value) && value.length === 0);
68
+ });
69
+ }
70
+ function assertListQuery(subject, capabilities, query, names) {
71
+ const used = usedFilters(query, names);
72
+ for (const name of used) {
73
+ if (!capabilities.filters.includes(name)) throw new TrackerError("unsupported", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
74
+ }
75
+ const sort = query.sort;
76
+ if (sort !== void 0 && !capabilities.sorts.includes(sort.field)) {
77
+ throw new TrackerError("unsupported", `${subject}: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E ${sort.field} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
78
+ }
79
+ if (sort?.direction !== void 0 && sort.direction !== "asc" && sort.direction !== "desc") {
80
+ throw new TrackerError("invalid_request", `${subject}: \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438 \u2014 asc \u0438\u043B\u0438 desc.`);
81
+ }
82
+ const limit = query.limit;
83
+ if (limit !== void 0 && !(typeof limit === "number" && Number.isSafeInteger(limit) && limit >= 1)) {
84
+ throw new TrackerError("invalid_request", `${subject}: limit \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1.`);
85
+ }
86
+ for (const name of used) {
87
+ const value = query[name];
88
+ if (isTimeFilter(name)) {
89
+ if (typeof value !== "number" || !(Number.isSafeInteger(value) && value >= 0)) {
90
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0432\u0440\u0435\u043C\u044F \u0432 \u043C\u0441, \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 0.`);
91
+ }
92
+ continue;
93
+ }
94
+ if (isArrayFilter(name)) {
95
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
96
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.`);
97
+ }
98
+ continue;
99
+ }
100
+ if (value === null) continue;
101
+ if (typeof value === "string") {
102
+ if (value.trim() === "") throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430.`);
103
+ continue;
104
+ }
105
+ if (Array.isArray(value)) {
106
+ if (value.some((item) => typeof item !== "string" || item.trim() === "")) {
107
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.`);
108
+ }
109
+ continue;
110
+ }
111
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u0442\u0440\u043E\u043A\u0430, \u0441\u043F\u0438\u0441\u043E\u043A \u0441\u0442\u0440\u043E\u043A \u0438\u043B\u0438 null.`);
112
+ }
113
+ }
114
+ function isTimeFilter(name) {
115
+ return name.endsWith("After") || name.endsWith("Before");
116
+ }
117
+ function isArrayFilter(name) {
118
+ return name === "status" || name === "labels";
119
+ }
120
+ function assertCommentQuery(subject, capabilities, query) {
121
+ assertListQuery(subject, { filters: capabilities.filters, clientSide: capabilities.clientSide, sorts: [] }, query, COMMENT_FILTERS);
122
+ if (query.order !== void 0 && !capabilities.order.includes(query.order)) {
123
+ throw new TrackerError("unsupported", `${subject}: \u043F\u043E\u0440\u044F\u0434\u043E\u043A ${query.order} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
124
+ }
125
+ }
126
+ function issueCapabilitiesOf(definition) {
127
+ return definition.issues ?? LEGACY_ISSUE_CAPABILITIES;
128
+ }
129
+ function isoTime(ms) {
130
+ return new Date(ms).toISOString();
131
+ }
132
+ function inTimeRange(value, after, before) {
133
+ if (after === void 0 && before === void 0) return true;
134
+ if (value === void 0) return false;
135
+ return (after === void 0 || value >= after) && (before === void 0 || value <= before);
136
+ }
137
+ function containsText(texts, needle) {
138
+ const lower = needle.toLowerCase();
139
+ return texts.some((value) => value !== void 0 && value.toLowerCase().includes(lower));
140
+ }
141
+ function quoted(value) {
142
+ return `"${value.replace(/[\\"]/g, "\\$&")}"`;
143
+ }
144
+ function keysFor(table, values) {
145
+ return Object.entries(table).filter(([, value]) => values.includes(value)).map(([key]) => key);
146
+ }
53
147
 
54
148
  // src/tracker/mapping.const.ts
55
149
  var SOURCECRAFT_STATUSES = {
@@ -66,6 +160,12 @@ var SOURCECRAFT_STATUS_SLUGS = {
66
160
  done: "closed",
67
161
  declined: "declined"
68
162
  };
163
+ var SOURCECRAFT_STATUS_FILTER = {
164
+ open: ["open", "paused"],
165
+ in_progress: ["in_progress"],
166
+ done: ["closed"],
167
+ declined: ["declined", "duplicate"]
168
+ };
69
169
  var SOURCECRAFT_PRIORITIES = {
70
170
  trivial: "trivial",
71
171
  minor: "minor",
@@ -115,6 +215,21 @@ var GITHUB_PRIORITY_LABELS = {
115
215
  "priority: blocker": "blocker"
116
216
  };
117
217
  var ALL_STATUSES = ["open", "in_progress", "done", "declined"];
218
+ var GITLAB_MERGE_REQUEST_STATES = {
219
+ opened: "open",
220
+ locked: "open",
221
+ merged: "merged",
222
+ closed: "closed"
223
+ };
224
+ var GITLAB_DRAFT_PREFIX = "Draft: ";
225
+ var GITLAB_DRAFT_PATTERN = /^\s*(?:\[draft\]|\(draft\)|draft:)\s*/i;
226
+ var SOURCECRAFT_PULL_STATUSES = {
227
+ draft: "draft",
228
+ open: "open",
229
+ merging: "open",
230
+ merged: "merged",
231
+ discarded: "closed"
232
+ };
118
233
 
119
234
  // src/tracker/http.lib.ts
120
235
  async function requestJson(url, request) {
@@ -130,11 +245,7 @@ async function requestJson(url, request) {
130
245
  throw new TrackerError("failed", redactSecretText(`\u0422\u0440\u0435\u043A\u0435\u0440 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B (${hostOf(url)}): ${error instanceof Error ? error.message : String(error)}`));
131
246
  }
132
247
  const raw = await response.text();
133
- if (!response.ok) {
134
- if (response.status === 404 && request.missingIsIssue === true) throw new TrackerError("unknown_issue", "\u0417\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0442\u0440\u0435\u043A\u0435\u0440\u0435.");
135
- const detail = redactSecretText(raw.replace(/\s+/g, " ").trim()).slice(0, TRACKER_ERROR_BODY_MAX);
136
- throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${String(response.status)}${detail === "" ? "" : `: ${detail}`}.`);
137
- }
248
+ if (!response.ok) throw failureOf(url, response, raw, request);
138
249
  if (raw === "") return { body: void 0, headers: response.headers };
139
250
  try {
140
251
  return { body: JSON.parse(raw), headers: response.headers };
@@ -142,6 +253,38 @@ async function requestJson(url, request) {
142
253
  throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 JSON.`);
143
254
  }
144
255
  }
256
+ function failureOf(url, response, raw, request) {
257
+ const status = response.status;
258
+ if (status === 404 && request.missing !== void 0) return new TrackerError(request.missing.failure, request.missing.message);
259
+ if (status === 404 && request.missingIsIssue === true) return new TrackerError("unknown_issue", "\u0417\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0442\u0440\u0435\u043A\u0435\u0440\u0435.");
260
+ const detail = redactSecretText(raw.replace(/\s+/g, " ").trim()).slice(0, TRACKER_ERROR_BODY_MAX);
261
+ const answered = `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${String(status)}${detail === "" ? "" : `: ${detail}`}.`;
262
+ if (request.conflictOn?.includes(status) === true) return new TrackerError("conflict", answered);
263
+ const secondaryLimit = status === 403 && (response.headers.get("x-ratelimit-remaining")?.trim() === "0" || retryAfterOf(response.headers.get("retry-after")?.trim(), Date.now()) !== void 0);
264
+ if (status === 429 || secondaryLimit) {
265
+ const delay = retryDelayOf(response.headers);
266
+ return new TrackerError("rate_limited", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u043B \u0447\u0430\u0441\u0442\u043E\u0442\u0443 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432${delay === void 0 ? "" : `, \u043F\u043E\u0432\u0442\u043E\u0440 \u0447\u0435\u0440\u0435\u0437 ${String(delay)} \u0441`}.`);
267
+ }
268
+ return new TrackerError("failed", answered);
269
+ }
270
+ function retryDelayOf(headers, now = Date.now()) {
271
+ const retryAfter = retryAfterOf(headers.get("retry-after")?.trim(), now);
272
+ if (retryAfter !== void 0) return retryAfter;
273
+ for (const name of ["x-ratelimit-reset", "ratelimit-reset"]) {
274
+ const reset = headers.get(name)?.trim();
275
+ if (reset !== void 0 && /^\d+$/.test(reset)) return resetDelay(Number(reset), now);
276
+ }
277
+ return void 0;
278
+ }
279
+ function retryAfterOf(value, now) {
280
+ if (value === void 0 || value === "") return void 0;
281
+ if (/^\d+$/.test(value)) return Number(value);
282
+ const at = Date.parse(value);
283
+ return Number.isFinite(at) ? Math.max(0, Math.ceil((at - now) / 1e3)) : void 0;
284
+ }
285
+ function resetDelay(reset, now) {
286
+ return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
287
+ }
145
288
  function hostOf(url) {
146
289
  try {
147
290
  return new URL(url).host;
@@ -175,11 +318,237 @@ function listTrackers() {
175
318
  return [...definitions.values()];
176
319
  }
177
320
 
178
- // src/tracker/github.lib.ts
321
+ // src/tracker/pull.schema.ts
322
+ import { z } from "zod";
323
+ var pullRequestStatusCategorySchema = z.enum(["draft", "open", "merged", "closed"]);
324
+ var pullRequestFilterSchema = z.enum(["status", "author", "source", "target", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"]);
325
+ var pullRequestSortFieldSchema = z.enum(["created", "updated", "title"]);
326
+ var mergeMethodSchema = z.enum(["merge", "squash", "rebase"]);
327
+ var pullRequestSchema = z.object({
328
+ /** Номер PR в своём трекере: GitHub number, GitLab iid, SourceCraft slug; для клиента непрозрачная строка. */
329
+ key: z.string().min(1).max(200),
330
+ title: z.string().min(1).max(1024),
331
+ description: z.string().max(1048576).optional(),
332
+ status: z.object({ category: pullRequestStatusCategorySchema, name: z.string().min(1).max(200) }),
333
+ /** Автор — логин или slug трекера. */
334
+ author: z.string().min(1).max(200).optional(),
335
+ /** Ветка-источник и целевая ветка — имя без refs/heads/. */
336
+ source: z.string().min(1).max(500),
337
+ target: z.string().min(1).max(500),
338
+ /** Коммит головы источника. */
339
+ headSha: z.string().regex(/^[0-9a-f]{7,64}$/).optional(),
340
+ labels: z.array(z.string().min(1).max(200)).max(100),
341
+ /** Запрошенные ревьюеры; нет — трекер их в PR не отдаёт. */
342
+ reviewers: z.array(z.string().min(1).max(200)).max(100).optional(),
343
+ links: z.array(issueLinkSchema).max(100),
344
+ createdAt: timestampSchema.optional(),
345
+ updatedAt: timestampSchema.optional(),
346
+ mergedAt: timestampSchema.optional(),
347
+ closedAt: timestampSchema.optional()
348
+ });
349
+
350
+ // src/tracker/github-pull.lib.ts
179
351
  var DEFAULT_API = "https://api.github.com";
180
352
  var TARGET = /^[\w.-]+\/[\w.-]+$/;
181
- var NUMBER = /^[1-9]\d{0,14}$/;
353
+ var NUMBER = /^[1-9]\d{0,19}$/;
354
+ var SHA = /^[0-9a-f]{7,64}$/;
355
+ var LIST_SUBJECT = "PR GitHub";
356
+ var COMMENTS_SUBJECT = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR GitHub";
357
+ var PULL_CAPABILITIES = {
358
+ list: {
359
+ filters: pullRequestFilterSchema.options,
360
+ clientSide: ["status", "author", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
361
+ sorts: ["created", "updated"]
362
+ },
363
+ comments: { filters: COMMENT_FILTERS, clientSide: [], order: ["asc"] },
364
+ create: true,
365
+ createDraft: true,
366
+ update: ["title", "description", "target"],
367
+ statuses: ["open", "closed"],
368
+ comment: true,
369
+ merge: ["merge", "squash", "rebase"],
370
+ deleteSourceOnMerge: false
371
+ };
372
+ function githubPulls() {
373
+ return {
374
+ capabilities: PULL_CAPABILITIES,
375
+ create: (context) => createGithubPullAdapter(context)
376
+ };
377
+ }
378
+ function createGithubPullAdapter({ connection, fetch }) {
379
+ const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
380
+ if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
381
+ const api = baseUrl(connection.url, DEFAULT_API);
382
+ const root = `${api}/repos/${target}/pulls`;
383
+ const issues = `${api}/repos/${target}/issues`;
384
+ const owner = target.split("/")[0] ?? target;
385
+ const headers = {
386
+ accept: "application/vnd.github+json",
387
+ "x-github-api-version": "2022-11-28",
388
+ ...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
389
+ };
390
+ const missing = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.` });
391
+ const number = (key) => {
392
+ if (!NUMBER.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.`);
393
+ return key;
394
+ };
395
+ const getPull = async (key) => pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, missing: missing(key) })).body);
396
+ return {
397
+ list: async (query) => {
398
+ assertListQuery(LIST_SUBJECT, PULL_CAPABILITIES.list, query, pullRequestFilterSchema.options);
399
+ const status = usedStatuses(query.status);
400
+ const page = pageOf(query.cursor);
401
+ const params = new URLSearchParams({ state: stateFilter(status), per_page: String(query.limit), page: String(page) });
402
+ if (query.source !== void 0) params.set("head", `${owner}:${query.source}`);
403
+ if (query.target !== void 0) params.set("base", query.target);
404
+ if (query.sort !== void 0) {
405
+ params.set("sort", query.sort.field);
406
+ params.set("direction", query.sort.direction ?? "desc");
407
+ }
408
+ const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
409
+ const items = (Array.isArray(body) ? body : []).map(pullOf).filter((pull) => matchesPage(pull, query, status));
410
+ return { items, ...nextOf(responseHeaders.get("link"), page) };
411
+ },
412
+ get: (key) => getPull(key),
413
+ create: async (input) => pullOf((await requestJson(root, { fetch, headers, method: "POST", body: createBody(input) })).body),
414
+ update: async (key, patch) => {
415
+ if (patch.status === "draft") throw new TrackerError("unsupported", "PR GitHub: \u043F\u0435\u0440\u0435\u0432\u043E\u0434 \u0432 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0447\u0435\u0440\u0435\u0437 REST.");
416
+ const current = patch.status === "open" ? await getPull(key) : void 0;
417
+ if (current?.status.category === "draft") throw new TrackerError("unsupported", "PR GitHub: \u0441\u043D\u044F\u0442\u0438\u0435 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0447\u0435\u0440\u0435\u0437 REST.");
418
+ if (current !== void 0 && current.status.category === "open" && patch.title === void 0 && patch.description === void 0 && patch.target === void 0) return current;
419
+ return pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody(patch), missing: missing(key) })).body);
420
+ },
421
+ merge: async (key, input) => {
422
+ if (input.deleteSource === true) throw new TrackerError("unsupported", "PR GitHub: \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0432\u0435\u0442\u043A\u0438-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \u043F\u0440\u0438 \u0441\u043B\u0438\u044F\u043D\u0438\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E.");
423
+ if (input.method !== void 0 && !PULL_CAPABILITIES.merge.includes(input.method)) {
424
+ throw new TrackerError("unsupported", `PR GitHub: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
425
+ }
426
+ await requestJson(`${root}/${number(key)}/merge`, { fetch, headers, method: "PUT", body: mergeBody(input), conflictOn: [405, 409], missing: missing(key) });
427
+ return getPull(key);
428
+ },
429
+ comments: async (key, query) => {
430
+ assertCommentQuery(COMMENTS_SUBJECT, PULL_CAPABILITIES.comments, query);
431
+ const page = pageOf(query.cursor);
432
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
433
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
434
+ const { body, headers: responseHeaders } = await requestJson(`${issues}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
435
+ return { items: (Array.isArray(body) ? body : []).map(commentOf), ...nextOf(responseHeaders.get("link"), page) };
436
+ },
437
+ comment: async (key, body) => commentOf((await requestJson(`${issues}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
438
+ };
439
+ }
440
+ function pageOf(cursor) {
441
+ const page = cursor === void 0 ? 1 : Number(cursor);
442
+ return Number.isInteger(page) && page >= 1 ? page : 1;
443
+ }
444
+ function nextOf(link, page) {
445
+ return link !== null && /rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {};
446
+ }
447
+ function stateFilter(status) {
448
+ if (status === void 0) return "all";
449
+ if (status.every((item) => item === "draft" || item === "open")) return "open";
450
+ if (status.every((item) => item === "merged" || item === "closed")) return "closed";
451
+ return "all";
452
+ }
453
+ function matchesPage(pull, query, status) {
454
+ if (status !== void 0 && !status.includes(pull.status.category)) return false;
455
+ if (query.author !== void 0 && pull.author !== query.author) return false;
456
+ if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
457
+ if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
458
+ return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
459
+ }
460
+ function usedStatuses(status) {
461
+ if (status === void 0 || status.length === 0) return void 0;
462
+ for (const item of status) {
463
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
464
+ throw new TrackerError("invalid_request", `PR GitHub: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
465
+ }
466
+ }
467
+ return status;
468
+ }
469
+ function createBody(input) {
470
+ return {
471
+ title: input.title,
472
+ head: input.source,
473
+ base: input.target,
474
+ ...input.description === void 0 ? {} : { body: input.description },
475
+ ...input.draft === true ? { draft: true } : {}
476
+ };
477
+ }
478
+ function patchBody(patch) {
479
+ return {
480
+ ...patch.title === void 0 ? {} : { title: patch.title },
481
+ ...patch.description === void 0 ? {} : { body: patch.description },
482
+ ...patch.target === void 0 ? {} : { base: patch.target },
483
+ ...patch.status === void 0 ? {} : { state: patch.status === "closed" ? "closed" : "open" }
484
+ };
485
+ }
486
+ function mergeBody(input) {
487
+ const lines = input.message?.split("\n") ?? [];
488
+ return {
489
+ ...input.method === void 0 ? {} : { merge_method: input.method },
490
+ ...input.headSha === void 0 ? {} : { sha: input.headSha },
491
+ ...input.message === void 0 ? {} : { commit_title: lines[0] ?? "" },
492
+ ...lines.length > 1 ? { commit_message: lines.slice(1).join("\n") } : {}
493
+ };
494
+ }
495
+ function pullOf(raw) {
496
+ const item = isRecord(raw) ? raw : {};
497
+ const head = isRecord(item.head) ? item.head : {};
498
+ const base = isRecord(item.base) ? item.base : {};
499
+ const key = typeof item.number === "number" ? String(item.number) : "?";
500
+ const description = text(item.body);
501
+ const author = isRecord(item.user) ? text(item.user.login) : void 0;
502
+ const sha = text(head.sha);
503
+ const url = text(item.html_url);
504
+ const createdAt = toTimestamp(item.created_at);
505
+ const updatedAt = toTimestamp(item.updated_at);
506
+ const mergedAt = toTimestamp(item.merged_at);
507
+ const closedAt = toTimestamp(item.closed_at);
508
+ const closed = item.state === "closed";
509
+ const merged = item.merged_at !== void 0 && item.merged_at !== null;
510
+ const category = closed ? merged ? "merged" : "closed" : item.draft === true ? "draft" : "open";
511
+ return {
512
+ key,
513
+ title: text(item.title) ?? key,
514
+ ...description === void 0 ? {} : { description },
515
+ status: { category, name: category },
516
+ ...author === void 0 ? {} : { author },
517
+ source: text(head.ref) ?? "?",
518
+ target: text(base.ref) ?? "?",
519
+ ...sha !== void 0 && SHA.test(sha) ? { headSha: sha } : {},
520
+ labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : label)) : [],
521
+ reviewers: logins(item.requested_reviewers),
522
+ links: url === void 0 ? [] : [{ url, title: "GitHub" }],
523
+ ...createdAt === void 0 ? {} : { createdAt },
524
+ ...updatedAt === void 0 ? {} : { updatedAt },
525
+ ...mergedAt === void 0 ? {} : { mergedAt },
526
+ ...closedAt === void 0 ? {} : { closedAt }
527
+ };
528
+ }
529
+ function logins(value) {
530
+ if (!Array.isArray(value)) return [];
531
+ const names = value.map((item) => isRecord(item) ? text(item.login) : void 0);
532
+ return [...new Set(names.filter((name) => name !== void 0))];
533
+ }
534
+ function commentOf(raw) {
535
+ const item = isRecord(raw) ? raw : {};
536
+ const author = isRecord(item.user) ? text(item.user.login) : void 0;
537
+ const createdAt = toTimestamp(item.created_at);
538
+ return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
539
+ }
540
+
541
+ // src/tracker/github.lib.ts
542
+ var DEFAULT_API2 = "https://api.github.com";
543
+ var TARGET2 = /^[\w.-]+\/[\w.-]+$/;
544
+ var NUMBER2 = /^[1-9]\d{0,14}$/;
545
+ var LIST_SUBJECT2 = "\u0417\u0430\u0434\u0430\u0447\u0438 GitHub";
546
+ var COMMENTS_SUBJECT2 = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitHub";
182
547
  function githubTracker() {
548
+ const issues = {
549
+ list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "comments"] },
550
+ comments: { filters: ["updatedAfter"], clientSide: [], order: ["asc"] }
551
+ };
183
552
  return {
184
553
  descriptor: {
185
554
  tracker: "github",
@@ -191,20 +560,23 @@ function githubTracker() {
191
560
  ],
192
561
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: ["open", "in_progress", "done", "declined"] }
193
562
  },
194
- create: (context) => createGithubAdapter(context)
563
+ create: (context) => createGithubAdapter(context, issues),
564
+ issues,
565
+ pulls: githubPulls()
195
566
  };
196
567
  }
197
- function createGithubAdapter({ connection, fetch }) {
568
+ function createGithubAdapter({ connection, fetch }, capabilities) {
198
569
  const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
199
- if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
200
- const root = `${baseUrl(connection.url, DEFAULT_API)}/repos/${target}/issues`;
570
+ if (!TARGET2.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
571
+ const api = baseUrl(connection.url, DEFAULT_API2);
572
+ const root = `${api}/repos/${target}/issues`;
201
573
  const headers = {
202
574
  accept: "application/vnd.github+json",
203
575
  "x-github-api-version": "2022-11-28",
204
576
  ...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
205
577
  };
206
578
  const number = (key) => {
207
- if (!NUMBER.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
579
+ if (!NUMBER2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
208
580
  return key;
209
581
  };
210
582
  const rawOf = async (key) => {
@@ -213,14 +585,13 @@ function createGithubAdapter({ connection, fetch }) {
213
585
  };
214
586
  return {
215
587
  list: async (query) => {
216
- const page = pageOf(query.cursor);
217
- const params = new URLSearchParams({ state: stateFilter(query.status), per_page: String(query.limit), page: String(page) });
218
- const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
219
- const items = (Array.isArray(body) ? body : []).filter((item) => isRecord(item) && item.pull_request === void 0).map(issueOf);
220
- return {
221
- items: query.status === void 0 ? items : items.filter((issue) => query.status?.includes(issue.status.category) === true),
222
- .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {}
223
- };
588
+ assertListQuery(LIST_SUBJECT2, capabilities.list, query, ISSUE_FILTERS);
589
+ assertCsvLabels(query.labels);
590
+ const page = pageOf2(query.cursor);
591
+ const search = needsSearch(query);
592
+ const params = search ? searchParams(target, query, page) : issuesParams(query, page);
593
+ const { body, headers: responseHeaders } = await requestJson(`${search ? `${api}/search/issues` : root}?${params.toString()}`, { fetch, headers });
594
+ return issuePageOf(rowsOf(body, search), query, responseHeaders.get("link") ?? "", page);
224
595
  },
225
596
  get: async (key) => issueOf(await rawOf(key)),
226
597
  create: async (input) => {
@@ -235,33 +606,97 @@ function createGithubAdapter({ connection, fetch }) {
235
606
  },
236
607
  update: async (key, patch) => {
237
608
  const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : issueOf(await rawOf(key));
238
- const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody(patch, current), missingIsIssue: true });
609
+ const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody2(patch, current), missingIsIssue: true });
239
610
  return issueOf(response.body);
240
611
  },
241
612
  comments: async (key, query) => {
242
- const page = pageOf(query.cursor);
613
+ assertCommentQuery(COMMENTS_SUBJECT2, capabilities.comments, query);
614
+ const page = pageOf2(query.cursor);
243
615
  const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
616
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
244
617
  const { body, headers: responseHeaders } = await requestJson(`${root}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
245
- return { items: (Array.isArray(body) ? body : []).map(commentOf), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
618
+ return { items: (Array.isArray(body) ? body : []).map(commentOf2), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
246
619
  },
247
- comment: async (key, body) => commentOf((await requestJson(`${root}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
620
+ comment: async (key, body) => commentOf2((await requestJson(`${root}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
248
621
  };
249
622
  }
250
- function pageOf(cursor) {
623
+ function pageOf2(cursor) {
251
624
  const page = cursor === void 0 ? 1 : Number(cursor);
252
625
  return Number.isInteger(page) && page >= 1 ? page : 1;
253
626
  }
254
- function stateFilter(status) {
627
+ function stateFilter2(status) {
255
628
  if (status === void 0) return "all";
256
629
  if (status.every((item) => item === "open" || item === "in_progress")) return "open";
257
630
  if (status.every((item) => item === "done" || item === "declined")) return "closed";
258
631
  return "all";
259
632
  }
633
+ function needsSearch(query) {
634
+ return query.search !== void 0 || query.createdAfter !== void 0 || query.createdBefore !== void 0 || query.updatedBefore !== void 0;
635
+ }
636
+ function assertCsvLabels(labels) {
637
+ if (labels?.some((label) => label.includes(",")) === true) throw new TrackerError("invalid_request", `${LIST_SUBJECT2}: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.`);
638
+ }
639
+ function issuesParams(query, page) {
640
+ const params = new URLSearchParams({ state: stateFilter2(query.status), per_page: String(query.limit), page: String(page) });
641
+ if (query.labels !== void 0 && query.labels.length > 0) params.set("labels", query.labels.join(","));
642
+ if (query.assignee !== void 0) params.set("assignee", query.assignee ?? "none");
643
+ if (query.author !== void 0) params.set("creator", query.author);
644
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
645
+ if (query.sort !== void 0) {
646
+ params.set("sort", query.sort.field);
647
+ params.set("direction", query.sort.direction ?? "desc");
648
+ }
649
+ return params;
650
+ }
651
+ function searchParams(target, query, page) {
652
+ const params = new URLSearchParams({ q: searchQuery(target, query), per_page: String(query.limit), page: String(page) });
653
+ const sort = query.sort;
654
+ params.set("sort", sort?.field ?? "created");
655
+ params.set("order", sort?.direction ?? "desc");
656
+ return params;
657
+ }
658
+ function searchQuery(target, query) {
659
+ const parts = [`repo:${target}`, "is:issue"];
660
+ const state = searchStateFilter(query.status);
661
+ if (state !== void 0) parts.push(state);
662
+ for (const label of query.labels ?? []) parts.push(`label:${quoted(label)}`);
663
+ if (query.assignee !== void 0) parts.push(query.assignee === null ? "no:assignee" : `assignee:${quoted(query.assignee)}`);
664
+ if (query.author !== void 0) parts.push(`author:${quoted(query.author)}`);
665
+ const created = timeRangeQualifier("created", query.createdAfter, query.createdBefore);
666
+ if (created !== void 0) parts.push(created);
667
+ const updated = timeRangeQualifier("updated", query.updatedAfter, query.updatedBefore);
668
+ if (updated !== void 0) parts.push(updated);
669
+ if (query.search !== void 0) parts.push(quoted(query.search));
670
+ return parts.join(" ");
671
+ }
672
+ function timeRangeQualifier(field, after, before) {
673
+ if (after !== void 0 && before !== void 0) return `${field}:${isoTime(after)}..${isoTime(before)}`;
674
+ if (after !== void 0) return `${field}:>=${isoTime(after)}`;
675
+ if (before !== void 0) return `${field}:<=${isoTime(before)}`;
676
+ return void 0;
677
+ }
678
+ function rowsOf(body, search) {
679
+ if (!search) return Array.isArray(body) ? body : [];
680
+ return isRecord(body) && Array.isArray(body.items) ? body.items : [];
681
+ }
682
+ function searchStateFilter(status) {
683
+ if (status === void 0) return void 0;
684
+ if (status.every((item) => item === "open" || item === "in_progress")) return "is:open";
685
+ if (status.every((item) => item === "done" || item === "declined")) return "is:closed";
686
+ return void 0;
687
+ }
688
+ function issuePageOf(rows, query, link, page) {
689
+ const items = rows.filter((row) => isRecord(row) && row.pull_request === void 0).map(issueOf);
690
+ return {
691
+ items: query.status === void 0 ? items : items.filter((issue) => query.status?.includes(issue.status.category) === true),
692
+ .../rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {}
693
+ };
694
+ }
260
695
  function composeLabels(plain, priority, status) {
261
696
  const priorityLabel = priority === void 0 ? void 0 : priorityName(GITHUB_PRIORITY_LABELS, priority);
262
697
  return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITHUB_IN_PROGRESS_LABEL] : []];
263
698
  }
264
- function patchBody(patch, current) {
699
+ function patchBody2(patch, current) {
265
700
  const status = patch.status ?? current?.status.category;
266
701
  const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
267
702
  return {
@@ -297,17 +732,221 @@ function issueOf(raw) {
297
732
  ...updatedAt === void 0 ? {} : { updatedAt }
298
733
  };
299
734
  }
300
- function commentOf(raw) {
735
+ function commentOf2(raw) {
301
736
  const item = isRecord(raw) ? raw : {};
302
737
  const author = isRecord(item.user) ? text(item.user.login) : void 0;
303
738
  const createdAt = toTimestamp(item.created_at);
304
739
  return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
305
740
  }
306
741
 
307
- // src/tracker/gitlab.lib.ts
742
+ // src/tracker/gitlab-pull.lib.ts
308
743
  var DEFAULT_URL = "https://gitlab.com";
309
- var TARGET2 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
310
- var IID = /^[1-9]\d{0,14}$/;
744
+ var TARGET3 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
745
+ var IID = /^[1-9]\d{0,19}$/;
746
+ var SHA2 = /^[0-9a-f]{7,64}$/;
747
+ var PULL_CAPABILITIES2 = {
748
+ list: { filters: pullRequestFilterSchema.options, clientSide: ["status"], sorts: ["created", "updated", "title"] },
749
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
750
+ create: true,
751
+ createDraft: true,
752
+ update: ["title", "description", "target"],
753
+ statuses: ["draft", "open", "closed"],
754
+ comment: true,
755
+ merge: ["merge", "squash"],
756
+ deleteSourceOnMerge: true
757
+ };
758
+ var ORDER_BY = { created: "created_at", updated: "updated_at", title: "title" };
759
+ function gitlabPulls() {
760
+ return {
761
+ capabilities: PULL_CAPABILITIES2,
762
+ create: (context) => createGitlabPullAdapter(context)
763
+ };
764
+ }
765
+ function createGitlabPullAdapter({ connection, fetch }) {
766
+ const target = requireTarget(connection.target, "\u043F\u0443\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 GitLab, group/project, \u0438\u043B\u0438 \u0435\u0433\u043E id");
767
+ if (!TARGET3.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
768
+ const root = `${baseUrl(connection.url, DEFAULT_URL)}/api/v4/projects/${encodeURIComponent(target)}/merge_requests`;
769
+ const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
770
+ const iid = (key) => {
771
+ if (!IID.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.`);
772
+ return key;
773
+ };
774
+ const currentOf = async (key) => pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
775
+ return {
776
+ list: async (query) => {
777
+ assertListQuery("PR GitLab", PULL_CAPABILITIES2.list, query, pullRequestFilterSchema.options);
778
+ const status = usedStatuses2(query.status);
779
+ const page = pageOf3(query.cursor);
780
+ const params = new URLSearchParams({
781
+ state: stateFilter3(status),
782
+ per_page: String(query.limit),
783
+ page: String(page),
784
+ order_by: query.sort === void 0 ? "created_at" : ORDER_BY[query.sort.field] ?? "created_at",
785
+ sort: query.sort?.direction ?? "desc"
786
+ });
787
+ if (query.labels !== void 0 && query.labels.length > 0) {
788
+ if (query.labels.some((label) => label.includes(","))) throw new TrackerError("invalid_request", "PR GitLab: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.");
789
+ params.set("labels", query.labels.join(","));
790
+ }
791
+ if (query.author !== void 0) params.set("author_username", query.author);
792
+ if (query.source !== void 0) params.set("source_branch", query.source);
793
+ if (query.target !== void 0) params.set("target_branch", query.target);
794
+ if (query.search !== void 0) params.set("search", query.search);
795
+ if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
796
+ if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
797
+ if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
798
+ if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
799
+ const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
800
+ const next = responseHeaders.get("x-next-page");
801
+ const items = (Array.isArray(body) ? body : []).map(pullOf2);
802
+ return {
803
+ items: status === void 0 ? items : items.filter((pr) => status.includes(pr.status.category) === true),
804
+ ...next === null || next === "" ? {} : { nextCursor: next }
805
+ };
806
+ },
807
+ get: currentOf,
808
+ create: async (input) => {
809
+ const body = {
810
+ source_branch: input.source,
811
+ target_branch: input.target,
812
+ title: (input.draft === true ? GITLAB_DRAFT_PREFIX : "") + input.title,
813
+ ...input.description === void 0 ? {} : { description: input.description }
814
+ };
815
+ return pullOf2((await requestJson(root, { fetch, headers, method: "POST", body, conflictOn: [409] })).body);
816
+ },
817
+ update: async (key, patch) => {
818
+ const current = patch.status === void 0 ? void 0 : await currentOf(key);
819
+ if (patch.status === "draft" && current?.status.category === "closed") {
820
+ throw new TrackerError("unsupported", "PR GitLab: \u043F\u0435\u0440\u0435\u0432\u043E\u0434 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u0433\u043E MR \u0432 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D.");
821
+ }
822
+ const body = patchBody3(patch, current);
823
+ if (Object.keys(body).length === 0) return current ?? pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
824
+ return pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
825
+ },
826
+ merge: async (key, input) => {
827
+ if (input.method !== void 0 && !PULL_CAPABILITIES2.merge.includes(input.method)) {
828
+ throw new TrackerError("unsupported", `PR GitLab: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
829
+ }
830
+ const body = {
831
+ // squash: true ставит squash; false GitLab игнорирует — при method 'merge' действует настройка MR/проекта.
832
+ ...input.method === "squash" ? { squash: true } : {},
833
+ ...input.message === void 0 ? {} : input.method === "squash" ? { squash_commit_message: input.message } : { merge_commit_message: input.message },
834
+ ...input.deleteSource === void 0 ? {} : { should_remove_source_branch: input.deleteSource },
835
+ ...input.headSha === void 0 ? {} : { sha: input.headSha }
836
+ };
837
+ return pullOf2((await requestJson(`${root}/${iid(key)}/merge`, { fetch, headers, method: "PUT", body, conflictOn: [405, 409, 422], missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
838
+ },
839
+ comments: async (key, query) => {
840
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", PULL_CAPABILITIES2.comments, query);
841
+ const page = pageOf3(query.cursor);
842
+ const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
843
+ const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } });
844
+ const next = responseHeaders.get("x-next-page");
845
+ const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf3);
846
+ return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
847
+ },
848
+ comment: async (key, body) => commentOf3((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body)
849
+ };
850
+ }
851
+ function pageOf3(cursor) {
852
+ const page = cursor === void 0 ? 1 : Number(cursor);
853
+ return Number.isInteger(page) && page >= 1 ? page : 1;
854
+ }
855
+ function stateFilter3(status) {
856
+ if (status === void 0) return "all";
857
+ if (status.every((item) => item === "draft" || item === "open")) return "opened";
858
+ if (status.every((item) => item === "merged")) return "merged";
859
+ if (status.every((item) => item === "closed")) return "closed";
860
+ return "all";
861
+ }
862
+ function patchBody3(patch, current) {
863
+ const body = {};
864
+ if (patch.description !== void 0) body.description = patch.description;
865
+ if (patch.target !== void 0) body.target_branch = patch.target;
866
+ if (patch.status === "closed" && current?.status.category !== "closed") body.state_event = "close";
867
+ if (patch.status === "open" && current?.status.category === "closed") body.state_event = "reopen";
868
+ if (patch.status === "draft" || patch.status === "open") {
869
+ const title = patch.title ?? current?.title ?? "";
870
+ const base = title.replace(GITLAB_DRAFT_PATTERN, "");
871
+ const next = patch.status === "draft" ? GITLAB_DRAFT_PREFIX + base : current?.status.category === "draft" || title !== base ? base : patch.title;
872
+ if (next !== void 0 && next !== current?.title) body.title = next;
873
+ } else if (patch.title !== void 0 && patch.title !== current?.title) {
874
+ body.title = patch.title;
875
+ }
876
+ return body;
877
+ }
878
+ function usedStatuses2(status) {
879
+ if (status === void 0 || status.length === 0) return void 0;
880
+ for (const item of status) {
881
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
882
+ throw new TrackerError("invalid_request", `PR GitLab: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
883
+ }
884
+ }
885
+ return status;
886
+ }
887
+ function labelNames(value) {
888
+ if (!Array.isArray(value)) return [];
889
+ const names = value.map((item) => typeof item === "string" ? item : isRecord(item) ? text(item.name) : void 0);
890
+ return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
891
+ }
892
+ function usernames(value) {
893
+ if (!Array.isArray(value)) return [];
894
+ const names = value.map((item) => isRecord(item) ? text(item.username) : void 0);
895
+ return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
896
+ }
897
+ function pullOf2(raw) {
898
+ const item = isRecord(raw) ? raw : {};
899
+ const state = text(item.state) ?? "opened";
900
+ const draft = item.draft === true;
901
+ const category = lookup(GITLAB_MERGE_REQUEST_STATES, state, "open");
902
+ const key = typeof item.iid === "number" ? String(item.iid) : "?";
903
+ const description = text(item.description);
904
+ const author = isRecord(item.author) ? text(item.author.username) : void 0;
905
+ const sha = text(item.sha);
906
+ const url = text(item.web_url);
907
+ const createdAt = toTimestamp(item.created_at);
908
+ const updatedAt = toTimestamp(item.updated_at);
909
+ const mergedAt = toTimestamp(item.merged_at);
910
+ const closedAt = toTimestamp(item.closed_at);
911
+ return {
912
+ key,
913
+ title: text(item.title) ?? key,
914
+ ...description === void 0 ? {} : { description },
915
+ status: { category: draft && category === "open" ? "draft" : category, name: draft && category === "open" ? "draft" : state },
916
+ ...author === void 0 ? {} : { author },
917
+ source: text(item.source_branch) ?? "?",
918
+ target: text(item.target_branch) ?? "?",
919
+ ...sha !== void 0 && SHA2.test(sha) ? { headSha: sha } : {},
920
+ labels: labelNames(item.labels),
921
+ reviewers: usernames(item.reviewers),
922
+ links: url === void 0 ? [] : [{ url, title: "GitLab" }],
923
+ ...createdAt === void 0 ? {} : { createdAt },
924
+ ...updatedAt === void 0 ? {} : { updatedAt },
925
+ ...mergedAt === void 0 ? {} : { mergedAt },
926
+ ...closedAt === void 0 ? {} : { closedAt }
927
+ };
928
+ }
929
+ function commentOf3(raw) {
930
+ const item = isRecord(raw) ? raw : {};
931
+ const author = isRecord(item.author) ? text(item.author.username) : void 0;
932
+ const createdAt = toTimestamp(item.created_at);
933
+ return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
934
+ }
935
+
936
+ // src/tracker/gitlab.lib.ts
937
+ var DEFAULT_URL2 = "https://gitlab.com";
938
+ var TARGET4 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
939
+ var IID2 = /^[1-9]\d{0,14}$/;
940
+ var ISSUE_CAPABILITIES = {
941
+ list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "priority", "title"] },
942
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
943
+ };
944
+ var ORDER_BY2 = {
945
+ created: "created_at",
946
+ updated: "updated_at",
947
+ priority: "priority",
948
+ title: "title"
949
+ };
311
950
  function gitlabTracker() {
312
951
  return {
313
952
  descriptor: {
@@ -320,23 +959,43 @@ function gitlabTracker() {
320
959
  ],
321
960
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
322
961
  },
323
- create: (context) => createGitlabAdapter(context)
962
+ create: (context) => createGitlabAdapter(context),
963
+ issues: ISSUE_CAPABILITIES,
964
+ pulls: gitlabPulls()
324
965
  };
325
966
  }
326
967
  function createGitlabAdapter({ connection, fetch }) {
327
968
  const target = requireTarget(connection.target, "\u043F\u0443\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 GitLab, group/project, \u0438\u043B\u0438 \u0435\u0433\u043E id");
328
- if (!TARGET2.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
329
- const root = `${baseUrl(connection.url, DEFAULT_URL)}/api/v4/projects/${encodeURIComponent(target)}/issues`;
969
+ if (!TARGET4.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
970
+ const root = `${baseUrl(connection.url, DEFAULT_URL2)}/api/v4/projects/${encodeURIComponent(target)}/issues`;
330
971
  const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
331
972
  const iid = (key) => {
332
- if (!IID.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
973
+ if (!IID2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
333
974
  return key;
334
975
  };
335
976
  const currentOf = async (key) => issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missingIsIssue: true })).body);
336
977
  return {
337
978
  list: async (query) => {
338
- const page = pageOf2(query.cursor);
339
- const params = new URLSearchParams({ state: stateFilter2(query.status), per_page: String(query.limit), page: String(page), order_by: "created_at", sort: "desc" });
979
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 GitLab", ISSUE_CAPABILITIES.list, query, ISSUE_FILTERS);
980
+ const page = pageOf4(query.cursor);
981
+ const params = new URLSearchParams({
982
+ state: stateFilter4(query.status),
983
+ per_page: String(query.limit),
984
+ page: String(page),
985
+ order_by: query.sort === void 0 ? "created_at" : ORDER_BY2[query.sort.field] ?? "created_at",
986
+ sort: query.sort?.direction ?? "desc"
987
+ });
988
+ if (query.labels !== void 0 && query.labels.length > 0) {
989
+ if (query.labels.some((label) => label.includes(","))) throw new TrackerError("invalid_request", "\u0417\u0430\u0434\u0430\u0447\u0438 GitLab: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.");
990
+ params.set("labels", query.labels.join(","));
991
+ }
992
+ if (query.assignee !== void 0) params.set(query.assignee === null ? "assignee_id" : "assignee_username", query.assignee === null ? "None" : query.assignee);
993
+ if (query.author !== void 0) params.set("author_username", query.author);
994
+ if (query.search !== void 0) params.set("search", query.search);
995
+ if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
996
+ if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
997
+ if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
998
+ if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
340
999
  const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
341
1000
  const items = (Array.isArray(body) ? body : []).map(issueOf2);
342
1001
  const next = responseHeaders.get("x-next-page");
@@ -353,24 +1012,25 @@ function createGitlabAdapter({ connection, fetch }) {
353
1012
  },
354
1013
  update: async (key, patch) => {
355
1014
  const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : await currentOf(key);
356
- return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody2(patch, current), missingIsIssue: true })).body);
1015
+ return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody4(patch, current), missingIsIssue: true })).body);
357
1016
  },
358
1017
  comments: async (key, query) => {
359
- const page = pageOf2(query.cursor);
360
- const params = new URLSearchParams({ sort: "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
1018
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", ISSUE_CAPABILITIES.comments, query);
1019
+ const page = pageOf4(query.cursor);
1020
+ const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
361
1021
  const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missingIsIssue: true });
362
1022
  const next = responseHeaders.get("x-next-page");
363
- const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true).map(commentOf2);
1023
+ const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf4);
364
1024
  return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
365
1025
  },
366
- comment: async (key, body) => commentOf2((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
1026
+ comment: async (key, body) => commentOf4((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
367
1027
  };
368
1028
  }
369
- function pageOf2(cursor) {
1029
+ function pageOf4(cursor) {
370
1030
  const page = cursor === void 0 ? 1 : Number(cursor);
371
1031
  return Number.isInteger(page) && page >= 1 ? page : 1;
372
1032
  }
373
- function stateFilter2(status) {
1033
+ function stateFilter4(status) {
374
1034
  if (status === void 0) return "all";
375
1035
  if (status.every((item) => item === "open" || item === "in_progress")) return "opened";
376
1036
  if (status.every((item) => item === "done" || item === "declined")) return "closed";
@@ -380,7 +1040,7 @@ function composeLabels2(plain, priority, status) {
380
1040
  const priorityLabel = priority === void 0 ? void 0 : priorityName(GITLAB_PRIORITY_LABELS, priority);
381
1041
  return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITLAB_IN_PROGRESS_LABEL] : []];
382
1042
  }
383
- function patchBody2(patch, current) {
1043
+ function patchBody4(patch, current) {
384
1044
  const status = patch.status ?? current?.status.category;
385
1045
  const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
386
1046
  if (patch.status === "declined") throw new TrackerError("invalid_request", "\u0412 GitLab \u043D\u0435\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \xAB\u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430\xBB.");
@@ -418,7 +1078,7 @@ function issueOf2(raw) {
418
1078
  ...updatedAt === void 0 ? {} : { updatedAt }
419
1079
  };
420
1080
  }
421
- function commentOf2(raw) {
1081
+ function commentOf4(raw) {
422
1082
  const item = isRecord(raw) ? raw : {};
423
1083
  const author = isRecord(item.author) ? text(item.author.username) : void 0;
424
1084
  const createdAt = toTimestamp(item.created_at);
@@ -430,6 +1090,11 @@ var PROJECT_KEY = /^[A-Z][A-Z0-9_]{0,49}$/;
430
1090
  var ISSUE_KEY = /^[A-Z][A-Z0-9_]{0,49}-[1-9]\d{0,14}$/;
431
1091
  var FIELDS = "summary,description,status,priority,labels,assignee,created,updated";
432
1092
  var CATEGORY_JQL = { open: '"To Do"', in_progress: '"In Progress"', done: "Done" };
1093
+ var ISSUE_CAPABILITIES2 = {
1094
+ list: { filters: ISSUE_FILTERS, clientSide: [], sorts: ["created", "updated", "priority", "title"] },
1095
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
1096
+ };
1097
+ var JIRA_SORT_FIELDS = { created: "created", updated: "updated", priority: "priority", title: "summary" };
433
1098
  function jiraTracker() {
434
1099
  return {
435
1100
  descriptor: {
@@ -443,7 +1108,8 @@ function jiraTracker() {
443
1108
  ],
444
1109
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
445
1110
  },
446
- create: (context) => createJiraAdapter(context)
1111
+ create: (context) => createJiraAdapter(context),
1112
+ issues: ISSUE_CAPABILITIES2
447
1113
  };
448
1114
  }
449
1115
  function createJiraAdapter({ connection, fetch }) {
@@ -462,7 +1128,8 @@ function createJiraAdapter({ connection, fetch }) {
462
1128
  const load = async (value) => toIssue2((await requestJson(`${api}/issue/${key(value)}?fields=${FIELDS}`, { fetch, headers, missingIsIssue: true })).body);
463
1129
  return {
464
1130
  list: async (query) => {
465
- const jql = listJql(project, query.status);
1131
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 Jira", ISSUE_CAPABILITIES2.list, query, ISSUE_FILTERS);
1132
+ const jql = listJql(project, query);
466
1133
  if (jql === void 0) return { items: [] };
467
1134
  if (query.cursor?.startsWith("s:") !== true) {
468
1135
  const params2 = new URLSearchParams({ jql, fields: FIELDS, maxResults: String(query.limit) });
@@ -507,23 +1174,44 @@ function createJiraAdapter({ connection, fetch }) {
507
1174
  return load(value);
508
1175
  },
509
1176
  comments: async (value, query) => {
1177
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 Jira", ISSUE_CAPABILITIES2.comments, query);
510
1178
  const startAt = query.cursor === void 0 ? 0 : Number(query.cursor);
511
- const params = new URLSearchParams({ startAt: String(Number.isInteger(startAt) && startAt >= 0 ? startAt : 0), maxResults: String(query.limit), orderBy: "created" });
1179
+ const params = new URLSearchParams({ startAt: String(Number.isInteger(startAt) && startAt >= 0 ? startAt : 0), maxResults: String(query.limit), orderBy: query.order === "desc" ? "-created" : "created" });
512
1180
  const { body } = await requestJson(`${api}/issue/${key(value)}/comment?${params.toString()}`, { fetch, headers, missingIsIssue: true });
513
1181
  const record = isRecord(body) ? body : {};
514
- const items = Array.isArray(record.comments) ? record.comments : [];
1182
+ const raw = Array.isArray(record.comments) ? record.comments : [];
1183
+ const items = raw.filter((item) => inTimeRange(toTimestamp(isRecord(item) ? item.updated : void 0), query.updatedAfter, void 0));
515
1184
  const total = typeof record.total === "number" ? record.total : 0;
516
1185
  const at = typeof record.startAt === "number" ? record.startAt : 0;
517
- return { items: items.map(commentOf3), ...at + items.length < total && items.length > 0 ? { nextCursor: String(at + items.length) } : {} };
1186
+ return { items: items.map(commentOf5), ...at + raw.length < total && raw.length > 0 ? { nextCursor: String(at + raw.length) } : {} };
518
1187
  },
519
- comment: async (value, body) => commentOf3((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
1188
+ comment: async (value, body) => commentOf5((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
520
1189
  };
521
1190
  }
522
- function listJql(project, status) {
523
- const base = `project = "${project}"`;
524
- if (status === void 0) return `${base} ORDER BY created DESC`;
525
- const names = status.flatMap((item) => CATEGORY_JQL[item] ?? []);
526
- return names.length === 0 ? void 0 : `${base} AND statusCategory in (${names.join(", ")}) ORDER BY created DESC`;
1191
+ function listJql(project, query) {
1192
+ const clauses = [];
1193
+ if (query.status !== void 0) {
1194
+ const names = query.status.flatMap((item) => CATEGORY_JQL[item] ?? []);
1195
+ if (names.length === 0) return void 0;
1196
+ clauses.push(`statusCategory in (${names.join(", ")})`);
1197
+ }
1198
+ for (const label of query.labels ?? []) clauses.push(`labels = ${quoted(label.replace(/\s+/g, "-"))}`);
1199
+ if (query.assignee !== void 0) clauses.push(query.assignee === null ? "assignee is EMPTY" : `assignee = ${quoted(query.assignee)}`);
1200
+ if (query.author !== void 0) clauses.push(`reporter = ${quoted(query.author)}`);
1201
+ if (query.search !== void 0) clauses.push(`text ~ ${quoted(query.search)}`);
1202
+ if (query.createdAfter !== void 0) clauses.push(`created >= ${quoted(jiraTime(query.createdAfter, "down"))}`);
1203
+ if (query.createdBefore !== void 0) clauses.push(`created <= ${quoted(jiraTime(query.createdBefore, "up"))}`);
1204
+ if (query.updatedAfter !== void 0) clauses.push(`updated >= ${quoted(jiraTime(query.updatedAfter, "down"))}`);
1205
+ if (query.updatedBefore !== void 0) clauses.push(`updated <= ${quoted(jiraTime(query.updatedBefore, "up"))}`);
1206
+ const sort = query.sort;
1207
+ const field = sort === void 0 ? "created" : sort.field;
1208
+ const direction = sort?.direction ?? "desc";
1209
+ return `${[`project = ${quoted(project)}`, ...clauses].join(" AND ")} ORDER BY ${JIRA_SORT_FIELDS[field]} ${direction.toUpperCase()}`;
1210
+ }
1211
+ function jiraTime(ms, outward) {
1212
+ const at = new Date(outward === "down" ? Math.floor(ms / 6e4) * 6e4 : Math.ceil(ms / 6e4) * 6e4);
1213
+ const pad = (value) => String(value).padStart(2, "0");
1214
+ return `${String(at.getUTCFullYear())}/${pad(at.getUTCMonth() + 1)}/${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())}`;
527
1215
  }
528
1216
  function fieldsOf(patch) {
529
1217
  return {
@@ -569,7 +1257,7 @@ function issueOf3(raw, site) {
569
1257
  ...updatedAt === void 0 ? {} : { updatedAt }
570
1258
  };
571
1259
  }
572
- function commentOf3(raw) {
1260
+ function commentOf5(raw) {
573
1261
  const item = isRecord(raw) ? raw : {};
574
1262
  const author = isRecord(item.author) ? text(item.author.displayName) : void 0;
575
1263
  const createdAt = toTimestamp(item.created);
@@ -578,6 +1266,14 @@ function commentOf3(raw) {
578
1266
 
579
1267
  // src/tracker/local.lib.ts
580
1268
  var STATUS_NAMES = { open: "Open", in_progress: "In progress", done: "Done", declined: "Declined" };
1269
+ var ISSUE_CAPABILITIES3 = {
1270
+ list: {
1271
+ filters: ["status", "labels", "assignee", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
1272
+ clientSide: ["labels", "assignee", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
1273
+ sorts: []
1274
+ },
1275
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc"] }
1276
+ };
581
1277
  function localTracker() {
582
1278
  return {
583
1279
  descriptor: {
@@ -586,6 +1282,7 @@ function localTracker() {
586
1282
  fields: [],
587
1283
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: [...ALL_STATUSES] }
588
1284
  },
1285
+ issues: ISSUE_CAPABILITIES3,
589
1286
  create: (context) => createLocalAdapter(context)
590
1287
  };
591
1288
  }
@@ -600,9 +1297,11 @@ function createLocalAdapter({ store, projectId, author }) {
600
1297
  return row;
601
1298
  };
602
1299
  return {
603
- list: (query) => {
1300
+ // list и comments async: отказ проверки запроса — rejected promise, как у остальных адаптеров.
1301
+ list: async (query) => {
1302
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 Local", ISSUE_CAPABILITIES3.list, query, ISSUE_FILTERS);
604
1303
  const page = store.listLocalIssues(projectId, { limit: query.limit, ...query.cursor === void 0 ? {} : { cursor: query.cursor }, ...query.status === void 0 ? {} : { status: query.status } });
605
- return Promise.resolve({ items: page.items.map(toIssue), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } });
1304
+ return { items: page.items.filter((row) => matchesIssueQuery(row, query)).map(toIssue), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } };
606
1305
  },
607
1306
  get: (key) => Promise.resolve(toIssue(mustGet(key))),
608
1307
  create: (input) => Promise.resolve(toIssue(store.createLocalIssue(projectId, input))),
@@ -611,13 +1310,22 @@ function createLocalAdapter({ store, projectId, author }) {
611
1310
  if (row === void 0) throw new TrackerError("unknown_issue", `\u0412 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0442\u0440\u0435\u043A\u0435\u0440\u0435 \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
612
1311
  return Promise.resolve(toIssue(row));
613
1312
  },
614
- comments: (key, query) => {
1313
+ comments: async (key, query) => {
1314
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 Local", ISSUE_CAPABILITIES3.comments, query);
615
1315
  const page = store.listLocalComments(projectId, mustGet(key).number, { limit: query.limit, ...query.cursor === void 0 ? {} : { cursor: query.cursor } });
616
- return Promise.resolve({ items: page.items.map(toComment), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } });
1316
+ const items = query.updatedAfter === void 0 ? page.items : page.items.filter((row) => inTimeRange(row.createdAt, query.updatedAfter, void 0));
1317
+ return { items: items.map(toComment), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } };
617
1318
  },
618
1319
  comment: (key, body) => Promise.resolve(toComment(store.addLocalComment(projectId, mustGet(key).number, body, author)))
619
1320
  };
620
1321
  }
1322
+ function matchesIssueQuery(row, query) {
1323
+ if (query.labels !== void 0 && !query.labels.every((label) => row.labels.includes(label))) return false;
1324
+ const assignee = query.assignee;
1325
+ if (assignee !== void 0 && (assignee === null ? row.assignee !== void 0 : row.assignee !== assignee)) return false;
1326
+ if (query.search !== void 0 && !containsText([row.title, row.description], query.search)) return false;
1327
+ return inTimeRange(row.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(row.updatedAt, query.updatedAfter, query.updatedBefore);
1328
+ }
621
1329
  function toIssue(row) {
622
1330
  return {
623
1331
  key: String(row.number),
@@ -636,11 +1344,222 @@ function toComment(row) {
636
1344
  return { id: String(row.id), ...row.author === void 0 ? {} : { author: row.author }, body: row.body, createdAt: row.createdAt };
637
1345
  }
638
1346
 
1347
+ // src/tracker/sourcecraft-pull.lib.ts
1348
+ var DEFAULT_API3 = "https://api.sourcecraft.tech";
1349
+ var TARGET5 = /^[\w.-]+\/[\w.-]+$/;
1350
+ var SLUG = /^[\w-]{1,100}$/;
1351
+ var SHA3 = /^[0-9a-f]{7,64}$/;
1352
+ var PULL_SORT_FIELDS_UPSTREAM = {
1353
+ created: "created_at",
1354
+ updated: "updated_at",
1355
+ title: "title"
1356
+ };
1357
+ var PULL_CAPABILITIES3 = {
1358
+ list: {
1359
+ filters: pullRequestFilterSchema.options,
1360
+ clientSide: ["labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
1361
+ sorts: ["created", "updated", "title"]
1362
+ },
1363
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
1364
+ create: true,
1365
+ createDraft: true,
1366
+ update: ["title", "description", "target"],
1367
+ statuses: ["draft", "open", "closed"],
1368
+ comment: true,
1369
+ merge: ["merge", "squash", "rebase"],
1370
+ deleteSourceOnMerge: true
1371
+ };
1372
+ function sourcecraftPulls() {
1373
+ return { capabilities: PULL_CAPABILITIES3, create: (context) => createPullAdapter(context) };
1374
+ }
1375
+ function createPullAdapter({ connection, fetch }) {
1376
+ const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
1377
+ if (!TARGET5.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
1378
+ const api = baseUrl(connection.url, DEFAULT_API3);
1379
+ const root = `${api}/repos/${target}/pulls`;
1380
+ const headers = { authorization: `Bearer ${connection.token ?? ""}` };
1381
+ const missing = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.` });
1382
+ const slug = (key) => {
1383
+ if (!SLUG.test(key)) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.`);
1384
+ return key;
1385
+ };
1386
+ const getPull = async (key) => pullOf3((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missing: missing(key) })).body);
1387
+ return {
1388
+ list: async (query) => {
1389
+ assertListQuery("PR SourceCraft", PULL_CAPABILITIES3.list, query, pullRequestFilterSchema.options);
1390
+ const status = usedStatuses3(query.status);
1391
+ const params = new URLSearchParams({ page_size: String(query.limit) });
1392
+ if (query.cursor !== void 0) params.set("page_token", query.cursor);
1393
+ const filter = pullFilterOf(query, status);
1394
+ if (filter !== void 0) params.set("filter", filter);
1395
+ if (query.sort !== void 0) params.set("sort_by", `${query.sort.direction === "asc" ? "" : "-"}${PULL_SORT_FIELDS_UPSTREAM[query.sort.field]}`);
1396
+ const { body } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
1397
+ const record = isRecord(body) ? body : {};
1398
+ const pulls = (Array.isArray(record.pull_requests) ? record.pull_requests : []).map(pullOf3).filter((pull) => matchesPage2(pull, query));
1399
+ return { items: pulls, ...nextOf2(record.next_page_token) };
1400
+ },
1401
+ get: (key) => getPull(key),
1402
+ create: async (input) => pullOf3((await requestJson(root, { fetch, headers, method: "POST", body: createBody2(input) })).body),
1403
+ update: async (key, patch) => {
1404
+ const id = slug(key);
1405
+ let updated;
1406
+ if (patch.title !== void 0 || patch.description !== void 0 || patch.target !== void 0) {
1407
+ const body = {
1408
+ ...patch.title === void 0 ? {} : { title: patch.title },
1409
+ ...patch.description === void 0 ? {} : { description: patch.description },
1410
+ ...patch.target === void 0 ? {} : { target_branch: patch.target }
1411
+ };
1412
+ updated = (await requestJson(`${root}/${id}`, { fetch, headers, method: "PATCH", body, missing: missing(key) })).body;
1413
+ }
1414
+ if (patch.status !== void 0) {
1415
+ const action = statusActionOf(patch.status, (await getPull(key)).status.category);
1416
+ if (action !== void 0) await requestJson(`${root}/${id}/${action}`, { fetch, headers, method: "POST", missing: missing(key) });
1417
+ return getPull(key);
1418
+ }
1419
+ return pullOf3(updated ?? (await requestJson(`${root}/${id}`, { fetch, headers, missing: missing(key) })).body);
1420
+ },
1421
+ merge: async (key, input) => {
1422
+ const id = slug(key);
1423
+ if (input.message !== void 0) throw new TrackerError("unsupported", "PR SourceCraft: \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0441\u043B\u0438\u044F\u043D\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
1424
+ if (input.method !== void 0 && !PULL_CAPABILITIES3.merge.includes(input.method)) {
1425
+ throw new TrackerError("unsupported", `PR SourceCraft: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
1426
+ }
1427
+ if (input.headSha !== void 0) {
1428
+ const current = await getPull(key);
1429
+ if ((current.headSha ?? "").toLowerCase() !== input.headSha.toLowerCase()) {
1430
+ throw new TrackerError("conflict", `\u0413\u043E\u043B\u043E\u0432\u0430 PR ${key} \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C: \u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F ${input.headSha}, \u0441\u0435\u0439\u0447\u0430\u0441 ${current.headSha ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`);
1431
+ }
1432
+ }
1433
+ const { body } = await requestJson(`${root}/${id}/merge`, {
1434
+ fetch,
1435
+ headers,
1436
+ method: "POST",
1437
+ // squash/rebase по умолчанию наследуются из настройки репозитория — без method не шлём явно false.
1438
+ body: {
1439
+ ...input.method === void 0 ? {} : { squash: input.method === "squash", rebase: input.method === "rebase" },
1440
+ ...input.deleteSource === void 0 ? {} : { delete_branch: input.deleteSource }
1441
+ },
1442
+ missing: missing(key)
1443
+ });
1444
+ const operation = isRecord(body) ? body : {};
1445
+ if (operation.status === "failed" || operation.status === "cancel") {
1446
+ const errorMessage = isRecord(operation.error) ? text(operation.error.message) : void 0;
1447
+ throw new TrackerError("conflict", `\u0421\u043B\u0438\u044F\u043D\u0438\u0435 PR ${key} \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E: ${redactSecretText(errorMessage ?? String(operation.status))}.`);
1448
+ }
1449
+ return getPull(key);
1450
+ },
1451
+ comments: async (key, query) => {
1452
+ const id = slug(key);
1453
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR SourceCraft", PULL_CAPABILITIES3.comments, query);
1454
+ const params = new URLSearchParams({ page_size: String(query.limit) });
1455
+ if (query.cursor !== void 0) params.set("page_token", query.cursor);
1456
+ if (query.order !== void 0) params.set("sort_by", query.order === "asc" ? "created_at" : "-created_at");
1457
+ const { body } = await requestJson(`${root}/${id}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
1458
+ const record = isRecord(body) ? body : {};
1459
+ const rawComments = Array.isArray(record.pull_request_comments) ? record.pull_request_comments : [];
1460
+ const visible = rawComments.filter((raw) => !(isRecord(raw) && raw.is_deleted === true));
1461
+ const filtered = query.updatedAfter === void 0 ? visible : visible.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
1462
+ return { items: filtered.map(commentOf6), ...nextOf2(record.next_page_token) };
1463
+ },
1464
+ comment: async (key, body) => commentOf6((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
1465
+ };
1466
+ }
1467
+ function createBody2(input) {
1468
+ return {
1469
+ title: input.title,
1470
+ source_branch: input.source,
1471
+ target_branch: input.target,
1472
+ ...input.description === void 0 ? {} : { description: input.description },
1473
+ publish: input.draft !== true
1474
+ };
1475
+ }
1476
+ function statusActionOf(requested, current) {
1477
+ if (current === requested) return void 0;
1478
+ if (requested === "draft") return "draft";
1479
+ if (requested === "open") return current === "draft" ? "publish" : "reopen";
1480
+ return "discard";
1481
+ }
1482
+ function usedStatuses3(status) {
1483
+ if (status === void 0 || status.length === 0) return void 0;
1484
+ for (const item of status) {
1485
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
1486
+ throw new TrackerError("invalid_request", `PR SourceCraft: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
1487
+ }
1488
+ }
1489
+ return status;
1490
+ }
1491
+ function matchesPage2(pull, query) {
1492
+ if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
1493
+ if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
1494
+ return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
1495
+ }
1496
+ function pullFilterOf(query, status) {
1497
+ const clauses = [];
1498
+ if (status !== void 0) {
1499
+ const slugs = [...new Set(keysFor(SOURCECRAFT_PULL_STATUSES, status))];
1500
+ clauses.push(`status in (${slugs.join(", ")})`);
1501
+ }
1502
+ if (query.author !== void 0) clauses.push(`author_slug=${quoted(query.author)}`);
1503
+ if (query.source !== void 0) clauses.push(`source_branch=${quoted(query.source)}`);
1504
+ if (query.target !== void 0) clauses.push(`target_branch=${quoted(query.target)}`);
1505
+ return clauses.length === 0 ? void 0 : clauses.join(" and ");
1506
+ }
1507
+ function nextOf2(token) {
1508
+ const value = text(token);
1509
+ return value === void 0 ? {} : { nextCursor: value };
1510
+ }
1511
+ function pullOf3(raw) {
1512
+ const item = isRecord(raw) ? raw : {};
1513
+ const source = isRecord(item.source) ? item.source : {};
1514
+ const target = isRecord(item.target) ? item.target : {};
1515
+ const headSha = text(source.sha);
1516
+ const description = text(item.description);
1517
+ const author = isRecord(item.author) ? text(item.author.slug) : void 0;
1518
+ const createdAt = toTimestamp(item.created_at);
1519
+ const updatedAt = toTimestamp(item.updated_at);
1520
+ const mergedAt = toTimestamp(item.merged_at);
1521
+ const closedAt = toTimestamp(item.closed_at);
1522
+ const key = text(item.slug) ?? "?";
1523
+ const statusName = text(item.status) ?? "open";
1524
+ return {
1525
+ key,
1526
+ title: text(item.title) ?? key,
1527
+ ...description === void 0 ? {} : { description },
1528
+ status: { category: lookup(SOURCECRAFT_PULL_STATUSES, statusName, "open"), name: statusName },
1529
+ ...author === void 0 ? {} : { author },
1530
+ source: text(source.ref) ?? text(item.source_branch) ?? "?",
1531
+ target: text(target.ref) ?? text(item.target_branch) ?? "?",
1532
+ ...headSha !== void 0 && SHA3.test(headSha) ? { headSha } : {},
1533
+ labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : void 0)) : [],
1534
+ links: [],
1535
+ ...createdAt === void 0 ? {} : { createdAt },
1536
+ ...updatedAt === void 0 ? {} : { updatedAt },
1537
+ ...mergedAt === void 0 ? {} : { mergedAt },
1538
+ ...closedAt === void 0 ? {} : { closedAt }
1539
+ };
1540
+ }
1541
+ function commentOf6(raw) {
1542
+ const item = isRecord(raw) ? raw : {};
1543
+ const author = isRecord(item.author) ? text(item.author.slug) : void 0;
1544
+ const createdAt = toTimestamp(item.created_at);
1545
+ return { id: text(item.id) ?? "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
1546
+ }
1547
+
639
1548
  // src/tracker/sourcecraft.lib.ts
640
- var DEFAULT_API2 = "https://api.sourcecraft.tech";
1549
+ var DEFAULT_API4 = "https://api.sourcecraft.tech";
641
1550
  var DEFAULT_WEB = "https://sourcecraft.dev";
642
- var TARGET3 = /^[\w.-]+\/[\w.-]+$/;
643
- var SLUG = /^[\w-]{1,100}$/;
1551
+ var TARGET6 = /^[\w.-]+\/[\w.-]+$/;
1552
+ var SLUG2 = /^[\w-]{1,100}$/;
1553
+ var ISSUE_SORT_FIELDS_UPSTREAM = {
1554
+ created: "created_at",
1555
+ updated: "updated_at",
1556
+ priority: "priority",
1557
+ title: "title"
1558
+ };
1559
+ var ISSUE_CAPABILITIES4 = {
1560
+ list: { filters: ISSUE_FILTERS, clientSide: ["assignee"], sorts: ["created", "updated", "priority", "title"] },
1561
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
1562
+ };
644
1563
  function sourcecraftTracker() {
645
1564
  return {
646
1565
  descriptor: {
@@ -653,47 +1572,59 @@ function sourcecraftTracker() {
653
1572
  ],
654
1573
  features: { create: true, update: true, comment: true, priority: true, labels: false, assignee: false, statuses: [...ALL_STATUSES] }
655
1574
  },
656
- create: (context) => createSourceCraftAdapter(context)
1575
+ issues: ISSUE_CAPABILITIES4,
1576
+ create: (context) => createSourceCraftAdapter(context),
1577
+ pulls: sourcecraftPulls()
657
1578
  };
658
1579
  }
659
1580
  function createSourceCraftAdapter({ connection, fetch }) {
660
1581
  const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
661
- if (!TARGET3.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
662
- const api = baseUrl(connection.url, DEFAULT_API2);
1582
+ if (!TARGET6.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
1583
+ const api = baseUrl(connection.url, DEFAULT_API4);
663
1584
  const root = `${api}/repos/${target}/issues`;
664
1585
  const headers = { authorization: `Bearer ${connection.token ?? ""}` };
665
1586
  const web = connection.url === void 0 ? `${DEFAULT_WEB}/${target}/issues` : void 0;
666
1587
  const slug = (key) => {
667
- if (!SLUG.test(key)) throw new TrackerError("unknown_issue", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
1588
+ if (!SLUG2.test(key)) throw new TrackerError("unknown_issue", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
668
1589
  return key;
669
1590
  };
670
1591
  const toIssue2 = (raw) => issueOf4(raw, web);
671
1592
  return {
672
1593
  list: async (query) => {
1594
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 SourceCraft", ISSUE_CAPABILITIES4.list, query, ISSUE_FILTERS);
673
1595
  const params = new URLSearchParams({ page_size: String(query.limit) });
674
1596
  if (query.cursor !== void 0) params.set("page_token", query.cursor);
1597
+ const filter = issueFilterOf(query);
1598
+ if (filter !== void 0) params.set("filter", filter);
1599
+ if (query.sort !== void 0) params.set("sort_by", `${query.sort.direction === "asc" ? "" : "-"}${issueSortFieldOf(query.sort.field)}`);
675
1600
  const { body } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
676
1601
  const record = isRecord(body) ? body : {};
677
- const issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
678
- return { items: query.status === void 0 ? issues : issues.filter((issue) => query.status?.includes(issue.status.category) === true), ...nextOf(record.next_page_token) };
1602
+ let issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
1603
+ if (query.status !== void 0) issues = issues.filter((issue) => query.status?.includes(issue.status.category) === true);
1604
+ if (query.assignee === null) issues = issues.filter((issue) => issue.assignee === void 0);
1605
+ return { items: issues, ...nextOf3(record.next_page_token) };
679
1606
  },
680
1607
  get: async (key) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missingIsIssue: true })).body),
681
1608
  create: async (input) => {
682
1609
  const body = { title: input.title, ...input.description === void 0 ? {} : { description: input.description }, ...input.priority === void 0 ? {} : { priority: input.priority } };
683
1610
  return toIssue2((await requestJson(root, { fetch, headers, method: "POST", body })).body);
684
1611
  },
685
- update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody3(patch), missingIsIssue: true })).body),
1612
+ update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody5(patch), missingIsIssue: true })).body),
686
1613
  comments: async (key, query) => {
1614
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 SourceCraft", ISSUE_CAPABILITIES4.comments, query);
687
1615
  const params = new URLSearchParams({ page_size: String(query.limit) });
688
1616
  if (query.cursor !== void 0) params.set("page_token", query.cursor);
1617
+ if (query.order !== void 0) params.set("sort_by", query.order === "asc" ? "created_at" : "-created_at");
689
1618
  const { body } = await requestJson(`${root}/${slug(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
690
1619
  const record = isRecord(body) ? body : {};
691
- return { items: (Array.isArray(record.issue_comments) ? record.issue_comments : []).map(commentOf4), ...nextOf(record.next_page_token) };
1620
+ const rawComments = Array.isArray(record.issue_comments) ? record.issue_comments : [];
1621
+ const filtered = query.updatedAfter === void 0 ? rawComments : rawComments.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
1622
+ return { items: filtered.map(commentOf7), ...nextOf3(record.next_page_token) };
692
1623
  },
693
- comment: async (key, body) => commentOf4((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
1624
+ comment: async (key, body) => commentOf7((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
694
1625
  };
695
1626
  }
696
- function patchBody3(patch) {
1627
+ function patchBody5(patch) {
697
1628
  return {
698
1629
  ...patch.title === void 0 ? {} : { title: patch.title },
699
1630
  ...patch.description === void 0 ? {} : { description: patch.description },
@@ -701,10 +1632,31 @@ function patchBody3(patch) {
701
1632
  ...patch.priority === void 0 ? {} : { priority: patch.priority }
702
1633
  };
703
1634
  }
704
- function nextOf(token) {
1635
+ function nextOf3(token) {
705
1636
  const value = text(token);
706
1637
  return value === void 0 ? {} : { nextCursor: value };
707
1638
  }
1639
+ function issueSortFieldOf(field) {
1640
+ const upstream = ISSUE_SORT_FIELDS_UPSTREAM[field];
1641
+ if (upstream === void 0) throw new TrackerError("unsupported", `\u0417\u0430\u0434\u0430\u0447\u0438 SourceCraft: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E ${field} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
1642
+ return upstream;
1643
+ }
1644
+ function issueFilterOf(query) {
1645
+ const clauses = [];
1646
+ if (query.status !== void 0 && query.status.length > 0) {
1647
+ const slugs = [...new Set(query.status.flatMap((category) => SOURCECRAFT_STATUS_FILTER[category]))];
1648
+ clauses.push(`status in (${slugs.join(", ")})`);
1649
+ }
1650
+ for (const label of query.labels ?? []) clauses.push(`label_slug=${quoted(label)}`);
1651
+ if (typeof query.assignee === "string") clauses.push(`assignee_slug=${quoted(query.assignee)}`);
1652
+ if (query.author !== void 0) clauses.push(`author_slug=${quoted(query.author)}`);
1653
+ if (query.search !== void 0) clauses.push(`title=${quoted(query.search)}`);
1654
+ if (query.createdAfter !== void 0) clauses.push(`created_at>=${quoted(isoTime(query.createdAfter))}`);
1655
+ if (query.createdBefore !== void 0) clauses.push(`created_at<=${quoted(isoTime(query.createdBefore))}`);
1656
+ if (query.updatedAfter !== void 0) clauses.push(`updated_at>=${quoted(isoTime(query.updatedAfter))}`);
1657
+ if (query.updatedBefore !== void 0) clauses.push(`updated_at<=${quoted(isoTime(query.updatedBefore))}`);
1658
+ return clauses.length === 0 ? void 0 : clauses.join(" and ");
1659
+ }
708
1660
  function issueOf4(raw, web) {
709
1661
  const item = isRecord(raw) ? raw : {};
710
1662
  const status = isRecord(item.status) ? item.status : {};
@@ -729,7 +1681,7 @@ function issueOf4(raw, web) {
729
1681
  ...updatedAt === void 0 ? {} : { updatedAt }
730
1682
  };
731
1683
  }
732
- function commentOf4(raw) {
1684
+ function commentOf7(raw) {
733
1685
  const item = isRecord(raw) ? raw : {};
734
1686
  const author = isRecord(item.author) ? text(item.author.slug) : void 0;
735
1687
  const createdAt = toTimestamp(item.created_at);
@@ -792,19 +1744,34 @@ function writeTrackerToken(dataDir, projectId, token, sink = defaultSecretSink)
792
1744
  function deleteTrackerToken(dataDir, projectId, sink = defaultSecretSink) {
793
1745
  return writeTrackerToken(dataDir, projectId, "", sink);
794
1746
  }
1747
+
1748
+ // src/tracker/pull.lib.ts
1749
+ function pullRequestAdapterOf(definition, context) {
1750
+ if (definition.pulls === void 0) throw new TrackerError("unsupported", `\u0422\u0440\u0435\u043A\u0435\u0440 ${definition.descriptor.title} \u043D\u0435 \u0432\u0435\u0434\u0451\u0442 pull request-\u044B.`);
1751
+ return definition.pulls.create(context);
1752
+ }
795
1753
  export {
796
1754
  ALL_STATUSES,
1755
+ COMMENT_FILTERS,
797
1756
  GITHUB_CLOSE_REASONS,
798
1757
  GITHUB_IN_PROGRESS_LABEL,
799
1758
  GITHUB_PRIORITY_LABELS,
1759
+ GITLAB_DRAFT_PATTERN,
1760
+ GITLAB_DRAFT_PREFIX,
800
1761
  GITLAB_IN_PROGRESS_LABEL,
1762
+ GITLAB_MERGE_REQUEST_STATES,
801
1763
  GITLAB_PRIORITY_LABELS,
1764
+ ISSUE_FILTERS,
1765
+ ISSUE_SORT_FIELDS,
802
1766
  JIRA_ISSUE_TYPE,
803
1767
  JIRA_PRIORITIES,
804
1768
  JIRA_PRIORITY_NAMES,
805
1769
  JIRA_STATUS_CATEGORIES,
1770
+ LEGACY_ISSUE_CAPABILITIES,
806
1771
  SOURCECRAFT_PRIORITIES,
1772
+ SOURCECRAFT_PULL_STATUSES,
807
1773
  SOURCECRAFT_STATUSES,
1774
+ SOURCECRAFT_STATUS_FILTER,
808
1775
  SOURCECRAFT_STATUS_SLUGS,
809
1776
  TRACKER_ERROR_BODY_MAX,
810
1777
  TRACKER_LOCAL_AUTHOR,
@@ -812,29 +1779,47 @@ export {
812
1779
  TRACKER_SECRETS_FILE,
813
1780
  TRACKER_TIMEOUT_MS,
814
1781
  TrackerError,
1782
+ assertCommentQuery,
1783
+ assertListQuery,
815
1784
  baseUrl,
816
1785
  clipText,
1786
+ containsText,
817
1787
  deleteTrackerToken,
818
1788
  findTracker,
1789
+ githubPulls,
819
1790
  githubTracker,
1791
+ gitlabPulls,
820
1792
  gitlabTracker,
821
1793
  hasTrackerToken,
1794
+ inTimeRange,
822
1795
  isRecord,
1796
+ isoTime,
1797
+ issueCapabilitiesOf,
823
1798
  jiraTracker,
1799
+ keysFor,
824
1800
  listTrackers,
825
1801
  localTracker,
826
1802
  lookup,
1803
+ mergeMethodSchema,
827
1804
  nameOf,
828
1805
  priorityName,
1806
+ pullRequestAdapterOf,
1807
+ pullRequestFilterSchema,
1808
+ pullRequestSchema,
1809
+ pullRequestSortFieldSchema,
1810
+ pullRequestStatusCategorySchema,
1811
+ quoted,
829
1812
  readTrackerToken,
830
1813
  registerBuiltinTrackers,
831
1814
  registerTracker,
832
1815
  requestJson,
833
1816
  requireTarget,
1817
+ sourcecraftPulls,
834
1818
  sourcecraftTracker,
835
1819
  stringList,
836
1820
  text,
837
1821
  toTimestamp,
1822
+ usedFilters,
838
1823
  writeTrackerToken
839
1824
  };
840
1825
  //# sourceMappingURL=index.js.map