@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/board-tool.js +18 -48
- package/lib/client.js +85 -176
- package/lib/commands.js +21 -38
- package/lib/config.js +29 -54
- package/lib/cron.js +13 -13
- package/lib/events.js +1 -1
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +51 -109
- package/lib/github.js +27 -31
- package/lib/import.js +46 -67
- package/lib/index.js +88 -137
- package/lib/intake.js +23 -31
- package/lib/launcher.js +100 -167
- package/lib/lifecycle.js +11 -17
- package/lib/markdown.js +16 -23
- package/lib/metrics.js +23 -25
- package/lib/order.js +25 -28
- package/lib/outbound.js +25 -26
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +86 -145
- package/lib/store.js +59 -78
- package/lib/sync.js +56 -98
- package/lib/templates.js +29 -29
- package/lib/time.js +8 -17
- package/lib/transitions.js +36 -57
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +58 -58
- package/lib/worktree.js +58 -58
- package/package.json +1 -1
package/lib/filters.js
CHANGED
|
@@ -1,58 +1,53 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Board filters: repository and label namespaces.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// из того, что реально пришло на карточки. Зашитый список начал бы врать при
|
|
6
|
-
// первой же правке разметки.
|
|
3
|
+
// Namespaces are dynamically collected from task cards rather than hardcoded,
|
|
4
|
+
// as Gitea labels may evolve independently without requiring code changes.
|
|
7
5
|
|
|
8
|
-
/**
|
|
6
|
+
/** Repository filter sits alongside labels without requiring a label tag. */
|
|
9
7
|
export const REPO = 'repo'
|
|
10
8
|
|
|
11
|
-
/**
|
|
9
|
+
/** Task author — filter dimension stored in its own field. */
|
|
12
10
|
export const AUTHOR = 'author'
|
|
13
11
|
|
|
14
|
-
/**
|
|
12
|
+
/** Assignee: who is working on the task, distinct from author. */
|
|
15
13
|
export const ASSIGNEE = 'assignee'
|
|
16
14
|
|
|
17
|
-
/**
|
|
15
|
+
/** Milestone assigned in Gitea. */
|
|
18
16
|
export const MILESTONE = 'milestone'
|
|
19
17
|
|
|
20
|
-
/**
|
|
18
|
+
/** Priority — local board property and filter dimension. */
|
|
21
19
|
export const PRIORITY = 'priority'
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* Value for tasks without an author.
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* ответ, и по нему тоже отбирают.
|
|
24
|
+
* Local board tasks have no author. Keeping an explicit placeholder ensures
|
|
25
|
+
* filtering covers all tasks rather than silently omitting authorless items.
|
|
29
26
|
*/
|
|
30
27
|
export const NO_AUTHOR = '—'
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
|
-
*
|
|
30
|
+
* Value for unassigned tasks.
|
|
34
31
|
*
|
|
35
|
-
*
|
|
36
|
-
* спрашивают «что свободно».
|
|
32
|
+
* Filtering by unassigned is a primary board workflow for finding available work.
|
|
37
33
|
*/
|
|
38
34
|
export const NOBODY = '—'
|
|
39
35
|
|
|
40
|
-
/**
|
|
36
|
+
/** Value for tasks not assigned to any milestone. */
|
|
41
37
|
export const NO_MILESTONE = '—'
|
|
42
38
|
|
|
43
|
-
/**
|
|
39
|
+
/** Priority ordering in filter UI: urgent at the top. */
|
|
44
40
|
const PRIORITY_ORDER = ['critical', 'high', 'medium', 'low']
|
|
45
41
|
|
|
46
42
|
/**
|
|
47
|
-
*
|
|
43
|
+
* Preferred namespace ordering.
|
|
48
44
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* заведут в Gitea новое — оно появится само, без правки кода.
|
|
45
|
+
* Prioritizes type and urgency over raw alphabetical order. Unknown namespaces
|
|
46
|
+
* are appended in alphabetical order.
|
|
52
47
|
*/
|
|
53
48
|
export const KNOWN_ORDER = ['type', 'priority', 'status', 'scope', 'risk', 'signal', 'release']
|
|
54
49
|
|
|
55
|
-
/**
|
|
50
|
+
/** Split label into namespace and value; returns undefined if no slash delimiter. */
|
|
56
51
|
export function splitLabel(name) {
|
|
57
52
|
const text = String(name ?? '')
|
|
58
53
|
const at = text.indexOf('/')
|
|
@@ -61,15 +56,13 @@ export function splitLabel(name) {
|
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
/**
|
|
64
|
-
*
|
|
59
|
+
* Build available filter dimensions and value counts.
|
|
65
60
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* поиском.
|
|
61
|
+
* Bare labels without a namespace (e.g. epic, hotfix) do not generate separate
|
|
62
|
+
* filter groups; they remain visible on cards and searchable via text query.
|
|
69
63
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* после моего же выбора».
|
|
64
|
+
* Counts are calculated across all provided tasks rather than currently filtered
|
|
65
|
+
* tasks so the badge reflects the total pool size.
|
|
73
66
|
*
|
|
74
67
|
* @returns {Array<{ns: string, values: Array<{value: string, count: number}>}>}
|
|
75
68
|
*/
|
|
@@ -87,9 +80,7 @@ export function facetsOf(tasks) {
|
|
|
87
80
|
add(ASSIGNEE, typeof task?.assignee === 'string' && task.assignee !== '' ? task.assignee : NOBODY)
|
|
88
81
|
add(MILESTONE, typeof task?.milestone === 'string' && task.milestone !== '' ? task.milestone : NO_MILESTONE)
|
|
89
82
|
add(AUTHOR, typeof task?.author === 'string' && task.author !== '' ? task.author : NO_AUTHOR)
|
|
90
|
-
//
|
|
91
|
-
// из Gitea). Оба попадают в одно измерение — отбор по срочности не должен
|
|
92
|
-
// зависеть от того, откуда пришло значение.
|
|
83
|
+
// Priority lives both in task.priority and in priority/* label: merge into one dimension.
|
|
93
84
|
add(PRIORITY, typeof task?.priority === 'string' ? task.priority : '')
|
|
94
85
|
for (const name of task?.labels ?? []) {
|
|
95
86
|
const parsed = splitLabel(name)
|
|
@@ -107,14 +98,12 @@ export function facetsOf(tasks) {
|
|
|
107
98
|
.sort((a, b) => rank(a.ns) - rank(b.ns) || a.ns.localeCompare(b.ns))
|
|
108
99
|
}
|
|
109
100
|
|
|
110
|
-
/**
|
|
101
|
+
/** Repository first, followed by known namespaces, then alphabetical. */
|
|
111
102
|
function rank(ns) {
|
|
112
103
|
if (ns === REPO) return -3
|
|
113
|
-
//
|
|
114
|
-
// завёл».
|
|
104
|
+
// Assignee precedes author: users look for active owner more frequently than creator.
|
|
115
105
|
if (ns === ASSIGNEE) return -2
|
|
116
|
-
//
|
|
117
|
-
// тогда же, когда «кто делает».
|
|
106
|
+
// Milestone immediately follows assignee.
|
|
118
107
|
if (ns === MILESTONE) return -1.5
|
|
119
108
|
if (ns === AUTHOR) return -1
|
|
120
109
|
const at = KNOWN_ORDER.indexOf(ns)
|
|
@@ -122,10 +111,9 @@ function rank(ns) {
|
|
|
122
111
|
}
|
|
123
112
|
|
|
124
113
|
/**
|
|
125
|
-
*
|
|
114
|
+
* Value ordering within a namespace.
|
|
126
115
|
*
|
|
127
|
-
*
|
|
128
|
-
* в самом низу. Для остальных — обычное сравнение строк.
|
|
116
|
+
* Priority uses urgency ordering (critical -> low); other namespaces use standard string comparison.
|
|
129
117
|
*/
|
|
130
118
|
function valueRank(ns, value) {
|
|
131
119
|
if (ns !== PRIORITY) return 0
|
|
@@ -134,14 +122,11 @@ function valueRank(ns, value) {
|
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
/**
|
|
137
|
-
*
|
|
125
|
+
* Check whether a task satisfies the active filter selection.
|
|
138
126
|
*
|
|
139
|
-
*
|
|
140
|
-
* `priority/high` И `type/bug`; а выбор `high` вместе с `critical` означает
|
|
141
|
-
* «любая из двух», потому что срочность у задачи одна и «И» внутри
|
|
142
|
-
* пространства всегда давало бы пусто.
|
|
127
|
+
* AND between namespaces, OR within each namespace.
|
|
143
128
|
*
|
|
144
|
-
* @param {object} selected
|
|
129
|
+
* @param {object} selected map of namespace -> array of selected values
|
|
145
130
|
*/
|
|
146
131
|
export function matchesFilters(task, selected) {
|
|
147
132
|
for (const [ns, values] of Object.entries(selected ?? {})) {
|
|
@@ -167,8 +152,7 @@ function hasAny(task, ns, wanted) {
|
|
|
167
152
|
return wanted.includes(author)
|
|
168
153
|
}
|
|
169
154
|
if (ns === PRIORITY) {
|
|
170
|
-
//
|
|
171
|
-
// зависеть от источника значения.
|
|
155
|
+
// Check both field value and priority/* label.
|
|
172
156
|
if (wanted.includes(typeof task?.priority === 'string' ? task.priority : '')) return true
|
|
173
157
|
for (const name of task?.labels ?? []) {
|
|
174
158
|
const parsed = splitLabel(name)
|
|
@@ -183,16 +167,15 @@ function hasAny(task, ns, wanted) {
|
|
|
183
167
|
return false
|
|
184
168
|
}
|
|
185
169
|
|
|
186
|
-
/**
|
|
170
|
+
/** True if any filter value is currently selected. */
|
|
187
171
|
export function anySelected(selected) {
|
|
188
172
|
return Object.values(selected ?? {}).some((values) => (values ?? []).length > 0)
|
|
189
173
|
}
|
|
190
174
|
|
|
191
175
|
/**
|
|
192
|
-
*
|
|
176
|
+
* Toggle a single filter value.
|
|
193
177
|
*
|
|
194
|
-
*
|
|
195
|
-
* React, и правка на месте не вызвала бы перерисовку.
|
|
178
|
+
* Returns a new object map for React state immutability.
|
|
196
179
|
*/
|
|
197
180
|
export function toggleValue(selected, ns, value) {
|
|
198
181
|
const current = (selected ?? {})[ns] ?? []
|
|
@@ -205,19 +188,17 @@ export function toggleValue(selected, ns, value) {
|
|
|
205
188
|
return out
|
|
206
189
|
}
|
|
207
190
|
|
|
208
|
-
/**
|
|
191
|
+
/** Reset all active filters. */
|
|
209
192
|
export function clearFilters() {
|
|
210
193
|
return {}
|
|
211
194
|
}
|
|
212
195
|
|
|
213
196
|
/**
|
|
214
|
-
*
|
|
197
|
+
* Extract task labels grouped by namespace.
|
|
215
198
|
*
|
|
216
|
-
*
|
|
217
|
-
* позвать этот модуль не может, а разбор, написанный там во второй раз,
|
|
218
|
-
* однажды разойдётся с этим.
|
|
199
|
+
* Included on task payload so client bundle does not need to duplicate parsing logic.
|
|
219
200
|
*
|
|
220
|
-
* @returns {object}
|
|
201
|
+
* @returns {object} map of namespace -> array of values
|
|
221
202
|
*/
|
|
222
203
|
export function facetsOfTask(task) {
|
|
223
204
|
const out = {}
|
|
@@ -231,13 +212,12 @@ export function facetsOfTask(task) {
|
|
|
231
212
|
}
|
|
232
213
|
|
|
233
214
|
/**
|
|
234
|
-
*
|
|
215
|
+
* Validate hex color code from Gitea response.
|
|
235
216
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* значение попадает прямо в стиль, и пускать туда что попало нельзя.
|
|
217
|
+
* Accepts 6-character hex string with or without leading '#'.
|
|
218
|
+
* Rejects invalid strings to prevent arbitrary values in CSS styles.
|
|
239
219
|
*
|
|
240
|
-
* @returns {string|undefined}
|
|
220
|
+
* @returns {string|undefined} lowercase 6-char hex code without '#'
|
|
241
221
|
*/
|
|
242
222
|
export function labelColor(value) {
|
|
243
223
|
const text = String(value ?? '').trim().replace(/^#/, '')
|
|
@@ -245,10 +225,9 @@ export function labelColor(value) {
|
|
|
245
225
|
}
|
|
246
226
|
|
|
247
227
|
/**
|
|
248
|
-
*
|
|
228
|
+
* Map label name -> hex color from issue labels.
|
|
249
229
|
*
|
|
250
|
-
*
|
|
251
|
-
* их устройство ради оформления было бы дорого и ни к чему.
|
|
230
|
+
* Stored separately from label names to keep name parsing and matching clean.
|
|
252
231
|
*/
|
|
253
232
|
export function colorsOfIssue(labels) {
|
|
254
233
|
const out = {}
|
|
@@ -262,13 +241,10 @@ export function colorsOfIssue(labels) {
|
|
|
262
241
|
}
|
|
263
242
|
|
|
264
243
|
/**
|
|
265
|
-
*
|
|
244
|
+
* Task rank for top-of-column sorting.
|
|
266
245
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* Ручной порядок не переписывается — внутри каждой ступени он сохраняется, а
|
|
271
|
-
* подъём работает поверх него.
|
|
246
|
+
* Waiting tasks take precedence over running tasks because waiting items require user action.
|
|
247
|
+
* Manual ordering within the same rank tier is preserved.
|
|
272
248
|
*/
|
|
273
249
|
export function liveRank(task) {
|
|
274
250
|
const state = task?.state
|
|
@@ -278,17 +254,18 @@ export function liveRank(task) {
|
|
|
278
254
|
return 3
|
|
279
255
|
}
|
|
280
256
|
|
|
281
|
-
/**
|
|
257
|
+
/** Priority weight for rank sorting: urgent precedes normal. */
|
|
282
258
|
function priorityWeight(priority) {
|
|
283
259
|
const at = PRIORITY_ORDER.indexOf(priority)
|
|
284
260
|
return at === -1 ? PRIORITY_ORDER.length : at
|
|
285
261
|
}
|
|
286
262
|
|
|
287
263
|
/**
|
|
288
|
-
*
|
|
264
|
+
* Sort tasks by active state and priority while preserving relative manual order.
|
|
289
265
|
*
|
|
290
|
-
*
|
|
291
|
-
* (critical
|
|
266
|
+
* Primary sort: lifecycle activity (waiting -> running -> stopped -> idle).
|
|
267
|
+
* Secondary sort: urgency (critical -> low).
|
|
268
|
+
* Tertiary sort: original array position.
|
|
292
269
|
*/
|
|
293
270
|
export function sortByLife(tasks) {
|
|
294
271
|
return (tasks ?? [])
|
|
@@ -300,11 +277,10 @@ export function sortByLife(tasks) {
|
|
|
300
277
|
}
|
|
301
278
|
|
|
302
279
|
/**
|
|
303
|
-
*
|
|
280
|
+
* Group tasks by repository.
|
|
304
281
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* обновлениями.
|
|
282
|
+
* Larger groups are placed first to keep active projects visible.
|
|
283
|
+
* Tied group sizes are sorted alphabetically by repo name.
|
|
308
284
|
*
|
|
309
285
|
* @returns {Array<{repo: string, tasks: Array<object>}>}
|
|
310
286
|
*/
|
package/lib/git-provider.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Composite router for Git providers (Gitea and GitHub).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// -
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
3
|
+
// Exposes unified interface across both platforms:
|
|
4
|
+
// - routes task queries to appropriate provider (task.provider / issueUrl);
|
|
5
|
+
// - in 'auto' mode queries configured provider or merges search results;
|
|
6
|
+
// - supports Gitea-only, GitHub-only, or simultaneous dual-forge operations.
|
|
7
7
|
|
|
8
8
|
export function createGitProvider({ gitea, github, getConfig }) {
|
|
9
9
|
async function resolveActiveProvider(hint) {
|
|
@@ -23,7 +23,7 @@ export function createGitProvider({ gitea, github, getConfig }) {
|
|
|
23
23
|
if (mode === 'github') return github
|
|
24
24
|
if (mode === 'gitea') return gitea
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// In 'auto' mode: check provider availability
|
|
27
27
|
const giteaReady = await gitea.isConfigured()
|
|
28
28
|
const githubReady = await github.isConfigured()
|
|
29
29
|
if (githubReady && !giteaReady) return github
|
|
@@ -40,10 +40,10 @@ export function createGitProvider({ gitea, github, getConfig }) {
|
|
|
40
40
|
gitea,
|
|
41
41
|
github,
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/** Resolve provider for task or context. */
|
|
44
44
|
resolve: resolveActiveProvider,
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** Whether any or specific provider is configured. */
|
|
47
47
|
async isConfigured(providerName) {
|
|
48
48
|
if (providerName === 'github') return github.isConfigured()
|
|
49
49
|
if (providerName === 'gitea') return gitea.isConfigured()
|
|
@@ -52,81 +52,81 @@ export function createGitProvider({ gitea, github, getConfig }) {
|
|
|
52
52
|
return github.isConfigured()
|
|
53
53
|
},
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/** Current user login. */
|
|
56
56
|
async me(target) {
|
|
57
57
|
const p = forTarget(target) || await resolveActiveProvider(target)
|
|
58
58
|
return p.me()
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/** List issues. */
|
|
62
62
|
async listIssues(options) {
|
|
63
63
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
64
64
|
return p.listIssues(options)
|
|
65
65
|
},
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** Get issue by number. */
|
|
68
68
|
async getIssue(options) {
|
|
69
69
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
70
70
|
return p.getIssue(options)
|
|
71
71
|
},
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/** Create issue. */
|
|
74
74
|
async createIssue(options) {
|
|
75
75
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
76
76
|
return p.createIssue(options)
|
|
77
77
|
},
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/** Close issue. */
|
|
80
80
|
async closeIssue(options) {
|
|
81
81
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
82
82
|
return p.closeIssue(options)
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
-
/**
|
|
85
|
+
/** Comment on issue. */
|
|
86
86
|
async comment(options) {
|
|
87
87
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
88
88
|
return p.comment(options)
|
|
89
89
|
},
|
|
90
90
|
|
|
91
|
-
/**
|
|
91
|
+
/** Assign responsible users. */
|
|
92
92
|
async setAssignees(options) {
|
|
93
93
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
94
94
|
return p.setAssignees(options)
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
-
/**
|
|
97
|
+
/** List Pull Requests. */
|
|
98
98
|
async listPulls(options) {
|
|
99
99
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
100
100
|
return p.listPulls(options)
|
|
101
101
|
},
|
|
102
102
|
|
|
103
|
-
/**
|
|
103
|
+
/** Create Pull Request. */
|
|
104
104
|
async createPullRequest(options) {
|
|
105
105
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
106
106
|
return p.createPullRequest(options)
|
|
107
107
|
},
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/** List repository branches. */
|
|
110
110
|
async listBranches(options) {
|
|
111
111
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
112
112
|
return p.listBranches(options)
|
|
113
113
|
},
|
|
114
114
|
|
|
115
|
-
/**
|
|
115
|
+
/** List organizations. */
|
|
116
116
|
async listOrgs(target) {
|
|
117
117
|
const p = forTarget(target) || await resolveActiveProvider(target)
|
|
118
118
|
return p.listOrgs()
|
|
119
119
|
},
|
|
120
120
|
|
|
121
|
-
/**
|
|
121
|
+
/** Organization repositories. */
|
|
122
122
|
async listOrgRepos(options) {
|
|
123
123
|
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
124
124
|
return p.listOrgRepos(options)
|
|
125
125
|
},
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* Search repositories: queries target provider or aggregates results
|
|
129
|
+
* from both if enabled.
|
|
130
130
|
*/
|
|
131
131
|
async searchRepos(options) {
|
|
132
132
|
const target = forTarget(options?.provider)
|
|
@@ -138,7 +138,7 @@ export function createGitProvider({ gitea, github, getConfig }) {
|
|
|
138
138
|
if (mode === 'github') return github.searchRepos(options)
|
|
139
139
|
if (mode === 'gitea') return gitea.searchRepos(options)
|
|
140
140
|
|
|
141
|
-
//
|
|
141
|
+
// If 'auto' / 'both' mode, query both configured providers
|
|
142
142
|
const [giteaConfigured, githubConfigured] = await Promise.all([
|
|
143
143
|
gitea.isConfigured(),
|
|
144
144
|
github.isConfigured(),
|