@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/README.md +7 -3
- package/README.ru.md +7 -3
- package/README.zh.md +4 -0
- package/lib/client.js +124 -23
- package/lib/commands.js +1 -0
- package/lib/config.js +8 -0
- package/lib/git-provider.js +156 -0
- package/lib/gitea.js +3 -1
- package/lib/github.js +302 -0
- package/lib/import.js +3 -1
- package/lib/index.js +89 -40
- package/lib/launcher.js +1 -1
- package/lib/markdown.js +1 -1
- package/lib/routes.js +44 -30
- package/lib/store.js +24 -8
- package/lib/time.js +2 -1
- package/lib/webhook.js +21 -4
- package/lib/worktree.js +6 -1
- package/package.json +3 -2
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 {
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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"
|