@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/README.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
Managing concurrent software tasks across multiple autonomous AI agents easily leads to context drift, lost pull request handoffs, and untracked branch lifecycles. Without a dedicated visual board, users must manually juggle disparate chat windows and verify issue states on remote VCS trackers.
|
|
41
41
|
|
|
42
|
-
**`@goodandready/dsh-kanban`** introduces an interactive **Kanban Board** embedded natively into DeepSeek Harness. Every card represents a concrete development task linked to its own **dedicated agent session**. Moving a card between workflow columns automatically dispatches lifecycle instructions directly to the agent (e.g. implementing, opening PRs, deploying, or cleaning up worktrees), with continuous **2-way Gitea
|
|
42
|
+
**`@goodandready/dsh-kanban`** introduces an interactive **Kanban Board** embedded natively into DeepSeek Harness. Every card represents a concrete development task linked to its own **dedicated agent session**. Moving a card between workflow columns automatically dispatches lifecycle instructions directly to the agent (e.g. implementing, opening PRs, deploying, or cleaning up worktrees), with continuous **2-way Gitea, Forgejo, and GitHub synchronization**.
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -64,7 +64,7 @@ graph LR
|
|
|
64
64
|
Agent2["Agent: Task #53<br/>(PR Review)"]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
subgraph VCS ["Gitea /
|
|
67
|
+
subgraph VCS ["Git Forge (Gitea / GitHub)"]
|
|
68
68
|
GiteaIssues["Issues & Milestones"]
|
|
69
69
|
GiteaPRs["Pull Requests & Branches"]
|
|
70
70
|
Webhook["Event Webhook (/dsh-kanban/webhook)"]
|
|
@@ -109,7 +109,7 @@ Unlike passive task boards, `dsh-kanban` actively drives the agents:
|
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
-
### 3. Continuous 2-Way Gitea / Forgejo Sync
|
|
112
|
+
### 3. Continuous 2-Way Gitea / Forgejo & GitHub Sync
|
|
113
113
|
|
|
114
114
|
* **Bi-directional Reconciliation**: Polling and Webhook receiver (`/dsh-kanban/webhook`) sync issue descriptions, comments, PR status, labels, and closed states.
|
|
115
115
|
* **Conflict Resolution**: Timestamp-based merge logic ensures manual edits in Gitea and board moves resolve cleanly without state clobbering.
|
|
@@ -207,6 +207,10 @@ dsh-kanban:
|
|
|
207
207
|
| Key | Type | Default | Description |
|
|
208
208
|
|:---|:---|:---|:---|
|
|
209
209
|
| `boardKind` | `string` | `"project"` | Board workflow mode: `"project"` (6 columns) or `"simple"` (4 columns) |
|
|
210
|
+
| `gitProvider` | `string` | `"auto"` | Active Git forge: `"auto"` (detect by credentials), `"gitea"`, or `"github"` |
|
|
211
|
+
| `githubTokenRef` | `string` | `""` | DSH credential name containing GitHub PAT or App token |
|
|
212
|
+
| `githubOwner` | `string` | `""` | Default GitHub organization or username |
|
|
213
|
+
| `githubWebhookSecretRef` | `string` | `""` | DSH credential name containing GitHub webhook secret |
|
|
210
214
|
| `giteaUrl` | `string` | `""` | Base URL of Gitea/Forgejo instance for task synchronization |
|
|
211
215
|
| `giteaTokenEnv` | `string` | `"GITEA_TOKEN"` | DSH Credential name containing the Gitea API access token |
|
|
212
216
|
| `giteaOwner` | `string` | `""` | Default organization or username for synced repositories |
|
package/README.ru.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
При ведении параллельной разработки силами нескольких автономных AI-агентов легко потерять контроль над статусом задач, проверкой пулл-реквестов (PR) и ветками в репозитории. Без наглядной доски разработчику приходится переключаться между десятками вкладок чата и вручную сверять тикеты в трекере.
|
|
41
41
|
|
|
42
|
-
**`@goodandready/dsh-kanban`** встраивает полноценную **Канбан-доску** прямо в Web UI DeepSeek Harness. Каждая карточка на доске связана со своей **выделенной сессией агента**. Перемещение карточки между колонками автоматически отправляет агенту целевую инструкцию (разработка, передача на ревью, деплой или очистка воркдеревьев), поддерживая непрерывную **двустороннюю синхронизацию с Gitea / Forgejo**.
|
|
42
|
+
**`@goodandready/dsh-kanban`** встраивает полноценную **Канбан-доску** прямо в Web UI DeepSeek Harness. Каждая карточка на доске связана со своей **выделенной сессией агента**. Перемещение карточки между колонками автоматически отправляет агенту целевую инструкцию (разработка, передача на ревью, деплой или очистка воркдеревьев), поддерживая непрерывную **двустороннюю синхронизацию с Gitea / Forgejo и GitHub**.
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -64,7 +64,7 @@ graph LR
|
|
|
64
64
|
Agent2["Агент: Задача #53<br/>(Ревью PR)"]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
subgraph VCS ["
|
|
67
|
+
subgraph VCS ["Git Forge (Gitea / GitHub)"]
|
|
68
68
|
GiteaIssues["Задачи и Вехи"]
|
|
69
69
|
GiteaPRs["Пулл-реквесты и Ветки"]
|
|
70
70
|
Webhook["Вебхук-приёмник (/dsh-kanban/webhook)"]
|
|
@@ -159,6 +159,10 @@ dsh-kanban:
|
|
|
159
159
|
| Параметр | Тип | По умолчанию | Описание |
|
|
160
160
|
|:---|:---|:---|:---|
|
|
161
161
|
| `boardKind` | `string` | `"project"` | Режим колонок: `"project"` (6 колонок) или `"simple"` (4 колонки) |
|
|
162
|
+
| `gitProvider` | `string` | `"auto"` | Активный Git-провайдер: `"auto"` (по доступным ключам), `"gitea"` или `"github"` |
|
|
163
|
+
| `githubTokenRef` | `string` | `""` | Имя DSH Credential с токеном доступа GitHub PAT / App |
|
|
164
|
+
| `githubOwner` | `string` | `""` | Организация или логин владельца репозитория GitHub |
|
|
165
|
+
| `githubWebhookSecretRef` | `string` | `""` | Имя DSH Credential с секретом вебхука GitHub |
|
|
162
166
|
| `giteaUrl` | `string` | `""` | URL инстанса Gitea/Forgejo для синхронизации задач |
|
|
163
167
|
| `giteaTokenEnv` | `string` | `"GITEA_TOKEN"` | Имя DSH Credential с токеном доступа к Gitea API |
|
|
164
168
|
| `giteaOwner` | `string` | `""` | Организация или логин владельца репозитория |
|
|
@@ -170,7 +174,7 @@ dsh-kanban:
|
|
|
170
174
|
|
|
171
175
|
## 🧪 Тестирование и верификация
|
|
172
176
|
|
|
173
|
-
Запуск полного набора из более чем
|
|
177
|
+
Запуск полного набора из более чем 710 юнит- и интеграционных тестов:
|
|
174
178
|
|
|
175
179
|
```bash
|
|
176
180
|
npm test
|
package/README.zh.md
CHANGED
|
@@ -159,6 +159,10 @@ dsh-kanban:
|
|
|
159
159
|
| 配置项 | 数据类型 | 默认值 | 功能说明 |
|
|
160
160
|
|:---|:---|:---|:---|
|
|
161
161
|
| `boardKind` | `string` | `"project"` | 看板模式:`"project"`(6 泳道)或 `"simple"`(4 泳道) |
|
|
162
|
+
| `gitProvider` | `string` | `"auto"` | 活动 Git 平台:`"auto"`(按凭证自动检测)、`"gitea"` 或 `"github"` |
|
|
163
|
+
| `githubTokenRef` | `string` | `""` | 包含 GitHub PAT 或应用令牌的 DSH 凭证名称 |
|
|
164
|
+
| `githubOwner` | `string` | `""` | 默认监听的 GitHub 组织或用户名 |
|
|
165
|
+
| `githubWebhookSecretRef` | `string` | `""` | 包含 GitHub Webhook 密钥的 DSH 凭证名称 |
|
|
162
166
|
| `giteaUrl` | `string` | `""` | 用于同步任务的 Gitea / Forgejo 实例基础地址 |
|
|
163
167
|
| `giteaTokenEnv` | `string` | `"GITEA_TOKEN"` | 存储 Gitea API Token 的 DSH 凭据键名 |
|
|
164
168
|
| `giteaOwner` | `string` | `""` | 默认同步的仓库所属组织或用户名 |
|
package/lib/client.js
CHANGED
|
@@ -608,6 +608,7 @@ window.__ModuleLoader__.load({
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
const helpers = {
|
|
611
|
+
ROW_ID, ROW_CONFIG_KEY, PKG, KanbanSettingsCard,
|
|
611
612
|
neighboursFor, tasksOf, sortTasks, packInfo, groupByAssignee, applyToEach, recallLaunch, rememberLaunch, stillOffered,
|
|
612
613
|
cardRef, taskRef, cardStatus, facetLabel, boardTitle,
|
|
613
614
|
columnTitle, planLine, stateLabel, isCollapsed,
|
|
@@ -721,6 +722,8 @@ window.__ModuleLoader__.load({
|
|
|
721
722
|
'.dkb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}' +'.dkb-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px;padding-bottom:4px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +'.dkb-section-badge{font-size:11px;font-weight:500;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}' +'.dkb-section-badge-ok{border-color:var(--dsw-alias-state-success-border,#34d399);color:var(--dsw-alias-state-success-primary,#059669);background:var(--dsw-alias-state-success-bg,rgba(16,185,129,0.1))}' +
|
|
722
723
|
'.dkb-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
723
724
|
'.dkb-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
725
|
+
'.dkb-page{padding:0}' +
|
|
726
|
+
'.dkb-page .dkb-body{border-top:none;margin:0}' +
|
|
724
727
|
'.dkb-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
725
728
|
'.dkb-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
726
729
|
'.dkb-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
@@ -907,7 +910,17 @@ window.__ModuleLoader__.load({
|
|
|
907
910
|
'title': 'Kanban',
|
|
908
911
|
'subtitle': 'Task board: Gitea issues and a session per task',
|
|
909
912
|
'section.label': 'Kanban',
|
|
913
|
+
'group.forge': 'Git Forge',
|
|
910
914
|
'group.gitea': 'Gitea',
|
|
915
|
+
'group.github': 'GitHub',
|
|
916
|
+
'field.gitProvider': 'Git Provider',
|
|
917
|
+
'hint.gitProvider': 'Active forge: "auto" (detect by credentials), "gitea", or "github".',
|
|
918
|
+
'field.githubTokenRef': 'GitHub credential token',
|
|
919
|
+
'hint.githubTokenRef': 'Name of DSH credential holding GitHub PAT or App token. Never type raw token.',
|
|
920
|
+
'field.githubOwner': 'GitHub organization/user',
|
|
921
|
+
'hint.githubOwner': 'Default GitHub account to watch. Empty uses authenticated user.',
|
|
922
|
+
'field.githubWebhookSecretRef': 'GitHub webhook secret',
|
|
923
|
+
'hint.githubWebhookSecretRef': 'Name of DSH credential holding GitHub webhook secret (HMAC-SHA256).',
|
|
911
924
|
'group.general': 'General',
|
|
912
925
|
'field.giteaUrl': 'Instance URL',
|
|
913
926
|
'field.giteaTokenRef': 'Credential name',
|
|
@@ -1285,7 +1298,17 @@ window.__ModuleLoader__.load({
|
|
|
1285
1298
|
'title': '看板',
|
|
1286
1299
|
'subtitle': '任务看板:Gitea issue,每个任务一个会话',
|
|
1287
1300
|
'section.label': '看板',
|
|
1301
|
+
'group.forge': 'Git 平台',
|
|
1288
1302
|
'group.gitea': 'Gitea',
|
|
1303
|
+
'group.github': 'GitHub',
|
|
1304
|
+
'field.gitProvider': 'Git 平台',
|
|
1305
|
+
'hint.gitProvider': '活动 Git 平台:"auto"(自动检测)、"gitea"或"github"。',
|
|
1306
|
+
'field.githubTokenRef': 'GitHub 凭据名称',
|
|
1307
|
+
'hint.githubTokenRef': '存储 GitHub PAT 或应用令牌的 DSH 凭据名称。切勿在此输入真实令牌。',
|
|
1308
|
+
'field.githubOwner': 'GitHub 组织或用户名',
|
|
1309
|
+
'hint.githubOwner': '默认监听的 GitHub 账户。留空使用认证用户。',
|
|
1310
|
+
'field.githubWebhookSecretRef': 'GitHub Webhook 密钥',
|
|
1311
|
+
'hint.githubWebhookSecretRef': '存储 GitHub Webhook 密钥的 DSH 凭据名称。',
|
|
1289
1312
|
'group.general': '通用',
|
|
1290
1313
|
'field.giteaUrl': '实例地址',
|
|
1291
1314
|
'field.giteaTokenRef': '凭据名称',
|
|
@@ -1660,8 +1683,12 @@ window.__ModuleLoader__.load({
|
|
|
1660
1683
|
|
|
1661
1684
|
|
|
1662
1685
|
const FIELDS = [
|
|
1686
|
+
{ field: 'gitProvider', kind: 'text', group: 'forge' },
|
|
1663
1687
|
{ field: 'giteaUrl', kind: 'text', group: 'gitea' },
|
|
1664
1688
|
{ field: 'giteaTokenRef', kind: 'text', group: 'gitea' },
|
|
1689
|
+
{ field: 'githubTokenRef', kind: 'text', group: 'github' },
|
|
1690
|
+
{ field: 'githubOwner', kind: 'text', group: 'github' },
|
|
1691
|
+
{ field: 'githubWebhookSecretRef', kind: 'text', group: 'github' },
|
|
1665
1692
|
{ field: 'defaultProjectRoot', kind: 'text', group: 'general' },
|
|
1666
1693
|
{ field: 'startPrompt', kind: 'area', group: 'general' },
|
|
1667
1694
|
{ field: 'replyInstruction', kind: 'text', group: 'general' },
|
|
@@ -1692,6 +1719,8 @@ window.__ModuleLoader__.load({
|
|
|
1692
1719
|
function hintKey(spec) {
|
|
1693
1720
|
if (spec.group === 'sync') return 'hint.' + spec.field
|
|
1694
1721
|
if (spec.group === 'gitea') return 'hint.' + spec.field
|
|
1722
|
+
if (spec.group === 'github') return 'hint.' + spec.field
|
|
1723
|
+
if (spec.group === 'forge') return 'hint.' + spec.field
|
|
1695
1724
|
if (spec.group === 'labels') return 'hint.label'
|
|
1696
1725
|
if (spec.group === 'limits') return 'hint.limit'
|
|
1697
1726
|
return 'hint.' + spec.field
|
|
@@ -1818,8 +1847,16 @@ window.__ModuleLoader__.load({
|
|
|
1818
1847
|
}
|
|
1819
1848
|
|
|
1820
1849
|
function KanbanSettingsCard(props) {
|
|
1821
|
-
const ctx = props.ctx
|
|
1822
|
-
const t = props.t || fallbackT
|
|
1850
|
+
const ctx = props && props.ctx
|
|
1851
|
+
const t = (props && props.t) || fallbackT
|
|
1852
|
+
ensureCss()
|
|
1853
|
+
|
|
1854
|
+
// Row seat (plugins.row.config): summary is a one-liner
|
|
1855
|
+
if (props && props.view === 'summary') {
|
|
1856
|
+
return h('span', { className: 'dkb-sub' }, t('settings.subtitle') || t('subtitle') || 'Kanban board for sessions and tasks')
|
|
1857
|
+
}
|
|
1858
|
+
const page = !!(props && props.view === 'page')
|
|
1859
|
+
|
|
1823
1860
|
const [open, setOpen] = React.useState(false)
|
|
1824
1861
|
const [draft, setDraft] = React.useState(null)
|
|
1825
1862
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -1891,7 +1928,7 @@ window.__ModuleLoader__.load({
|
|
|
1891
1928
|
ensureCss()
|
|
1892
1929
|
const style = null
|
|
1893
1930
|
|
|
1894
|
-
if (!open) return h('li', { className: 'dkb-card' }, head)
|
|
1931
|
+
if (!page && !open) return h('li', { className: 'dkb-card' }, head)
|
|
1895
1932
|
|
|
1896
1933
|
let body
|
|
1897
1934
|
if (status === 'loading') {
|
|
@@ -1950,6 +1987,11 @@ window.__ModuleLoader__.load({
|
|
|
1950
1987
|
badge = h('span', {
|
|
1951
1988
|
className: 'dkb-section-badge' + (hasUrl ? ' dkb-section-badge-ok' : ''),
|
|
1952
1989
|
}, hasUrl ? 'Gitea Configured' : 'No URL')
|
|
1990
|
+
} else if (grp === 'github') {
|
|
1991
|
+
const hasGh = Boolean(valueOf('githubTokenRef'))
|
|
1992
|
+
badge = h('span', {
|
|
1993
|
+
className: 'dkb-section-badge' + (hasGh ? ' dkb-section-badge-ok' : ''),
|
|
1994
|
+
}, hasGh ? 'GitHub Configured' : 'No Token')
|
|
1953
1995
|
} else if (grp === 'sync') {
|
|
1954
1996
|
const interval = Number(valueOf('syncIntervalSec')) || 0
|
|
1955
1997
|
badge = h('span', {
|
|
@@ -1980,15 +2022,10 @@ window.__ModuleLoader__.load({
|
|
|
1980
2022
|
body = rows
|
|
1981
2023
|
}
|
|
1982
2024
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
if (props && props.view === 'summary') {
|
|
1986
|
-
return h('span', { className: 'dkb-sub' }, t('settings.subtitle') || 'Kanban board for sessions and tasks')
|
|
2025
|
+
if (page) {
|
|
2026
|
+
return h('div', { className: 'dkb-page' }, h('div', { className: 'dkb-body' }, body))
|
|
1987
2027
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
return h(page ? 'div' : 'li', { className: page ? 'dkb-page' : 'dkb-card dkb-cardOpen' },
|
|
1991
|
-
page ? null : head, h('div', { className: 'dkb-body' }, body))
|
|
2028
|
+
return h('li', { className: 'dkb-card dkb-cardOpen' }, head, h('div', { className: 'dkb-body' }, body))
|
|
1992
2029
|
}
|
|
1993
2030
|
|
|
1994
2031
|
/** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
|
|
@@ -2238,6 +2275,36 @@ window.__ModuleLoader__.load({
|
|
|
2238
2275
|
const [syncing, setSyncing] = React.useState(false)
|
|
2239
2276
|
const [picked, setPicked] = React.useState({})
|
|
2240
2277
|
const [archive, setArchive] = React.useState(null)
|
|
2278
|
+
const [diffExpanded, setDiffExpanded] = React.useState(false)
|
|
2279
|
+
const [diffLoading, setDiffLoading] = React.useState(false)
|
|
2280
|
+
const [diffData, setDiffData] = React.useState(null)
|
|
2281
|
+
const [commitsData, setCommitsData] = React.useState([])
|
|
2282
|
+
const [merging, setMerging] = React.useState(false)
|
|
2283
|
+
|
|
2284
|
+
const loadDiffAndCommits = React.useCallback(async () => {
|
|
2285
|
+
if (!openTask) return
|
|
2286
|
+
setDiffLoading(true)
|
|
2287
|
+
try {
|
|
2288
|
+
const [dRes, cRes] = await Promise.all([
|
|
2289
|
+
api('/task/' + encodeURIComponent(openTask.id) + '/diff'),
|
|
2290
|
+
api('/task/' + encodeURIComponent(openTask.id) + '/commits'),
|
|
2291
|
+
])
|
|
2292
|
+
setDiffData(dRes)
|
|
2293
|
+
setCommitsData(cRes?.commits || [])
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
setError(t(e.key || e.error || 'error.unknown'))
|
|
2296
|
+
} finally {
|
|
2297
|
+
setDiffLoading(false)
|
|
2298
|
+
}
|
|
2299
|
+
}, [openTask, t])
|
|
2300
|
+
|
|
2301
|
+
React.useEffect(() => {
|
|
2302
|
+
setDiffExpanded(false)
|
|
2303
|
+
setDiffLoading(false)
|
|
2304
|
+
setDiffData(null)
|
|
2305
|
+
setCommitsData([])
|
|
2306
|
+
setMerging(false)
|
|
2307
|
+
}, [openTask?.id])
|
|
2241
2308
|
const [metrics, setMetrics] = React.useState(null)
|
|
2242
2309
|
const [onlyWaiting, setOnlyWaiting] = React.useState(false)
|
|
2243
2310
|
const [onlyOverdue, setOnlyOverdue] = React.useState(false)
|
|
@@ -4518,25 +4585,59 @@ window.__ModuleLoader__.load({
|
|
|
4518
4585
|
// below — the seat is additive, not an either/or candidate. The label is a static
|
|
4519
4586
|
// string on purpose: it is resolved while the page renders, and a locale lookup
|
|
4520
4587
|
// there would take the whole client batch down with it.
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
ctx.slots.register
|
|
4588
|
+
const trySlot = (name, register) => {
|
|
4589
|
+
try {
|
|
4590
|
+
if (typeof ctx.slots?.inject === 'function') ctx.slots.inject(name, register)
|
|
4591
|
+
else register()
|
|
4592
|
+
} catch (e) {
|
|
4593
|
+
console.warn('[dsh-kanban] slot registration failed for ' + name + ':', e && e.message)
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
// #274: Newest slot first, older slots as fallbacks:
|
|
4598
|
+
// 1. plugins.row.config (Row seat on Plugins page, DSH 0.1.6-alpha.2)
|
|
4599
|
+
let cardSlot = undefined
|
|
4600
|
+
trySlot('plugins.row.config', () => {
|
|
4601
|
+
ctx.slots.register(
|
|
4602
|
+
{
|
|
4603
|
+
name: 'plugins.row.config',
|
|
4604
|
+
key: ROW_CONFIG_KEY,
|
|
4605
|
+
locale: NS,
|
|
4606
|
+
inject: () => ({ ctx }),
|
|
4607
|
+
},
|
|
4608
|
+
SafeSettingsCard,
|
|
4609
|
+
)
|
|
4610
|
+
if (!cardSlot) cardSlot = 'plugins.row.config'
|
|
4611
|
+
})
|
|
4612
|
+
|
|
4613
|
+
// 2. plugins.item (Official group on Plugins page)
|
|
4614
|
+
trySlot('plugins.item', () => {
|
|
4615
|
+
ctx.slots.register(
|
|
4616
|
+
{
|
|
4524
4617
|
name: 'plugins.item',
|
|
4525
4618
|
id: ROW_ID,
|
|
4526
4619
|
order: 60,
|
|
4527
4620
|
label: () => 'Kanban',
|
|
4528
4621
|
locale: NS,
|
|
4529
4622
|
inject: () => ({ ctx }),
|
|
4530
|
-
},
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
}
|
|
4623
|
+
},
|
|
4624
|
+
SafeSettingsCard,
|
|
4625
|
+
)
|
|
4626
|
+
})
|
|
4535
4627
|
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4628
|
+
// 3. settings.plugin.item (legacy fallback for older cores)
|
|
4629
|
+
trySlot('settings.plugin.item', () => {
|
|
4630
|
+
ctx.slots.register(
|
|
4631
|
+
{
|
|
4632
|
+
name: 'settings.plugin.item',
|
|
4633
|
+
key: NS,
|
|
4634
|
+
locale: NS,
|
|
4635
|
+
inject: () => ({ ctx }),
|
|
4636
|
+
},
|
|
4637
|
+
SafeSettingsCard,
|
|
4638
|
+
)
|
|
4639
|
+
if (!cardSlot) cardSlot = 'settings.plugin.item'
|
|
4640
|
+
})
|
|
4540
4641
|
|
|
4541
4642
|
const toggle = createToggle()
|
|
4542
4643
|
ctx.effect(() => {
|
package/lib/commands.js
CHANGED
|
@@ -51,6 +51,7 @@ const TABLE = {
|
|
|
51
51
|
* `undefined` — такой колонки на этой доске нет.
|
|
52
52
|
*/
|
|
53
53
|
export function commandFor(column, kind = 'project') {
|
|
54
|
+
if (!columnsOfKind(kind).includes(column)) return undefined
|
|
54
55
|
const board = TABLE[kind]
|
|
55
56
|
if (board === undefined) return undefined
|
|
56
57
|
const found = board[column]
|
package/lib/config.js
CHANGED
|
@@ -55,8 +55,10 @@ export const DEFAULT_BOARDS = [
|
|
|
55
55
|
|
|
56
56
|
/** Значения по умолчанию. Из них же собирается схема в `lib/index.js`. */
|
|
57
57
|
export const CONFIG_DEFAULTS = {
|
|
58
|
+
gitProvider: 'auto',
|
|
58
59
|
giteaUrl: '',
|
|
59
60
|
giteaTokenRef: 'GITEA_TOKEN',
|
|
61
|
+
githubTokenRef: 'GITHUB_TOKEN',
|
|
60
62
|
defaultProjectRoot: '',
|
|
61
63
|
startPrompt: '',
|
|
62
64
|
replyInstruction: '',
|
|
@@ -65,11 +67,13 @@ export const CONFIG_DEFAULTS = {
|
|
|
65
67
|
syncIntervalSec: 120,
|
|
66
68
|
staleAfterMin: 60,
|
|
67
69
|
giteaOwner: '',
|
|
70
|
+
githubOwner: '',
|
|
68
71
|
columnNames: '',
|
|
69
72
|
watchRepos: '',
|
|
70
73
|
archiveAfterDays: 7,
|
|
71
74
|
boardToolEnabled: false,
|
|
72
75
|
webhookSecretRef: '',
|
|
76
|
+
githubWebhookSecretRef: '',
|
|
73
77
|
pushToGitea: true,
|
|
74
78
|
worktreeIsolation: true,
|
|
75
79
|
worktreeRoot: '',
|
|
@@ -86,8 +90,10 @@ export const CONFIG_DEFAULTS = {
|
|
|
86
90
|
|
|
87
91
|
/** Пояснения к полям; повторно используются схемой и карточкой настроек. */
|
|
88
92
|
export const CONFIG_HINTS = {
|
|
93
|
+
gitProvider: 'Active Git forge: "auto" (detect by credentials), "gitea", or "github".',
|
|
89
94
|
giteaUrl: 'Gitea or Forgejo instance URL, e.g. https://gitea.example.com. Empty disables import.',
|
|
90
95
|
giteaTokenRef: 'Name of DSH credential holding the access token. Do not enter raw token.',
|
|
96
|
+
githubTokenRef: 'Name of DSH credential holding GitHub Personal Access Token or App token. Do not enter raw token.',
|
|
91
97
|
defaultProjectRoot: 'Directory containing project repositories. Empty uses harness working directory.',
|
|
92
98
|
startPrompt: 'Template for initial message to agent session. Empty uses built-in default.',
|
|
93
99
|
replyInstruction: 'Instruction appended to initial message (e.g. language/format constraints). Empty adds nothing.',
|
|
@@ -96,11 +102,13 @@ export const CONFIG_HINTS = {
|
|
|
96
102
|
syncIntervalSec: 'Interval in seconds for polling Gitea synchronization. 0 disables polling.',
|
|
97
103
|
staleAfterMin: 'Minutes of inactivity before active task session is flagged stale. 0 disables.',
|
|
98
104
|
giteaOwner: 'Default Gitea organization/user to track. Auto-detected if single organization.',
|
|
105
|
+
githubOwner: 'Default GitHub user or organization to track. Empty uses the authenticated user.',
|
|
99
106
|
columnNames: 'Custom column title mappings as "id=Name" pairs comma-separated.',
|
|
100
107
|
watchRepos: 'Filter sync to specific repository names comma-separated. Empty watches all repos.',
|
|
101
108
|
archiveAfterDays: 'Days completed tasks remain before automatic archiving. 0 disables.',
|
|
102
109
|
boardToolEnabled: 'Allow autonomous agent to advance cards via board_move tool.',
|
|
103
110
|
webhookSecretRef: 'Name of DSH credential holding Gitea webhook secret.',
|
|
111
|
+
githubWebhookSecretRef: 'Name of DSH credential holding GitHub webhook secret (HMAC-SHA256).',
|
|
104
112
|
pushToGitea: 'Automatically close corresponding Gitea issue when card moves to Done.',
|
|
105
113
|
worktreeIsolation: 'Isolate task sessions in dedicated git worktrees to prevent workspace collisions.',
|
|
106
114
|
worktreeRoot: 'Custom root directory for task worktrees. Empty defaults to $DSH_HOME/worktrees.',
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Композитный маршрутизатор Git-провайдеров (Gitea и GitHub).
|
|
2
|
+
//
|
|
3
|
+
// Предоставляет единый интерфейс к обеим системам:
|
|
4
|
+
// - прозрачно направляет запросы задач в соответствующий провайдер (task.provider / issueUrl);
|
|
5
|
+
// - в режиме 'auto' опрашивает настроенный провайдер или объединяет результаты поиска;
|
|
6
|
+
// - позволяет доске бесшовно работать как только с Gitea, так и только с GitHub или с обоими сразу.
|
|
7
|
+
|
|
8
|
+
export function createGitProvider({ gitea, github, getConfig }) {
|
|
9
|
+
async function resolveActiveProvider(hint) {
|
|
10
|
+
if (hint && typeof hint === 'object') {
|
|
11
|
+
if (hint.provider === 'github') return github
|
|
12
|
+
if (hint.provider === 'gitea') return gitea
|
|
13
|
+
if (typeof hint.issueUrl === 'string' && hint.issueUrl.includes('github.com')) {
|
|
14
|
+
return github
|
|
15
|
+
}
|
|
16
|
+
} else if (hint === 'github') {
|
|
17
|
+
return github
|
|
18
|
+
} else if (hint === 'gitea') {
|
|
19
|
+
return gitea
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mode = String(getConfig()?.gitProvider || 'auto').toLowerCase()
|
|
23
|
+
if (mode === 'github') return github
|
|
24
|
+
if (mode === 'gitea') return gitea
|
|
25
|
+
|
|
26
|
+
// В режиме 'auto': проверяем доступность провайдеров
|
|
27
|
+
const giteaReady = await gitea.isConfigured()
|
|
28
|
+
const githubReady = await github.isConfigured()
|
|
29
|
+
if (githubReady && !giteaReady) return github
|
|
30
|
+
return gitea
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function forTarget(target) {
|
|
34
|
+
if (target === github || target?.provider === 'github') return github
|
|
35
|
+
if (target === gitea || target?.provider === 'gitea') return gitea
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
gitea,
|
|
41
|
+
github,
|
|
42
|
+
|
|
43
|
+
/** Определить провайдера для задачи или контекста. */
|
|
44
|
+
resolve: resolveActiveProvider,
|
|
45
|
+
|
|
46
|
+
/** Настроен ли хотя бы один или конкретный провайдер. */
|
|
47
|
+
async isConfigured(providerName) {
|
|
48
|
+
if (providerName === 'github') return github.isConfigured()
|
|
49
|
+
if (providerName === 'gitea') return gitea.isConfigured()
|
|
50
|
+
const gReady = await gitea.isConfigured()
|
|
51
|
+
if (gReady) return true
|
|
52
|
+
return github.isConfigured()
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/** Логин текущего пользователя. */
|
|
56
|
+
async me(target) {
|
|
57
|
+
const p = forTarget(target) || await resolveActiveProvider(target)
|
|
58
|
+
return p.me()
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/** Список issues. */
|
|
62
|
+
async listIssues(options) {
|
|
63
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
64
|
+
return p.listIssues(options)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** Получить issue по номеру. */
|
|
68
|
+
async getIssue(options) {
|
|
69
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
70
|
+
return p.getIssue(options)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Создать issue. */
|
|
74
|
+
async createIssue(options) {
|
|
75
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
76
|
+
return p.createIssue(options)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** Закрыть issue. */
|
|
80
|
+
async closeIssue(options) {
|
|
81
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
82
|
+
return p.closeIssue(options)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/** Комментарий в issue. */
|
|
86
|
+
async comment(options) {
|
|
87
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
88
|
+
return p.comment(options)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** Назначить ответственных. */
|
|
92
|
+
async setAssignees(options) {
|
|
93
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
94
|
+
return p.setAssignees(options)
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** Список Pull Requests. */
|
|
98
|
+
async listPulls(options) {
|
|
99
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
100
|
+
return p.listPulls(options)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** Создать Pull Request. */
|
|
104
|
+
async createPullRequest(options) {
|
|
105
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
106
|
+
return p.createPullRequest(options)
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** Список веток репозитория. */
|
|
110
|
+
async listBranches(options) {
|
|
111
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
112
|
+
return p.listBranches(options)
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/** Список организаций. */
|
|
116
|
+
async listOrgs(target) {
|
|
117
|
+
const p = forTarget(target) || await resolveActiveProvider(target)
|
|
118
|
+
return p.listOrgs()
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/** Репозитории организации. */
|
|
122
|
+
async listOrgRepos(options) {
|
|
123
|
+
const p = forTarget(options?.provider) || await resolveActiveProvider(options)
|
|
124
|
+
return p.listOrgRepos(options)
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Поиск репозиториев: ищет в целевом провайдере либо агрегирует результаты
|
|
129
|
+
* из обоих, если включены оба.
|
|
130
|
+
*/
|
|
131
|
+
async searchRepos(options) {
|
|
132
|
+
const target = forTarget(options?.provider)
|
|
133
|
+
if (target) {
|
|
134
|
+
return target.searchRepos(options)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mode = String(getConfig()?.gitProvider || 'auto').toLowerCase()
|
|
138
|
+
if (mode === 'github') return github.searchRepos(options)
|
|
139
|
+
if (mode === 'gitea') return gitea.searchRepos(options)
|
|
140
|
+
|
|
141
|
+
// Если режим 'auto' / 'both', опрашиваем оба настроенных
|
|
142
|
+
const [giteaConfigured, githubConfigured] = await Promise.all([
|
|
143
|
+
gitea.isConfigured(),
|
|
144
|
+
github.isConfigured(),
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
const requests = []
|
|
148
|
+
if (giteaConfigured) requests.push(gitea.searchRepos(options).catch(() => []))
|
|
149
|
+
if (githubConfigured) requests.push(github.searchRepos(options).catch(() => []))
|
|
150
|
+
|
|
151
|
+
if (requests.length === 0) return []
|
|
152
|
+
const results = await Promise.all(requests)
|
|
153
|
+
return results.flat()
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
package/lib/gitea.js
CHANGED
|
@@ -33,7 +33,9 @@ export function safeSegment(value) {
|
|
|
33
33
|
|
|
34
34
|
/** Имя учётной записи — имя переменной окружения, а не сам секрет. */
|
|
35
35
|
export function isCredentialName(value) {
|
|
36
|
-
|
|
36
|
+
const str = String(value || '')
|
|
37
|
+
if (looksLikeSecret(str)) return false
|
|
38
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Похоже на сам токен, а не на его имя. */
|