@goodandready/dsh-kanban 0.2.18 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/gitea.js CHANGED
@@ -1,28 +1,24 @@
1
- // Клиент Gitea для доски.
1
+ // Gitea HTTP API client.
2
2
  //
3
- // Своего клиента канбан держит намеренно: токен всё равно живёт в службе
4
- // учётных данных ядра, а не в соседнем плагине, и шесть вызовов REST дешевле,
5
- // чем зависимость от чужого выпуска.
3
+ // Standalone module without external dependencies, communicating directly with Gitea v1 API.
4
+ // Network calls use injectable fetch (`fetchImpl`), and token resolution is provided via callback.
5
+ // The client can be unit tested without making real network requests.
6
6
  //
7
- // Сеть приходит параметром (`fetchImpl`), токен — функцией разрешения.
8
- // Поэтому клиент проверяется без единого реального запроса.
9
- //
10
- // Токен в настройках не хранится: там только ИМЯ учётной записи. Наружу
11
- // значение не отдаётся никогда, включая сообщения об ошибках.
7
+ // Secrets are not stored in settings; only the credential name is stored.
8
+ // Token values are never emitted to logs or error messages.
12
9
 
13
- /** Убрать хвостовые слэши: адрес приходит из поля, куда человек пишет как хочет. */
10
+ /** Strip trailing slashes from base URL. */
14
11
  export function normalizeBaseUrl(url) {
15
12
  return String(url || '').trim().replace(/\/+$/, '')
16
13
  }
17
14
 
18
15
  /**
19
- * Сегмент пути, пригодный для подстановки в URL.
16
+ * Path segment sanitized for URL interpolation.
20
17
  *
21
- * `owner`, `repo` и номер приходят из данных Gitea и из браузера, а не из
22
- * кода. Без проверки сюда подставится `..` или слэш, и запрос уйдёт не туда,
23
- * куда собирались.
18
+ * `owner`, `repo`, and issue index originate from external data or user input.
19
+ * Sanitization prevents directory traversal or path manipulation.
24
20
  *
25
- * @returns {string|undefined} закодированный сегмент либо undefined
21
+ * @returns {string|undefined} encoded segment or undefined if invalid
26
22
  */
27
23
  export function safeSegment(value) {
28
24
  const s = String(value ?? '').trim()
@@ -31,27 +27,23 @@ export function safeSegment(value) {
31
27
  return encodeURIComponent(s)
32
28
  }
33
29
 
34
- /** Имя учётной записи — имя переменной окружения, а не сам секрет. */
30
+ /** Credential reference name (e.g. env var name), distinct from the raw secret. */
35
31
  export function isCredentialName(value) {
36
32
  const str = String(value || '')
37
33
  if (looksLikeSecret(str)) return false
38
34
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str)
39
35
  }
40
36
 
41
- /** Похоже на сам токен, а не на его имя. */
37
+ /** Check if value looks like a raw token secret rather than a credential reference name. */
42
38
  export function looksLikeSecret(value) {
43
39
  return /^[A-Za-z0-9_-]{32,}$/.test(String(value || '').trim())
44
40
  }
45
41
 
46
42
  /**
47
- * Упорядочить репозитории по актуальности.
48
- *
49
- * Сверху те, где работа идёт прямо сейчас. Признак «идёт» выбран самый честный
50
- * из доступных без лишних запросов: есть открытые задачи И репозиторий недавно
51
- * трогали. Архивные уходят вниз всегда — там работы не ведут по определению.
43
+ * Rank repositories by activity.
52
44
  *
53
- * Точного «ведётся прямо сейчас» инстанс не знает, и выдумывать его не из чего;
54
- * это приближение, а не истина.
45
+ * Places active repositories with open tasks and recent updates at the top.
46
+ * Archived repositories are sorted to the bottom.
55
47
  */
