@smwb/onreel-git-mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,12 @@
1
1
  import {
2
+ REDACTED,
2
3
  defaultSecretSink,
3
4
  redactSecretText
4
- } from "../chunk-L7CPE2SO.js";
5
+ } from "../chunk-AGGJK2XZ.js";
6
+ import {
7
+ issueLinkSchema,
8
+ timestampSchema
9
+ } from "../chunk-4ZGRXIVA.js";
5
10
 
6
11
  // src/tracker/tracker.const.ts
7
12
  var TRACKER_TIMEOUT_MS = 15e3;
@@ -16,6 +21,12 @@ var LEGACY_ISSUE_CAPABILITIES = {
16
21
  list: { filters: ["status"], clientSide: ["status"], sorts: [] },
17
22
  comments: { filters: [], clientSide: [], order: ["asc"] }
18
23
  };
24
+ var CI_LOG_DEFAULT_BYTES = 16 * 1024;
25
+ var CI_LOG_MAX_BYTES = 1024 * 1024;
26
+ var CI_LOG_READ_MAX_BYTES = 64 * 1024 * 1024;
27
+ var CI_LOG_TIMEOUT_MS = 6e4;
28
+ var CI_LOG_PAGES_MAX = 50;
29
+ var CI_FINISHED_STATUSES = ["success", "failed", "canceled", "skipped"];
19
30
 
20
31
  // src/tracker/tracker.lib.ts
21
32
  var TrackerError = class extends Error {
@@ -211,6 +222,99 @@ var GITHUB_PRIORITY_LABELS = {
211
222
  "priority: blocker": "blocker"
212
223
  };
213
224
  var ALL_STATUSES = ["open", "in_progress", "done", "declined"];
225
+ var GITLAB_MERGE_REQUEST_STATES = {
226
+ opened: "open",
227
+ locked: "open",
228
+ merged: "merged",
229
+ closed: "closed"
230
+ };
231
+ var GITLAB_DRAFT_PREFIX = "Draft: ";
232
+ var GITLAB_DRAFT_PATTERN = /^\s*(?:\[draft\]|\(draft\)|draft:)\s*/i;
233
+ var SOURCECRAFT_PULL_STATUSES = {
234
+ draft: "draft",
235
+ open: "open",
236
+ merging: "open",
237
+ merged: "merged",
238
+ discarded: "closed"
239
+ };
240
+ var GITHUB_RUN_STATUSES = {
241
+ queued: "queued",
242
+ requested: "queued",
243
+ pending: "queued",
244
+ waiting: "waiting",
245
+ in_progress: "running"
246
+ };
247
+ var GITHUB_RUN_CONCLUSIONS = {
248
+ success: "success",
249
+ neutral: "success",
250
+ failure: "failed",
251
+ timed_out: "failed",
252
+ startup_failure: "failed",
253
+ cancelled: "canceled",
254
+ stale: "canceled",
255
+ skipped: "skipped",
256
+ action_required: "waiting"
257
+ };
258
+ var GITHUB_RUN_EVENTS = {
259
+ push: "push",
260
+ pull_request: "pull_request",
261
+ pull_request_target: "pull_request",
262
+ merge_group: "pull_request",
263
+ workflow_dispatch: "manual",
264
+ schedule: "schedule",
265
+ repository_dispatch: "trigger",
266
+ workflow_run: "trigger",
267
+ workflow_call: "trigger"
268
+ };
269
+ var GITLAB_CI_STATUSES = {
270
+ created: "queued",
271
+ waiting_for_resource: "queued",
272
+ preparing: "queued",
273
+ pending: "queued",
274
+ running: "running",
275
+ canceling: "running",
276
+ waiting_for_callback: "waiting",
277
+ manual: "waiting",
278
+ scheduled: "waiting",
279
+ success: "success",
280
+ failed: "failed",
281
+ canceled: "canceled",
282
+ skipped: "skipped"
283
+ };
284
+ var GITLAB_PIPELINE_SOURCES = {
285
+ push: "push",
286
+ merge_request_event: "pull_request",
287
+ external_pull_request_event: "pull_request",
288
+ web: "manual",
289
+ api: "manual",
290
+ chat: "manual",
291
+ webide: "manual",
292
+ schedule: "schedule",
293
+ pipeline_execution_policy_schedule: "schedule",
294
+ trigger: "trigger",
295
+ pipeline: "trigger",
296
+ parent_pipeline: "trigger"
297
+ };
298
+ var SOURCECRAFT_CI_STATUSES = {
299
+ created: "queued",
300
+ prepared: "queued",
301
+ processing: "running",
302
+ awaiting_approval: "waiting",
303
+ success: "success",
304
+ failed: "failed",
305
+ timeout: "failed",
306
+ canceled: "canceled",
307
+ rejected: "canceled",
308
+ skipped: "skipped"
309
+ };
310
+ var SOURCECRAFT_RUN_EVENTS = {
311
+ push: "push",
312
+ pr_update: "pull_request",
313
+ manual: "manual",
314
+ restart: "retry",
315
+ schedule: "schedule",
316
+ repository_event: "trigger"
317
+ };
214
318
 
215
319
  // src/tracker/http.lib.ts
216
320
  async function requestJson(url, request) {
@@ -223,7 +327,7 @@ async function requestJson(url, request) {
223
327
  signal: AbortSignal.timeout(TRACKER_TIMEOUT_MS)
224
328
  });
225
329
  } catch (error) {
226
- 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)}`));
330
+ throw notAnswered(url, error);
227
331
  }
228
332
  const raw = await response.text();
229
333
  if (!response.ok) throw failureOf(url, response, raw, request);
@@ -234,6 +338,131 @@ async function requestJson(url, request) {
234
338
  throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 JSON.`);
235
339
  }
236
340
  }
