@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/import.js CHANGED
@@ -1,20 +1,19 @@
1
- // Перенос issue Gitea в задачу доски.
1
+ // Transfer Gitea issue to a board task.
2
2
  //
3
- // Синхронизация в 0.1 односторонняя: Gitea → канбан, в момент импорта и по
4
- // кнопке обновления. Двусторонняя синхронизация — отдельный выпуск, и тянуть
5
- // её сюда нельзя.
3
+ // Synchronization in 0.1 is one-way: Gitea -> kanban, at import time and on
4
+ // manual refresh. Two-way synchronization is handled separately.
6
5
  //
7
- // Клиент Gitea приходит параметром — его собирает `lib/gitea.js`. Здесь только
8
- // перенос полей и решение, что делать, когда доступ не настроен.
6
+ // Gitea client is passed as a parameter — configured by `lib/gitea.js`. Here
7
+ // we only map fields and handle cases where access is not configured.
9
8
 
10
9
  import { safeSegment } from './gitea.js'
11
10
  import { colorsOfIssue } from './filters.js'
12
11
 
13
12
  /**
14
- * Когда issue завели в Gitea.
13
+ * When the issue was created in Gitea.
15
14
  *
16
- * Без этого карточка показывала бы дату подхвата: задача годичной давности
17
- * выглядела бы заведённой сегодня, и по дате нельзя было бы понять ничего.
15
+ * Without this, the card would show the import timestamp: a year-old issue
16
+ * would appear created today, losing chronological context.
18
17
  */