56
48
  export function rankRepos(repos) {
57
49
  const rank = (r) => {
@@ -69,12 +61,12 @@ export function rankRepos(repos) {
69
61
  }
70
62
 
71
63
  /**
72
- * Клиент Gitea.
64
+ * Gitea client factory.
73
65
  *
74
66
  * @param {object} options
75
- * @param {() => object} options.getConfig текущие настройки плагина
76
- * @param {(name: string) => Promise<string>} options.resolveToken разрешение учётной записи
77
- * @param {Function} [options.fetchImpl] реализация fetch
67
+ * @param {() => object} options.getConfig current plugin configuration
68
+ * @param {(name: string) => Promise<string>} options.resolveToken credential resolver callback
69
+ * @param {Function} [options.fetchImpl] fetch implementation
78
70
  * @param {number} [options.timeoutMs]
79
71
  */
80
72
  export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch, timeoutMs = 30_000 }) {
@@ -92,8 +84,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
92
84
  const res = await fetchImpl(`${baseUrl}/api/v1${path}`, {
93
85
  method,
94
86
  headers: {
95
- // Заголовком, а не basic-авторизацией: на внутренних сетях basic
96
- // отдаёт «Missing API key».
87
+ // Use header authorization rather than basic auth to avoid 'Missing API key' on private instances.
97
88
  'Authorization': `token ${token}`,
98
89
  'Content-Type': 'application/json',
99
90
  'Accept': 'application/json',
@@ -105,7 +96,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
105
96
  let payload
106
97
  try { payload = text.trim() === '' ? null : JSON.parse(text) } catch { payload = null }
107
98
  if (!res.ok) {
108
- // Тело ответа наружу не выносим: в нём может быть эхо заголовков.
99
+ // Do not leak raw response body in error messages to prevent echoing sensitive headers.
109
100
  throw Object.assign(new Error(`Gitea HTTP error ${res.status}`), { code: 'http', status: res.status })
110
101
  }
111
102
  return payload
@@ -124,18 +115,13 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
124
115
  }
125
116
 
126
117
  /**
127
- * Собрать голый массив со всех страниц.
128
- *
129
- * Gitea отдаёт списки порциями, и запрос без перелистывания молча теряет
130
- * хвост. Признак конца — неполная страница: инстанс отдал меньше, чем
131
- * просили, значит дальше пусто. Верхний предел стоит, чтобы очень большой
132
- * ответ не превратил сверку в бесконечный поход.
118
+ * Paginate and collect all items from Gitea endpoints up to maxPages.
133
119
  *
134
120
  * @param {object} options
135
- * @param {(page: number, size: number) => string} options.buildPath путь страницы
136
- * @param {number} [options.limit] размер страницы
137
- * @param {number} [options.maxPages] верхний предел страниц
138
- * @returns {Promise<Array>} все строки со всех прочитанных страниц
121
+ * @param {(page: number, size: number) => string} options.buildPath page path builder
122
+ * @param {number} [options.limit] page size
123
+ * @param {number} [options.maxPages] maximum pages to retrieve
124
+ * @returns {Promise<Array>} concatenated results from all pages
139
125
  */
140
126
  async function collectPages({ buildPath, limit, maxPages = 10 }) {
141
127
  const size = Math.min(Math.max(Number(limit) || 50, 1), 50)
@@ -149,10 +135,8 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
149
135
  return out
150
136
  }
151
137
 
152
- // Признак «настроено» кэшируется по ключу настроек. `isConfigured` стоит в
153
- // горячем пути — его спрашивают на каждый запрос списка issue и поиска
154
- // репозиториев, — а резолв токена это поход в хранилище учётных данных.
155
- // Пока адрес и имя учётной записи не менялись, ответ не мог измениться.
138
+ // Cache configuration check result keyed on URL and token reference to avoid
139
+ // repeated credential lookups on hot paths.
156
140
  let configuredCache
157
141
  const configuredKey = () => {
158
142
  const config = getConfig()
@@ -160,7 +144,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
160
144
  }
161
145
 
162
146
  return {
163
- /** Настроен ли доступ. Значение токена наружу не отдаётся. */
147
+ /** Check whether access is configured. Token value is never exposed. */
164
148
  async isConfigured() {
165
149
  const key = configuredKey()
166
150
  if (configuredCache?.key === key) return configuredCache.value
@@ -173,46 +157,38 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
173
157
  },
174
158
 
175
159
  async listIssues({ owner, repo, state = 'open', limit = 50 }) {
176
- // API отдаёт issue и pull request одним списком; доске нужны только issue.
177
- // Перелистываем до конца: при 50+ открытых issue хвост иначе теряется
178
- // молча, и подхват задач смотрит на неполную картину.
160
+ // API returns issues and PRs together; filter out pull requests and paginate completely.
179
161
  const rows = await collectPages({
180
162
  limit,
181
163
  buildPath: (page, size) => repoPath(owner, repo,
182
- `/issues?state=${encodeURIComponent(state)}&page=${page}&limit=${size}&sort=updated&direction=desc`),
164
+ `/issues?state=${encodeURIComponent(state)}&page=${page}&limit=${size}`),
183
165
  })
184
- return rows.filter((r) => !r.pull_request)
166
+ return rows.filter((i) => i.pull_request === undefined)
185
167
  },
186
168
 
187
169
  async getIssue({ owner, repo, index }) {
188
170
  const n = safeSegment(index)
189
- if (n === undefined) throw Object.assign(new Error('недопустимый номер issue'), { code: 'bad-segment' })
171
+ if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
190
172
  return request('GET', repoPath(owner, repo, `/issues/${n}`))
191
173
  },
192
174
 
193
175
  async comment({ owner, repo, index, body }) {
194
176
  const n = safeSegment(index)
195
- if (n === undefined) throw Object.assign(new Error('недопустимый номер issue'), { code: 'bad-segment' })
177
+ if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
196
178
  return request('POST', repoPath(owner, repo, `/issues/${n}/comments`), { body })
197
179
  },
198
180
 
199
- /** Завести issue. Метки не ставим: доска чужой разметкой не распоряжается. */
181
+ /** Create issue. Labels are omitted here to respect repository conventions. */
200
182
  async createIssue({ owner, repo, title, body }) {
201
183
  const name = String(title ?? '').trim()
202
- if (name === '') throw Object.assign(new Error('пустой заголовок'), { code: 'bad-title' })
184
+ if (name === '') throw Object.assign(new Error('empty title'), { code: 'bad-title' })
203
185
  return request('POST', repoPath(owner, repo, '/issues'), { title: name, body: String(body ?? '') })
204
186
  },
205
187
 
206
- /**
207
- * Завести репозиторий в организации.
208
- *
209
- * Приватный и пустой — так решил владелец: расширить права потом дешевле,
210
- * чем убирать лишнее из публичного, а пустой репозиторий честнее чужих
211
- * заготовок.
212
- */
188
+ /** Create private, empty repository in organization. */
213
189
  async createRepo({ owner, name, description = '' }) {
214
190
  const safe = safeSegment(name)
215
- if (safe === undefined) throw Object.assign(new Error('недопустимое имя'), { code: 'bad-segment' })
191
+ if (safe === undefined) throw Object.assign(new Error('invalid name'), { code: 'bad-segment' })
216
192
  return request('POST', `/orgs/${safeSegment(owner)}/repos`, {
217
193
  name: safe, description, private: true, auto_init: false,
218
194
  })
@@ -220,39 +196,27 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
220
196
 
221
197
  async closeIssue({ owner, repo, index }) {
222
198
  const n = safeSegment(index)
223
- if (n === undefined) throw Object.assign(new Error('недопустимый номер issue'), { code: 'bad-segment' })
199
+ if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
224
200
  return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { state: 'closed' })
225
201
  },
226
202
 
227
- /**
228
- * Назначить ответственного за issue.
229
- *
230
- * Пустой список снимает назначение: Gitea понимает это как «никто», и
231
- * доска обязана уметь то же самое — иначе снять с себя задачу можно было
232
- * бы только руками в Gitea.
233
- */
203
+ /** Set issue assignees. Empty array clears assignees. */
234
204
  async setAssignees({ owner, repo, index, logins }) {
235
205
  const n = safeSegment(index)
236
- if (n === undefined) throw Object.assign(new Error('недопустимый номер issue'), { code: 'bad-segment' })
206
+ if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
237
207
  const list = (Array.isArray(logins) ? logins : [])
238
208
  .filter((one) => typeof one === 'string' && one !== '')
239
209
  return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { assignees: list })
240
210
  },
241
211
 
242
- /**
243
- * Кто мы для Gitea.
244
- *
245
- * «Взять себе» на доске значит «назначить того, чьим токеном доска ходит»:
246
- * своего понятия пользователя у харнесса нет, и выдумывать его вместо
247
- * ответа сервера нельзя.
248
- */
212
+ /** Authenticated user identity for the token. */
249
213
  async me() {
250
214
  const row = await request('GET', '/user')
251
215
  const login = row?.login
252
216
  return typeof login === 'string' ? login : ''
253
217
  },
254
218
 
255
- /** Организации, доступные токену. Нужны, чтобы не спрашивать владельца лишний раз. */
219
+ /** List organizations accessible to the token. */
256
220
  async listOrgs() {
257
221
  const rows = await request('GET', '/user/orgs?limit=50')
258
222
  return (Array.isArray(rows) ? rows : [])
@@ -260,14 +224,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
260
224
  .filter((name) => name !== '')
261
225
  },
262
226
 
263
- /**
264
- * Репозитории организации с числом открытых задач.
265
- *
266
- * Число приходит в том же ответе, и по нему отсеиваются пустые: ходить за
267
- * задачами в репозиторий, где их нет, — полсотни запросов впустую каждые
268
- * две минуты. Перелистываем: при 100+ репозиториях одна страница теряла бы
269
- * хвост, и доска смотрела бы на неполную организацию.
270
- */
227
+ /** List organization repositories with open issue counts, paginating to handle large orgs. */
271
228
  async listOrgRepos({ owner, limit = 100 }) {
272
229
  const rows = await collectPages({
273
230
  limit,
@@ -280,7 +237,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
280
237
  })).filter((r) => r.name !== '')
281
238
  },
282
239
 
283
- /** Pull request-ы репозитория. */
240
+ /** List pull requests for repository. */
284
241
  async listPulls({ owner, repo, state = 'all', limit = 50 }) {
285
242
  return collectPages({
286
243
  limit,
@@ -289,13 +246,13 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
289
246
  })
290
247
  },
291
248
 
292
- /** Создать Pull Request в репозитории. */
249
+ /** Create pull request in repository. */
293
250
  async createPullRequest({ owner, repo, title, body, head, base = 'main' }) {
294
251
  const name = String(title ?? '').trim()
295
- if (name === '') throw Object.assign(new Error('пустой заголовок'), { code: 'bad-title' })
252
+ if (name === '') throw Object.assign(new Error('empty title'), { code: 'bad-title' })
296
253
  const h = String(head ?? '').trim()
297
254
  const b = String(base ?? 'main').trim()
298
- if (!h) throw Object.assign(new Error('ветка head обязательна'), { code: 'bad-head' })
255
+ if (!h) throw Object.assign(new Error('head branch required'), { code: 'bad-head' })
299
256
  return request('POST', repoPath(owner, repo, '/pulls'), {
300
257
  title: name,
301
258
  body: String(body ?? ''),
@@ -304,13 +261,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
304
261
  })
305
262
  },
306
263
 
307
- /**
308
- * Ветки репозитория: по ним видно, начата ли работа и сделан ли cleanup.
309
- *
310
- * Ветки перелистываются не ради полноты списка, а ради правды наблюдения:
311
- * `branchOfTask` не найдёт ветку задачи на усечённой странице и сочтёт её
312
- * отсутствующей — карточка уедет не туда на пустом месте.
313
- */
264
+ /** List repository branches with pagination to avoid false negatives when detecting active branches. */
314
265
  async listBranches({ owner, repo, limit = 100 }) {
315
266
  const rows = await collectPages({
316
267
  limit,
@@ -319,16 +270,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
319
270
  return rows.map((b) => ({ name: b?.name ?? '' })).filter((b) => b.name !== '')
320
271
  },
321
272
 
322
- /**
323
- * Репозитории инстанса, все страницы.
324
- *
325
- * Одной страницы не хватает: Gitea отдаёт их порциями, и запрос без
326
- * перелистывания молча теряет хвост. Верхний предел стоит, чтобы очень
327
- * большой инстанс не превратил открытие диалога в долгую загрузку.
328
- *
329
- * Число открытых задач и время правки приходят в том же ответе — считать
330
- * их отдельными запросами не нужно.
331
- */
273
+ /** Search repositories across instance with pagination up to maxPages. */
332
274
  async searchRepos({ query, limit = 50, maxPages = 10 }) {
333
275
  const q = encodeURIComponent(String(query || ''))
334
276
  const size = Math.min(Math.max(Number(limit) || 50, 1), 50)
package/lib/github.js CHANGED
@@ -1,12 +1,8 @@
1
- // Клиент GitHub REST API v3.
2
- //
3
- // Выполняет те же операции, что и GiteaClient: чтение issues, pull requests,
4
- // веток, репозиториев, создание PR и комментирование.
5
- // Дополнительно поддерживает условные запросы с ETag (If-None-Match) для защиты
6
- // от исчерпания часового лимита запросов GitHub (5 000 req/hr).
7
- //
8
- // Модуль изолирован от файловой системы и DSH: токен и конфигурация
9
- // передаются фабрикой createGitHubClient.
1
+ // GitHub REST API v3 client.
2
+ // Equivalent functionality to GiteaClient: reading issues, pull requests,
3
+ // branches, repositories, creating PRs and comments.
4
+ // Supports conditional requests with ETag (If-None-Match) to avoid rate limits.
5
+ // Isolated from filesystem and DSH: token and configuration injected via factory.
10
6
 
11
7
  const DEFAULT_BASE_URL = 'https://api.github.com'
12
8
 
@@ -32,16 +28,16 @@ export function isCredentialName(value) {
32
28
  }
33
29
 
34
30
  /**
35
- * Создать клиент GitHub API.
31
+ * Create GitHub API client.
36
32
  *
37
33
  * @param {object} options
38
- * @param {() => object} options.getConfig функция получения конфигурации
39
- * @param {(name: string) => Promise<string>} options.resolveToken получение токена
40
- * @param {typeof fetch} [options.fetchImpl] реализация fetch
41
- * @param {number} [options.timeoutMs] таймаут запроса в мс
34
+ * @param {() => object} options.getConfig config getter
35
+ * @param {(name: string) => Promise<string>} options.resolveToken token resolver
36
+ * @param {typeof fetch} [options.fetchImpl] fetch implementation
37
+ * @param {number} [options.timeoutMs] request timeout in ms
42
38
  */
43
39
  export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch, timeoutMs = 30_000 }) {
44
- // Кэш условных запросов ETag: url -> { etag, data }
40
+ // ETag conditional request cache: url -> { etag, data }
45
41
  const etagCache = new Map()
46
42
 
47
43
  async function request(method, path, body, { useEtag = false } = {}) {
@@ -144,7 +140,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
144
140
  return {
145
141
  provider: 'github',
146
142
 
147
- /** Проверен ли токен доступа. */
143
+ /** Whether access token is valid. */
148
144
  async isConfigured() {
149
145
  const key = configuredKey()
150
146
  if (configuredCache?.key === key) return configuredCache.value
@@ -156,12 +152,12 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
156
152
  return value
157
153
  },
158
154
 
159
- /** Очистить кэш ETag (например, при явном ручном обновлении). */
155
+ /** Clear ETag cache (e.g. on manual refresh). */
160
156
  clearCache() {
161
157
  etagCache.clear()
162
158
  },
163
159
 
164
- /** Список issues репозитория (без PR). */
160
+ /** List repository issues (excluding PRs). */
165
161
  async listIssues({ owner, repo, state = 'open', limit = 50 }) {
166
162
  const rows = await collectPages({
167
163
  limit,
@@ -172,28 +168,28 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
172
168
  return rows.filter((r) => !r.pull_request)
173
169
  },
174
170
 
175
- /** Получить один issue по номеру. */
171
+ /** Get single issue by number. */
176
172
  async getIssue({ owner, repo, index }) {
177
173
  const n = safeSegment(index)
178
174
  if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
179
175
  return request('GET', repoPath(owner, repo, `/issues/${n}`))
180
176
  },
181
177
 
182
- /** Оставить комментарий в issue. */
178
+ /** Post comment on issue. */
183
179
  async comment({ owner, repo, index, body }) {
184
180
  const n = safeSegment(index)
185
181
  if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
186
182
  return request('POST', repoPath(owner, repo, `/issues/${n}/comments`), { body })
187
183
  },
188
184
 
189
- /** Создать новый issue. */
185
+ /** Create new issue. */
190
186
  async createIssue({ owner, repo, title, body }) {
191
187
  const name = String(title ?? '').trim()
192
188
  if (name === '') throw Object.assign(new Error('empty issue title'), { code: 'bad-title' })
193
189
  return request('POST', repoPath(owner, repo, '/issues'), { title: name, body: String(body ?? '') })
194
190
  },
195
191
 
196
- /** Закрыть issue. */
192
+ /** Close issue. */
197
193
  async closeIssue({ owner, repo, index }) {
198
194
  const n = safeSegment(index)
199
195
  if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
@@ -201,8 +197,8 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
201
197
  },
202
198
 
203
199
  /**
204
- * Назначить ответственных за issue.
205
- * В GitHub REST API PATCH /issues/{n} со свойством assignees заменяет ответственных целиком.
200
+ * Set assignees for issue.
201
+ * In GitHub REST API PATCH /issues/{n} with assignees replaces the entire list.
206
202
  */
207
203
  async setAssignees({ owner, repo, index, logins }) {
208
204
  const n = safeSegment(index)
@@ -212,14 +208,14 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
212
208
  return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { assignees: list })
213
209
  },
214
210
 
215
- /** Логин текущего пользователя токена. */
211
+ /** Login of current token owner. */
216
212
  async me() {
217
213
  const row = await request('GET', '/user')
218
214
  const login = row?.login
219
215
  return typeof login === 'string' ? login : ''
220
216
  },
221
217
 
222
- /** Организации, доступные токену. */
218
+ /** Organizations available to token. */
223
219
  async listOrgs() {
224
220
  const rows = await request('GET', '/user/orgs?per_page=50')
225
221
  return (Array.isArray(rows) ? rows : [])
@@ -227,7 +223,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
227
223
  .filter((name) => name !== '')
228
224
  },
229
225
 
230
- /** Репозитории организации с числом открытых задач. */
226
+ /** Organization repositories with open task count. */
231
227
  async listOrgRepos({ owner, limit = 100 }) {
232
228
  const rows = await collectPages({
233
229
  limit,
@@ -241,7 +237,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
241
237
  })).filter((r) => r.name !== '')
242
238
  },
243
239
 
244
- /** Pull requests репозитория. */
240
+ /** Repository pull requests. */
245
241
  async listPulls({ owner, repo, state = 'all', limit = 50 }) {
246
242
  return collectPages({
247
243
  limit,
@@ -251,7 +247,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
251
247
  })
252
248
  },
253
249
 
254
- /** Создать Pull Request. */
250
+ /** Create Pull Request. */
255
251
  async createPullRequest({ owner, repo, title, body, head, base = 'main' }) {
256
252
  const name = String(title ?? '').trim()
257
253
  if (name === '') throw Object.assign(new Error('empty PR title'), { code: 'bad-title' })
@@ -266,7 +262,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
266
262
  })
267
263
  },
268
264
 
269
- /** Список веток репозитория. */
265
+ /** List repository branches. */
270
266
  async listBranches({ owner, repo, limit = 100 }) {
271
267
  const rows = await collectPages({
272
268
  limit,
@@ -275,7 +271,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
275
271
  return rows.map((b) => ({ name: String(b?.name ?? '') })).filter((b) => b.name !== '')
276
272
  },
277
273
 
278
- /** Поиск репозиториев. */
274
+ /** Search repositories. */
279
275
  async searchRepos({ query, limit = 50, maxPages = 5 }) {
280
276
  const q = encodeURIComponent(String(query || ''))
281
277
  const size = Math.min(Math.max(Number(limit) || 50, 1), 50)