341
+ var ERROR_BODY_READ_MAX = 16 * 1024;
342
+ async function requestText(url, request) {
343
+ const skip = request.skip ?? 0;
344
+ const { bytes, from } = request.keep;
345
+ const readMax = request.readMax ?? CI_LOG_READ_MAX_BYTES;
346
+ if (![skip, bytes, readMax].every((value) => Number.isSafeInteger(value) && value >= 0)) {
347
+ throw new TrackerError("invalid_request", "\u041E\u043A\u043D\u043E \u0447\u0442\u0435\u043D\u0438\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u2014 \u0446\u0435\u043B\u044B\u0435 \u0447\u0438\u0441\u043B\u0430 \u0431\u0430\u0439\u0442 \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 0.");
348
+ }
349
+ let response;
350
+ try {
351
+ response = await request.fetch(url, {
352
+ method: "GET",
353
+ headers: { accept: request.accept ?? "text/plain", ...request.headers },
354
+ signal: AbortSignal.timeout(request.timeoutMs ?? CI_LOG_TIMEOUT_MS)
355
+ });
356
+ } catch (error) {
357
+ throw notAnswered(url, error);
358
+ }
359
+ if (!response.ok) {
360
+ const head = await readBody(url, response, { skip: 0, bytes: ERROR_BODY_READ_MAX, from: "head", readMax: ERROR_BODY_READ_MAX });
361
+ throw failureOf(url, response, decodeUtf8(head.window), request);
362
+ }
363
+ const body = await readBody(url, response, { skip, bytes, from, readMax });
364
+ let start = Math.min(body.start, body.end);
365
+ let end = body.end;
366
+ let window = body.window;
367
+ const lead = start > 0 ? leadingContinuation(window) : 0;
368
+ const tail = trailingPartial(window.subarray(lead));
369
+ if (lead > 0 || tail > 0) {
370
+ window = window.subarray(lead, window.length - tail);
371
+ start += lead;
372
+ end -= tail;
373
+ }
374
+ return { text: decodeUtf8(window), start, end, read: body.read, whole: body.whole, headers: response.headers };
375
+ }
376
+ async function readBody(url, response, window) {
377
+ const { skip, bytes, from, readMax } = window;
378
+ const reader = response.body?.getReader();
379
+ const kept = from === "head" ? headBuffer(bytes) : ringBuffer(bytes);
380
+ let read = 0;
381
+ let whole = true;
382
+ if (reader !== void 0) {
383
+ try {
384
+ for (; ; ) {
385
+ const { done, value } = await reader.read();
386
+ if (done) break;
387
+ const chunk = value.subarray(0, Math.max(0, readMax - read));
388
+ const cut = chunk.length < value.length;
389
+ const offset = Math.max(0, skip - read);
390
+ if (offset < chunk.length) kept.push(chunk.subarray(offset));
391
+ read += chunk.length;
392
+ if (cut || kept.full()) {
393
+ whole = false;
394
+ await reader.cancel().catch(() => void 0);
395
+ break;
396
+ }
397
+ }
398
+ } catch (error) {
399
+ await reader.cancel().catch(() => void 0);
400
+ throw notAnswered(url, error);
401
+ }
402
+ }
403
+ const data = kept.bytes();
404
+ const start = from === "head" ? skip : Math.max(skip, read - data.length);
405
+ return { window: data, start, end: from === "head" ? skip + data.length : read, read, whole };
406
+ }
407
+ function headBuffer(capacity) {
408
+ const buffer = new Uint8Array(capacity);
409
+ let length = 0;
410
+ let overflow = false;
411
+ return {
412
+ push(chunk) {
413
+ const room = capacity - length;
414
+ buffer.set(chunk.subarray(0, room), length);
415
+ length += Math.min(room, chunk.length);
416
+ if (chunk.length > room) overflow = true;
417
+ },
418
+ full: () => overflow,
419
+ bytes: () => buffer.subarray(0, length)
420
+ };
421
+ }
422
+ function ringBuffer(capacity) {
423
+ const buffer = new Uint8Array(capacity);
424
+ let next = 0;
425
+ let length = 0;
426
+ return {
427
+ push(chunk) {
428
+ if (capacity === 0) return;
429
+ const part = chunk.subarray(Math.max(0, chunk.length - capacity));
430
+ const first = Math.min(part.length, capacity - next);
431
+ buffer.set(part.subarray(0, first), next);
432
+ buffer.set(part.subarray(first), 0);
433
+ next = (next + part.length) % capacity;
434
+ length = Math.min(capacity, length + part.length);
435
+ },
436
+ full: () => false,
437
+ bytes() {
438
+ if (length < capacity) return buffer.subarray(0, length);
439
+ const ordered = new Uint8Array(capacity);
440
+ ordered.set(buffer.subarray(next), 0);
441
+ ordered.set(buffer.subarray(0, next), capacity - next);
442
+ return ordered;
443
+ }
444
+ };
445
+ }
446
+ function leadingContinuation(window) {
447
+ let count = 0;
448
+ while (count < 3 && count < window.length && ((window[count] ?? 0) & 192) === 128) count += 1;
449
+ return count;
450
+ }
451
+ function trailingPartial(window) {
452
+ for (let back = 1; back <= Math.min(4, window.length); back += 1) {
453
+ const byte = window[window.length - back] ?? 0;
454
+ if ((byte & 192) === 128) continue;
455
+ const length = byte >= 240 ? 4 : byte >= 224 ? 3 : byte >= 192 ? 2 : 1;
456
+ return length > back ? back : 0;
457
+ }
458
+ return 0;
459
+ }
460
+ function decodeUtf8(bytes) {
461
+ return new TextDecoder("utf-8").decode(bytes);
462
+ }
463
+ function notAnswered(url, error) {
464
+ return 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)}`));
465
+ }
237
466
  function failureOf(url, response, raw, request) {
238
467
  const status = response.status;
239
468
  if (status === 404 && request.missing !== void 0) return new TrackerError(request.missing.failure, request.missing.message);
@@ -263,48 +492,1262 @@ function retryAfterOf(value, now) {
263
492
  const at = Date.parse(value);
264
493
  return Number.isFinite(at) ? Math.max(0, Math.ceil((at - now) / 1e3)) : void 0;
265
494
  }
266
- function resetDelay(reset, now) {
267
- return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
495
+ function resetDelay(reset, now) {
496
+ return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
497
+ }
498
+ function hostOf(url) {
499
+ try {
500
+ return new URL(url).host;
501
+ } catch {
502
+ return "\u0442\u0440\u0435\u043A\u0435\u0440";
503
+ }
504
+ }
505
+ function baseUrl(value, fallback) {
506
+ const raw = (value ?? fallback).replace(/\/+$/, "");
507
+ let parsed;
508
+ try {
509
+ parsed = new URL(raw);
510
+ } catch {
511
+ throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u2014 \u043D\u0435 URL.");
512
+ }
513
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C http \u0438\u043B\u0438 https.");
514
+ return raw;
515
+ }
516
+
517
+ // src/tracker/ci.schema.ts
518
+ import { z } from "zod";
519
+ var ciStatusCategorySchema = z.enum(["queued", "running", "waiting", "success", "failed", "canceled", "skipped"]);
520
+ var ciEventCategorySchema = z.enum(["push", "pull_request", "manual", "schedule", "retry", "trigger", "other"]);
521
+ var ciRunFilterSchema = z.enum(["status", "ref", "sha", "event", "workflow", "actor", "pullRequest", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"]);
522
+ var ciRunSortFieldSchema = z.enum(["created", "updated"]);
523
+ var ciJobFilterSchema = z.enum(["status", "allAttempts"]);
524
+ var ciStatusSchema = z.object({ category: ciStatusCategorySchema, name: z.string().min(1).max(100) });
525
+ var ciWorkflowSchema = z.object({
526
+ /** Ключ workflow: GitHub — id строкой; подходит в trigger.workflows и фильтр workflow. */
527
+ key: z.string().min(1).max(200),
528
+ name: z.string().min(1).max(500),
529
+ path: z.string().min(1).max(1e3).optional(),
530
+ enabled: z.boolean()
531
+ });
532
+ var ciRunSchema = z.object({
533
+ /** Ключ прогона: GitHub run id, GitLab pipeline id, SourceCraft slug-счётчик; непрозрачная строка. */
534
+ key: z.string().min(1).max(200),
535
+ /** Номер для людей: GitHub run_number, GitLab iid, SourceCraft — счётчик. */
536
+ number: z.number().int().nonnegative().optional(),
537
+ /** Workflow прогона: GitHub — один, SourceCraft — все в прогоне, GitLab — имя пайплайна, если задано. */
538
+ workflows: z.array(z.string().min(1).max(500)).max(100),
539
+ status: ciStatusSchema,
540
+ event: z.object({ category: ciEventCategorySchema, name: z.string().min(1).max(100) }).optional(),
541
+ /** Ветка или тег прогона. */
542
+ ref: z.string().min(1).max(500).optional(),
543
+ sha: z.string().regex(/^[0-9a-f]{7,64}$/).optional(),
544
+ /** Кто запустил — логин или slug трекера. */
545
+ actor: z.string().min(1).max(200).optional(),
546
+ /** Ключ PR того же трекера, для которого прогон. */
547
+ pullRequest: z.string().min(1).max(200).optional(),
548
+ /** Попытка прогона (GitHub run_attempt). */
549
+ attempt: z.number().int().positive().optional(),
550
+ /** Ошибки прогона от трекера (SourceCraft error_messages). */
551
+ errors: z.array(z.string().min(1).max(4e3)).max(20).optional(),
552
+ links: z.array(issueLinkSchema).max(10),
553
+ createdAt: timestampSchema.optional(),
554
+ startedAt: timestampSchema.optional(),
555
+ finishedAt: timestampSchema.optional(),
556
+ updatedAt: timestampSchema.optional()
557
+ });
558
+ var ciJobSchema = z.object({
559
+ /** Ключ задания: GitHub и GitLab — job id, SourceCraft — `<run>/<workflow>/<task>/<cube>`, части через encodeURIComponent. */
560
+ key: z.string().min(1).max(1e3),
561
+ runKey: z.string().min(1).max(200),
562
+ name: z.string().min(1).max(500),
563
+ /** Группа: GitLab — stage, SourceCraft — `<workflow>/<task>`; у GitHub нет. */
564
+ stage: z.string().min(1).max(1e3).optional(),
565
+ status: ciStatusSchema,
566
+ /** Шаги GitHub: где упало; у остальных нет. */
567
+ steps: z.array(z.object({ number: z.number().int().nonnegative(), name: z.string().min(1).max(500), status: ciStatusSchema })).max(1e3).optional(),
568
+ attempt: z.number().int().positive().optional(),
569
+ links: z.array(issueLinkSchema).max(10),
570
+ createdAt: timestampSchema.optional(),
571
+ startedAt: timestampSchema.optional(),
572
+ finishedAt: timestampSchema.optional()
573
+ });
574
+ var ciLogSchema = z.object({
575
+ /** Окно лога: очищено от ANSI (если не raw), секреты замаскированы. */
576
+ text: z.string().max(CI_LOG_MAX_BYTES),
577
+ /** Окно не покрывает весь лог от курсора: срезано спереди (tail), сзади (head) или упёрлись в потолок чтения. */
578
+ truncated: z.boolean(),
579
+ /** Задание закончилось — лог больше не растёт. */
580
+ complete: z.boolean(),
581
+ /** Курсор дочитывания: передайте в log() — придёт текст после этого места. Нет — дальше ничего не будет. */
582
+ nextCursor: z.string().min(1).max(200).optional(),
583
+ /** Размер лога в байтах, если тело прочитано целиком. */
584
+ size: z.number().int().nonnegative().optional()
585
+ });
586
+
587
+ // src/tracker/ci.lib.ts
588
+ function ciAdapterOf(definition, context) {
589
+ if (definition.ci === void 0) throw new TrackerError("unsupported", `\u0422\u0440\u0435\u043A\u0435\u0440 ${definition.descriptor.title} \u043D\u0435 \u0432\u0435\u0434\u0451\u0442 CI.`);
590
+ return definition.ci.create(context);
591
+ }
592
+ var LOG_CURSOR = /^c:(\d{1,15}):(\d{1,15})$/;
593
+ function logWindow(query) {
594
+ const bytes = query.maxBytes ?? CI_LOG_DEFAULT_BYTES;
595
+ if (!Number.isInteger(bytes) || bytes < 1 || bytes > CI_LOG_MAX_BYTES) {
596
+ throw new TrackerError("invalid_request", `\u041E\u043A\u043D\u043E \u043B\u043E\u0433\u0430 maxBytes \u2014 \u0446\u0435\u043B\u043E\u0435 \u043E\u0442 1 \u0434\u043E ${String(CI_LOG_MAX_BYTES)}.`);
597
+ }
598
+ const from = query.from ?? "tail";
599
+ if (from !== "head" && from !== "tail") throw new TrackerError("invalid_request", "\u041D\u0430\u0447\u0430\u043B\u043E \u043E\u043A\u043D\u0430 \u043B\u043E\u0433\u0430 from \u2014 head \u0438\u043B\u0438 tail.");
600
+ if (query.cursor === void 0) return { bytes, from };
601
+ const match = LOG_CURSOR.exec(query.cursor);
602
+ if (match === null) {
603
+ throw new TrackerError("invalid_request", "\u041A\u0443\u0440\u0441\u043E\u0440 \u043B\u043E\u0433\u0430 \u043D\u0435\u0432\u0435\u0440\u0435\u043D: \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 nextCursor \u043F\u0440\u043E\u0448\u043B\u043E\u0433\u043E \u043E\u0442\u0432\u0435\u0442\u0430 log() \u0438\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439\u0442\u0435 cursor.");
604
+ }
605
+ return { bytes, from: "head", resume: { page: Number(match[1]), offset: Number(match[2]) } };
606
+ }
607
+ function logCursor(page, offset) {
608
+ return `c:${String(page)}:${String(offset)}`;
609
+ }
610
+ function logOf(input) {
611
+ const stalled = input.text === "" && input.more;
612
+ const follow = !input.complete || input.more && !stalled;
613
+ const window = lineWindow(input.text, { start: input.startCut ?? (input.truncated && !input.more), end: follow, endBytes: input.end });
614
+ const clean = maskKeyTail(input.raw ? window.text : cleanLogText(window.text));
615
+ const text2 = clipText(redactSecretText(clean), CI_LOG_MAX_BYTES);
616
+ return {
617
+ text: text2,
618
+ truncated: input.truncated,
619
+ complete: input.complete,
620
+ ...follow ? { nextCursor: logCursor(input.page, input.end - window.held) } : {},
621
+ ...input.size === void 0 ? {} : { size: input.size }
622
+ };
623
+ }
624
+ var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
625
+ var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/;
626
+ var WHOLE_ANSI_SEQUENCE = /^(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[ -/]*(?![[\]])[0-~])/;
627
+ var VALUE_KEY = /(?:[A-Za-z][\w.-]{0,63}["']?\s*[=:]|\b(?:Bearer|Basic|Token))\s*["']?$/i;
628
+ var VALUE_KEY_LOOKBACK = 80;
629
+ function lineWindow(text2, cut) {
630
+ let to = text2.length;
631
+ if (cut.end) {
632
+ const line = text2.lastIndexOf("\n");
633
+ to = keyStart(text2, line >= 0 ? line + 1 : wordCut(text2, byteCut(text2)));
634
+ }
635
+ let held = to === text2.length ? 0 : utf8Length(text2.slice(to));
636
+ if (held > cut.endBytes) {
637
+ to = suffixStart(text2, cut.endBytes);
638
+ held = cut.endBytes;
639
+ }
640
+ let from = 0;
641
+ if (cut.start) {
642
+ const line = text2.indexOf("\n");
643
+ from = Math.min(line >= 0 ? line + 1 : firstWordEnd(text2), to);
644
+ }
645
+ return { text: text2.slice(from, to), held };
646
+ }
647
+ function byteCut(text2) {
648
+ let to = text2.length;
649
+ const escape = text2.lastIndexOf("\x1B");
650
+ if (escape > 0 && !WHOLE_ANSI_SEQUENCE.test(text2.slice(escape))) to = escape;
651
+ if (to > 1 && text2[to - 1] === "\r") to -= 1;
652
+ return to;
653
+ }
654
+ function wordCut(text2, to) {
655
+ let space = to - 1;
656
+ while (space >= 0 && !/\s/.test(text2.charAt(space))) space -= 1;
657
+ if (space < 0) return to;
658
+ const word = space + 1;
659
+ const before = text2.slice(Math.max(0, word - VALUE_KEY_LOOKBACK), word);
660
+ const key = VALUE_KEY.exec(before);
661
+ const keyed = key === null ? word : word - before.length + key.index;
662
+ return keyed > 0 ? keyed : word;
663
+ }
664
+ function firstWordEnd(text2) {
665
+ const space = /\s+/.exec(text2);
666
+ return space === null ? 0 : space.index + space[0].length;
667
+ }
668
+ function keyStart(text2, to) {
669
+ const head = text2.slice(0, to);
670
+ const begin = [...head.matchAll(KEY_BEGIN)].at(-1);
671
+ if (begin === void 0 || KEY_END.test(head.slice(begin.index))) return to;
672
+ const line = head.lastIndexOf("\n", begin.index) + 1;
673
+ return line > 0 ? line : begin.index > 0 ? begin.index : to;
674
+ }
675
+ function maskKeyTail(text2) {
676
+ const end = KEY_END.exec(text2);
677
+ if (end !== null && [...text2.slice(0, end.index).matchAll(KEY_BEGIN)].length === 0) return REDACTED + text2.slice(end.index + end[0].length);
678
+ let at = 0;
679
+ for (; ; ) {
680
+ const next = text2.indexOf("\n", at);
681
+ if (!isKeyBodyLine(text2.slice(at, next < 0 ? text2.length : next))) break;
682
+ at = next < 0 ? text2.length : next + 1;
683
+ if (next < 0) break;
684
+ }
685
+ if (at === 0) return text2;
686
+ return REDACTED + (text2[at - 1] === "\n" ? text2.slice(at - 1) : "");
687
+ }
688
+ function isKeyBodyLine(line) {
689
+ const body = line.endsWith("\r") ? line.slice(0, -1) : line;
690
+ return /^[A-Za-z0-9+/]{16,76}={0,2}$/.test(body) && /[A-Z]/.test(body) && /[a-z]/.test(body) && /\d/.test(body);
691
+ }
692
+ var utf8 = new TextEncoder();
693
+ function utf8Length(text2) {
694
+ return utf8.encode(text2).length;
695
+ }
696
+ function suffixStart(text2, bytes) {
697
+ let index = text2.length;
698
+ let count = 0;
699
+ while (index > 0 && count < bytes) {
700
+ const low = text2.charCodeAt(index - 1);
701
+ const pair = index > 1 && low >= 56320 && low <= 57343 && text2.charCodeAt(index - 2) >= 55296 && text2.charCodeAt(index - 2) <= 56319;
702
+ count += utf8Length(text2.slice(index - (pair ? 2 : 1), index));
703
+ index -= pair ? 2 : 1;
704
+ }
705
+ return index;
706
+ }
707
+ var ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)|\x1b[ -/]*[0-~]/g;
708
+ var GITLAB_SECTION_MARKER = /section_(?:start|end):\d+:[A-Za-z0-9_.-]+(?:\[[^\]\r\n]*\])?\r?/g;
709
+ function cleanLogText(text2) {
710
+ const plain = text2.replace(ANSI_SEQUENCE, "").replace(GITLAB_SECTION_MARKER, "").replace(/\r+\n/g, "\n");
711
+ if (!plain.includes("\r")) return plain;
712
+ return plain.split("\n").map((line) => line.includes("\r") ? line.split("\r").findLast((part) => part !== "") ?? "" : line).join("\n");
713
+ }
714
+ function isFinished(category) {
715
+ return CI_FINISHED_STATUSES.includes(category);
716
+ }
717
+
718
+ // src/tracker/github-ci.lib.ts
719
+ var DEFAULT_API = "https://api.github.com";
720
+ var TARGET = /^[\w.-]+\/[\w.-]+$/;
721
+ var ID = /^[1-9]\d{0,19}$/;
722
+ var SHA = /^[0-9a-f]{7,64}$/;
723
+ var DISPATCH_INPUTS_MAX = 25;
724
+ var RUN_FILTERS = ciRunFilterSchema.options;
725
+ var JOB_FILTERS = ciJobFilterSchema.options;
726
+ var RUNS_SUBJECT = "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitHub";
727
+ var JOBS_SUBJECT = "\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI GitHub";
728
+ var WORKFLOWS_SUBJECT = "\u0421\u043F\u0438\u0441\u043E\u043A workflow GitHub";
729
+ var TRIGGER_SUBJECT = "\u0417\u0430\u043F\u0443\u0441\u043A CI GitHub";
730
+ var CAPABILITIES = {
731
+ workflows: true,
732
+ runs: { filters: RUN_FILTERS, clientSide: ["status", "event", "updatedAfter", "updatedBefore"], sorts: [] },
733
+ jobs: { filters: JOB_FILTERS, clientSide: ["status"], sorts: [] },
734
+ log: { live: false, cursor: true },
735
+ trigger: { supported: true, workflows: "one", inputs: true, tags: true },
736
+ rerun: { all: true, failedOnly: true, job: true, debug: true },
737
+ cancel: { run: true, job: false }
738
+ };
739
+ function githubCi() {
740
+ return { capabilities: CAPABILITIES, create: (context) => createGithubCiAdapter(context) };
741
+ }
742
+ function createGithubCiAdapter({ connection, fetch }) {
743
+ const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
744
+ if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
745
+ const api = baseUrl(connection.url, DEFAULT_API);
746
+ const root = `${api}/repos/${target}`;
747
+ const actions = `${root}/actions`;
748
+ const headers = {
749
+ accept: "application/vnd.github+json",
750
+ "x-github-api-version": "2022-11-28",
751
+ ...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
752
+ };
753
+ const runKey = (key) => {
754
+ if (!ID.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
755
+ return key;
756
+ };
757
+ const jobKey = (key) => {
758
+ if (!ID.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
759
+ return key;
760
+ };
761
+ const missingRun = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
762
+ const missingJob = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
763
+ const getRun = async (key) => runOf((await requestJson(`${actions}/runs/${runKey(key)}`, { fetch, headers, missing: missingRun(key) })).body);
764
+ const headShaOf = async (query) => {
765
+ if (query.pullRequest === void 0) return query.sha;
766
+ if (query.sha !== void 0) throw new TrackerError("invalid_request", `${RUNS_SUBJECT}: sha \u0438 pullRequest \u0437\u0430\u0434\u0430\u044E\u0442\u0441\u044F \u043F\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u0438 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u044B PR \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u043F\u043E \u0435\u0433\u043E \u0433\u043E\u043B\u043E\u0432\u0435.`);
767
+ const pull = query.pullRequest;
768
+ if (!ID.test(pull)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 pull request ${pull}.`);
769
+ const { body } = await requestJson(`${root}/pulls/${pull}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 pull request ${pull}.` } });
770
+ const head = isRecord(body) && isRecord(body.head) ? text(body.head.sha) : void 0;
771
+ if (head === void 0) throw new TrackerError("failed", "GitHub \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0433\u043E\u043B\u043E\u0432\u0443 pull request.");
772
+ return head;
773
+ };
774
+ return {
775
+ listWorkflows: async (query) => {
776
+ assertListQuery(WORKFLOWS_SUBJECT, { filters: [], clientSide: [], sorts: [] }, query, []);
777
+ const page = pageOf(query.cursor);
778
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
779
+ const { body, headers: responseHeaders } = await requestJson(`${actions}/workflows?${params.toString()}`, { fetch, headers });
780
+ return { items: rowsOf(body, "workflows").map(workflowOf), ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
781
+ },
782
+ listRuns: async (query) => {
783
+ assertListQuery(RUNS_SUBJECT, CAPABILITIES.runs, query, RUN_FILTERS);
784
+ const page = pageOf(query.cursor);
785
+ const sha = await headShaOf(query);
786
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
787
+ if (query.ref !== void 0) params.set("branch", query.ref);
788
+ if (sha !== void 0) params.set("head_sha", sha);
789
+ if (query.actor !== void 0) params.set("actor", query.actor);
790
+ const status = statusParam(query.status);
791
+ if (status !== void 0) params.set("status", status);
792
+ const event = eventParam(query.event);
793
+ if (event !== void 0) params.set("event", event);
794
+ const created = createdParam(query);
795
+ if (created !== void 0) params.set("created", created);
796
+ const path = query.workflow === void 0 ? `${actions}/runs` : `${actions}/workflows/${encodeURIComponent(query.workflow)}/runs`;
797
+ const { body, headers: responseHeaders } = await requestJson(`${path}?${params.toString()}`, { fetch, headers });
798
+ const items = rowsOf(body, "workflow_runs").map(runOf).filter(
799
+ (run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore)
800
+ );
801
+ return { items, ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
802
+ },
803
+ getRun,
804
+ listJobs: async (key, query) => {
805
+ const run = runKey(key);
806
+ const { allAttempts, ...rest } = query;
807
+ assertListQuery(JOBS_SUBJECT, CAPABILITIES.jobs, rest, JOB_FILTERS);
808
+ const page = pageOf(query.cursor);
809
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(page), filter: allAttempts === true ? "all" : "latest" });
810
+ const { body, headers: responseHeaders } = await requestJson(`${actions}/runs/${run}/jobs?${params.toString()}`, { fetch, headers, missing: missingRun(run) });
811
+ const items = rowsOf(body, "jobs").map(jobOf).filter((job) => query.status === void 0 || query.status.includes(job.status.category));
812
+ return { items, ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
813
+ },
814
+ getJob: async (key) => {
815
+ const id = jobKey(key);
816
+ return jobOf((await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body);
817
+ },
818
+ log: async (key, query) => {
819
+ const id = jobKey(key);
820
+ const window = logWindow(query);
821
+ const job = (await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body;
822
+ const complete = isFinished(statusOf(isRecord(job) ? job : {}).category);
823
+ if (!complete) return { text: "", truncated: false, complete: false, nextCursor: logCursor(0, 0) };
824
+ const response = await requestText(`${actions}/jobs/${id}/logs`, {
825
+ fetch,
826
+ headers,
827
+ skip: window.resume?.offset ?? 0,
828
+ keep: { bytes: window.bytes, from: window.from },
829
+ missing: missingJob(id)
830
+ });
831
+ return logOf({
832
+ text: response.text,
833
+ truncated: window.from === "tail" ? response.start > 0 : !response.whole,
834
+ complete,
835
+ page: 0,
836
+ end: response.end,
837
+ // Курсор дальше имеет смысл только когда окно head оборвалось раньше конца тела: tail уже читает конец.
838
+ more: window.from === "head" && !response.whole,
839
+ ...response.whole ? { size: response.read } : {},
840
+ raw: query.raw ?? false
841
+ });
842
+ },
843
+ trigger: async (input) => {
844
+ const workflows = input.workflows ?? [];
845
+ const workflow = workflows[0];
846
+ if (workflows.length !== 1 || workflow === void 0) {
847
+ throw new TrackerError("invalid_request", `${TRIGGER_SUBJECT}: \u043D\u0443\u0436\u0435\u043D \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u0438\u043D workflow \u2014 id \u0438\u043B\u0438 \u0438\u043C\u044F \u0444\u0430\u0439\u043B\u0430.`);
848
+ }
849
+ const inputs = input.inputs ?? {};
850
+ const count = Object.keys(inputs).length;
851
+ if (count > DISPATCH_INPUTS_MAX) throw new TrackerError("invalid_request", `${TRIGGER_SUBJECT}: workflow_dispatch \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${String(DISPATCH_INPUTS_MAX)} \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432.`);
852
+ const { body } = await requestJson(`${actions}/workflows/${encodeURIComponent(workflow)}/dispatches`, {
853
+ fetch,
854
+ headers,
855
+ method: "POST",
856
+ body: { ref: input.ref, ...count === 0 ? {} : { inputs }, return_run_details: true }
857
+ });
858
+ const run = runIdOf(isRecord(body) ? body.workflow_run_id : void 0);
859
+ return run === void 0 ? {} : { run: await getRun(run) };
860
+ },
861
+ rerun: async (key, input) => {
862
+ const run = runKey(key);
863
+ await requestJson(`${actions}/runs/${run}/${input.failedOnly === true ? "rerun-failed-jobs" : "rerun"}`, {
864
+ fetch,
865
+ headers,
866
+ method: "POST",
867
+ body: { enable_debug_logging: input.debug === true },
868
+ missing: missingRun(run)
869
+ });
870
+ return getRun(run);
871
+ },
872
+ rerunJob: async (key, input) => {
873
+ const id = jobKey(key);
874
+ const job = (await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body;
875
+ const run = runIdOf(isRecord(job) ? job.run_id : void 0);
876
+ if (run === void 0) throw new TrackerError("failed", "GitHub \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0434\u0430\u043D\u0438\u044F.");
877
+ await requestJson(`${actions}/jobs/${id}/rerun`, { fetch, headers, method: "POST", body: { enable_debug_logging: input.debug === true }, missing: missingJob(id) });
878
+ return getRun(run);
879
+ },
880
+ cancel: async (key) => {
881
+ const run = runKey(key);
882
+ await requestJson(`${actions}/runs/${run}/cancel`, { fetch, headers, method: "POST", conflictOn: [409], missing: missingRun(run) });
883
+ return getRun(run);
884
+ },
885
+ cancelJob: async () => {
886
+ throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 CI GitHub: \u0437\u0430\u0434\u0430\u043D\u0438\u044F \u043F\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u0438 \u043D\u0435 \u043E\u0442\u043C\u0435\u043D\u044F\u044E\u0442\u0441\u044F, \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D.");
887
+ }
888
+ };
889
+ }
890
+ function pageOf(cursor) {
891
+ const page = cursor === void 0 ? 1 : Number(cursor);
892
+ return Number.isInteger(page) && page >= 1 ? page : 1;
893
+ }
894
+ function nextCursorOf(page, link) {
895
+ return /rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {};
896
+ }
897
+ function rowsOf(body, field) {
898
+ const rows = isRecord(body) ? body[field] : void 0;
899
+ return Array.isArray(rows) ? rows : [];
900
+ }
901
+ function keyOf(value) {
902
+ if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
903
+ return typeof value === "string" && value !== "" ? value : "?";
904
+ }
905
+ function positiveInt(value) {
906
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
907
+ }
908
+ function runIdOf(value) {
909
+ const id = positiveInt(value);
910
+ return id === void 0 ? void 0 : String(id);
911
+ }
912
+ function statusOf(item) {
913
+ const raw = text(item.status) ?? "unknown";
914
+ if (raw !== "completed") return { category: lookup(GITHUB_RUN_STATUSES, raw, "running"), name: raw };
915
+ const conclusion = text(item.conclusion);
916
+ return { category: lookup(GITHUB_RUN_CONCLUSIONS, conclusion, "failed"), name: conclusion ?? raw };
917
+ }
918
+ function statusParam(status) {
919
+ if (status === void 0 || status.length === 0) return void 0;
920
+ if (status.every(isFinished)) return "completed";
921
+ return status.length === 1 && status[0] === "running" ? "in_progress" : void 0;
922
+ }
923
+ function eventParam(event) {
924
+ if (event === void 0 || event.length !== 1) return void 0;
925
+ const raws = keysFor(GITHUB_RUN_EVENTS, event);
926
+ return raws.length === 1 ? raws[0] : void 0;
927
+ }
928
+ function createdParam(query) {
929
+ const { createdAfter: after, createdBefore: before } = query;
930
+ if (after !== void 0 && before !== void 0) return `${isoTime(after)}..${isoTime(before)}`;
931
+ if (after !== void 0) return `>=${isoTime(after)}`;
932
+ return before !== void 0 ? `<=${isoTime(before)}` : void 0;
933
+ }
934
+ function workflowOf(raw) {
935
+ const item = isRecord(raw) ? raw : {};
936
+ const key = keyOf(item.id);
937
+ const path = text(item.path);
938
+ return { key, name: text(item.name) ?? key, ...path === void 0 ? {} : { path }, enabled: item.state === "active" };
939
+ }
940
+ function runOf(raw) {
941
+ const item = isRecord(raw) ? raw : {};
942
+ const key = keyOf(item.id);
943
+ const status = statusOf(item);
944
+ const name = text(item.name) ?? text(item.path);
945
+ const branch = text(item.head_branch);
946
+ const sha = text(item.head_sha);
947
+ const actor = isRecord(item.actor) ? text(item.actor.login) : void 0;
948
+ const pull = Array.isArray(item.pull_requests) ? item.pull_requests[0] : void 0;
949
+ const pullNumber = isRecord(pull) && typeof pull.number === "number" ? String(pull.number) : void 0;
950
+ const number = positiveInt(item.run_number);
951
+ const attempt = positiveInt(item.run_attempt);
952
+ const url = text(item.html_url);
953
+ const createdAt = toTimestamp(item.created_at);
954
+ const startedAt = toTimestamp(item.run_started_at);
955
+ const updatedAt = toTimestamp(item.updated_at);
956
+ const finishedAt = text(item.status) === "completed" ? updatedAt : void 0;
957
+ const event = text(item.event);
958
+ return {
959
+ key,
960
+ ...number === void 0 ? {} : { number },
961
+ workflows: name === void 0 ? [] : [name],
962
+ status,
963
+ ...event === void 0 ? {} : { event: { category: lookup(GITHUB_RUN_EVENTS, event, "other"), name: event } },
964
+ ...branch === void 0 ? {} : { ref: branch },
965
+ ...sha === void 0 || !SHA.test(sha) ? {} : { sha },
966
+ ...actor === void 0 ? {} : { actor },
967
+ ...pullNumber === void 0 ? {} : { pullRequest: pullNumber },
968
+ ...attempt === void 0 ? {} : { attempt },
969
+ links: url === void 0 ? [] : [{ url, title: "GitHub Actions" }],
970
+ ...createdAt === void 0 ? {} : { createdAt },
971
+ ...startedAt === void 0 ? {} : { startedAt },
972
+ ...finishedAt === void 0 ? {} : { finishedAt },
973
+ ...updatedAt === void 0 ? {} : { updatedAt }
974
+ };
975
+ }
976
+ function jobOf(raw) {
977
+ const item = isRecord(raw) ? raw : {};
978
+ const key = keyOf(item.id);
979
+ const url = text(item.html_url);
980
+ const startedAt = toTimestamp(item.started_at);
981
+ const finishedAt = toTimestamp(item.completed_at);
982
+ const steps = Array.isArray(item.steps) ? item.steps.map(stepOf) : void 0;
983
+ return {
984
+ key,
985
+ runKey: runIdOf(item.run_id) ?? "?",
986
+ name: text(item.name) ?? key,
987
+ status: statusOf(item),
988
+ ...steps === void 0 ? {} : { steps },
989
+ links: url === void 0 ? [] : [{ url, title: "GitHub Actions" }],
990
+ ...startedAt === void 0 ? {} : { startedAt },
991
+ ...finishedAt === void 0 ? {} : { finishedAt }
992
+ };
993
+ }
994
+ function stepOf(raw) {
995
+ const item = isRecord(raw) ? raw : {};
996
+ const number = positiveInt(item.number) ?? 0;
997
+ return { number, name: text(item.name) ?? String(number), status: statusOf(item) };
998
+ }
999
+
1000
+ // src/tracker/gitlab-ci.lib.ts
1001
+ var DEFAULT_URL = "https://gitlab.com";
1002
+ var TARGET2 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
1003
+ var ID2 = /^[1-9]\d{0,19}$/;
1004
+ var RUN_FILTERS2 = ciRunFilterSchema.options;
1005
+ var JOB_FILTERS2 = ciJobFilterSchema.options;
1006
+ var CAPABILITIES2 = {
1007
+ workflows: false,
1008
+ runs: { filters: RUN_FILTERS2, clientSide: ["status", "event"], sorts: ["created", "updated"] },
1009
+ jobs: { filters: JOB_FILTERS2, clientSide: [], sorts: [] },
1010
+ log: { live: true, cursor: true },
1011
+ trigger: { supported: true, workflows: "none", inputs: true, tags: true },
1012
+ rerun: { all: false, failedOnly: true, job: true, debug: false },
1013
+ cancel: { run: true, job: true }
1014
+ };
1015
+ function gitlabCi() {
1016
+ return { capabilities: CAPABILITIES2, create: (context) => createGitlabCiAdapter(context) };
1017
+ }
1018
+ function createGitlabCiAdapter({ connection, fetch }) {
1019
+ 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");
1020
+ 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.");
1021
+ const api = `${baseUrl(connection.url, DEFAULT_URL)}/api/v4`;
1022
+ const project = `${api}/projects/${encodeURIComponent(target)}`;
1023
+ const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
1024
+ const rawOf = (table, categories) => {
1025
+ if (categories === void 0 || categories.length !== 1) return void 0;
1026
+ const raws = keysFor(table, categories);
1027
+ return raws.length === 1 ? raws[0] : void 0;
1028
+ };
1029
+ const pipelineKey = (key) => {
1030
+ if (!ID2.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
1031
+ return key;
1032
+ };
1033
+ const jobKey = (key) => {
1034
+ if (!ID2.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
1035
+ return key;
1036
+ };
1037
+ const missingPipeline = (key) => ({ failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
1038
+ const missingJob = (key) => ({ failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
1039
+ const getPipeline = async (key) => runOf2((await requestJson(`${project}/pipelines/${pipelineKey(key)}`, { fetch, headers, missing: missingPipeline(key) })).body);
1040
+ return {
1041
+ listWorkflows: async () => {
1042
+ throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab: \u0441\u043F\u0438\u0441\u043A\u0430 workflow \u0443 GitLab \u043D\u0435\u0442.");
1043
+ },
1044
+ listRuns: async (query) => {
1045
+ assertListQuery("\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab", CAPABILITIES2.runs, query, RUN_FILTERS2);
1046
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(pageOf2(query.cursor)) });
1047
+ let body;
1048
+ let responseHeaders;
1049
+ let keep;
1050
+ if (query.pullRequest !== void 0) {
1051
+ const mr = query.pullRequest;
1052
+ if (!ID2.test(mr)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 merge request ${mr}.`);
1053
+ if (query.sort !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
1054
+ if (query.actor !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0444\u0438\u043B\u044C\u0442\u0440 actor \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
1055
+ if (query.workflow !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0444\u0438\u043B\u044C\u0442\u0440 workflow \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
1056
+ const result = await requestJson(`${project}/merge_requests/${mr}/pipelines?${params.toString()}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 merge request ${mr}.` } });
1057
+ body = result.body;
1058
+ responseHeaders = result.headers;
1059
+ keep = (run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && (query.ref === void 0 || run.ref === query.ref) && (query.sha === void 0 || run.sha === query.sha) && inTimeRange(run.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore);
1060
+ } else {
1061
+ if (query.ref !== void 0) params.set("ref", query.ref);
1062
+ if (query.sha !== void 0) params.set("sha", query.sha);
1063
+ if (query.actor !== void 0) params.set("username", query.actor);
1064
+ if (query.workflow !== void 0) params.set("name", query.workflow);
1065
+ if (query.status !== void 0 && query.status.length > 0) {
1066
+ const status = rawOf(GITLAB_CI_STATUSES, query.status);
1067
+ if (status !== void 0) params.set("status", status);
1068
+ if (status === void 0 && query.status.every(isFinished)) params.set("scope", "finished");
1069
+ }
1070
+ const source = rawOf(GITLAB_PIPELINE_SOURCES, query.event);
1071
+ if (source !== void 0) params.set("source", source);
1072
+ if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
1073
+ if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
1074
+ if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
1075
+ if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
1076
+ params.set("order_by", query.sort?.field === "updated" ? "updated_at" : "id");
1077
+ params.set("sort", query.sort?.direction ?? "desc");
1078
+ const result = await requestJson(`${project}/pipelines?${params.toString()}`, { fetch, headers });
1079
+ body = result.body;
1080
+ responseHeaders = result.headers;
1081
+ keep = (run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category));
1082
+ }
1083
+ const items = (Array.isArray(body) ? body : []).map(runOf2).filter(keep);
1084
+ const next = responseHeaders.get("x-next-page");
1085
+ return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
1086
+ },
1087
+ getRun: getPipeline,
1088
+ listJobs: async (runKey, query) => {
1089
+ const { allAttempts, ...rest } = query;
1090
+ assertListQuery("\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI GitLab", CAPABILITIES2.jobs, rest, JOB_FILTERS2);
1091
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(pageOf2(query.cursor)), include_retried: String(allAttempts ?? false) });
1092
+ for (const scope of keysFor(GITLAB_CI_STATUSES, query.status ?? [])) params.append("scope[]", scope);
1093
+ const { body, headers: responseHeaders } = await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/jobs?${params.toString()}`, { fetch, headers, missing: missingPipeline(runKey) });
1094
+ const items = (Array.isArray(body) ? body : []).map(jobOf2);
1095
+ const next = responseHeaders.get("x-next-page");
1096
+ return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
1097
+ },
1098
+ getJob: async (key) => jobOf2((await requestJson(`${project}/jobs/${jobKey(key)}`, { fetch, headers, missing: missingJob(key) })).body),
1099
+ log: async (key, query) => {
1100
+ const window = logWindow(query);
1101
+ const job = (await requestJson(`${project}/jobs/${jobKey(key)}`, { fetch, headers, missing: missingJob(key) })).body;
1102
+ const rawStatus = isRecord(job) && typeof job.status === "string" ? job.status : "unknown";
1103
+ const complete = isFinished(lookup(GITLAB_CI_STATUSES, rawStatus, "running"));
1104
+ const response = await requestText(`${project}/jobs/${jobKey(key)}/trace`, { fetch, headers, skip: window.resume?.offset ?? 0, keep: { bytes: window.bytes, from: window.from }, missing: missingJob(key) });
1105
+ return logOf({
1106
+ text: response.text,
1107
+ truncated: window.from === "tail" ? response.start > 0 : !response.whole,
1108
+ complete,
1109
+ page: 0,
1110
+ end: response.end,
1111
+ more: !response.whole,
1112
+ ...response.whole ? { size: response.read } : {},
1113
+ // Кольцевой буфер отрезал начало хвоста и при потолке чтения (whole=false): там default
1114
+ // startCut (truncated && !more) молчит, а первая неполная строка в окне остаётся.
1115
+ startCut: window.from === "tail" && response.start > 0,
1116
+ raw: query.raw ?? false
1117
+ });
1118
+ },
1119
+ trigger: async (input) => {
1120
+ if (input.workflows !== void 0 && input.workflows.length > 0) {
1121
+ throw new TrackerError("unsupported", "\u0417\u0430\u043F\u0443\u0441\u043A CI GitLab: workflow \u043D\u0435 \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F \u2014 \u0442\u043E\u043B\u044C\u043A\u043E ref \u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435.");
1122
+ }
1123
+ const variables = Object.entries(input.inputs ?? {}).map(([key, value]) => ({ key, value, variable_type: "env_var" }));
1124
+ const { body } = await requestJson(`${project}/pipeline`, {
1125
+ fetch,
1126
+ headers,
1127
+ method: "POST",
1128
+ body: { ref: input.ref, ...variables.length === 0 ? {} : { variables } },
1129
+ missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u0432\u0435\u0442\u043A\u0438 \u0438\u043B\u0438 \u0442\u0435\u0433\u0430 ${input.ref}.` }
1130
+ });
1131
+ return { run: runOf2(body) };
1132
+ },
1133
+ rerun: async (runKey, input) => {
1134
+ if (input.failedOnly !== true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A CI GitLab \u0443\u043C\u0435\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0443\u043F\u0430\u0432\u0448\u0438\u0435 \u0437\u0430\u0434\u0430\u043D\u0438\u044F: \u0437\u0430\u0434\u0430\u0439\u0442\u0435 failedOnly.");
1135
+ if (input.debug === true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A CI GitLab \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043E\u0442\u043B\u0430\u0434\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435.");
1136
+ return runOf2((await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/retry`, { fetch, headers, method: "POST", missing: missingPipeline(runKey) })).body);
1137
+ },
1138
+ rerunJob: async (jobKeyValue, input) => {
1139
+ if (input.debug === true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0437\u0430\u0434\u0430\u043D\u0438\u044F GitLab \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043E\u0442\u043B\u0430\u0434\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435.");
1140
+ const body = (await requestJson(`${project}/jobs/${jobKey(jobKeyValue)}/retry`, { fetch, headers, method: "POST", missing: missingJob(jobKeyValue) })).body;
1141
+ const pipeline = isRecord(body) && isRecord(body.pipeline) && typeof body.pipeline.id === "number" ? String(body.pipeline.id) : void 0;
1142
+ if (pipeline === void 0) throw new TrackerError("failed", "GitLab \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u043F\u0430\u0439\u043F\u043B\u0430\u0439\u043D \u043D\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u0434\u0430\u043D\u0438\u044F.");
1143
+ return getPipeline(pipeline);
1144
+ },
1145
+ cancel: async (runKey) => runOf2((await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/cancel`, { fetch, headers, method: "POST", missing: missingPipeline(runKey) })).body),
1146
+ cancelJob: async (jobKeyValue) => jobOf2((await requestJson(`${project}/jobs/${jobKey(jobKeyValue)}/cancel`, { fetch, headers, method: "POST", missing: missingJob(jobKeyValue) })).body)
1147
+ };
1148
+ }
1149
+ function pageOf2(cursor) {
1150
+ const page = cursor === void 0 ? 1 : Number(cursor);
1151
+ return Number.isInteger(page) && page >= 1 ? page : 1;
1152
+ }
1153
+ function runOf2(raw) {
1154
+ const item = isRecord(raw) ? raw : {};
1155
+ const key = typeof item.id === "number" ? String(item.id) : "?";
1156
+ const rawStatus = typeof item.status === "string" ? item.status : "unknown";
1157
+ const rawSource = text(item.source);
1158
+ const mr = typeof item.ref === "string" ? /^refs\/merge-requests\/(\d+)\/head$/.exec(item.ref) : null;
1159
+ const user = isRecord(item.user) ? text(item.user.username) : void 0;
1160
+ const url = text(item.web_url);
1161
+ const name = text(item.name);
1162
+ const createdAt = toTimestamp(item.created_at);
1163
+ const startedAt = toTimestamp(item.started_at);
1164
+ const finishedAt = toTimestamp(item.finished_at);
1165
+ const updatedAt = toTimestamp(item.updated_at);
1166
+ return {
1167
+ key,
1168
+ ...typeof item.iid === "number" ? { number: item.iid } : {},
1169
+ workflows: name === void 0 ? [] : [name],
1170
+ status: { category: lookup(GITLAB_CI_STATUSES, rawStatus, "running"), name: rawStatus },
1171
+ ...rawSource === void 0 ? {} : { event: { category: lookup(GITLAB_PIPELINE_SOURCES, rawSource, "other"), name: rawSource } },
1172
+ ...typeof item.ref === "string" && item.ref !== "" ? { ref: item.ref } : {},
1173
+ ...typeof item.sha === "string" && /^[0-9a-f]{7,64}$/.test(item.sha) ? { sha: item.sha } : {},
1174
+ ...user === void 0 ? {} : { actor: user },
1175
+ ...mr === null ? {} : { pullRequest: mr[1] },
1176
+ links: url === void 0 ? [] : [{ url, title: "GitLab" }],
1177
+ ...createdAt === void 0 ? {} : { createdAt },
1178
+ ...startedAt === void 0 ? {} : { startedAt },
1179
+ ...finishedAt === void 0 ? {} : { finishedAt },
1180
+ ...updatedAt === void 0 ? {} : { updatedAt }
1181
+ };
1182
+ }
1183
+ function jobOf2(raw) {
1184
+ const item = isRecord(raw) ? raw : {};
1185
+ const key = typeof item.id === "number" ? String(item.id) : "?";
1186
+ const rawStatus = typeof item.status === "string" ? item.status : "unknown";
1187
+ const pipeline = isRecord(item.pipeline) && typeof item.pipeline.id === "number" ? String(item.pipeline.id) : "?";
1188
+ const url = text(item.web_url);
1189
+ const stage = text(item.stage);
1190
+ const startedAt = toTimestamp(item.started_at);
1191
+ const finishedAt = toTimestamp(item.finished_at);
1192
+ return {
1193
+ key,
1194
+ runKey: pipeline,
1195
+ name: text(item.name) ?? key,
1196
+ ...stage === void 0 ? {} : { stage },
1197
+ status: { category: lookup(GITLAB_CI_STATUSES, rawStatus, "running"), name: rawStatus },
1198
+ links: url === void 0 ? [] : [{ url, title: "GitLab" }],
1199
+ ...startedAt === void 0 ? {} : { startedAt },
1200
+ ...finishedAt === void 0 ? {} : { finishedAt }
1201
+ };
1202
+ }
1203
+
1204
+ // src/tracker/sourcecraft-ci.lib.ts
1205
+ var DEFAULT_API2 = "https://api.sourcecraft.tech";
1206
+ var TARGET3 = /^[\w.-]+\/[\w.-]+$/;
1207
+ var RUN_SLUG = /^\d{1,20}$/;
1208
+ var PR_SLUG = /^[\w-]{1,100}$/;
1209
+ var JOB_KEY = /^([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$/;
1210
+ var EXTERNAL_USER = "<external-user>";
1211
+ var JOB_OFFSET_CURSOR = /^o:(\d{1,15})$/;
1212
+ var RUN_FILTER_NAMES = ciRunFilterSchema.options;
1213
+ var RUN_FILTERS3 = RUN_FILTER_NAMES.filter((name) => name !== "ref" && name !== "sha");
1214
+ var JOB_FILTER_NAMES = ciJobFilterSchema.options;
1215
+ var JOB_FILTERS3 = ["status"];
1216
+ var CAPABILITIES3 = {
1217
+ workflows: false,
1218
+ runs: { filters: RUN_FILTERS3, clientSide: RUN_FILTERS3, sorts: [] },
1219
+ jobs: { filters: JOB_FILTERS3, clientSide: JOB_FILTERS3, sorts: [] },
1220
+ log: { live: true, cursor: true },
1221
+ trigger: { supported: true, workflows: "many", inputs: true, tags: true },
1222
+ rerun: { all: false, failedOnly: false, job: false, debug: false },
1223
+ cancel: { run: false, job: false }
1224
+ };
1225
+ function sourcecraftCi() {
1226
+ return { capabilities: CAPABILITIES3, create: (context) => createSourceCraftCiAdapter(context) };
1227
+ }
1228
+ function createSourceCraftCiAdapter({ connection, fetch }) {
1229
+ const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
1230
+ if (!TARGET3.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
1231
+ const repo = `${baseUrl(connection.url, DEFAULT_API2)}/repos/${target}`;
1232
+ const root = `${repo}/cicd`;
1233
+ const headers = { authorization: `Bearer ${connection.token ?? ""}` };
1234
+ const missingRun = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
1235
+ const missingJob = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
1236
+ const missingWorkflow = (run, workflow) => ({ failure: "not_found", message: `\u0412 \u043F\u0440\u043E\u0433\u043E\u043D\u0435 ${run} SourceCraft \u043D\u0435\u0442 workflow ${workflow}.` });
1237
+ const runKey = (key) => {
1238
+ if (!RUN_SLUG.test(key)) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
1239
+ return key;
1240
+ };
1241
+ const jobPath = (key) => {
1242
+ const match = JOB_KEY.exec(key);
1243
+ if (match === null) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
1244
+ const path = { run: partOf(match[1], key), workflow: partOf(match[2], key), task: partOf(match[3], key), cube: partOf(match[4], key) };
1245
+ runKey(path.run);
1246
+ return path;
1247
+ };
1248
+ const getRun = async (key) => runOf3((await requestJson(`${root}/runs/${runKey(key)}`, { fetch, headers, missing: missingRun(key) })).body);
1249
+ const getWorkflow = async (run, workflow) => {
1250
+ const { body } = await requestJson(`${root}/runs/${runKey(run)}/${encodeURIComponent(workflow)}`, { fetch, headers, missing: missingWorkflow(run, workflow) });
1251
+ return isRecord(body) ? body : {};
1252
+ };
1253
+ const pullIdOf = async (slug) => {
1254
+ if (!PR_SLUG.test(slug)) throw new TrackerError("invalid_request", `\u041A\u043B\u044E\u0447 pull request SourceCraft \u2014 slug \u0438\u0437 \u0431\u0443\u043A\u0432, \u0446\u0438\u0444\u0440, _ \u0438 - \u0434\u043B\u0438\u043D\u043E\u0439 \u0434\u043E 100.`);
1255
+ const { body } = await requestJson(`${repo}/pulls/${slug}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 pull request ${slug}.` } });
1256
+ const id = isRecord(body) ? text(body.id) : void 0;
1257
+ if (id === void 0) throw new TrackerError("failed", `\u041E\u0442\u0432\u0435\u0442 SourceCraft \u043D\u0430 /pulls/${slug} \u043D\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 id.`);
1258
+ return id;
1259
+ };
1260
+ const flattenJobs = async (run) => {
1261
+ const jobs = [];
1262
+ for (const workflow of (await getRun(run)).workflows) {
1263
+ const tree = await getWorkflow(run, workflow);
1264
+ for (const task of recordList(tree.tasks)) {
1265
+ const taskSlug = text(task.slug);
1266
+ if (taskSlug === void 0) continue;
1267
+ for (const cube of recordList(task.cubes)) {
1268
+ const cubeSlug = text(cube.slug);
1269
+ if (cubeSlug === void 0) continue;
1270
+ jobs.push(jobOf3(run, workflow, taskSlug, cubeSlug, cube));
1271
+ }
1272
+ }
1273
+ }
1274
+ return jobs;
1275
+ };
1276
+ return {
1277
+ listWorkflows: async () => {
1278
+ throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI SourceCraft: \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u0441\u043F\u0438\u0441\u043A\u0430 workflow \u043D\u0435\u0442, workflow \u0435\u0441\u0442\u044C \u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.");
1279
+ },
1280
+ listRuns: async (query) => {
1281
+ assertListQuery("\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI SourceCraft", CAPABILITIES3.runs, query, RUN_FILTER_NAMES);
1282
+ const pullId = query.pullRequest === void 0 ? void 0 : await pullIdOf(query.pullRequest);
1283
+ const params = new URLSearchParams({ page_size: String(query.limit) });
1284
+ if (query.cursor !== void 0) params.set("page_token", query.cursor);
1285
+ const { body } = await requestJson(`${root}/runs?${params.toString()}`, { fetch, headers });
1286
+ const record = isRecord(body) ? body : {};
1287
+ const items = recordList(record.runs).map((raw) => pullSlugOf(runOf3(raw), raw, pullId, query.pullRequest)).filter((run) => keepsRun(run, query));
1288
+ const token = text(record.next_page_token);
1289
+ return { items, ...token === void 0 ? {} : { nextCursor: token } };
1290
+ },
1291
+ getRun,
1292
+ listJobs: async (runKeyValue, query) => {
1293
+ assertListQuery("\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft", CAPABILITIES3.jobs, query, JOB_FILTER_NAMES);
1294
+ const jobs = await flattenJobs(runKey(runKeyValue));
1295
+ const from = offsetOf(query.cursor);
1296
+ const next = from + query.limit;
1297
+ const page = jobs.slice(from, next);
1298
+ const items = query.status === void 0 ? page : page.filter((job) => query.status?.includes(job.status.category) === true);
1299
+ return { items, ...next < jobs.length ? { nextCursor: `o:${String(next)}` } : {} };
1300
+ },
1301
+ getJob: async (key) => {
1302
+ const path = jobPath(key);
1303
+ const tree = await getWorkflow(path.run, path.workflow);
1304
+ for (const task of recordList(tree.tasks)) {
1305
+ if (text(task.slug) !== path.task) continue;
1306
+ const cube = recordList(task.cubes).find((item) => text(item.slug) === path.cube);
1307
+ if (cube !== void 0) return jobOf3(path.run, path.workflow, path.task, path.cube, cube);
1308
+ }
1309
+ throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
1310
+ },
1311
+ log: async (key, query) => {
1312
+ const parsed = jobPath(key);
1313
+ if (parsed.cube.startsWith("#")) {
1314
+ throw new TrackerError("unsupported", `\u041B\u043E\u0433 \u0437\u0430\u0434\u0430\u043D\u0438\u044F SourceCraft ${key}: \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 \u043A\u0443\u0431, \u043B\u043E\u0433 \u0447\u0435\u0440\u0435\u0437 API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D.`);
1315
+ }
1316
+ const window = logWindow(query);
1317
+ const path = pathOf(parsed);
1318
+ const tail = window.resume === void 0 && window.from === "tail";
1319
+ const first = window.resume?.page ?? 1;
1320
+ const deadline = Date.now() + CI_LOG_TIMEOUT_MS;
1321
+ let page = first;
1322
+ let skip = window.resume?.offset ?? 0;
1323
+ let kept = new Uint8Array(0);
1324
+ let dropped = 0;
1325
+ let seen = 0;
1326
+ let lastPage = first;
1327
+ let lastEnd = 0;
1328
+ let done = false;
1329
+ let unread = false;
1330
+ const encoder = new TextEncoder();
1331
+ const decoder = new TextDecoder("utf-8");
1332
+ for (; ; ) {
1333
+ if (!tail && kept.length >= window.bytes) {
1334
+ unread = true;
1335
+ break;
1336
+ }
1337
+ const { body } = await requestJson(`${root}/logs/${path}?page=${String(page)}`, { fetch, headers, missing: missingJob(key) });
1338
+ const record = isRecord(body) ? body : {};
1339
+ const data = encoder.encode(text(record.logs) ?? "");
1340
+ const rest = skip >= data.length ? new Uint8Array(0) : data.subarray(skip);
1341
+ let slice = rest;
1342
+ if (!tail) {
1343
+ const room = rest.subarray(0, Math.min(window.bytes - kept.length, rest.length));
1344
+ slice = room.subarray(0, room.length - trailingByte(room));
1345
+ }
1346
+ const merged = concat(kept, slice);
1347
+ if (merged.length > window.bytes) {
1348
+ const fitted = merged.subarray(merged.length - window.bytes);
1349
+ dropped += merged.length - fitted.length;
1350
+ kept = fitted;
1351
+ } else {
1352
+ kept = merged;
1353
+ }
1354
+ seen += data.length;
1355
+ lastPage = page;
1356
+ lastEnd = tail ? data.length : skip + slice.length;
1357
+ skip = 0;
1358
+ done = record.done === true;
1359
+ if (!tail && rest.length > slice.length) {
1360
+ unread = true;
1361
+ break;
1362
+ }
1363
+ if (done || record.page_complete === false) break;
1364
+ if (page - first + 1 >= CI_LOG_PAGES_MAX || seen >= CI_LOG_READ_MAX_BYTES || Date.now() >= deadline) {
1365
+ unread = true;
1366
+ break;
1367
+ }
1368
+ page += 1;
1369
+ }
1370
+ return logOf({
1371
+ text: decoder.decode(kept.subarray(leadingByte(kept))),
1372
+ truncated: dropped > 0 || unread,
1373
+ complete: done,
1374
+ page: lastPage,
1375
+ end: lastEnd,
1376
+ more: unread,
1377
+ // Размер всего лога известен, только если с первой страницы прочитали их все.
1378
+ size: window.resume === void 0 && !unread ? seen : void 0,
1379
+ // Страниц было прочитано больше окна: начало хвоста отрезано кольцевым буфером, и первая неполная
1380
+ // строка уходит из окна даже когда за ним ещё есть страницы (default startCut там молчит).
1381
+ startCut: tail && dropped > 0,
1382
+ raw: query.raw ?? false
1383
+ });
1384
+ },
1385
+ trigger: async (input) => {
1386
+ const workflows = input.workflows ?? [];
1387
+ if (workflows.length === 0) throw new TrackerError("invalid_request", "\u0417\u0430\u043F\u0443\u0441\u043A CI SourceCraft: \u043D\u0443\u0436\u0435\u043D \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D workflow.");
1388
+ const values = Object.entries(input.inputs ?? {}).map(([name, value]) => ({ name, value }));
1389
+ const head = input.refType === "tag" ? { tag: input.ref } : { branch: input.ref };
1390
+ const { body } = await requestJson(`${root}/runs`, { fetch, headers, method: "POST", body: { head, workflows: workflows.map((name) => ({ name, values })) } });
1391
+ return { run: runOf3(body) };
1392
+ },
1393
+ // Публичный API SourceCraft не умеет ни перезапускать, ни отменять (§1.3): отказ без запроса (§3.6).
1394
+ rerun: async () => {
1395
+ throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0430 CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
1396
+ },
1397
+ rerunJob: async () => {
1398
+ throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0437\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
1399
+ },
1400
+ cancel: async () => {
1401
+ throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
1402
+ },
1403
+ cancelJob: async () => {
1404
+ throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 \u0437\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
1405
+ }
1406
+ };
1407
+ }
1408
+ function partOf(value, key) {
1409
+ const refusal = new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
1410
+ if (value === void 0) throw refusal;
1411
+ let decoded;
1412
+ try {
1413
+ decoded = decodeURIComponent(value);
1414
+ } catch {
1415
+ throw refusal;
1416
+ }
1417
+ if (decoded === "") throw refusal;
1418
+ return decoded;
1419
+ }
1420
+ function pathOf(path) {
1421
+ return [path.run, path.workflow, path.task, path.cube].map(encodeURIComponent).join("/");
1422
+ }
1423
+ function recordList(value) {
1424
+ return Array.isArray(value) ? value.filter(isRecord) : [];
1425
+ }
1426
+ function offsetOf(cursor) {
1427
+ if (cursor === void 0) return 0;
1428
+ const match = JOB_OFFSET_CURSOR.exec(cursor);
1429
+ if (match === null) return 0;
1430
+ const from = Number(match[1]);
1431
+ return Number.isSafeInteger(from) && from >= 0 ? from : 0;
1432
+ }
1433
+ function runOf3(raw) {
1434
+ const item = isRecord(raw) ? raw : {};
1435
+ const key = text(item.slug) ?? "?";
1436
+ const rawStatus = text(item.status) ?? "unknown";
1437
+ const rawEvent = text(item.event_type);
1438
+ const user = isRecord(item.user) ? text(item.user.slug) : void 0;
1439
+ const pull = isRecord(item.pull) ? text(item.pull.slug) : void 0;
1440
+ const dates = isRecord(item.dates) ? item.dates : {};
1441
+ const createdAt = toTimestamp(dates.created_at);
1442
+ const startedAt = toTimestamp(dates.started_at);
1443
+ const finishedAt = toTimestamp(dates.finished_at);
1444
+ const updatedAt = toTimestamp(dates.updated_at);
1445
+ const errors = stringList(item.error_messages);
1446
+ return {
1447
+ key,
1448
+ ...RUN_SLUG.test(key) ? { number: Number(key) } : {},
1449
+ workflows: stringList(recordList(item.workflows).map((workflow) => workflow.slug)),
1450
+ status: { category: lookup(SOURCECRAFT_CI_STATUSES, rawStatus, "running"), name: rawStatus },
1451
+ ...rawEvent === void 0 ? {} : { event: { category: lookup(SOURCECRAFT_RUN_EVENTS, rawEvent, "other"), name: rawEvent } },
1452
+ ...user === void 0 || user === EXTERNAL_USER ? {} : { actor: user },
1453
+ ...pull === void 0 ? {} : { pullRequest: pull },
1454
+ ...errors.length === 0 ? {} : { errors },
1455
+ links: [],
1456
+ ...createdAt === void 0 ? {} : { createdAt },
1457
+ ...startedAt === void 0 ? {} : { startedAt },
1458
+ ...finishedAt === void 0 ? {} : { finishedAt },
1459
+ ...updatedAt === void 0 ? {} : { updatedAt }
1460
+ };
1461
+ }
1462
+ function jobOf3(run, workflow, task, cubeSlug, cube) {
1463
+ const rawStatus = text(cube.status) ?? "unknown";
1464
+ const dates = isRecord(cube.dates) ? cube.dates : {};
1465
+ const createdAt = toTimestamp(dates.created_at);
1466
+ const startedAt = toTimestamp(dates.started_at);
1467
+ const finishedAt = toTimestamp(dates.finished_at);
1468
+ return {
1469
+ key: [run, workflow, task, cubeSlug].map(encodeURIComponent).join("/"),
1470
+ runKey: run,
1471
+ name: cubeSlug,
1472
+ stage: `${workflow}/${task}`,
1473
+ status: { category: lookup(SOURCECRAFT_CI_STATUSES, rawStatus, "running"), name: rawStatus },
1474
+ links: [],
1475
+ ...createdAt === void 0 ? {} : { createdAt },
1476
+ ...startedAt === void 0 ? {} : { startedAt },
1477
+ ...finishedAt === void 0 ? {} : { finishedAt }
1478
+ };
1479
+ }
1480
+ function keepsRun(run, query) {
1481
+ return (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && (query.workflow === void 0 || run.workflows.includes(query.workflow)) && (query.actor === void 0 || run.actor === query.actor) && (query.pullRequest === void 0 || run.pullRequest === query.pullRequest) && inTimeRange(run.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore);
1482
+ }
1483
+ function pullSlugOf(run, raw, pullId, slug) {
1484
+ if (run.pullRequest !== void 0 || pullId === void 0 || slug === void 0) return run;
1485
+ const id = isRecord(raw.pull) ? text(raw.pull.id) : void 0;
1486
+ return id === pullId ? { ...run, pullRequest: slug } : run;
1487
+ }
1488
+ function leadingByte(window) {
1489
+ let count = 0;
1490
+ while (count < 3 && count < window.length && ((window[count] ?? 0) & 192) === 128) count += 1;
1491
+ return count;
1492
+ }
1493
+ function trailingByte(window) {
1494
+ for (let back = 1; back <= Math.min(4, window.length); back += 1) {
1495
+ const byte = window[window.length - back] ?? 0;
1496
+ if ((byte & 192) === 128) continue;
1497
+ const size = byte >= 240 ? 4 : byte >= 224 ? 3 : byte >= 192 ? 2 : 1;
1498
+ return size > back ? back : 0;
1499
+ }
1500
+ return 0;
1501
+ }
1502
+ function concat(head, tail) {
1503
+ if (head.length === 0) return tail;
1504
+ if (tail.length === 0) return head;
1505
+ const merged = new Uint8Array(head.length + tail.length);
1506
+ merged.set(head, 0);
1507
+ merged.set(tail, head.length);
1508
+ return merged;
1509
+ }
1510
+
1511
+ // src/tracker/registry.lib.ts
1512
+ var definitions = /* @__PURE__ */ new Map();
1513
+ function registerTracker(definition) {
1514
+ const kind = definition.descriptor.tracker;
1515
+ if (definitions.has(kind)) throw new Error(`\u0422\u0440\u0435\u043A\u0435\u0440 ${kind} \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D.`);
1516
+ definitions.set(kind, definition);
1517
+ }
1518
+ function findTracker(kind) {
1519
+ return definitions.get(kind);
1520
+ }
1521
+ function listTrackers() {
1522
+ return [...definitions.values()];
1523
+ }
1524
+
1525
+ // src/tracker/pull.schema.ts
1526
+ import { z as z2 } from "zod";
1527
+ var pullRequestStatusCategorySchema = z2.enum(["draft", "open", "merged", "closed"]);
1528
+ var pullRequestFilterSchema = z2.enum(["status", "author", "source", "target", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"]);
1529
+ var pullRequestSortFieldSchema = z2.enum(["created", "updated", "title"]);
1530
+ var mergeMethodSchema = z2.enum(["merge", "squash", "rebase"]);
1531
+ var pullRequestSchema = z2.object({
1532
+ /** Номер PR в своём трекере: GitHub number, GitLab iid, SourceCraft slug; для клиента непрозрачная строка. */
1533
+ key: z2.string().min(1).max(200),
1534
+ title: z2.string().min(1).max(1024),
1535
+ description: z2.string().max(1048576).optional(),
1536
+ status: z2.object({ category: pullRequestStatusCategorySchema, name: z2.string().min(1).max(200) }),
1537
+ /** Автор — логин или slug трекера. */
1538
+ author: z2.string().min(1).max(200).optional(),
1539
+ /** Ветка-источник и целевая ветка — имя без refs/heads/. */
1540
+ source: z2.string().min(1).max(500),
1541
+ target: z2.string().min(1).max(500),
1542
+ /** Коммит головы источника. */
1543
+ headSha: z2.string().regex(/^[0-9a-f]{7,64}$/).optional(),
1544
+ labels: z2.array(z2.string().min(1).max(200)).max(100),
1545
+ /** Запрошенные ревьюеры; нет — трекер их в PR не отдаёт. */
1546
+ reviewers: z2.array(z2.string().min(1).max(200)).max(100).optional(),
1547
+ links: z2.array(issueLinkSchema).max(100),
1548
+ createdAt: timestampSchema.optional(),
1549
+ updatedAt: timestampSchema.optional(),
1550
+ mergedAt: timestampSchema.optional(),
1551
+ closedAt: timestampSchema.optional()
1552
+ });
1553
+
1554
+ // src/tracker/github-pull.lib.ts
1555
+ var DEFAULT_API3 = "https://api.github.com";
1556
+ var TARGET4 = /^[\w.-]+\/[\w.-]+$/;
1557
+ var NUMBER = /^[1-9]\d{0,19}$/;
1558
+ var SHA2 = /^[0-9a-f]{7,64}$/;
1559
+ var LIST_SUBJECT = "PR GitHub";
1560
+ var COMMENTS_SUBJECT = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR GitHub";
1561
+ var PULL_CAPABILITIES = {
1562
+ list: {
1563
+ filters: pullRequestFilterSchema.options,
1564
+ clientSide: ["status", "author", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
1565
+ sorts: ["created", "updated"]
1566
+ },
1567
+ comments: { filters: COMMENT_FILTERS, clientSide: [], order: ["asc"] },
1568
+ create: true,
1569
+ createDraft: true,
1570
+ update: ["title", "description", "target"],
1571
+ statuses: ["open", "closed"],
1572
+ comment: true,
1573
+ merge: ["merge", "squash", "rebase"],
1574
+ deleteSourceOnMerge: false
1575
+ };
1576
+ function githubPulls() {
1577
+ return {
1578
+ capabilities: PULL_CAPABILITIES,
1579
+ create: (context) => createGithubPullAdapter(context)
1580
+ };
1581
+ }
1582
+ function createGithubPullAdapter({ connection, fetch }) {
1583
+ const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
1584
+ if (!TARGET4.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
1585
+ const api = baseUrl(connection.url, DEFAULT_API3);
1586
+ const root = `${api}/repos/${target}/pulls`;
1587
+ const issues = `${api}/repos/${target}/issues`;
1588
+ const owner = target.split("/")[0] ?? target;
1589
+ const headers = {
1590
+ accept: "application/vnd.github+json",
1591
+ "x-github-api-version": "2022-11-28",
1592
+ ...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
1593
+ };
1594
+ const missing = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.` });
1595
+ const number = (key) => {
1596
+ if (!NUMBER.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.`);
1597
+ return key;
1598
+ };
1599
+ const getPull = async (key) => pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, missing: missing(key) })).body);
1600
+ return {
1601
+ list: async (query) => {
1602
+ assertListQuery(LIST_SUBJECT, PULL_CAPABILITIES.list, query, pullRequestFilterSchema.options);
1603
+ const status = usedStatuses(query.status);
1604
+ const page = pageOf3(query.cursor);
1605
+ const params = new URLSearchParams({ state: stateFilter(status), per_page: String(query.limit), page: String(page) });
1606
+ if (query.source !== void 0) params.set("head", `${owner}:${query.source}`);
1607
+ if (query.target !== void 0) params.set("base", query.target);
1608
+ if (query.sort !== void 0) {
1609
+ params.set("sort", query.sort.field);
1610
+ params.set("direction", query.sort.direction ?? "desc");
1611
+ }
1612
+ const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
1613
+ const items = (Array.isArray(body) ? body : []).map(pullOf).filter((pull) => matchesPage(pull, query, status));
1614
+ return { items, ...nextOf(responseHeaders.get("link"), page) };
1615
+ },
1616
+ get: (key) => getPull(key),
1617
+ create: async (input) => pullOf((await requestJson(root, { fetch, headers, method: "POST", body: createBody(input) })).body),
1618
+ update: async (key, patch) => {
1619
+ 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.");
1620
+ const current = patch.status === "open" ? await getPull(key) : void 0;
1621
+ 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.");
1622
+ if (current !== void 0 && current.status.category === "open" && patch.title === void 0 && patch.description === void 0 && patch.target === void 0) return current;
1623
+ return pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody(patch), missing: missing(key) })).body);
1624
+ },
1625
+ merge: async (key, input) => {
1626
+ 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.");
1627
+ if (input.method !== void 0 && !PULL_CAPABILITIES.merge.includes(input.method)) {
1628
+ 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.`);
1629
+ }
1630
+ await requestJson(`${root}/${number(key)}/merge`, { fetch, headers, method: "PUT", body: mergeBody(input), conflictOn: [405, 409], missing: missing(key) });
1631
+ return getPull(key);
1632
+ },
1633
+ comments: async (key, query) => {
1634
+ assertCommentQuery(COMMENTS_SUBJECT, PULL_CAPABILITIES.comments, query);
1635
+ const page = pageOf3(query.cursor);
1636
+ const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
1637
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
1638
+ const { body, headers: responseHeaders } = await requestJson(`${issues}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
1639
+ return { items: (Array.isArray(body) ? body : []).map(commentOf), ...nextOf(responseHeaders.get("link"), page) };
1640
+ },
1641
+ comment: async (key, body) => commentOf((await requestJson(`${issues}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
1642
+ };
1643
+ }
1644
+ function pageOf3(cursor) {
1645
+ const page = cursor === void 0 ? 1 : Number(cursor);
1646
+ return Number.isInteger(page) && page >= 1 ? page : 1;
1647
+ }
1648
+ function nextOf(link, page) {
1649
+ return link !== null && /rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {};
268
1650
  }
269
- function hostOf(url) {
270
- try {
271
- return new URL(url).host;
272
- } catch {
273
- return "\u0442\u0440\u0435\u043A\u0435\u0440";
274
- }
1651
+ function stateFilter(status) {
1652
+ if (status === void 0) return "all";
1653
+ if (status.every((item) => item === "draft" || item === "open")) return "open";
1654
+ if (status.every((item) => item === "merged" || item === "closed")) return "closed";
1655
+ return "all";
275
1656
  }
276
- function baseUrl(value, fallback) {
277
- const raw = (value ?? fallback).replace(/\/+$/, "");
278
- let parsed;
279
- try {
280
- parsed = new URL(raw);
281
- } catch {
282
- throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u2014 \u043D\u0435 URL.");
1657
+ function matchesPage(pull, query, status) {
1658
+ if (status !== void 0 && !status.includes(pull.status.category)) return false;
1659
+ if (query.author !== void 0 && pull.author !== query.author) return false;
1660
+ if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
1661
+ if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
1662
+ return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
1663
+ }
1664
+ function usedStatuses(status) {
1665
+ if (status === void 0 || status.length === 0) return void 0;
1666
+ for (const item of status) {
1667
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
1668
+ 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)}.`);
1669
+ }
283
1670
  }
284
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C http \u0438\u043B\u0438 https.");
285
- return raw;
1671
+ return status;
286
1672
  }
287
-
288
- // src/tracker/registry.lib.ts
289
- var definitions = /* @__PURE__ */ new Map();
290
- function registerTracker(definition) {
291
- const kind = definition.descriptor.tracker;
292
- if (definitions.has(kind)) throw new Error(`\u0422\u0440\u0435\u043A\u0435\u0440 ${kind} \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D.`);
293
- definitions.set(kind, definition);
1673
+ function createBody(input) {
1674
+ return {
1675
+ title: input.title,
1676
+ head: input.source,
1677
+ base: input.target,
1678
+ ...input.description === void 0 ? {} : { body: input.description },
1679
+ ...input.draft === true ? { draft: true } : {}
1680
+ };
294
1681
  }
295
- function findTracker(kind) {
296
- return definitions.get(kind);
1682
+ function patchBody(patch) {
1683
+ return {
1684
+ ...patch.title === void 0 ? {} : { title: patch.title },
1685
+ ...patch.description === void 0 ? {} : { body: patch.description },
1686
+ ...patch.target === void 0 ? {} : { base: patch.target },
1687
+ ...patch.status === void 0 ? {} : { state: patch.status === "closed" ? "closed" : "open" }
1688
+ };
297
1689
  }
298
- function listTrackers() {
299
- return [...definitions.values()];
1690
+ function mergeBody(input) {
1691
+ const lines = input.message?.split("\n") ?? [];
1692
+ return {
1693
+ ...input.method === void 0 ? {} : { merge_method: input.method },
1694
+ ...input.headSha === void 0 ? {} : { sha: input.headSha },
1695
+ ...input.message === void 0 ? {} : { commit_title: lines[0] ?? "" },
1696
+ ...lines.length > 1 ? { commit_message: lines.slice(1).join("\n") } : {}
1697
+ };
1698
+ }
1699
+ function pullOf(raw) {
1700
+ const item = isRecord(raw) ? raw : {};
1701
+ const head = isRecord(item.head) ? item.head : {};
1702
+ const base = isRecord(item.base) ? item.base : {};
1703
+ const key = typeof item.number === "number" ? String(item.number) : "?";
1704
+ const description = text(item.body);
1705
+ const author = isRecord(item.user) ? text(item.user.login) : void 0;
1706
+ const sha = text(head.sha);
1707
+ const url = text(item.html_url);
1708
+ const createdAt = toTimestamp(item.created_at);
1709
+ const updatedAt = toTimestamp(item.updated_at);
1710
+ const mergedAt = toTimestamp(item.merged_at);
1711
+ const closedAt = toTimestamp(item.closed_at);
1712
+ const closed = item.state === "closed";
1713
+ const merged = item.merged_at !== void 0 && item.merged_at !== null;
1714
+ const category = closed ? merged ? "merged" : "closed" : item.draft === true ? "draft" : "open";
1715
+ return {
1716
+ key,
1717
+ title: text(item.title) ?? key,
1718
+ ...description === void 0 ? {} : { description },
1719
+ status: { category, name: category },
1720
+ ...author === void 0 ? {} : { author },
1721
+ source: text(head.ref) ?? "?",
1722
+ target: text(base.ref) ?? "?",
1723
+ ...sha !== void 0 && SHA2.test(sha) ? { headSha: sha } : {},
1724
+ labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : label)) : [],
1725
+ reviewers: logins(item.requested_reviewers),
1726
+ links: url === void 0 ? [] : [{ url, title: "GitHub" }],
1727
+ ...createdAt === void 0 ? {} : { createdAt },
1728
+ ...updatedAt === void 0 ? {} : { updatedAt },
1729
+ ...mergedAt === void 0 ? {} : { mergedAt },
1730
+ ...closedAt === void 0 ? {} : { closedAt }
1731
+ };
1732
+ }
1733
+ function logins(value) {
1734
+ if (!Array.isArray(value)) return [];
1735
+ const names = value.map((item) => isRecord(item) ? text(item.login) : void 0);
1736
+ return [...new Set(names.filter((name) => name !== void 0))];
1737
+ }
1738
+ function commentOf(raw) {
1739
+ const item = isRecord(raw) ? raw : {};
1740
+ const author = isRecord(item.user) ? text(item.user.login) : void 0;
1741
+ const createdAt = toTimestamp(item.created_at);
1742
+ return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
300
1743
  }
301
1744
 
302
1745
  // src/tracker/github.lib.ts
303
- var DEFAULT_API = "https://api.github.com";
304
- var TARGET = /^[\w.-]+\/[\w.-]+$/;
305
- var NUMBER = /^[1-9]\d{0,14}$/;
306
- var LIST_SUBJECT = "\u0417\u0430\u0434\u0430\u0447\u0438 GitHub";
307
- var COMMENTS_SUBJECT = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitHub";
1746
+ var DEFAULT_API4 = "https://api.github.com";
1747
+ var TARGET5 = /^[\w.-]+\/[\w.-]+$/;
1748
+ var NUMBER2 = /^[1-9]\d{0,14}$/;
1749
+ var LIST_SUBJECT2 = "\u0417\u0430\u0434\u0430\u0447\u0438 GitHub";
1750
+ var COMMENTS_SUBJECT2 = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitHub";
308
1751
  function githubTracker() {
309
1752
  const issues = {
310
1753
  list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "comments"] },
@@ -322,13 +1765,15 @@ function githubTracker() {
322
1765
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: ["open", "in_progress", "done", "declined"] }
323
1766
  },
324
1767
  create: (context) => createGithubAdapter(context, issues),
325
- issues
1768
+ issues,
1769
+ pulls: githubPulls(),
1770
+ ci: githubCi()
326
1771
  };
327
1772
  }
328
1773
  function createGithubAdapter({ connection, fetch }, capabilities) {
329
1774
  const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
330
- if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
331
- const api = baseUrl(connection.url, DEFAULT_API);
1775
+ if (!TARGET5.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
1776
+ const api = baseUrl(connection.url, DEFAULT_API4);
332
1777
  const root = `${api}/repos/${target}/issues`;
333
1778
  const headers = {
334
1779
  accept: "application/vnd.github+json",
@@ -336,7 +1781,7 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
336
1781
  ...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
337
1782
  };
338
1783
  const number = (key) => {
339
- if (!NUMBER.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
1784
+ if (!NUMBER2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
340
1785
  return key;
341
1786
  };
342
1787
  const rawOf = async (key) => {
@@ -345,13 +1790,13 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
345
1790
  };
346
1791
  return {
347
1792
  list: async (query) => {
348
- assertListQuery(LIST_SUBJECT, capabilities.list, query, ISSUE_FILTERS);
1793
+ assertListQuery(LIST_SUBJECT2, capabilities.list, query, ISSUE_FILTERS);
349
1794
  assertCsvLabels(query.labels);
350
- const page = pageOf(query.cursor);
1795
+ const page = pageOf4(query.cursor);
351
1796
  const search = needsSearch(query);
352
1797
  const params = search ? searchParams(target, query, page) : issuesParams(query, page);
353
1798
  const { body, headers: responseHeaders } = await requestJson(`${search ? `${api}/search/issues` : root}?${params.toString()}`, { fetch, headers });
354
- return issuePageOf(rowsOf(body, search), query, responseHeaders.get("link") ?? "", page);
1799
+ return issuePageOf(rowsOf2(body, search), query, responseHeaders.get("link") ?? "", page);
355
1800
  },
356
1801
  get: async (key) => issueOf(await rawOf(key)),
357
1802
  create: async (input) => {
@@ -366,25 +1811,25 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
366
1811
  },
367
1812
  update: async (key, patch) => {
368
1813
  const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : issueOf(await rawOf(key));
369
- const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody(patch, current), missingIsIssue: true });
1814
+ const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody2(patch, current), missingIsIssue: true });
370
1815
  return issueOf(response.body);
371
1816
  },
372
1817
  comments: async (key, query) => {
373
- assertCommentQuery(COMMENTS_SUBJECT, capabilities.comments, query);
374
- const page = pageOf(query.cursor);
1818
+ assertCommentQuery(COMMENTS_SUBJECT2, capabilities.comments, query);
1819
+ const page = pageOf4(query.cursor);
375
1820
  const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
376
1821
  if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
377
1822
  const { body, headers: responseHeaders } = await requestJson(`${root}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
378
- return { items: (Array.isArray(body) ? body : []).map(commentOf), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
1823
+ return { items: (Array.isArray(body) ? body : []).map(commentOf2), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
379
1824
  },
380
- comment: async (key, body) => commentOf((await requestJson(`${root}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
1825
+ comment: async (key, body) => commentOf2((await requestJson(`${root}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
381
1826
  };
382
1827
  }
383
- function pageOf(cursor) {
1828
+ function pageOf4(cursor) {
384
1829
  const page = cursor === void 0 ? 1 : Number(cursor);
385
1830
  return Number.isInteger(page) && page >= 1 ? page : 1;
386
1831
  }
387
- function stateFilter(status) {
1832
+ function stateFilter2(status) {
388
1833
  if (status === void 0) return "all";
389
1834
  if (status.every((item) => item === "open" || item === "in_progress")) return "open";
390
1835
  if (status.every((item) => item === "done" || item === "declined")) return "closed";
@@ -394,10 +1839,10 @@ function needsSearch(query) {
394
1839
  return query.search !== void 0 || query.createdAfter !== void 0 || query.createdBefore !== void 0 || query.updatedBefore !== void 0;
395
1840
  }
396
1841
  function assertCsvLabels(labels) {
397
- if (labels?.some((label) => label.includes(",")) === true) throw new TrackerError("invalid_request", `${LIST_SUBJECT}: \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.`);
1842
+ 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.`);
398
1843
  }
399
1844
  function issuesParams(query, page) {
400
- const params = new URLSearchParams({ state: stateFilter(query.status), per_page: String(query.limit), page: String(page) });
1845
+ const params = new URLSearchParams({ state: stateFilter2(query.status), per_page: String(query.limit), page: String(page) });
401
1846
  if (query.labels !== void 0 && query.labels.length > 0) params.set("labels", query.labels.join(","));
402
1847
  if (query.assignee !== void 0) params.set("assignee", query.assignee ?? "none");
403
1848
  if (query.author !== void 0) params.set("creator", query.author);
@@ -435,7 +1880,7 @@ function timeRangeQualifier(field, after, before) {
435
1880
  if (before !== void 0) return `${field}:<=${isoTime(before)}`;
436
1881
  return void 0;
437
1882
  }
438
- function rowsOf(body, search) {
1883
+ function rowsOf2(body, search) {
439
1884
  if (!search) return Array.isArray(body) ? body : [];
440
1885
  return isRecord(body) && Array.isArray(body.items) ? body.items : [];
441
1886
  }
@@ -456,7 +1901,7 @@ function composeLabels(plain, priority, status) {
456
1901
  const priorityLabel = priority === void 0 ? void 0 : priorityName(GITHUB_PRIORITY_LABELS, priority);
457
1902
  return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITHUB_IN_PROGRESS_LABEL] : []];
458
1903
  }
459
- function patchBody(patch, current) {
1904
+ function patchBody2(patch, current) {
460
1905
  const status = patch.status ?? current?.status.category;
461
1906
  const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
462
1907
  return {
@@ -492,22 +1937,216 @@ function issueOf(raw) {
492
1937
  ...updatedAt === void 0 ? {} : { updatedAt }
493
1938
  };
494
1939
  }
495
- function commentOf(raw) {
1940
+ function commentOf2(raw) {
496
1941
  const item = isRecord(raw) ? raw : {};
497
1942
  const author = isRecord(item.user) ? text(item.user.login) : void 0;
498
1943
  const createdAt = toTimestamp(item.created_at);
499
1944
  return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
500
1945
  }
501
1946
 
1947
+ // src/tracker/gitlab-pull.lib.ts
1948
+ var DEFAULT_URL2 = "https://gitlab.com";
1949
+ var TARGET6 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
1950
+ var IID = /^[1-9]\d{0,19}$/;
1951
+ var SHA3 = /^[0-9a-f]{7,64}$/;
1952
+ var PULL_CAPABILITIES2 = {
1953
+ list: { filters: pullRequestFilterSchema.options, clientSide: ["status"], sorts: ["created", "updated", "title"] },
1954
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
1955
+ create: true,
1956
+ createDraft: true,
1957
+ update: ["title", "description", "target"],
1958
+ statuses: ["draft", "open", "closed"],
1959
+ comment: true,
1960
+ merge: ["merge", "squash"],
1961
+ deleteSourceOnMerge: true
1962
+ };
1963
+ var ORDER_BY = { created: "created_at", updated: "updated_at", title: "title" };
1964
+ function gitlabPulls() {
1965
+ return {
1966
+ capabilities: PULL_CAPABILITIES2,
1967
+ create: (context) => createGitlabPullAdapter(context)
1968
+ };
1969
+ }
1970
+ function createGitlabPullAdapter({ connection, fetch }) {
1971
+ 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");
1972
+ if (!TARGET6.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
1973
+ const root = `${baseUrl(connection.url, DEFAULT_URL2)}/api/v4/projects/${encodeURIComponent(target)}/merge_requests`;
1974
+ const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
1975
+ const iid = (key) => {
1976
+ if (!IID.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.`);
1977
+ return key;
1978
+ };
1979
+ 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);
1980
+ return {
1981
+ list: async (query) => {
1982
+ assertListQuery("PR GitLab", PULL_CAPABILITIES2.list, query, pullRequestFilterSchema.options);
1983
+ const status = usedStatuses2(query.status);
1984
+ const page = pageOf5(query.cursor);
1985
+ const params = new URLSearchParams({
1986
+ state: stateFilter3(status),
1987
+ per_page: String(query.limit),
1988
+ page: String(page),
1989
+ order_by: query.sort === void 0 ? "created_at" : ORDER_BY[query.sort.field] ?? "created_at",
1990
+ sort: query.sort?.direction ?? "desc"
1991
+ });
1992
+ if (query.labels !== void 0 && query.labels.length > 0) {
1993
+ 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.");
1994
+ params.set("labels", query.labels.join(","));
1995
+ }
1996
+ if (query.author !== void 0) params.set("author_username", query.author);
1997
+ if (query.source !== void 0) params.set("source_branch", query.source);
1998
+ if (query.target !== void 0) params.set("target_branch", query.target);
1999
+ if (query.search !== void 0) params.set("search", query.search);
2000
+ if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
2001
+ if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
2002
+ if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
2003
+ if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
2004
+ const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
2005
+ const next = responseHeaders.get("x-next-page");
2006
+ const items = (Array.isArray(body) ? body : []).map(pullOf2);
2007
+ return {
2008
+ items: status === void 0 ? items : items.filter((pr) => status.includes(pr.status.category) === true),
2009
+ ...next === null || next === "" ? {} : { nextCursor: next }
2010
+ };
2011
+ },
2012
+ get: currentOf,
2013
+ create: async (input) => {
2014
+ const body = {
2015
+ source_branch: input.source,
2016
+ target_branch: input.target,
2017
+ title: (input.draft === true ? GITLAB_DRAFT_PREFIX : "") + input.title,
2018
+ ...input.description === void 0 ? {} : { description: input.description }
2019
+ };
2020
+ return pullOf2((await requestJson(root, { fetch, headers, method: "POST", body, conflictOn: [409] })).body);
2021
+ },
2022
+ update: async (key, patch) => {
2023
+ const current = patch.status === void 0 ? void 0 : await currentOf(key);
2024
+ if (patch.status === "draft" && current?.status.category === "closed") {
2025
+ 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.");
2026
+ }
2027
+ const body = patchBody3(patch, current);
2028
+ 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);
2029
+ 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);
2030
+ },
2031
+ merge: async (key, input) => {
2032
+ if (input.method !== void 0 && !PULL_CAPABILITIES2.merge.includes(input.method)) {
2033
+ 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.`);
2034
+ }
2035
+ const body = {
2036
+ // squash: true ставит squash; false GitLab игнорирует — при method 'merge' действует настройка MR/проекта.
2037
+ ...input.method === "squash" ? { squash: true } : {},
2038
+ ...input.message === void 0 ? {} : input.method === "squash" ? { squash_commit_message: input.message } : { merge_commit_message: input.message },
2039
+ ...input.deleteSource === void 0 ? {} : { should_remove_source_branch: input.deleteSource },
2040
+ ...input.headSha === void 0 ? {} : { sha: input.headSha }
2041
+ };
2042
+ 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);
2043
+ },
2044
+ comments: async (key, query) => {
2045
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", PULL_CAPABILITIES2.comments, query);
2046
+ const page = pageOf5(query.cursor);
2047
+ const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
2048
+ 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}.` } });
2049
+ const next = responseHeaders.get("x-next-page");
2050
+ const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf3);
2051
+ return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
2052
+ },
2053
+ 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)
2054
+ };
2055
+ }
2056
+ function pageOf5(cursor) {
2057
+ const page = cursor === void 0 ? 1 : Number(cursor);
2058
+ return Number.isInteger(page) && page >= 1 ? page : 1;
2059
+ }
2060
+ function stateFilter3(status) {
2061
+ if (status === void 0) return "all";
2062
+ if (status.every((item) => item === "draft" || item === "open")) return "opened";
2063
+ if (status.every((item) => item === "merged")) return "merged";
2064
+ if (status.every((item) => item === "closed")) return "closed";
2065
+ return "all";
2066
+ }
2067
+ function patchBody3(patch, current) {
2068
+ const body = {};
2069
+ if (patch.description !== void 0) body.description = patch.description;
2070
+ if (patch.target !== void 0) body.target_branch = patch.target;
2071
+ if (patch.status === "closed" && current?.status.category !== "closed") body.state_event = "close";
2072
+ if (patch.status === "open" && current?.status.category === "closed") body.state_event = "reopen";
2073
+ if (patch.status === "draft" || patch.status === "open") {
2074
+ const title = patch.title ?? current?.title ?? "";
2075
+ const base = title.replace(GITLAB_DRAFT_PATTERN, "");
2076
+ const next = patch.status === "draft" ? GITLAB_DRAFT_PREFIX + base : current?.status.category === "draft" || title !== base ? base : patch.title;
2077
+ if (next !== void 0 && next !== current?.title) body.title = next;
2078
+ } else if (patch.title !== void 0 && patch.title !== current?.title) {
2079
+ body.title = patch.title;
2080
+ }
2081
+ return body;
2082
+ }
2083
+ function usedStatuses2(status) {
2084
+ if (status === void 0 || status.length === 0) return void 0;
2085
+ for (const item of status) {
2086
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
2087
+ 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)}.`);
2088
+ }
2089
+ }
2090
+ return status;
2091
+ }
2092
+ function labelNames(value) {
2093
+ if (!Array.isArray(value)) return [];
2094
+ const names = value.map((item) => typeof item === "string" ? item : isRecord(item) ? text(item.name) : void 0);
2095
+ return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
2096
+ }
2097
+ function usernames(value) {
2098
+ if (!Array.isArray(value)) return [];
2099
+ const names = value.map((item) => isRecord(item) ? text(item.username) : void 0);
2100
+ return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
2101
+ }
2102
+ function pullOf2(raw) {
2103
+ const item = isRecord(raw) ? raw : {};
2104
+ const state = text(item.state) ?? "opened";
2105
+ const draft = item.draft === true;
2106
+ const category = lookup(GITLAB_MERGE_REQUEST_STATES, state, "open");
2107
+ const key = typeof item.iid === "number" ? String(item.iid) : "?";
2108
+ const description = text(item.description);
2109
+ const author = isRecord(item.author) ? text(item.author.username) : void 0;
2110
+ const sha = text(item.sha);
2111
+ const url = text(item.web_url);
2112
+ const createdAt = toTimestamp(item.created_at);
2113
+ const updatedAt = toTimestamp(item.updated_at);
2114
+ const mergedAt = toTimestamp(item.merged_at);
2115
+ const closedAt = toTimestamp(item.closed_at);
2116
+ return {
2117
+ key,
2118
+ title: text(item.title) ?? key,
2119
+ ...description === void 0 ? {} : { description },
2120
+ status: { category: draft && category === "open" ? "draft" : category, name: draft && category === "open" ? "draft" : state },
2121
+ ...author === void 0 ? {} : { author },
2122
+ source: text(item.source_branch) ?? "?",
2123
+ target: text(item.target_branch) ?? "?",
2124
+ ...sha !== void 0 && SHA3.test(sha) ? { headSha: sha } : {},
2125
+ labels: labelNames(item.labels),
2126
+ reviewers: usernames(item.reviewers),
2127
+ links: url === void 0 ? [] : [{ url, title: "GitLab" }],
2128
+ ...createdAt === void 0 ? {} : { createdAt },
2129
+ ...updatedAt === void 0 ? {} : { updatedAt },
2130
+ ...mergedAt === void 0 ? {} : { mergedAt },
2131
+ ...closedAt === void 0 ? {} : { closedAt }
2132
+ };
2133
+ }
2134
+ function commentOf3(raw) {
2135
+ const item = isRecord(raw) ? raw : {};
2136
+ const author = isRecord(item.author) ? text(item.author.username) : void 0;
2137
+ const createdAt = toTimestamp(item.created_at);
2138
+ return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
2139
+ }
2140
+
502
2141
  // src/tracker/gitlab.lib.ts
503
- var DEFAULT_URL = "https://gitlab.com";
504
- var TARGET2 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
505
- var IID = /^[1-9]\d{0,14}$/;
2142
+ var DEFAULT_URL3 = "https://gitlab.com";
2143
+ var TARGET7 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
2144
+ var IID2 = /^[1-9]\d{0,14}$/;
506
2145
  var ISSUE_CAPABILITIES = {
507
2146
  list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "priority", "title"] },
508
2147
  comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
509
2148
  };
510
- var ORDER_BY = {
2149
+ var ORDER_BY2 = {
511
2150
  created: "created_at",
512
2151
  updated: "updated_at",
513
2152
  priority: "priority",
@@ -526,28 +2165,30 @@ function gitlabTracker() {
526
2165
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
527
2166
  },
528
2167
  create: (context) => createGitlabAdapter(context),
529
- issues: ISSUE_CAPABILITIES
2168
+ issues: ISSUE_CAPABILITIES,
2169
+ pulls: gitlabPulls(),
2170
+ ci: gitlabCi()
530
2171
  };
531
2172
  }
532
2173
  function createGitlabAdapter({ connection, fetch }) {
533
2174
  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");
534
- 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.");
535
- const root = `${baseUrl(connection.url, DEFAULT_URL)}/api/v4/projects/${encodeURIComponent(target)}/issues`;
2175
+ if (!TARGET7.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
2176
+ const root = `${baseUrl(connection.url, DEFAULT_URL3)}/api/v4/projects/${encodeURIComponent(target)}/issues`;
536
2177
  const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
537
2178
  const iid = (key) => {
538
- if (!IID.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
2179
+ if (!IID2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
539
2180
  return key;
540
2181
  };
541
2182
  const currentOf = async (key) => issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missingIsIssue: true })).body);
542
2183
  return {
543
2184
  list: async (query) => {
544
2185
  assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 GitLab", ISSUE_CAPABILITIES.list, query, ISSUE_FILTERS);
545
- const page = pageOf2(query.cursor);
2186
+ const page = pageOf6(query.cursor);
546
2187
  const params = new URLSearchParams({
547
- state: stateFilter2(query.status),
2188
+ state: stateFilter4(query.status),
548
2189
  per_page: String(query.limit),
549
2190
  page: String(page),
550
- order_by: query.sort === void 0 ? "created_at" : ORDER_BY[query.sort.field] ?? "created_at",
2191
+ order_by: query.sort === void 0 ? "created_at" : ORDER_BY2[query.sort.field] ?? "created_at",
551
2192
  sort: query.sort?.direction ?? "desc"
552
2193
  });
553
2194
  if (query.labels !== void 0 && query.labels.length > 0) {
@@ -577,25 +2218,25 @@ function createGitlabAdapter({ connection, fetch }) {
577
2218
  },
578
2219
  update: async (key, patch) => {
579
2220
  const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : await currentOf(key);
580
- return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody2(patch, current), missingIsIssue: true })).body);
2221
+ return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody4(patch, current), missingIsIssue: true })).body);
581
2222
  },
582
2223
  comments: async (key, query) => {
583
2224
  assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", ISSUE_CAPABILITIES.comments, query);
584
- const page = pageOf2(query.cursor);
2225
+ const page = pageOf6(query.cursor);
585
2226
  const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
586
2227
  const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missingIsIssue: true });
587
2228
  const next = responseHeaders.get("x-next-page");
588
- const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf2);
2229
+ const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf4);
589
2230
  return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
590
2231
  },
591
- comment: async (key, body) => commentOf2((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
2232
+ comment: async (key, body) => commentOf4((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
592
2233
  };
593
2234
  }
594
- function pageOf2(cursor) {
2235
+ function pageOf6(cursor) {
595
2236
  const page = cursor === void 0 ? 1 : Number(cursor);
596
2237
  return Number.isInteger(page) && page >= 1 ? page : 1;
597
2238
  }
598
- function stateFilter2(status) {
2239
+ function stateFilter4(status) {
599
2240
  if (status === void 0) return "all";
600
2241
  if (status.every((item) => item === "open" || item === "in_progress")) return "opened";
601
2242
  if (status.every((item) => item === "done" || item === "declined")) return "closed";
@@ -605,7 +2246,7 @@ function composeLabels2(plain, priority, status) {
605
2246
  const priorityLabel = priority === void 0 ? void 0 : priorityName(GITLAB_PRIORITY_LABELS, priority);
606
2247
  return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITLAB_IN_PROGRESS_LABEL] : []];
607
2248
  }
608
- function patchBody2(patch, current) {
2249
+ function patchBody4(patch, current) {
609
2250
  const status = patch.status ?? current?.status.category;
610
2251
  const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
611
2252
  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.");
@@ -643,7 +2284,7 @@ function issueOf2(raw) {
643
2284
  ...updatedAt === void 0 ? {} : { updatedAt }
644
2285
  };
645
2286
  }
646
- function commentOf2(raw) {
2287
+ function commentOf4(raw) {
647
2288
  const item = isRecord(raw) ? raw : {};
648
2289
  const author = isRecord(item.author) ? text(item.author.username) : void 0;
649
2290
  const createdAt = toTimestamp(item.created_at);
@@ -748,9 +2389,9 @@ function createJiraAdapter({ connection, fetch }) {
748
2389
  const items = raw.filter((item) => inTimeRange(toTimestamp(isRecord(item) ? item.updated : void 0), query.updatedAfter, void 0));
749
2390
  const total = typeof record.total === "number" ? record.total : 0;
750
2391
  const at = typeof record.startAt === "number" ? record.startAt : 0;
751
- return { items: items.map(commentOf3), ...at + raw.length < total && raw.length > 0 ? { nextCursor: String(at + raw.length) } : {} };
2392
+ return { items: items.map(commentOf5), ...at + raw.length < total && raw.length > 0 ? { nextCursor: String(at + raw.length) } : {} };
752
2393
  },
753
- comment: async (value, body) => commentOf3((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
2394
+ comment: async (value, body) => commentOf5((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
754
2395
  };
755
2396
  }
756
2397
  function listJql(project, query) {
@@ -822,7 +2463,7 @@ function issueOf3(raw, site) {
822
2463
  ...updatedAt === void 0 ? {} : { updatedAt }
823
2464
  };
824
2465
  }
825
- function commentOf3(raw) {
2466
+ function commentOf5(raw) {
826
2467
  const item = isRecord(raw) ? raw : {};
827
2468
  const author = isRecord(item.author) ? text(item.author.displayName) : void 0;
828
2469
  const createdAt = toTimestamp(item.created);
@@ -909,11 +2550,212 @@ function toComment(row) {
909
2550
  return { id: String(row.id), ...row.author === void 0 ? {} : { author: row.author }, body: row.body, createdAt: row.createdAt };
910
2551
  }
911
2552
 
2553
+ // src/tracker/sourcecraft-pull.lib.ts
2554
+ var DEFAULT_API5 = "https://api.sourcecraft.tech";
2555
+ var TARGET8 = /^[\w.-]+\/[\w.-]+$/;
2556
+ var SLUG = /^[\w-]{1,100}$/;
2557
+ var SHA4 = /^[0-9a-f]{7,64}$/;
2558
+ var PULL_SORT_FIELDS_UPSTREAM = {
2559
+ created: "created_at",
2560
+ updated: "updated_at",
2561
+ title: "title"
2562
+ };
2563
+ var PULL_CAPABILITIES3 = {
2564
+ list: {
2565
+ filters: pullRequestFilterSchema.options,
2566
+ clientSide: ["labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
2567
+ sorts: ["created", "updated", "title"]
2568
+ },
2569
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
2570
+ create: true,
2571
+ createDraft: true,
2572
+ update: ["title", "description", "target"],
2573
+ statuses: ["draft", "open", "closed"],
2574
+ comment: true,
2575
+ merge: ["merge", "squash", "rebase"],
2576
+ deleteSourceOnMerge: true
2577
+ };
2578
+ function sourcecraftPulls() {
2579
+ return { capabilities: PULL_CAPABILITIES3, create: (context) => createPullAdapter(context) };
2580
+ }
2581
+ function createPullAdapter({ connection, fetch }) {
2582
+ const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
2583
+ if (!TARGET8.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
2584
+ const api = baseUrl(connection.url, DEFAULT_API5);
2585
+ const root = `${api}/repos/${target}/pulls`;
2586
+ const headers = { authorization: `Bearer ${connection.token ?? ""}` };
2587
+ const missing = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.` });
2588
+ const slug = (key) => {
2589
+ if (!SLUG.test(key)) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.`);
2590
+ return key;
2591
+ };
2592
+ const getPull = async (key) => pullOf3((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missing: missing(key) })).body);
2593
+ return {
2594
+ list: async (query) => {
2595
+ assertListQuery("PR SourceCraft", PULL_CAPABILITIES3.list, query, pullRequestFilterSchema.options);
2596
+ const status = usedStatuses3(query.status);
2597
+ const params = new URLSearchParams({ page_size: String(query.limit) });
2598
+ if (query.cursor !== void 0) params.set("page_token", query.cursor);
2599
+ const filter = pullFilterOf(query, status);
2600
+ if (filter !== void 0) params.set("filter", filter);
2601
+ if (query.sort !== void 0) params.set("sort_by", `${query.sort.direction === "asc" ? "" : "-"}${PULL_SORT_FIELDS_UPSTREAM[query.sort.field]}`);
2602
+ const { body } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
2603
+ const record = isRecord(body) ? body : {};
2604
+ const pulls = (Array.isArray(record.pull_requests) ? record.pull_requests : []).map(pullOf3).filter((pull) => matchesPage2(pull, query));
2605
+ return { items: pulls, ...nextOf2(record.next_page_token) };
2606
+ },
2607
+ get: (key) => getPull(key),
2608
+ create: async (input) => pullOf3((await requestJson(root, { fetch, headers, method: "POST", body: createBody2(input) })).body),
2609
+ update: async (key, patch) => {
2610
+ const id = slug(key);
2611
+ let updated;
2612
+ if (patch.title !== void 0 || patch.description !== void 0 || patch.target !== void 0) {
2613
+ const body = {
2614
+ ...patch.title === void 0 ? {} : { title: patch.title },
2615
+ ...patch.description === void 0 ? {} : { description: patch.description },
2616
+ ...patch.target === void 0 ? {} : { target_branch: patch.target }
2617
+ };
2618
+ updated = (await requestJson(`${root}/${id}`, { fetch, headers, method: "PATCH", body, missing: missing(key) })).body;
2619
+ }
2620
+ if (patch.status !== void 0) {
2621
+ const action = statusActionOf(patch.status, (await getPull(key)).status.category);
2622
+ if (action !== void 0) await requestJson(`${root}/${id}/${action}`, { fetch, headers, method: "POST", missing: missing(key) });
2623
+ return getPull(key);
2624
+ }
2625
+ return pullOf3(updated ?? (await requestJson(`${root}/${id}`, { fetch, headers, missing: missing(key) })).body);
2626
+ },
2627
+ merge: async (key, input) => {
2628
+ const id = slug(key);
2629
+ 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.");
2630
+ if (input.method !== void 0 && !PULL_CAPABILITIES3.merge.includes(input.method)) {
2631
+ 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.`);
2632
+ }
2633
+ if (input.headSha !== void 0) {
2634
+ const current = await getPull(key);
2635
+ if ((current.headSha ?? "").toLowerCase() !== input.headSha.toLowerCase()) {
2636
+ 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"}.`);
2637
+ }
2638
+ }
2639
+ const { body } = await requestJson(`${root}/${id}/merge`, {
2640
+ fetch,
2641
+ headers,
2642
+ method: "POST",
2643
+ // squash/rebase по умолчанию наследуются из настройки репозитория — без method не шлём явно false.
2644
+ body: {
2645
+ ...input.method === void 0 ? {} : { squash: input.method === "squash", rebase: input.method === "rebase" },
2646
+ ...input.deleteSource === void 0 ? {} : { delete_branch: input.deleteSource }
2647
+ },
2648
+ missing: missing(key)
2649
+ });
2650
+ const operation = isRecord(body) ? body : {};
2651
+ if (operation.status === "failed" || operation.status === "cancel") {
2652
+ const errorMessage = isRecord(operation.error) ? text(operation.error.message) : void 0;
2653
+ 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))}.`);
2654
+ }
2655
+ return getPull(key);
2656
+ },
2657
+ comments: async (key, query) => {
2658
+ const id = slug(key);
2659
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR SourceCraft", PULL_CAPABILITIES3.comments, query);
2660
+ const params = new URLSearchParams({ page_size: String(query.limit) });
2661
+ if (query.cursor !== void 0) params.set("page_token", query.cursor);
2662
+ if (query.order !== void 0) params.set("sort_by", query.order === "asc" ? "created_at" : "-created_at");
2663
+ const { body } = await requestJson(`${root}/${id}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
2664
+ const record = isRecord(body) ? body : {};
2665
+ const rawComments = Array.isArray(record.pull_request_comments) ? record.pull_request_comments : [];
2666
+ const visible = rawComments.filter((raw) => !(isRecord(raw) && raw.is_deleted === true));
2667
+ const filtered = query.updatedAfter === void 0 ? visible : visible.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
2668
+ return { items: filtered.map(commentOf6), ...nextOf2(record.next_page_token) };
2669
+ },
2670
+ comment: async (key, body) => commentOf6((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
2671
+ };
2672
+ }
2673
+ function createBody2(input) {
2674
+ return {
2675
+ title: input.title,
2676
+ source_branch: input.source,
2677
+ target_branch: input.target,
2678
+ ...input.description === void 0 ? {} : { description: input.description },
2679
+ publish: input.draft !== true
2680
+ };
2681
+ }
2682
+ function statusActionOf(requested, current) {
2683
+ if (current === requested) return void 0;
2684
+ if (requested === "draft") return "draft";
2685
+ if (requested === "open") return current === "draft" ? "publish" : "reopen";
2686
+ return "discard";
2687
+ }
2688
+ function usedStatuses3(status) {
2689
+ if (status === void 0 || status.length === 0) return void 0;
2690
+ for (const item of status) {
2691
+ if (!pullRequestStatusCategorySchema.options.includes(item)) {
2692
+ 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)}.`);
2693
+ }
2694
+ }
2695
+ return status;
2696
+ }
2697
+ function matchesPage2(pull, query) {
2698
+ if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
2699
+ if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
2700
+ return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
2701
+ }
2702
+ function pullFilterOf(query, status) {
2703
+ const clauses = [];
2704
+ if (status !== void 0) {
2705
+ const slugs = [...new Set(keysFor(SOURCECRAFT_PULL_STATUSES, status))];
2706
+ clauses.push(`status in (${slugs.join(", ")})`);
2707
+ }
2708
+ if (query.author !== void 0) clauses.push(`author_slug=${quoted(query.author)}`);
2709
+ if (query.source !== void 0) clauses.push(`source_branch=${quoted(query.source)}`);
2710
+ if (query.target !== void 0) clauses.push(`target_branch=${quoted(query.target)}`);
2711
+ return clauses.length === 0 ? void 0 : clauses.join(" and ");
2712
+ }
2713
+ function nextOf2(token) {
2714
+ const value = text(token);
2715
+ return value === void 0 ? {} : { nextCursor: value };
2716
+ }
2717
+ function pullOf3(raw) {
2718
+ const item = isRecord(raw) ? raw : {};
2719
+ const source = isRecord(item.source) ? item.source : {};
2720
+ const target = isRecord(item.target) ? item.target : {};
2721
+ const headSha = text(source.sha);
2722
+ const description = text(item.description);
2723
+ const author = isRecord(item.author) ? text(item.author.slug) : void 0;
2724
+ const createdAt = toTimestamp(item.created_at);
2725
+ const updatedAt = toTimestamp(item.updated_at);
2726
+ const mergedAt = toTimestamp(item.merged_at);
2727
+ const closedAt = toTimestamp(item.closed_at);
2728
+ const key = text(item.slug) ?? "?";
2729
+ const statusName = text(item.status) ?? "open";
2730
+ return {
2731
+ key,
2732
+ title: text(item.title) ?? key,
2733
+ ...description === void 0 ? {} : { description },
2734
+ status: { category: lookup(SOURCECRAFT_PULL_STATUSES, statusName, "open"), name: statusName },
2735
+ ...author === void 0 ? {} : { author },
2736
+ source: text(source.ref) ?? text(item.source_branch) ?? "?",
2737
+ target: text(target.ref) ?? text(item.target_branch) ?? "?",
2738
+ ...headSha !== void 0 && SHA4.test(headSha) ? { headSha } : {},
2739
+ labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : void 0)) : [],
2740
+ links: [],
2741
+ ...createdAt === void 0 ? {} : { createdAt },
2742
+ ...updatedAt === void 0 ? {} : { updatedAt },
2743
+ ...mergedAt === void 0 ? {} : { mergedAt },
2744
+ ...closedAt === void 0 ? {} : { closedAt }
2745
+ };
2746
+ }
2747
+ function commentOf6(raw) {
2748
+ const item = isRecord(raw) ? raw : {};
2749
+ const author = isRecord(item.author) ? text(item.author.slug) : void 0;
2750
+ const createdAt = toTimestamp(item.created_at);
2751
+ return { id: text(item.id) ?? "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
2752
+ }
2753
+
912
2754
  // src/tracker/sourcecraft.lib.ts
913
- var DEFAULT_API2 = "https://api.sourcecraft.tech";
2755
+ var DEFAULT_API6 = "https://api.sourcecraft.tech";
914
2756
  var DEFAULT_WEB = "https://sourcecraft.dev";
915
- var TARGET3 = /^[\w.-]+\/[\w.-]+$/;
916
- var SLUG = /^[\w-]{1,100}$/;
2757
+ var TARGET9 = /^[\w.-]+\/[\w.-]+$/;
2758
+ var SLUG2 = /^[\w-]{1,100}$/;
917
2759
  var ISSUE_SORT_FIELDS_UPSTREAM = {
918
2760
  created: "created_at",
919
2761
  updated: "updated_at",
@@ -937,18 +2779,20 @@ function sourcecraftTracker() {
937
2779
  features: { create: true, update: true, comment: true, priority: true, labels: false, assignee: false, statuses: [...ALL_STATUSES] }
938
2780
  },
939
2781
  issues: ISSUE_CAPABILITIES4,
940
- create: (context) => createSourceCraftAdapter(context)
2782
+ create: (context) => createSourceCraftAdapter(context),
2783
+ pulls: sourcecraftPulls(),
2784
+ ci: sourcecraftCi()
941
2785
  };
942
2786
  }
943
2787
  function createSourceCraftAdapter({ connection, fetch }) {
944
2788
  const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
945
- if (!TARGET3.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
946
- const api = baseUrl(connection.url, DEFAULT_API2);
2789
+ if (!TARGET9.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
2790
+ const api = baseUrl(connection.url, DEFAULT_API6);
947
2791
  const root = `${api}/repos/${target}/issues`;
948
2792
  const headers = { authorization: `Bearer ${connection.token ?? ""}` };
949
2793
  const web = connection.url === void 0 ? `${DEFAULT_WEB}/${target}/issues` : void 0;
950
2794
  const slug = (key) => {
951
- if (!SLUG.test(key)) throw new TrackerError("unknown_issue", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
2795
+ if (!SLUG2.test(key)) throw new TrackerError("unknown_issue", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
952
2796
  return key;
953
2797
  };
954
2798
  const toIssue2 = (raw) => issueOf4(raw, web);
@@ -965,14 +2809,14 @@ function createSourceCraftAdapter({ connection, fetch }) {
965
2809
  let issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
966
2810
  if (query.status !== void 0) issues = issues.filter((issue) => query.status?.includes(issue.status.category) === true);
967
2811
  if (query.assignee === null) issues = issues.filter((issue) => issue.assignee === void 0);
968
- return { items: issues, ...nextOf(record.next_page_token) };
2812
+ return { items: issues, ...nextOf3(record.next_page_token) };
969
2813
  },
970
2814
  get: async (key) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missingIsIssue: true })).body),
971
2815
  create: async (input) => {
972
2816
  const body = { title: input.title, ...input.description === void 0 ? {} : { description: input.description }, ...input.priority === void 0 ? {} : { priority: input.priority } };
973
2817
  return toIssue2((await requestJson(root, { fetch, headers, method: "POST", body })).body);
974
2818
  },
975
- update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody3(patch), missingIsIssue: true })).body),
2819
+ update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody5(patch), missingIsIssue: true })).body),
976
2820
  comments: async (key, query) => {
977
2821
  assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 SourceCraft", ISSUE_CAPABILITIES4.comments, query);
978
2822
  const params = new URLSearchParams({ page_size: String(query.limit) });
@@ -982,12 +2826,12 @@ function createSourceCraftAdapter({ connection, fetch }) {
982
2826
  const record = isRecord(body) ? body : {};
983
2827
  const rawComments = Array.isArray(record.issue_comments) ? record.issue_comments : [];
984
2828
  const filtered = query.updatedAfter === void 0 ? rawComments : rawComments.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
985
- return { items: filtered.map(commentOf4), ...nextOf(record.next_page_token) };
2829
+ return { items: filtered.map(commentOf7), ...nextOf3(record.next_page_token) };
986
2830
  },
987
- comment: async (key, body) => commentOf4((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
2831
+ comment: async (key, body) => commentOf7((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
988
2832
  };
989
2833
  }
990
- function patchBody3(patch) {
2834
+ function patchBody5(patch) {
991
2835
  return {
992
2836
  ...patch.title === void 0 ? {} : { title: patch.title },
993
2837
  ...patch.description === void 0 ? {} : { description: patch.description },
@@ -995,7 +2839,7 @@ function patchBody3(patch) {
995
2839
  ...patch.priority === void 0 ? {} : { priority: patch.priority }
996
2840
  };
997
2841
  }
998
- function nextOf(token) {
2842
+ function nextOf3(token) {
999
2843
  const value = text(token);
1000
2844
  return value === void 0 ? {} : { nextCursor: value };
1001
2845
  }
@@ -1044,7 +2888,7 @@ function issueOf4(raw, web) {
1044
2888
  ...updatedAt === void 0 ? {} : { updatedAt }
1045
2889
  };
1046
2890
  }
1047
- function commentOf4(raw) {
2891
+ function commentOf7(raw) {
1048
2892
  const item = isRecord(raw) ? raw : {};
1049
2893
  const author = isRecord(item.author) ? text(item.author.slug) : void 0;
1050
2894
  const createdAt = toTimestamp(item.created_at);
@@ -1107,13 +2951,33 @@ function writeTrackerToken(dataDir, projectId, token, sink = defaultSecretSink)
1107
2951
  function deleteTrackerToken(dataDir, projectId, sink = defaultSecretSink) {
1108
2952
  return writeTrackerToken(dataDir, projectId, "", sink);
1109
2953
  }
2954
+
2955
+ // src/tracker/pull.lib.ts
2956
+ function pullRequestAdapterOf(definition, context) {
2957
+ 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.`);
2958
+ return definition.pulls.create(context);
2959
+ }
1110
2960
  export {
1111
2961
  ALL_STATUSES,
2962
+ CI_FINISHED_STATUSES,
2963
+ CI_LOG_DEFAULT_BYTES,
2964
+ CI_LOG_MAX_BYTES,
2965
+ CI_LOG_PAGES_MAX,
2966
+ CI_LOG_READ_MAX_BYTES,
2967
+ CI_LOG_TIMEOUT_MS,
1112
2968
  COMMENT_FILTERS,
1113
2969
  GITHUB_CLOSE_REASONS,
1114
2970
  GITHUB_IN_PROGRESS_LABEL,
1115
2971
  GITHUB_PRIORITY_LABELS,
2972
+ GITHUB_RUN_CONCLUSIONS,
2973
+ GITHUB_RUN_EVENTS,
2974
+ GITHUB_RUN_STATUSES,
2975
+ GITLAB_CI_STATUSES,
2976
+ GITLAB_DRAFT_PATTERN,
2977
+ GITLAB_DRAFT_PREFIX,
1116
2978
  GITLAB_IN_PROGRESS_LABEL,
2979
+ GITLAB_MERGE_REQUEST_STATES,
2980
+ GITLAB_PIPELINE_SOURCES,
1117
2981
  GITLAB_PRIORITY_LABELS,
1118
2982
  ISSUE_FILTERS,
1119
2983
  ISSUE_SORT_FIELDS,
@@ -1122,7 +2986,10 @@ export {
1122
2986
  JIRA_PRIORITY_NAMES,
1123
2987
  JIRA_STATUS_CATEGORIES,
1124
2988
  LEGACY_ISSUE_CAPABILITIES,
2989
+ SOURCECRAFT_CI_STATUSES,
1125
2990
  SOURCECRAFT_PRIORITIES,
2991
+ SOURCECRAFT_PULL_STATUSES,
2992
+ SOURCECRAFT_RUN_EVENTS,
1126
2993
  SOURCECRAFT_STATUSES,
1127
2994
  SOURCECRAFT_STATUS_FILTER,
1128
2995
  SOURCECRAFT_STATUS_SLUGS,
@@ -1135,14 +3002,31 @@ export {
1135
3002
  assertCommentQuery,
1136
3003
  assertListQuery,
1137
3004
  baseUrl,
3005
+ ciAdapterOf,
3006
+ ciEventCategorySchema,
3007
+ ciJobFilterSchema,
3008
+ ciJobSchema,
3009
+ ciLogSchema,
3010
+ ciRunFilterSchema,
3011
+ ciRunSchema,
3012
+ ciRunSortFieldSchema,
3013
+ ciStatusCategorySchema,
3014
+ ciStatusSchema,
3015
+ ciWorkflowSchema,
3016
+ cleanLogText,
1138
3017
  clipText,
1139
3018
  containsText,
1140
3019
  deleteTrackerToken,
1141
3020
  findTracker,
3021
+ githubCi,
3022
+ githubPulls,
1142
3023
  githubTracker,
3024
+ gitlabCi,
3025
+ gitlabPulls,
1143
3026
  gitlabTracker,
1144
3027
  hasTrackerToken,
1145
3028
  inTimeRange,
3029
+ isFinished,
1146
3030
  isRecord,
1147
3031
  isoTime,
1148
3032
  issueCapabilitiesOf,
@@ -1150,15 +3034,27 @@ export {
1150
3034
  keysFor,
1151
3035
  listTrackers,
1152
3036
  localTracker,
3037
+ logCursor,
3038
+ logOf,
3039
+ logWindow,
1153
3040
  lookup,
3041
+ mergeMethodSchema,
1154
3042
  nameOf,
1155
3043
  priorityName,
3044
+ pullRequestAdapterOf,
3045
+ pullRequestFilterSchema,
3046
+ pullRequestSchema,
3047
+ pullRequestSortFieldSchema,
3048
+ pullRequestStatusCategorySchema,
1156
3049
  quoted,
1157
3050
  readTrackerToken,
1158
3051
  registerBuiltinTrackers,
1159
3052
  registerTracker,
1160
3053
  requestJson,
3054
+ requestText,
1161
3055
  requireTarget,
3056
+ sourcecraftCi,
3057
+ sourcecraftPulls,
1162
3058
  sourcecraftTracker,
1163
3059
  stringList,
1164
3060
  text,