@goodandready/dsh-kanban 0.2.13 → 0.2.15

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/routes.js CHANGED
@@ -402,12 +402,18 @@ export function taskLog({ store, id }) {
402
402
  }
403
403
 
404
404
  /** Задача, привязанная к сессии; отсутствие задачи — штатный ответ. */
405
- export function taskBySession({ store, sessionId }) {
405
+ export function taskBySession({ store, sessionId, agents }) {
406
406
  const task = store.findTaskBySession(sessionId)
407
407
  if (task === undefined) return { task: null }
408
+ let live
409
+ try { live = agents?.get?.(sessionId) } catch { live = undefined }
408
410
  // Вид доски едет вместе с задачей: чип предлагает колонки, а предлагать
409
411
  // `deploy` задаче с простой доски значит звать в колонку, которой нет.
410
- return { task, columns: columnsOf(store.boardKind(task.board)) }
412
+ return {
413
+ task,
414
+ columns: columnsOf(store.boardKind(task.board)),
415
+ canRevive: canRevive(task, live),
416
+ }
411
417
  }
412
418
 
413
419
  /**
@@ -443,34 +449,42 @@ export function importBoard({ store, input }) {
443
449
  let imported = 0
444
450
  let skipped = 0
445
451
 
446
- for (const board of Array.isArray(input.boards) ? input.boards : []) {
447
- if (typeof board?.id !== 'string' || board.id === '') continue
448
- store.createBoard({ id: board.id, title: board.title, kind: board.kind })
449
- }
450
-
451
- for (const task of input.tasks) {
452
- if (typeof task?.id !== 'string' || task.id === '') continue
453
- if (store.getTask(task.id) !== undefined) { skipped += 1; continue }
454
- store.createTask({
455
- id: task.id,
456
- board: task.board ?? 'main',
457
- column: task.column ?? 'backlog',
458
- title: task.title ?? '',
459
- body: task.body ?? '',
460
- owner: task.owner, repo: task.repo,
461
- issueNumber: task.issueNumber, issueUrl: task.issueUrl,
462
- labels: Array.isArray(task.labels) ? task.labels : [],
463
- branch: task.branch, worktree: task.worktree,
464
- sessionId: task.sessionId, model: task.model, provider: task.provider,
465
- waiting: task.waiting === true,
466
- plan: task.plan ?? '',
467
- labelColors: task.labelColors ?? {},
468
- author: task.author ?? '',
469
- priority: task.priority ?? '',
470
- dueAt: typeof task.dueAt === 'number' ? task.dueAt : 0,
471
- createdAt: task.createdAt,
472
- })
473
- imported += 1
452
+ const doImport = () => {
453
+ for (const board of Array.isArray(input.boards) ? input.boards : []) {
454
+ if (typeof board?.id !== 'string' || board.id === '') continue
455
+ store.createBoard({ id: board.id, title: board.title, kind: board.kind })
456
+ }
457
+
458
+ for (const task of input.tasks) {
459
+ if (typeof task?.id !== 'string' || task.id === '') continue
460
+ if (store.getTask(task.id) !== undefined) { skipped += 1; continue }
461
+ store.createTask({
462
+ id: task.id,
463
+ board: task.board ?? 'main',
464
+ column: task.column ?? 'backlog',
465
+ title: task.title ?? '',
466
+ body: task.body ?? '',
467
+ owner: task.owner, repo: task.repo,
468
+ issueNumber: task.issueNumber, issueUrl: task.issueUrl,
469
+ labels: Array.isArray(task.labels) ? task.labels : [],
470
+ branch: task.branch, worktree: task.worktree,
471
+ sessionId: task.sessionId, model: task.model, provider: task.provider,
472
+ waiting: task.waiting === true,
473
+ plan: task.plan ?? '',
474
+ labelColors: task.labelColors ?? {},
475
+ author: task.author ?? '',
476
+ priority: task.priority ?? '',
477
+ dueAt: typeof task.dueAt === 'number' ? task.dueAt : 0,
478
+ createdAt: task.createdAt,
479
+ })
480
+ imported += 1
481
+ }
482
+ }
483
+
484
+ if (typeof store.withTransaction === 'function') {
485
+ store.withTransaction(doImport)
486
+ } else {
487
+ doImport()
474
488
  }
475
489
 
476
490
  return { imported, skipped, backup: backupMeta?.backup }
package/lib/store.js CHANGED
@@ -106,7 +106,7 @@ const WRITABLE = [
106
106
  ]
107
107
 
108
108
  /** Источники перехода; чужое значение не пишем, чтобы журнал оставался читаемым. */
109
- const SOURCES = new Set(['manual', 'session', 'gitea', 'tool'])
109
+ const SOURCES = new Set(['manual', 'session', 'gitea', 'github', 'tool'])
110
110
 
111
111
  function rowToTask(row) {
112
112
  if (row === undefined) return undefined
@@ -205,6 +205,7 @@ export function openStore({ dir }) {
205
205
  ['parentId', "TEXT NOT NULL DEFAULT ''"],
206
206
  ['isEpic', "INTEGER NOT NULL DEFAULT 0"],
207
207
  ['externalRef', "TEXT"],
208
+ ['provider', "TEXT NOT NULL DEFAULT 'gitea'"],
208
209
  ]) {
209
210
  try { db.exec(`ALTER TABLE tasks ADD COLUMN ${column} ${definition}`) } catch { /* столбец уже есть */ }
210
211
  }
@@ -220,11 +221,20 @@ export function openStore({ dir }) {
220
221
  db.exec('CREATE INDEX IF NOT EXISTS tasks_cron_next ON tasks(archivedAt, nextRunAt)')
221
222
  db.exec('CREATE INDEX IF NOT EXISTS tasks_queued ON tasks(archivedAt, queuedAt)')
222
223
  db.exec('CREATE INDEX IF NOT EXISTS tasks_parent_id ON tasks(parentId)')
224
+ db.exec('CREATE INDEX IF NOT EXISTS tasks_updated ON tasks(updatedAt)')
225
+ db.exec('CREATE INDEX IF NOT EXISTS tasks_archivable ON tasks(archivedAt, col, columnAt)')
223
226
  db.exec('PRAGMA synchronous = NORMAL')
224
227
  try { db.exec('PRAGMA optimize') } catch { /* pragma optimize best-effort */ }
225
228
 
226
229
  const q = {
227
230
  boards: db.prepare('SELECT * FROM boards ORDER BY position'),
231
+ insertTask: db.prepare(`INSERT INTO tasks
232
+ (id, board, col, position, title, body, owner, repo, issueNumber, issueUrl, externalRef,
233
+ labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
234
+ labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
235
+ cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
236
+ checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
228
238
  boardById: db.prepare('SELECT * FROM boards WHERE id = ?'),
229
239
  insertBoard: db.prepare('INSERT INTO boards (id, title, kind, position, createdAt) VALUES (?, ?, ?, ?, ?)'),
230
240
  lastBoard: db.prepare('SELECT position FROM boards ORDER BY position DESC LIMIT 1'),
@@ -285,6 +295,18 @@ export function openStore({ dir }) {
285
295
  }
286
296
 
287
297
  return {
298
+ withTransaction(fn) {
299
+ db.exec('BEGIN IMMEDIATE')
300
+ try {
301
+ const res = fn()
302
+ db.exec('COMMIT')
303
+ return res
304
+ } catch (err) {
305
+ db.exec('ROLLBACK')
306
+ throw err
307
+ }
308
+ },
309
+
288
310
  backupDatabase() {
289
311
  const backupsDir = join(dir, 'backups')
290
312
  mkdirSync(backupsDir, { recursive: true })
@@ -378,13 +400,7 @@ export function openStore({ dir }) {
378
400
  // генерирует новый.
379
401
  const id = typeof input.id === 'string' && input.id !== '' ? input.id : randomUUID()
380
402
  const position = positionFor(board, column, input.beforeId, input.afterId)
381
- db.prepare(`INSERT INTO tasks
382
- (id, board, col, position, title, body, owner, repo, issueNumber, issueUrl, externalRef,
383
- labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
384
- labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
385
- cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
386
- checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic)
387
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
403
+ q.insertTask.run(
388
404
  id, board, column, position,
389
405
  input.title ?? '', input.body ?? '',
390
406
  input.owner ?? null, input.repo ?? null,
package/lib/time.js CHANGED
@@ -40,6 +40,7 @@ export function isStale({ now, updatedAt, sessionId, afterMinutes }) {
40
40
  const limit = Number(afterMinutes)
41
41
  if (!Number.isFinite(limit) || limit <= 0) return false
42
42
  if (typeof sessionId !== 'string' || sessionId === '') return false
43
- if (typeof updatedAt !== 'number' || !Number.isFinite(updatedAt)) return false
43
+ const parts = relativeParts(now, updatedAt)
44
+ if (!parts) return false
44
45
  return now - updatedAt >= limit * MINUTE
45
46
  }
package/lib/webhook.js CHANGED
@@ -1,4 +1,4 @@
1
- // Приём событий Gitea.
1
+ // Приём событий Gitea и GitHub.
2
2
  //
3
3
  // Вебхук даёт мгновенную сверку вместо ожидания следующего опроса. Опрос при
4
4
  // этом остаётся: вебхук может быть не настроен, недоступен снаружи или просто
@@ -9,7 +9,7 @@
9
9
  import { createHmac, timingSafeEqual } from 'node:crypto'
10
10
 
11
11
  /**
12
- * Проверить подпись доставки.
12
+ * Проверить подпись доставки (поддерживает Gitea X-Gitea-Signature и GitHub X-Hub-Signature-256).
13
13
  *
14
14
  * Сравнение постоянного времени: обычное посимвольное сравнение по времени
15
15
  * ответа выдаёт, сколько первых байтов угадано, и подпись подбирается за
@@ -17,16 +17,33 @@ import { createHmac, timingSafeEqual } from 'node:crypto'
17
17
  *
18
18
  * @param {string} secret общий секрет
19
19
  * @param {string|Buffer} body сырое тело запроса
20
- * @param {string} signature значение заголовка X-Gitea-Signature
20
+ * @param {string} signature значение заголовка X-Gitea-Signature или X-Hub-Signature-256
21
21
  */
22
22
  export function verifySignature(secret, body, signature) {
23
23
  if (!secret) return false
24
- const provided = String(signature ?? '').trim().toLowerCase()
24
+ let provided = String(signature ?? '').trim().toLowerCase()
25
+ if (provided.startsWith('sha256=')) {
26
+ provided = provided.slice(7).trim()
27
+ }
25
28
  if (!/^[0-9a-f]{64}$/.test(provided)) return false
26
29
  const expected = createHmac('sha256', secret).update(body ?? '').digest('hex')
27
30
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'))
28
31
  }
29
32
 
33
+ /**
34
+ * Определить провайдера вебхука по входящим заголовкам запроса.
35
+ *
36
+ * @param {object} headers заголовки HTTP-запроса
37
+ * @returns {'github'|'gitea'}
38
+ */
39
+ export function detectWebhookProvider(headers) {
40
+ if (!headers || typeof headers !== 'object') return 'gitea'
41
+ if (headers['x-github-event'] || headers['x-hub-signature-256']) {
42
+ return 'github'
43
+ }
44
+ return 'gitea'
45
+ }
46
+
30
47
  /**
31
48
  * Что за задачу затронуло событие.
32
49
  *
package/lib/worktree.js CHANGED
@@ -168,7 +168,12 @@ export async function createTaskWorktree({
168
168
  env,
169
169
  cwdOf,
170
170
  }) {
171
- if (!repoDir) return undefined
171
+ if (!repoDir) {
172
+ if (task?.workspacePath) {
173
+ return prepareMultiRepoMirror({ task, workspacePath: task.workspacePath, gitRunner, config })
174
+ }
175
+ return undefined
176
+ }
172
177
  const isGit = await isGitRepo(repoDir, gitRunner)
173
178
  if (!isGit) return undefined
174
179
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-kanban",
3
- "version": "0.2.13",
4
- "description": "Visual Kanban board for DeepSeek Harness: Gitea-backed tasks, workflow columns, and a dedicated agent session per task.",
3
+ "version": "0.2.15",
4
+ "description": "Visual Kanban board for DeepSeek Harness: Gitea and GitHub-backed tasks, workflow columns, and a dedicated agent session per task.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -27,6 +27,7 @@
27
27
  "dsh-plugin",
28
28
  "kanban",
29
29
  "gitea",
30
+ "github",
30
31
  "tasks",
31
32
  "agents",
32
33
  "deepseek-harness"