19
18
  export function issueBornAt(issue) {
20
19
  const at = Date.parse(issue?.created_at ?? '')
@@ -22,11 +21,10 @@ export function issueBornAt(issue) {
22
21
  }
23
22
 
24
23
  /**
25
- * Кто завёл issue.
24
+ * Issue author.
26
25
  *
27
- * Берём `login`: он же стоит в Gitea под задачей и по нему её ищут. Полное
28
- * имя заполнено не у всех, и подставлять его вместо логина значило бы
29
- * показывать то одно, то другое.
26
+ * Uses `login`: matches what Gitea displays under the issue and search queries.
27
+ * Full name is not set for all users, so using it would cause inconsistencies.
30
28
  */
31
29
  export function issueAuthor(issue) {
32
30
  const login = issue?.user?.login
@@ -34,12 +32,11 @@ export function issueAuthor(issue) {
34
32
  }
35
33
 
36
34
  /**
37
- * Кто отвечает за issue.
35
+ * Issue assignee.
38
36
  *
39
- * Gitea отдаёт назначенного двумя способами: одиночным `assignee` и списком
40
- * `assignees`. Берём первого из списка, когда одиночного нет: доска показывает
41
- * ОДНОГО ответственного, потому что «отвечают все» означает «не отвечает
42
- * никто».
37
+ * Gitea returns assignees in two ways: single `assignee` and list `assignees`.
38
+ * Pick the first from the list if single is absent: board tracks a single
39
+ * responsible assignee.
43
40
  */
44
41
  export function issueAssignee(issue) {
45
42
  const one = issue?.assignee?.login
@@ -50,10 +47,10 @@ export function issueAssignee(issue) {
50
47
  }
51
48
 
52
49
  /**
53
- * К какому выпуску относится issue.
50
+ * Milestone the issue belongs to.
54
51
  *
55
- * Релизы ведут майлстоунами Gitea, а доска о них не знала: «что осталось до
56
- * 0.2.0» приходилось смотреть в другом месте, хотя карточки лежат здесь.
52
+ * Releases are tracked via Gitea milestones; board stores milestone title
53
+ * so release progress can be monitored directly on cards.
57
54
  */
58
55
  export function issueMilestone(issue) {
59
56
  const title = issue?.milestone?.title
@@ -61,13 +58,13 @@ export function issueMilestone(issue) {
61
58
  }
62
59
 
63
60
  /**
64
- * Поля задачи из issue.
61
+ * Map task fields from an issue.
65
62
  *
66
- * `owner` и `repo` берутся из запроса, а не из issue: API Gitea возвращает их
67
- * не во всех формах ответа, и полагаться на это нельзя.
63
+ * `owner` and `repo` come from request context, not the issue payload: Gitea API
64
+ * does not return them in all response shapes.
68
65
  *
69
- * Поля `branch`, `worktree`, `sessionId`, `model` НЕ заполняются: они
70
- * наблюдаются позже, когда работа начнётся. Плагин ветки не создаёт.
66
+ * Fields `branch`, `worktree`, `sessionId`, `model` are NOT populated here:
67
+ * they are assigned later when work begins. The plugin does not create branches on import.
71
68
  */
72
69
  export function issueToTask(issue, { board = 'main', column = 'backlog', owner, repo, provider }) {
73
70
  const p = provider || (typeof issue?.html_url === 'string' && issue.html_url.includes('github.com') ? 'github' : 'gitea')
@@ -80,8 +77,7 @@ export function issueToTask(issue, { board = 'main', column = 'backlog', owner,
80
77
  labels: Array.isArray(issue?.labels)
81
78
  ? issue.labels.map((l) => (typeof l === 'string' ? l : l?.name)).filter((n) => typeof n === 'string')
82
79
  : [],
83
- // Цвета едут рядом с именами, а не вместо них: имена читают отборы,
84
- // сверка, чип и поиск.
80
+ // Colors are kept alongside names for filters, reconciler, chip, and search.
85
81
  labelColors: colorsOfIssue(issue?.labels),
86
82
  owner,
87
83
  repo,
@@ -95,13 +91,10 @@ export function issueToTask(issue, { board = 'main', column = 'backlog', owner,
95
91
  }
96
92
 
97
93
  /**
98
- * Пометить issue, которые уже лежат на доске.
94
+ * Mark issues that already exist on the board.
99
95
  *
100
- * Сравнение идёт по тройке владелец–репозиторий–номер. Сравнение по одному
101
- * номеру пометило бы задачу №1 из чужого репозитория как уже импортированную.
102
- *
103
- * Помеченные не прячутся, а приглушаются: задачу иногда заводят второй раз
104
- * осознанно.
96
+ * Compared by owner-repo-number tuple to avoid colliding with issue #1 in another repo.
97
+ * Existing issues are dimmed rather than hidden, as re-importing may be intentional.
105
98
  */
106
99
  export function markImported(issues, existing, { owner, repo }) {
107
100
  const seen = new Set(
@@ -113,11 +106,10 @@ export function markImported(issues, existing, { owner, repo }) {
113
106
  }
114
107
 
115
108
  /**
116
- * Что переписать в задаче при обновлении из issue.
109
+ * Fields to overwrite when updating a task from an issue.
117
110
  *
118
- * Переписываются только поля, пришедшие из Gitea. Локальные — колонка,
119
- * позиция, сессия, модель — не трогаются: иначе обновление сбрасывало бы
120
- * работу, которая уже идёт.
111
+ * Only fields originating from Gitea are updated. Local fields (column,
112
+ * position, session, model) are preserved to avoid resetting active work.
121
113
  */
122
114
  export function refreshPatch(task, issue) {
123
115
  const patch = {}
@@ -134,24 +126,20 @@ export function refreshPatch(task, issue) {
134
126
  if (born !== undefined) patch.createdAt = born
135
127
  const author = issueAuthor(issue)
136
128
  if (author !== '') patch.author = author
137
- // Снятие ответственного — тоже факт, поэтому пустое значение пишется наравне
138
- // с именем: иначе доска показывала бы вчерашнего владельца задачи, с которой
139
- // его уже сняли.
129
+ // Explicit empty assignee when cleared in Gitea so board does not retain stale assignee.
140
130
  if (issue?.assignee !== undefined || Array.isArray(issue?.assignees)) {
141
131
  patch.assignee = issueAssignee(issue)
142
132
  }
143
- // Майлстоун снимают так же часто, как ставят, поэтому пустое значение —
144
- // тоже новость, а не отсутствие новостей.
133
+ // Milestone removal is also explicitly recorded.
145
134
  if ('milestone' in (issue ?? {})) patch.milestone = issueMilestone(issue)
146
135
  return patch
147
136
  }
148
137
 
149
138
  /**
150
- * Клиент Gitea готов к работе. Отсутствие клиента и ненастроенный доступ —
151
- * разные беды, и человеку они говорят разное.
139
+ * Check if Gitea client is configured and ready.
152
140
  *
153
- * Проверка асинхронная, потому что готовность включает разрешение учётной
154
- * записи с токеном, а это обращение к службе учётных данных.
141
+ * Missing client vs unconfigured access are distinct error states.
142
+ * Async check includes token credential verification.
155
143
  */
156
144
  export async function giteaState(gitea) {
157
145
  if (gitea === undefined || gitea === null) return 'absent'
@@ -163,9 +151,7 @@ export async function giteaState(gitea) {
163
151
  }
164
152
  }
165
153
 
166
- /**
167
- * Список issue для диалога импорта: открытые, уже импортированные помечены.
168
- */
154
+ /** List open issues for import dialog, marking already-imported ones. */
169
155
  export async function listImportable({ gitea, store, owner, repo, board = 'main' }) {
170
156
  const state = await giteaState(gitea)
171
157
  if (state !== 'ready') return { error: `gitea-${state}`, status: 409 }
@@ -174,20 +160,19 @@ export async function listImportable({ gitea, store, owner, repo, board = 'main'
174
160
  return { issues: markImported(issues, existing, { owner, repo }) }
175
161
  }
176
162
 
177
- /** Завести задачу из issue. */
163
+ /** Create a task from an issue. */
178
164
  export async function importIssue({ gitea, store, owner, repo, issueNumber, board = 'main', column = 'backlog' }) {
179
165
  const state = await giteaState(gitea)
180
166
  if (state !== 'ready') return { error: `gitea-${state}`, status: 409 }
181
167
  if (typeof issueNumber !== 'number') return { error: 'issue-number-required', status: 400 }
182
- // Простая доска — для задач без issue. Импорт на неё привёл бы задачу с
183
- // веткой и PR в набор колонок, где их некуда показать.
168
+ // Simple board is for non-issue tasks. Importing here would bring branches/PRs into columns unable to display them.
184
169
  if (store.boardKind(board) !== 'project') return { error: 'board-not-for-issues', status: 400 }
185
170
  const issue = await gitea.getIssue({ owner, repo, index: issueNumber })
186
171
  if (issue === undefined || issue === null) return { error: 'issue-not-found', status: 404 }
187
172
  return { task: store.createTask(issueToTask(issue, { board, column, owner, repo })) }
188
173
  }
189
174
 
190
- /** Перечитать issue и обновить задачу, не трогая локальные поля. */
175
+ /** Re-read issue and update task, preserving local state. */
191
176
  export async function refreshTask({ gitea, store, id }) {
192
177
  const state = await giteaState(gitea)
193
178
  if (state !== 'ready') return { error: `gitea-${state}`, status: 409 }
@@ -199,7 +184,7 @@ export async function refreshTask({ gitea, store, id }) {
199
184
  return { task: store.updateTask(id, refreshPatch(task, issue)) }
200
185
  }
201
186
 
202
- /** Поиск репозиториев для диалога импорта. */
187
+ /** Search repositories for import dialog. */
203
188
  export async function searchRepos({ gitea, query, limit = 20 }) {
204
189
  const state = await giteaState(gitea)
205
190
  if (state !== 'ready') return { error: `gitea-${state}`, status: 409 }
@@ -207,16 +192,11 @@ export async function searchRepos({ gitea, query, limit = 20 }) {
207
192
  }
208
193
 
209
194
  /**
210
- * Завести проектную задачу: issue в Gitea плюс карточка, привязанная к нему.
211
- *
212
- * Репозиторий либо выбирается из существующих, либо заводится по имени.
213
- * Заводит его ЧЕЛОВЕК кнопкой: инструмента у агента для этого нет — он
214
- * пользуется `gitea_issue_create` из соседнего плагина, а карточка приезжает
215
- * подхватом.
195
+ * Create a project task: Gitea issue plus linked board card.
216
196
  *
217
- * Порядок неслучаен: сперва репозиторий, потом issue, потом карточка. Обрыв на
218
- * середине не должен оставить карточку привязанной к репозиторию без issue —
219
- * лучше не завести карточку вовсе и сказать об этом, чем завести полупустую.
197
+ * Repository is selected from existing list or created by name.
198
+ * Order of operations: repo first, then issue, then board card.
199
+ * Failure midway avoids leaving orphaned cards.
220
200
  */
221
201
  export async function createProjectTask({
222
202
  gitea, store, owner, repo, newRepo, title, body, board = 'main',
@@ -231,7 +211,7 @@ export async function createProjectTask({
231
211
  if (targetRepo === '') {
232
212
  const wanted = typeof newRepo === 'string' ? newRepo.trim() : ''
233
213
  if (wanted === '') return { error: 'repo-required', status: 400 }
234
- // Имя проверяем ДО запроса: отказ Gitea пришёл бы позже и невнятнее.
214
+ // Validate repository name before API call.
235
215
  if (safeSegment(wanted) === undefined) return { error: 'bad-repo-name', status: 400 }
236
216
  try {
237
217
  await gitea.createRepo({ owner, name: wanted })
@@ -245,8 +225,7 @@ export async function createProjectTask({
245
225
  try {
246
226
  issue = await gitea.createIssue({ owner, repo: targetRepo, title: name, body })
247
227
  } catch (error) {
248
- // Репозиторий, возможно, уже создан. Не удаляем его: удаление чужого
249
- // добра из-за своей неудачи хуже лишнего пустого репозитория.
228
+ // Repository may have already been created; avoid deleting on issue creation failure.
250
229
  return { error: 'issue-not-created', status: 502, detail: String(error?.message ?? error) }
251
230
  }
252
231