nx-kv 0.11.3 → 0.11.4
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/CHANGELOG.md +7 -1
- package/README.md +1 -1
- package/assets/groups.json +4 -0
- package/assets/kv-repo/SKILL.md +83 -0
- package/assets/nx-kv/references/repo-registry.md +7 -5
- package/assets/nx-kv/references/todo-commands.md +1 -1
- package/package.json +1 -1
- package/src/core/config.js +1 -1
- package/src/modules/localtask/view.jsx +6 -18
- package/src/modules/repos/boards.js +60 -9
- package/src/modules/repos/index.js +22 -5
- package/src/modules/repos/service.js +8 -14
- package/src/modules/repos/view.jsx +178 -85
- package/src/web/frontend/style.css +109 -44
- package/src/web/public/assets/{CliHints-BOQFPVm4.js → CliHints-DltGgA21.js} +1 -1
- package/src/web/public/assets/{index-Da2WAZF_.js → index-Cg8J6GZg.js} +2 -2
- package/src/web/public/assets/index-DozxuMsm.css +1 -0
- package/src/web/public/assets/view-CCiJUhF6.js +1 -0
- package/src/web/public/assets/{view-CCZJEV-o.js → view-CV6w4TG6.js} +1 -1
- package/src/web/public/assets/view-bE7JPI0N.js +13 -0
- package/src/web/public/assets/{view-AjdqnO_6.js → view-k4YFJYVz.js} +1 -1
- package/src/web/public/index.html +2 -2
- package/src/web/public/assets/index-Cup5oKfo.css +0 -1
- package/src/web/public/assets/view-ChZEGZ7Y.js +0 -10
- package/src/web/public/assets/view-T-dQZ480.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.11.4] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 本地任务页的仓库上下文只显示仓库名与用途,不再展示看板归属(列表 chip 与投递提示同步收敛)。
|
|
12
|
+
|
|
7
13
|
## [0.11.3] - 2026-10-23 工作空间模块并入清单(子弹窗),不再单独占一个 tab
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -858,7 +864,7 @@
|
|
|
858
864
|
|
|
859
865
|
## [0.1.0] - 2026-09-16
|
|
860
866
|
|
|
861
|
-
首个版本。KV 清单(todo)的 CLI + Web 面板,对接 GoFrame KV 后端(默认 `
|
|
867
|
+
首个版本。KV 清单(todo)的 CLI + Web 面板,对接 GoFrame KV 后端(默认 `192.0.2.47:8988`)。
|
|
862
868
|
|
|
863
869
|
### Added
|
|
864
870
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
KV 清单(todo)管理中枢。一个 `npx` 命令起一个 Web 面板,同时提供同构 CLI——
|
|
4
4
|
**面板上的每个按钮,底层都是同一条 CLI 命令**,输出 `--json` 即可被任何 agent 直接消费。
|
|
5
5
|
|
|
6
|
-
对接 GoFrame KV 后端(默认 `http://
|
|
6
|
+
对接 GoFrame KV 后端(默认 `http://192.0.2.47:8988`),
|
|
7
7
|
数据落在**四把 key** 上:`todo:open` / `todo:done` / `todo:freeze` / `todo:topics`。
|
|
8
8
|
|
|
9
9
|
任务正文还能直接**嵌图片与文档**(标准 markdown:`` / `[文件名](fileId)`)——
|
package/assets/groups.json
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"kv-timegoal": {
|
|
22
22
|
"skills": ["kv-timegoal"],
|
|
23
23
|
"summary": "把「持续运行 N 小时」任务固化到 nx-kv:每轮回显原文提示词防偏移 + 时间到点自动停手"
|
|
24
|
+
},
|
|
25
|
+
"kv-repo": {
|
|
26
|
+
"skills": ["kv-repo"],
|
|
27
|
+
"summary": "仓库组识别与关系报告——/kv-repo <组名> 一步加载看板相关项目;board/repo 查询、读并维护领域描述(中心/终端、关键路径、搜索关键词)"
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kv-repo
|
|
3
|
+
description: 识别并汇报本机仓库的相关分组(仓库组)——快捷入口 `/kv-repo <组名>`:一步到位加载该看板的成员仓库、领域描述与配合关系,输出关系报告。用 nx-kv repo / board 命令回答「这台机器上有哪些仓库、按领域怎么分组、组内谁是中心谁是终端、关键路径与搜索关键词在哪」。看板的领域描述(desc)+ 成员仓库的组内描述(note:它在组里的角色)共同构成写给 agent 读的上下文——领域侧重、仓库间配合关系(如 fr 组内 API 关联、go-ah 为 OIDC 中心其余为终端)、入口路径、搜什么关键词。触发词:/kv-repo、kv-repo、仓库组、相关仓库、这个仓库属于哪个组、组里都有什么、它们什么关系、中心/终端、领域描述、关键路径、加载 fr 的看板、看板相关项目、board list、repo list。反例:登记/扫描/增删仓库与看板用 nx-kv 主 skill 的 repo-registry;git 状态与 diff 一律找 git 自己。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# kv-repo — 仓库组识别与关系报告
|
|
7
|
+
|
|
8
|
+
回答两类问题:**「这组仓库是什么、怎么配合」**(给人看的关系报告)和
|
|
9
|
+
**「我现在这个任务该去哪些仓库找上下文」**(给 agent 自己用的定位)。
|
|
10
|
+
数据都在 nx-kv 的仓库模块里,两条命令足够。
|
|
11
|
+
|
|
12
|
+
## 快捷 SOP:`/kv-repo <组名|仓库名|路径>`
|
|
13
|
+
|
|
14
|
+
**带参数就是一步到位,不要反问、不要展开流程**:定位组 → 拉成员与领域描述 → 直接输出该组的关系报告。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
nx-kv board list --json
|
|
18
|
+
# → { boards:[{ id, name, desc, repos:[{ id, name, desc, path }] }], unassigned:[…] }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- 参数是**组名**(如 `/kv-repo fr`)→ 在 `boards[]` 里按 name 找到它(找不到再试包含匹配),取该组输出报告;
|
|
22
|
+
- 参数是**仓库名或路径**(如 `/kv-repo go-ah`、`/kv-repo D:\code\a_go\proj\go-ah`)→ `nx-kv repo list --json` 定位仓库,报告它 `boards[]` 里的**每个**组(一个仓库可同时归入多个组);
|
|
23
|
+
- 本地任务的上下文若已带 repo/boards,直接用,不必重查。
|
|
24
|
+
|
|
25
|
+
报告形状(desc 里的关键路径与关键词**原样带上**——它们是前人留下的搜索入口):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
【fr】Flutter 客户端组 —— 组内共享 API 契约
|
|
29
|
+
fr D:\code\a_dart\prj\fr (组内)API 契约方:lib/api/ 是唯一入口,搜 fr_ 前缀
|
|
30
|
+
…
|
|
31
|
+
(领域描述)<组的 desc 全文>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
每行优先读**组内描述(note)**——它讲这个仓库在本组里的角色;没写 note 再回退仓库自己的 desc。
|
|
35
|
+
组领域描述(desc)与各仓库的关键路径 / 关键词**原样带上**——它们是前人留下的搜索入口。
|
|
36
|
+
|
|
37
|
+
**无参数**(`/kv-repo`):输出全部组速览——每行「组名 · 成员数 · desc 首行」+ 未分组计数,
|
|
38
|
+
然后问用户要深入哪一组(只问这一次,别连环追问)。
|
|
39
|
+
|
|
40
|
+
## 模型(30 秒版)
|
|
41
|
+
|
|
42
|
+
- **仓库登记** = 本机事实(name / path / desc / notes),一张中心清单。
|
|
43
|
+
- **看板** = 领域分组;一个仓库可同时归入多个组,分组只此一条路(没有标签)。
|
|
44
|
+
- **看板的领域描述(desc)+ 成员的组内描述(note)是写给你(agent)读的上下文**:
|
|
45
|
+
desc 讲这个领域的侧重与配合方式;note 讲单个仓库在组里的角色(谁是中心 / 终端、
|
|
46
|
+
API 关联在哪、关键路径、搜什么关键词)。**接到任务先读它们,比翻代码快。**
|
|
47
|
+
|
|
48
|
+
## 完整命令(速查)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
nx-kv board list --json # 首选:全部组 + 成员(含组内描述 note)+ 各组领域描述
|
|
52
|
+
# → { boards:[{ id, name, desc, repos:[{ id, name, desc, path, note }] }], unassigned:[…] }
|
|
53
|
+
nx-kv repo list --json # 扁平清单,每条带 boards[](查单个仓库的归属)
|
|
54
|
+
nx-kv repo get <id|绝对路径> --json # 单仓库详情 + 所在组
|
|
55
|
+
nx-kv board get <id> --json # 单组详情(成员 + 组内描述)
|
|
56
|
+
nx-kv board note <id|绝对路径> --board <看板id> --note "角色/配合关系/关键路径/关键词"
|
|
57
|
+
# 写或清(--note 留空 = 清除)组内描述
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
定位顺序:拿到任务/目录 → `repo list` 按**路径或目录名**找到仓库
|
|
61
|
+
→ 它的 `boards[]` 给出所属组 → 回 `board list` 读该组的 `desc` 与**兄弟仓库**。
|
|
62
|
+
|
|
63
|
+
## 维护:发现过期就修(这是本 skill 的另一半职责)
|
|
64
|
+
|
|
65
|
+
读到的 desc 与代码现实不符(路径变了、关系变了、压根没写)时,顺手补上:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nx-kv board update <id> --desc "侧重…;谁配合谁(谁是中心/终端、API 关联在哪);关键路径…;搜索关键词…"
|
|
69
|
+
nx-kv board note <仓库id|路径> --board <看板id> --note "这个仓库在组里的角色…;关键路径…;搜什么"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
写领域描述的约定(一个看板一个领域,侧重各不相同):
|
|
73
|
+
|
|
74
|
+
1. **一句话侧重**:这组仓库共同在做什么;
|
|
75
|
+
2. **配合关系**:谁是中心、谁是终端、谁调谁的 API(如 fr 组内 API 关联、go-ah 与其接入方)——组级关系写看板 desc,单个仓库的角色写 board note;
|
|
76
|
+
3. **关键路径**:契约文件 / 配置 / 入口代码的位置(如 `lib/api/`、`issuer 192.0.2.47:8082`);
|
|
77
|
+
4. **搜索关键词**:下次 agent 该搜什么(如 `fr_` 前缀、模块名)。
|
|
78
|
+
|
|
79
|
+
## 边界
|
|
80
|
+
|
|
81
|
+
- 本 skill **只读分组与描述**;登记/扫描/增删仓库看板走 nx-kv 主 skill(repo-registry)。
|
|
82
|
+
- 不碰 git:状态 / diff / pull / push 一律直接用 git 自己。
|
|
83
|
+
- 数据按 groupId 隔离,查询前先确认工作空间对不对(`nx-kv group current`)。
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
- 用户问「这台机器上有哪些仓库 / 某个仓库在哪」
|
|
8
8
|
- 需要把某个目录登记为受管仓库,或扫描某目录批量发现仓库
|
|
9
|
-
-
|
|
9
|
+
- 需要维护仓库的元信息:名称、描述、备注,或改登记路径
|
|
10
10
|
- **需要 git 状态、差异、拉取、推送时不要进这里**——本工具不提供 git 操作(见下)
|
|
11
|
+
- **只查分组与关系(不改动)时用 [[kv-repo]]**——那是仓库组的查询/报告侧
|
|
11
12
|
|
|
12
13
|
## 边界:本工具不管 git
|
|
13
14
|
|
|
@@ -35,7 +36,6 @@ git -C <path> pull --no-edit
|
|
|
35
36
|
"id": "r_xxx",
|
|
36
37
|
"name": "ext",
|
|
37
38
|
"path": "D:\\a_js\\js_proj\\br_controller\\ext",
|
|
38
|
-
"tags": ["eco-import"],
|
|
39
39
|
"desc": "对外扩展模块",
|
|
40
40
|
"notes": "",
|
|
41
41
|
"addedAt": "…",
|
|
@@ -44,8 +44,10 @@ git -C <path> pull --no-edit
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
- **id 或绝对路径**都能定位仓库(agent 直接给路径更省事)
|
|
47
|
-
- tags 是自由标签数组,用于分组
|
|
48
47
|
- 路径不必是 git 仓库:登记不校验目录内容,也不要求目录存在
|
|
48
|
+
- **没有标签**:分组只走看板一条路(2026-10-24 废除仓库 tags,两套平行分组只会打架)
|
|
49
|
+
- **看板的 desc 是领域描述**:写该组仓库的侧重、配合关系(谁是中心/终端)、
|
|
50
|
+
关键路径与搜索关键词——它是给人和 agent 读的上下文,写法约定见 [[kv-repo]]
|
|
49
51
|
|
|
50
52
|
## SOP
|
|
51
53
|
|
|
@@ -59,7 +61,7 @@ nx-kv repo list --json # 程序消费
|
|
|
59
61
|
### 2. 登记
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
|
-
nx-kv repo add <path> [--name N] [--desc D] [--
|
|
64
|
+
nx-kv repo add <path> [--name N] [--desc D] [--notes T]
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
- `name` 缺省取目录名
|
|
@@ -80,7 +82,7 @@ nx-kv repo scan [root] [--depth 3] [--dry-run] # 不带 --dry-run 会真的全
|
|
|
80
82
|
|
|
81
83
|
```bash
|
|
82
84
|
nx-kv repo get <id|path> [--json]
|
|
83
|
-
nx-kv repo update <id> [--name N] [--desc D] [--
|
|
85
|
+
nx-kv repo update <id> [--name N] [--desc D] [--notes T] [--path P]
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
- `update --path` 用来纠正登记错位的目录;改到**已被别的记录占用**的路径会显式报错
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
1. **命令就绪**:`nx-kv` 在可执行路径上(`npx nx-kv` 亦可)
|
|
9
9
|
2. **登录态**:`nx-kv auth login <email>`,token 存 `~/.nx-kv/config.json`
|
|
10
|
-
3. **后端可达**:默认 `http://
|
|
10
|
+
3. **后端可达**:默认 `http://192.0.2.47:8988`;用 `--baseUrl` 临时覆盖,或写进配置
|
|
11
11
|
|
|
12
12
|
所有清单操作都需要登录,否则报「未登录」并 exit 1。
|
|
13
13
|
|
package/package.json
CHANGED
package/src/core/config.js
CHANGED
|
@@ -14,7 +14,7 @@ export function configPathFromEnv() {
|
|
|
14
14
|
return process.env.NX_KV_CONFIG || CONFIG_PATH;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export const DEFAULT_BASE_URL = 'http://
|
|
17
|
+
export const DEFAULT_BASE_URL = 'http://192.0.2.47:8988';
|
|
18
18
|
|
|
19
19
|
const EMPTY = () => ({
|
|
20
20
|
baseUrl: process.env.NX_KV_BASE_URL || DEFAULT_BASE_URL,
|
|
@@ -850,21 +850,14 @@ export default function SessionTaskView() {
|
|
|
850
850
|
<strong>{row.name}</strong>
|
|
851
851
|
{row.isGlobal && <span className="tag badge">全局池</span>}
|
|
852
852
|
{t?.manual && <span className="tag">已登记</span>}
|
|
853
|
-
{/*
|
|
854
|
-
|
|
853
|
+
{/* 仓库上下文:这个目录属于哪个已登记仓库。一枚 chip 一行不占。
|
|
854
|
+
看板归属不在本地任务页显示(太吵)——要看去「仓库」页。 */}
|
|
855
855
|
{t?.repo && (
|
|
856
856
|
<span
|
|
857
857
|
className="tag repo-boards"
|
|
858
|
-
title={
|
|
859
|
-
`仓库「${t.repo.name}」${t.repo.desc ? ':' + t.repo.desc : ''}` +
|
|
860
|
-
(t.repo.boards?.length
|
|
861
|
-
? `\n所属看板:${t.repo.boards.map((b) => b.name).join('、')}`
|
|
862
|
-
: '\n(未归入任何看板)')
|
|
863
|
-
}
|
|
858
|
+
title={`仓库「${t.repo.name}」${t.repo.desc ? ':' + t.repo.desc : ''}`}
|
|
864
859
|
>
|
|
865
|
-
{t.repo.
|
|
866
|
-
? t.repo.boards.map((b) => b.name).join(' · ')
|
|
867
|
-
: t.repo.name}
|
|
860
|
+
{t.repo.name}
|
|
868
861
|
</span>
|
|
869
862
|
)}
|
|
870
863
|
{row.cwd && cwdTail.length > 0 && (
|
|
@@ -1098,17 +1091,12 @@ export default function SessionTaskView() {
|
|
|
1098
1091
|
|
|
1099
1092
|
{active && showQueue && (
|
|
1100
1093
|
<>
|
|
1101
|
-
{/*
|
|
1102
|
-
|
|
1094
|
+
{/* 仓库上下文提示:投递到这个目录时,先告诉用户「这是哪个仓库、干什么用」——
|
|
1095
|
+
写任务时不用再切到「仓库」页去查。看板归属不在这里显示。 */}
|
|
1103
1096
|
{active.repo && (
|
|
1104
1097
|
<div className="compose-context">
|
|
1105
1098
|
<span className="repo-name">仓库 {active.repo.name}</span>
|
|
1106
1099
|
{active.repo.desc ? <span className="repo-desc">{active.repo.desc}</span> : null}
|
|
1107
|
-
{active.repo.boards?.length ? (
|
|
1108
|
-
<span className="repo-boards-list">
|
|
1109
|
-
看板:{active.repo.boards.map((b) => b.name).join('、')}
|
|
1110
|
-
</span>
|
|
1111
|
-
) : null}
|
|
1112
1100
|
</div>
|
|
1113
1101
|
)}
|
|
1114
1102
|
<div className="toolbar compose-toolbar">
|
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
// 与 repo 登记的关系:看板只存 id 引用,登记数据(name/desc/path…)的唯一真相在 s.repos——
|
|
4
4
|
// 删看板不动登记,删登记要同步摘引用(见 service.js 的 removeRepo)。
|
|
5
5
|
// 本文件不含传输层概念(不认识 argv 与 HTTP)。
|
|
6
|
+
import { resolve } from 'node:path';
|
|
6
7
|
import { loadStore, mutateStore, newId } from './service.js';
|
|
7
8
|
import { badInput, notFound, conflict } from '../../core/errors.js';
|
|
8
9
|
|
|
9
10
|
// 卡片投影:面板方格与 CLI 速览共用的最小字段
|
|
10
|
-
const card = (r) => ({ id: r.id, name: r.name, desc: r.desc, path: r.path
|
|
11
|
+
const card = (r) => ({ id: r.id, name: r.name, desc: r.desc, path: r.path });
|
|
11
12
|
|
|
12
13
|
const cleanText = (v) => String(v || '').trim();
|
|
13
14
|
|
|
15
|
+
// 组内描述:同一个仓库在不同看板里的角色可以各不相同(「fr 组里它是 API 契约方」,
|
|
16
|
+
// 「nx 组里它只是同族工具」),所以描述挂在**看板的成员关系**上(repoNotes),
|
|
17
|
+
// 而不是仓库登记上。登记里的 desc 回答「这个仓库是干什么的」,
|
|
18
|
+
// 组内描述回答「它在这个组里占什么位置、怎么配合」。
|
|
19
|
+
const noteOf = (b, rid) => (b.repoNotes || {})[rid] || '';
|
|
20
|
+
|
|
14
21
|
export async function listBoards() {
|
|
15
22
|
const s = await loadStore();
|
|
16
23
|
const inBoard = new Set(s.boards.flatMap((b) => b.repoIds || []));
|
|
17
|
-
const reposOf = (
|
|
18
|
-
|
|
24
|
+
const reposOf = (b) =>
|
|
25
|
+
(b.repoIds || [])
|
|
26
|
+
.map((id) => s.repos.find((r) => r.id === id))
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((r) => ({ ...card(r), note: noteOf(b, r.id) }));
|
|
19
29
|
return {
|
|
20
|
-
boards: s.boards.map((b) => ({ id: b.id, name: b.name, desc: b.desc, repos: reposOf(b
|
|
30
|
+
boards: s.boards.map((b) => ({ id: b.id, name: b.name, desc: b.desc, repos: reposOf(b) })),
|
|
21
31
|
unassigned: s.repos.filter((r) => !inBoard.has(r.id)).map(card),
|
|
22
32
|
};
|
|
23
33
|
}
|
|
@@ -26,7 +36,10 @@ export async function getBoard(id) {
|
|
|
26
36
|
const s = await loadStore();
|
|
27
37
|
const b = s.boards.find((x) => x.id === id);
|
|
28
38
|
if (!b) throw notFound('看板不存在: ' + id);
|
|
29
|
-
const repos = (b.repoIds || [])
|
|
39
|
+
const repos = (b.repoIds || [])
|
|
40
|
+
.map((rid) => s.repos.find((r) => r.id === rid))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((r) => ({ ...card(r), note: noteOf(b, r.id) }));
|
|
30
43
|
return { id: b.id, name: b.name, desc: b.desc, repos };
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -87,12 +100,15 @@ export async function assignRepo({ id, board, before, remove } = {}) {
|
|
|
87
100
|
target.repoIds.splice(at, 1);
|
|
88
101
|
n = 1;
|
|
89
102
|
}
|
|
103
|
+
// 摘出成员时组内描述一起带走(它只对这个成员关系有意义)
|
|
104
|
+
if (target.repoNotes && n) delete target.repoNotes[id];
|
|
90
105
|
} else {
|
|
91
106
|
for (const b of s.boards) {
|
|
92
107
|
const at = (b.repoIds || []).indexOf(id);
|
|
93
108
|
if (at >= 0) {
|
|
94
109
|
b.repoIds.splice(at, 1);
|
|
95
110
|
n += 1;
|
|
111
|
+
if (b.repoNotes) delete b.repoNotes[id];
|
|
96
112
|
}
|
|
97
113
|
}
|
|
98
114
|
}
|
|
@@ -115,6 +131,32 @@ export async function assignRepo({ id, board, before, remove } = {}) {
|
|
|
115
131
|
});
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
// 设置组内描述:id = 仓库(id 或绝对路径均可),board = 看板 id。
|
|
135
|
+
// 只对**已在板内**的成员有意义——不是成员就显式报错,别静默挂到没人读的地方。
|
|
136
|
+
// note 为空 = 清除。
|
|
137
|
+
export async function setBoardRepoNote({ id, board, note } = {}) {
|
|
138
|
+
if (!id) throw badInput('缺少仓库 id 或路径');
|
|
139
|
+
if (!board) throw badInput('缺少看板 id(--board)');
|
|
140
|
+
return mutateStore((s) => {
|
|
141
|
+
let repo = s.repos.find((r) => r.id === id);
|
|
142
|
+
if (!repo) {
|
|
143
|
+
const p = resolve(String(id));
|
|
144
|
+
repo = s.repos.find((r) => r.path.toLowerCase() === p.toLowerCase());
|
|
145
|
+
}
|
|
146
|
+
if (!repo) throw notFound('repo 不存在: ' + id);
|
|
147
|
+
const b = s.boards.find((x) => x.id === board);
|
|
148
|
+
if (!b) throw notFound('看板不存在: ' + board);
|
|
149
|
+
if (!(b.repoIds || []).includes(repo.id)) {
|
|
150
|
+
throw conflict(`仓库「${repo.name}」不在看板「${b.name}」里——组内描述只对成员有意义`);
|
|
151
|
+
}
|
|
152
|
+
const text = String(note || '').trim();
|
|
153
|
+
if (!b.repoNotes) b.repoNotes = {};
|
|
154
|
+
if (text) b.repoNotes[repo.id] = text;
|
|
155
|
+
else delete b.repoNotes[repo.id];
|
|
156
|
+
return { id: repo.id, name: repo.name, board: b.id, boardName: b.name, note: text };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
118
160
|
// 导入:一段 JSON = 一批主题。{boards:[{name, desc?, repos:[{path|name, desc?}]}]}
|
|
119
161
|
// 条目按 path(大小写不敏感)或名称匹配**已登记**仓库——导入不硬造登记,
|
|
120
162
|
// 匹配不上的如实报告(数量与清单)。同名看板 = 合并(追加匹配到的仓库)。
|
|
@@ -162,13 +204,18 @@ export async function importBoards({ json } = {}) {
|
|
|
162
204
|
b.repoIds.push(id);
|
|
163
205
|
matched += 1;
|
|
164
206
|
}
|
|
207
|
+
const note = cleanText(e.note);
|
|
208
|
+
if (note) {
|
|
209
|
+
if (!b.repoNotes) b.repoNotes = {};
|
|
210
|
+
b.repoNotes[id] = note;
|
|
211
|
+
}
|
|
165
212
|
}
|
|
166
213
|
}
|
|
167
214
|
return { created, merged, matched, unmatched };
|
|
168
215
|
});
|
|
169
216
|
}
|
|
170
217
|
|
|
171
|
-
// 导出:看板 + 仓库的 path/name/desc(可回灌给 import 的同一结构)
|
|
218
|
+
// 导出:看板 + 仓库的 path/name/desc + 组内描述 note(可回灌给 import 的同一结构)
|
|
172
219
|
export async function exportBoards() {
|
|
173
220
|
const s = await loadStore();
|
|
174
221
|
return {
|
|
@@ -177,9 +224,13 @@ export async function exportBoards() {
|
|
|
177
224
|
name: b.name,
|
|
178
225
|
desc: b.desc,
|
|
179
226
|
repos: (b.repoIds || [])
|
|
180
|
-
.map((id) =>
|
|
181
|
-
|
|
182
|
-
|
|
227
|
+
.map((id) => {
|
|
228
|
+
const r = s.repos.find((x) => x.id === id);
|
|
229
|
+
if (!r) return null;
|
|
230
|
+
const note = noteOf(b, id);
|
|
231
|
+
return note ? { path: r.path, name: r.name, desc: r.desc, note } : { path: r.path, name: r.name, desc: r.desc };
|
|
232
|
+
})
|
|
233
|
+
.filter(Boolean),
|
|
183
234
|
})),
|
|
184
235
|
};
|
|
185
236
|
}
|
|
@@ -42,7 +42,7 @@ function renderRepoList(list) {
|
|
|
42
42
|
if (!list.length) return '(暂无仓库,用 repo add <path> 登记)';
|
|
43
43
|
const lines = [`共 ${list.length} 个仓库`];
|
|
44
44
|
for (const r of list) {
|
|
45
|
-
lines.push(`${r.name.padEnd(18)} ${r.path}
|
|
45
|
+
lines.push(`${r.name.padEnd(18)} ${r.path}`);
|
|
46
46
|
}
|
|
47
47
|
return lines.join('\n');
|
|
48
48
|
}
|
|
@@ -52,10 +52,12 @@ function renderRepo(r) {
|
|
|
52
52
|
`${r.name}${r.desc ? ' ' + r.desc : ''}`,
|
|
53
53
|
` id: ${r.id}`,
|
|
54
54
|
` 路径: ${r.path}`,
|
|
55
|
-
` 标签: ${r.tags.length ? r.tags.join(', ') : '-'}`,
|
|
56
55
|
` 登记: ${(r.addedAt || '').slice(0, 10)}`,
|
|
57
56
|
` 更新: ${(r.updatedAt || '').slice(0, 10)}`,
|
|
58
57
|
];
|
|
58
|
+
if (r.boards?.length) {
|
|
59
|
+
lines.push(` 看板: ${r.boards.map((b) => b.name).join('、')}`);
|
|
60
|
+
}
|
|
59
61
|
if (r.notes) lines.push(' 备注: ' + r.notes);
|
|
60
62
|
return lines.join('\n');
|
|
61
63
|
}
|
|
@@ -100,7 +102,6 @@ export default {
|
|
|
100
102
|
flags: {
|
|
101
103
|
name: { type: 'string' },
|
|
102
104
|
desc: { type: 'string' },
|
|
103
|
-
tags: { type: 'array' },
|
|
104
105
|
notes: { type: 'string' },
|
|
105
106
|
},
|
|
106
107
|
run: (ctx) => service.addRepo(ctx),
|
|
@@ -110,13 +111,12 @@ export default {
|
|
|
110
111
|
id: 'repo.update',
|
|
111
112
|
cli: ['repo', 'update'],
|
|
112
113
|
http: ['PATCH', '/api/repos/:id'],
|
|
113
|
-
summary: '修改仓库路径 / 名称 / 描述 /
|
|
114
|
+
summary: '修改仓库路径 / 名称 / 描述 / 备注',
|
|
114
115
|
args: ['id'],
|
|
115
116
|
flags: {
|
|
116
117
|
path: { type: 'string' },
|
|
117
118
|
name: { type: 'string' },
|
|
118
119
|
desc: { type: 'string' },
|
|
119
|
-
tags: { type: 'array' },
|
|
120
120
|
notes: { type: 'string' },
|
|
121
121
|
},
|
|
122
122
|
run: (ctx) => service.updateRepo(ctx.id, ctx),
|
|
@@ -235,6 +235,23 @@ export default {
|
|
|
235
235
|
? `已移出: ${d.name}(${d.count} 个看板)`
|
|
236
236
|
: `${d.added ? '已聚合' : '已在(位次已调)'}: ${d.name} → ${d.board}`),
|
|
237
237
|
},
|
|
238
|
+
{
|
|
239
|
+
// 组内描述:同一个仓库在不同看板里的角色各不相同,描述挂在成员关系上而非登记上。
|
|
240
|
+
// 登记的 desc = 「这个仓库是干什么的」;组内描述 = 「它在这个组里占什么位置、怎么配合」。
|
|
241
|
+
id: 'board.note',
|
|
242
|
+
cli: ['board', 'note'],
|
|
243
|
+
http: ['POST', '/api/boards/note'],
|
|
244
|
+
summary: '写/清组内描述:仓库在看板里的角色与配合关系(--note 留空 = 清除;仓库给 id 或绝对路径)',
|
|
245
|
+
args: ['id'],
|
|
246
|
+
flags: {
|
|
247
|
+
board: { type: 'string', required: true, hint: '看板 id' },
|
|
248
|
+
note: { type: 'string', hint: '组内描述(多行可含关键路径/关键词;留空 = 清除)' },
|
|
249
|
+
},
|
|
250
|
+
run: (ctx) => boards.setBoardRepoNote({ id: ctx.id, board: ctx.board, note: ctx.note }),
|
|
251
|
+
render: (d) => (d.note
|
|
252
|
+
? `已写组内描述: ${d.name} @ 「${d.boardName}」`
|
|
253
|
+
: `已清除组内描述: ${d.name} @ 「${d.boardName}」`),
|
|
254
|
+
},
|
|
238
255
|
{
|
|
239
256
|
id: 'board.import',
|
|
240
257
|
cli: ['board', 'import'],
|
|
@@ -80,19 +80,14 @@ export function newId(prefix) {
|
|
|
80
80
|
return prefix + '_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return [];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function normalizeRepoInput({ path, name, tags, notes, desc }) {
|
|
83
|
+
// 标签已废除(2026-10-24):分组只走看板一条路,两套平行的分组机制只会互相打架。
|
|
84
|
+
// 旧数据里残留的 tags 字段留在盘上不动,但不再写入、不再外发。
|
|
85
|
+
function normalizeRepoInput({ path, name, notes, desc }) {
|
|
90
86
|
const abs = resolve(String(path || ''));
|
|
91
87
|
return {
|
|
92
88
|
id: newId('r'),
|
|
93
89
|
name: (name || basename(abs) || 'repo').trim(),
|
|
94
90
|
path: abs,
|
|
95
|
-
tags: parseTags(tags),
|
|
96
91
|
desc: (desc || '').trim(),
|
|
97
92
|
notes: (notes || '').trim(),
|
|
98
93
|
addedAt: new Date().toISOString(),
|
|
@@ -119,11 +114,11 @@ export async function listRepos() {
|
|
|
119
114
|
for (const b of s.boards) {
|
|
120
115
|
for (const rid of b.repoIds || []) {
|
|
121
116
|
if (!boardsOf.has(rid)) boardsOf.set(rid, []);
|
|
122
|
-
boardsOf.get(rid).push({ id: b.id, name: b.name, desc: b.desc || '' });
|
|
117
|
+
boardsOf.get(rid).push({ id: b.id, name: b.name, desc: b.desc || '', note: (b.repoNotes || {})[rid] || '' });
|
|
123
118
|
}
|
|
124
119
|
}
|
|
125
120
|
return s.repos
|
|
126
|
-
.map((r) => ({ ...r, boards: boardsOf.get(r.id) || [] }))
|
|
121
|
+
.map(({ tags: _dropped, ...r }) => ({ ...r, boards: boardsOf.get(r.id) || [] }))
|
|
127
122
|
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
128
123
|
}
|
|
129
124
|
|
|
@@ -144,7 +139,6 @@ export async function updateRepo(id, patch) {
|
|
|
144
139
|
const r = s.repos.find((x) => x.id === id);
|
|
145
140
|
if (!r) throw notFound('repo 不存在: ' + id);
|
|
146
141
|
if (patch.name !== undefined) r.name = String(patch.name).trim();
|
|
147
|
-
if (patch.tags !== undefined) r.tags = parseTags(patch.tags);
|
|
148
142
|
if (patch.desc !== undefined) r.desc = String(patch.desc).trim();
|
|
149
143
|
if (patch.notes !== undefined) r.notes = String(patch.notes);
|
|
150
144
|
if (patch.path !== undefined) {
|
|
@@ -183,7 +177,8 @@ export async function getRepo(idOrPath) {
|
|
|
183
177
|
const boards = s.boards
|
|
184
178
|
.filter((b) => (b.repoIds || []).includes(r.id))
|
|
185
179
|
.map((b) => ({ id: b.id, name: b.name, desc: b.desc || '' }));
|
|
186
|
-
|
|
180
|
+
const { tags: _dropped, ...base } = r;
|
|
181
|
+
return { ...base, boards };
|
|
187
182
|
}
|
|
188
183
|
|
|
189
184
|
const SKIP_DIRS = new Set([
|
|
@@ -320,7 +315,7 @@ export async function contextMap() {
|
|
|
320
315
|
for (const b of s.boards) {
|
|
321
316
|
for (const rid of b.repoIds || []) {
|
|
322
317
|
if (!boardsOf.has(rid)) boardsOf.set(rid, []);
|
|
323
|
-
boardsOf.get(rid).push({ id: b.id, name: b.name, desc: b.desc || '' });
|
|
318
|
+
boardsOf.get(rid).push({ id: b.id, name: b.name, desc: b.desc || '', note: (b.repoNotes || {})[rid] || '' });
|
|
324
319
|
}
|
|
325
320
|
}
|
|
326
321
|
const map = new Map();
|
|
@@ -329,7 +324,6 @@ export async function contextMap() {
|
|
|
329
324
|
id: r.id,
|
|
330
325
|
name: r.name,
|
|
331
326
|
path: r.path,
|
|
332
|
-
tags: r.tags || [],
|
|
333
327
|
desc: r.desc || '',
|
|
334
328
|
notes: r.notes || '',
|
|
335
329
|
boards: boardsOf.get(r.id) || [],
|