@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 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/Forgejo synchronization**.
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 / Forgejo Instance"]
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 ["Сервер Gitea / Forgejo"]
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
- Запуск полного набора из более чем 520 юнит- и интеграционных тестов:
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
- // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
1984
- // padding, so the summary is a one-liner and the page drops our card chrome.
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
- const page = !!(props && props.view === 'page')
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
- try {
4522
- ctx.slots.inject('plugins.item', () => {
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
- }, SafeSettingsCard)
4531
- })
4532
- } catch (e) {
4533
- console.warn('[dsh-kanban] plugins.item seat unavailable', e)
4534
- }
4623
+ },
4624
+ SafeSettingsCard,
4625
+ )
4626
+ })
4535
4627
 
4536
- const cardSlot = registerFirst(ctx, [
4537
- { name: 'plugins.row.config', key: ROW_CONFIG_KEY, locale: NS, inject: () => ({ ctx }) },
4538
- { name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
4539
- ], SafeSettingsCard)
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
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(String(value || ''))
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
  /** Похоже на сам токен, а не на его имя. */