@stofone/dsh-agent-roster 0.13.1 → 1.1.0
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 +58 -0
- package/README.md +16 -10
- package/README.zh.md +27 -9
- package/bin/cli.mjs +87 -8
- package/client.js +126 -24
- package/docs/roster-v2.md +1 -1
- package/examples/space.yaml +11 -0
- package/examples-orchestration/space.yaml +10 -0
- package/index.js +30 -1
- package/lib/clone.js +318 -0
- package/lib/library.js +3 -3
- package/lib/registry.js +30 -30
- package/lib/render.js +6 -6
- package/lib/service.js +47 -0
- package/lib/space.js +382 -0
- package/lib/tools.js +3 -3
- package/locale/en.json +7 -2
- package/locale/zh.json +7 -2
- package/orgs/agent-roster-collective/space.yaml +30 -0
- package/package.json +1 -1
- package/schema/space.v1.schema.json +60 -0
- package/lib/organization.js +0 -331
- package/orgs/agent-roster-collective/organization.yaml +0 -118
- package/schema/organization.v1.schema.json +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.1.0] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### The library reads as spaces, not groups
|
|
10
|
+
|
|
11
|
+
1.0.0 made the space a real level in the format. The panel still showed collections as small labels, so the
|
|
12
|
+
level existed in the files and not on screen. Each group header is now the space itself: its id sits in the
|
|
13
|
+
same column as the roster ids (the register keeps one spine), the right side states two facts you can check
|
|
14
|
+
on the spot — how many rosters it holds, how many are already here — and one button pulls the whole space.
|
|
15
|
+
|
|
16
|
+
Rows also stopped lying about state. A roster that is already on this machine says so, and its action
|
|
17
|
+
becomes **Open** instead of **Pull**: the panel no longer offers to fetch something you already have. The
|
|
18
|
+
local group drops the per-row "local" label, because the group header already says it.
|
|
19
|
+
|
|
20
|
+
On a narrow panel the space header folds into two lines (id + action, then title + facts) instead of
|
|
21
|
+
squeezing a long space name into a vertical stack one character wide.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.0.0] - 2026-11-03
|
|
25
|
+
|
|
26
|
+
### A space is the thing a repository holds
|
|
27
|
+
|
|
28
|
+
Until now a pulled roster was a flattened copy: whatever the source repository looked like, it arrived as
|
|
29
|
+
`<id>/roster.yaml` and the editor had no idea which file upstream it corresponded to. That is fine for "I
|
|
30
|
+
only need this one", and wrong for every monorepo — which is what this project's own libraries have been
|
|
31
|
+
since 0.10.0.
|
|
32
|
+
|
|
33
|
+
`space.yaml` (`kind: dsh-agent-roster/space@1`) is the level that was missing. It declares what a
|
|
34
|
+
directory in a repository *is*: which rosters it holds, whether the layout is `flat` (`<id>/roster.yaml`)
|
|
35
|
+
or `monorepo` (`roles/` + `rosters/` + `dist/`), and — only on local copies — where it came from
|
|
36
|
+
(`origin: {repo, ref, path}`). Layout is now part of the format instead of something the puller guesses.
|
|
37
|
+
Clone a space and the local tree is the repository's tree: edit `rosters/<id>/roster.yaml` and you are
|
|
38
|
+
editing the file upstream.
|
|
39
|
+
|
|
40
|
+
### Changed (breaking)
|
|
41
|
+
|
|
42
|
+
- `organization.yaml` is replaced by `space.yaml`. The old file, its `kind`
|
|
43
|
+
(`dsh-agent-roster-organization/v1`), its schema and its diagnostics codes (`organization-*`) are gone;
|
|
44
|
+
the equivalents are `space.yaml`, `dsh-agent-roster/space@1` and `space-*`. Migration: rename the file,
|
|
45
|
+
change `kind`, rename the `organization:` key to `space:`, and add `layout: flat|monorepo`.
|
|
46
|
+
- A space file inside a repository may not declare `origin`; the validator rejects it. `origin` describes
|
|
47
|
+
a local copy, and a repository claiming a source is a second copy of the truth.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `lib/space.js` + `schema/space.v1.schema.json`: declaration, cross-check (missing = error, undeclared =
|
|
52
|
+
warn, archived = info) and strict validation, with `layout` and `origin`.
|
|
53
|
+
- `lib/clone.js` and `dsh-agent-roster clone --repo <owner/repo> [--path] [--ref] [--space]`: pull a whole
|
|
54
|
+
space with its layout intact — rosters, the roles they reference, and each role's `parts/`. The written
|
|
55
|
+
`space.yaml` is the source's cover plus `origin`.
|
|
56
|
+
- `POST /plugins/dsh-agent-roster/install-space` and a "Pull whole space" button on every source group in
|
|
57
|
+
the Library tab; pulled files land in `<library>/<space>/` with the repository's paths.
|
|
58
|
+
- The Library tab marks rosters that are already installed locally, so "everything is pulled" and "what
|
|
59
|
+
do I actually have" stop looking the same.
|
|
60
|
+
- `examples/`, `examples-orchestration/` and `orgs/agent-roster-collective/` each ship a `space.yaml`.
|
|
61
|
+
|
|
62
|
+
|
|
7
63
|
## [0.13.1] - 2026-11-03
|
|
8
64
|
|
|
9
65
|
### The local rosters are the front page
|
|
@@ -628,6 +684,8 @@ point is now one search field above the tabs.
|
|
|
628
684
|
|
|
629
685
|
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...main
|
|
630
686
|
[0.13.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.13.0...v0.13.1
|
|
687
|
+
[1.0.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.13.1...v1.0.0
|
|
688
|
+
[1.1.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v1.0.0...v1.1.0
|
|
631
689
|
[0.13.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...v0.13.0
|
|
632
690
|
[0.12.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.11.1...v0.12.0
|
|
633
691
|
[0.11.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.11.0...v0.11.1
|
package/README.md
CHANGED
|
@@ -187,13 +187,14 @@ Behaviour: under `onMissing: refuse` (default) `roster_create` emits **no** `age
|
|
|
187
187
|
|
|
188
188
|
The sibling file has its own JSON Schema: [`schema/capabilities.v1.schema.json`](./schema/capabilities.v1.schema.json), kept aligned with `lib/capabilities.js` by `test/capabilities.test.js`.
|
|
189
189
|
|
|
190
|
-
##
|
|
190
|
+
## Spaces (`space.yaml`)
|
|
191
191
|
|
|
192
192
|
A roster answers *who the members are*; it cannot answer **which rosters belong together, which one is production and which is retired**. When a library holds twenty archives from different sources, the directory listing stops being an index. An optional file at the **library root** declares that, and nothing else:
|
|
193
193
|
|
|
194
194
|
```yaml
|
|
195
|
-
kind: dsh-agent-roster
|
|
196
|
-
|
|
195
|
+
kind: dsh-agent-roster/space@1
|
|
196
|
+
space: agent-roster-collective # must equal the directory name
|
|
197
|
+
layout: monorepo # flat = <id>/roster.yaml; monorepo = roles/ + rosters/ + dist/
|
|
197
198
|
title: 团队档案合集
|
|
198
199
|
maintainers: [STofone]
|
|
199
200
|
rosters:
|
|
@@ -207,13 +208,13 @@ The declaration is mechanically checked against disk, and the three outcomes are
|
|
|
207
208
|
|
|
208
209
|
| Situation | Code | Severity | Why |
|
|
209
210
|
|---|---|---|---|
|
|
210
|
-
| declared, directory missing | `
|
|
211
|
-
| directory present, not declared | `
|
|
212
|
-
| declared `archived`, directory still there | `
|
|
211
|
+
| declared, directory missing | `space-roster-missing` | **error** | the organization page is lying — worse than no page |
|
|
212
|
+
| directory present, not declared | `space-roster-undeclared` | warn | someone added a roster around the index; it still loads |
|
|
213
|
+
| declared `archived`, directory still there | `space-roster-archived` | info | archiving is a statement, not a delete |
|
|
213
214
|
|
|
214
|
-
`
|
|
215
|
+
`space` must equal the directory name, exactly like `roster` does: position is identity, and two ids for one library would make every diagnostic ambiguous. Unknown keys are rejected — a field nobody can check is another way to look green.
|
|
215
216
|
|
|
216
|
-
This file creates nothing. It does not write `team.json`, does not create members, and never reaches `agent_teams_add_member`. Its JSON Schema is [`schema/
|
|
217
|
+
This file creates nothing. It does not write `team.json`, does not create members, and never reaches `agent_teams_add_member`. Its JSON Schema is [`schema/space.v1.schema.json`](./schema/space.v1.schema.json), kept aligned with `lib/organization.js` by `test/space.test.js`.
|
|
217
218
|
|
|
218
219
|
**A worked example ships in this repository:** [`orgs/agent-roster-collective/`](./orgs/agent-roster-collective/) holds 21 rosters (63 members) covering API review, incident response, data quality, database migration, localization, security audit, performance, frontend, docs, release, support triage, recruiting, market research, finance reconciliation, contract review, content ops, regression testing, model evaluation, SRE on-call, literature review and product discovery. Every roster is a three-person team with a captain, and each was written for this repository (no upstream text is copied). Validate it with:
|
|
219
220
|
|
|
@@ -325,6 +326,11 @@ response, the tag strip summarises every tag in view with its count (multiple ta
|
|
|
325
326
|
segmented control switches between **by source** (this machine, then one group per collection) and **flat**
|
|
326
327
|
(one list, source named per row). Tag filtering is view state only; it never writes back to a file.
|
|
327
328
|
|
|
329
|
+
By source, each group's head **is the space**: its id sits in the same column as the roster ids, the
|
|
330
|
+
right side states two facts you can check on the spot — how many rosters it holds, how many are already
|
|
331
|
+
here — and carries one button that pulls the whole space. A roster that is already here says so in its row
|
|
332
|
+
and its button reads **Open**: the panel never asks you to pull something you already have.
|
|
333
|
+
|
|
328
334
|
Multiple words are AND, not OR: `data kubernetes` returns nothing rather than every roster that merely
|
|
329
335
|
mentions "data". Ranking is a pure function in `lib/search.js`, so it is unit-tested rather than eyeballed.
|
|
330
336
|
|
|
@@ -510,7 +516,7 @@ bin/cli.mjs validate/list/compile/migrate/registry for CI
|
|
|
510
516
|
scripts/ host-smoke.mjs, client-smoke.mjs, check-locale.mjs (dev only, not shipped)
|
|
511
517
|
examples/ a v2 library migrated from the v1 archives
|
|
512
518
|
examples-orchestration/ a v2 library with orchestration, gates and a construction plan
|
|
513
|
-
orgs/ a worked example library with its own
|
|
519
|
+
orgs/ a worked example library with its own space.yaml (21 rosters)
|
|
514
520
|
test/ unit tests
|
|
515
521
|
```
|
|
516
522
|
|
|
@@ -518,7 +524,7 @@ test/ unit tests
|
|
|
518
524
|
|
|
519
525
|
Verified on this machine:
|
|
520
526
|
|
|
521
|
-
- `npm test` —
|
|
527
|
+
- `npm test` — 313/313 passing (format, parse, validate, registry, paths, bridge, verify buckets, digests and provenance, roster diff, capability requirements and the capability gate, library organization, registry schema, lossless JSON, write plan/apply, all routes, panel snapshot and hints, dictionary consistency, tool contracts, client loader contract, library snapshot, config view, package manifest, declared fields and captain block, schema/constant alignment, the v2 role/roster/compiled layers and their schema alignment, v1→v2 migration, run records feeding the plan, remote registry and one-click install).
|
|
522
528
|
- `npm run smoke` — `apply()` registers all six tools through a real `defineTool` from a DSH bundle; `roster_list` / `roster_show` / `roster_create` / `roster_verify` / `roster_diff` execute, every output survives `lossless` checks, `path` overrides are honored, effect disposal runs cleanly. The same command then loads `client.js` against a stubbed module table and asserts the three slot registrations, style injection and effect disposal.
|
|
523
529
|
- CLI: `validate --dir examples` returns exit 0 over both example libraries.
|
|
524
530
|
- Installed into a real profile with the plugin manager (`plugin_manager install_bundle` against this directory → `link:` dependency + bundle entry); the profile re-reads the linked files on restart.
|
package/README.zh.md
CHANGED
|
@@ -183,13 +183,14 @@ notes: 建队前先确认成员会话可用。
|
|
|
183
183
|
|
|
184
184
|
兄弟文件有自己的 JSON Schema:[`schema/capabilities.v1.schema.json`](./schema/capabilities.v1.schema.json),由 `test/capabilities.test.js` 钉住与 `lib/capabilities.js` 一致。
|
|
185
185
|
|
|
186
|
-
##
|
|
186
|
+
## 档案空间(`space.yaml`)
|
|
187
187
|
|
|
188
188
|
档案回答的是**成员是谁**,回答不了**哪几份档案属于同一套、哪份是生产、哪份已退役**。一座库放二十份来自不同来源的档案时,目录列表就不再是索引。库**根目录**上一个可选文件只声明这件事:
|
|
189
189
|
|
|
190
190
|
```yaml
|
|
191
|
-
kind: dsh-agent-roster
|
|
192
|
-
|
|
191
|
+
kind: dsh-agent-roster/space@1
|
|
192
|
+
space: agent-roster-collective # 必须等于目录名
|
|
193
|
+
layout: monorepo # flat = <id>/roster.yaml;monorepo = roles/ + rosters/ + dist/
|
|
193
194
|
title: 团队档案合集
|
|
194
195
|
maintainers: [STofone]
|
|
195
196
|
rosters:
|
|
@@ -203,13 +204,26 @@ rosters:
|
|
|
203
204
|
|
|
204
205
|
| 情况 | 代码 | 严重度 | 理由 |
|
|
205
206
|
|---|---|---|---|
|
|
206
|
-
| 声明了,目录不在 | `
|
|
207
|
-
| 目录在,没登记 | `
|
|
208
|
-
| 声明 `archived`,目录还在 | `
|
|
207
|
+
| 声明了,目录不在 | `space-roster-missing` | **error** | 组织页在撒谎,比没有组织页更糟 |
|
|
208
|
+
| 目录在,没登记 | `space-roster-undeclared` | warn | 有人绕过索引加了档案,但它仍会被加载 |
|
|
209
|
+
| 声明 `archived`,目录还在 | `space-roster-archived` | info | 归档是声明,不是删除 |
|
|
209
210
|
|
|
210
|
-
`
|
|
211
|
+
`space` 必须等于目录名,和 `roster` 同一条规则:位置即身份,一座库两个 id 会让所有诊断都变得没法定位。未知键一律拒收——没人核得了的字段,只是换一种方式发绿。
|
|
211
212
|
|
|
212
|
-
这份文件什么都不创建:不写 `team.json`、不建成员,也永远不会进入 `agent_teams_add_member`
|
|
213
|
+
这份文件什么都不创建:不写 `team.json`、不建成员,也永远不会进入 `agent_teams_add_member` 参数。
|
|
214
|
+
|
|
215
|
+
**它同时固定了布局。** `layout: flat` 是 `<id>/roster.yaml`(v1 时代的一仓多档案);`layout: monorepo` 是 `roles/<role>/AGENT.md` + `rosters/<id>/roster.yaml` + `dist/`。布局进了格式,编辑器才知道该改哪个文件——不再是"拉下来摊平、改的和源文件没关系"。
|
|
216
|
+
|
|
217
|
+
**本地副本靠 `origin` 认祖归宗。** 从远端拉下来的空间会在 `space.yaml` 里多一段 `origin: {repo, ref, path}`,其余字段原样保留;仓库里的空间文件**不许**写 `origin`(源就是这个仓库本身,再写一遍就是第二真源),校验器会拒收。
|
|
218
|
+
|
|
219
|
+
拉一整座空间、并且保留原布局(拉完就能改,改的就是源仓库里那个文件):
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
dsh-agent-roster clone --repo dsh-agent-roster/agent-teams --space agent-teams
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
面板「档案库」里每组来源都有一个「拉取整个空间」按钮,等价于这条命令;单份档案的「拉取」仍然只摊平那一份(适合"我只要这一份、不打算往回改")。
|
|
226
|
+
它的 JSON Schema 在 [`schema/space.v1.schema.json`](./schema/space.v1.schema.json),由 `test/space.test.js` 钉住与 `lib/space.js` 一致。
|
|
213
227
|
|
|
214
228
|
**仓内带了一个可跑的示例**:[`orgs/agent-roster-collective/`](./orgs/agent-roster-collective/) 里有 21 份档案(63 名成员),覆盖接口评审、事故响应、数据质量、数据库迁移、本地化、安全审计、性能、前端、文档、发布、客服分诊、招聘、市场研究、财务对账、合同审阅、内容运营、回归测试、模型评测、SRE 值守、文献综述与产品发现。每份都是"队长 + 两名专家"的三人组,全部为本项目自撰(未复制任何上游文本)。校验方式:
|
|
215
229
|
|
|
@@ -309,6 +323,10 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
309
323
|
多选是「且」;右侧可以在**按来源**(本机 + 每个 collection 各一组)与**平铺**(一份清单,来源写在行内)
|
|
310
324
|
之间切换。标签筛选只影响视图,不写回文件。
|
|
311
325
|
|
|
326
|
+
按来源时,每一组的头就是**空间**本身:等宽的空间 id 落在与档案 id 同一列,右侧写两件当场能核对的事实
|
|
327
|
+
——「几份 · 已装几份」——并带一个「拉取整个空间」按钮。已经在本机的那份,行内写「已在本机」,
|
|
328
|
+
按钮变成「打开」:面板不会让你去拉一份自己已经有的档案。窄屏上这个头折成两行,空间名不会被挤成竖排。
|
|
329
|
+
|
|
312
330
|
多个词是「且」不是「或」:`data kubernetes` 一条都不返,而不是把所有提到 data 的档案都堆上来。
|
|
313
331
|
排序是 `lib/search.js` 里的纯函数,所以它被单测钉住,而不是靠肉眼看着顺。
|
|
314
332
|
|
|
@@ -487,7 +505,7 @@ test/ 单测
|
|
|
487
505
|
|
|
488
506
|
本机已验证:
|
|
489
507
|
|
|
490
|
-
- `npm test` ——
|
|
508
|
+
- `npm test` —— 313/313 通过(格式、解析、校验、扫描、路径、桥接、核对四桶、摘要与溯源、档案差异、能力要求与能力闸门、档案库组织、索引 schema、无损 JSON、写入计划与执行、六条路由、快照与提示、两本词典一致、工具契约、客户端加载器契约、包清单、配置视图、样式 token 约束、声明层字段与队长段、schema 与常量对齐、v2 角色/组装/编译三层及其 schema 对齐、v1→v2 迁移、执行回执并入施工图)。
|
|
491
509
|
- `npm run smoke` —— 用真实 DSH bundle 的 `defineTool` 注册六个工具,逐个调用:输出全部通过无损校验、`path` 覆盖生效、释放干净;同一条命令随后用桩模块表加载 `client.js`,断言三处槽位注册、样式只注入一次、effect 可回收。
|
|
492
510
|
- CLI:`validate --dir examples` 对两座示例库退出码 0。
|
|
493
511
|
- 用插件管理器真的装进了本机 profile(`plugin_manager install_bundle` 指向本仓库 → `link:` 依赖 + bundle 条目),重启后按链接读取本仓库文件。
|
package/bin/cli.mjs
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* dsh-agent-roster migrate --dir examples [--write] [--json]
|
|
16
16
|
* dsh-agent-roster migrate --dir . --from rosters --repo owner/repo --write
|
|
17
17
|
*/
|
|
18
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import nodePath from "node:path";
|
|
20
20
|
|
|
21
21
|
import { resolveDirSpec } from "../lib/home.js";
|
|
22
|
+
import { globalRosterRoot } from "../lib/home.js";
|
|
23
|
+
import { cloneSpace } from "../lib/clone.js";
|
|
22
24
|
import { scanRosterLibrary } from "../lib/registry.js";
|
|
23
25
|
import { renderRosterList } from "../lib/render.js";
|
|
24
26
|
import { REGISTRY_KIND } from "../lib/format.js";
|
|
@@ -40,6 +42,8 @@ const USAGE = `用法:
|
|
|
40
42
|
dsh-agent-roster compile --roster <目录> 把 v2 组装编译成自包含产物(不写盘)
|
|
41
43
|
dsh-agent-roster migrate --dir <目录> [--from <子目录>] [--repo <owner/repo>] [--write]
|
|
42
44
|
把 v1 档案原地迁成 v2(roles/ + rosters/ + dist/)
|
|
45
|
+
dsh-agent-roster clone --repo <owner/repo> [--ref <分支>] [--path <子目录>] [--space <id>] [--dir <目录>]
|
|
46
|
+
把一个远端空间按原布局拉成本地副本(可编辑)
|
|
43
47
|
dsh-agent-roster --version 输出版本号
|
|
44
48
|
|
|
45
49
|
选项:
|
|
@@ -50,7 +54,10 @@ const USAGE = `用法:
|
|
|
50
54
|
--write sync 把重算结果写回文件
|
|
51
55
|
--roster <path> v2 组装目录(compile 用)
|
|
52
56
|
--from <path> v1 档案所在目录(migrate 用;缺省与 --dir 相同,用于仓库根 + rosters/ 布局)
|
|
53
|
-
--repo <owner/repo> 写进 dist 产物的仓库坐标(migrate
|
|
57
|
+
--repo <owner/repo> 写进 dist 产物的仓库坐标(migrate 用);clone 的源仓库
|
|
58
|
+
--ref <branch|tag> clone 的分支/tag(缺省 main)
|
|
59
|
+
--path <subdir> clone 的仓库内子目录(缺省仓库根)
|
|
60
|
+
--space <id> clone 落到本地后的空间 id(缺省取仓库名)
|
|
54
61
|
--run <path> 执行回执(compile 用;把上一轮失败并入重试卡)
|
|
55
62
|
--roles <path> 角色库目录(compile 用;缺省按 rosters/<id> 推同级的 roles/)
|
|
56
63
|
--json 以 JSON 输出(validate / list / compile 都支持)
|
|
@@ -73,6 +80,9 @@ function parseArgs(argv) {
|
|
|
73
80
|
roles: null,
|
|
74
81
|
run: null,
|
|
75
82
|
from: null,
|
|
83
|
+
ref: null,
|
|
84
|
+
path: null,
|
|
85
|
+
space: null,
|
|
76
86
|
repo: null,
|
|
77
87
|
};
|
|
78
88
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -112,6 +122,21 @@ function parseArgs(argv) {
|
|
|
112
122
|
if (value === undefined) throw new UsageError("--run 需要一个路径");
|
|
113
123
|
options.run = value;
|
|
114
124
|
index += 1;
|
|
125
|
+
} else if (arg === "--ref") {
|
|
126
|
+
const value = argv[index + 1];
|
|
127
|
+
if (value === undefined) throw new UsageError("--ref 需要一个分支名");
|
|
128
|
+
options.ref = value;
|
|
129
|
+
index += 1;
|
|
130
|
+
} else if (arg === "--path") {
|
|
131
|
+
const value = argv[index + 1];
|
|
132
|
+
if (value === undefined) throw new UsageError("--path 需要一个相对目录");
|
|
133
|
+
options.path = value;
|
|
134
|
+
index += 1;
|
|
135
|
+
} else if (arg === "--space") {
|
|
136
|
+
const value = argv[index + 1];
|
|
137
|
+
if (value === undefined) throw new UsageError("--space 需要一个 id");
|
|
138
|
+
options.space = value;
|
|
139
|
+
index += 1;
|
|
115
140
|
} else if (arg === "--roles") {
|
|
116
141
|
const value = argv[index + 1];
|
|
117
142
|
if (value === undefined) throw new UsageError("--roles 需要一个路径");
|
|
@@ -159,6 +184,7 @@ async function main(argv) {
|
|
|
159
184
|
return 0;
|
|
160
185
|
}
|
|
161
186
|
if (options.command === "compile") return runCompile(options);
|
|
187
|
+
if (options.command === "clone") return runClone(options);
|
|
162
188
|
if (options.command === "migrate") return runMigrate(options);
|
|
163
189
|
if (options.command === "registry") {
|
|
164
190
|
if (options.file === null) {
|
|
@@ -195,8 +221,8 @@ async function main(argv) {
|
|
|
195
221
|
rootDir: report.rootDir,
|
|
196
222
|
missing: report.scan.missing,
|
|
197
223
|
stats: report.scan.stats,
|
|
198
|
-
|
|
199
|
-
|
|
224
|
+
space: report.scan.space,
|
|
225
|
+
spaceCheck: report.scan.spaceCheck,
|
|
200
226
|
rosters: report.scan.rosters.map((roster) => ({ id: roster.id, file: roster.file })),
|
|
201
227
|
diagnostics: report.scan.diagnostics,
|
|
202
228
|
})),
|
|
@@ -212,11 +238,11 @@ async function main(argv) {
|
|
|
212
238
|
process.stdout.write(`${renderRosterList(report.scan)}\n`);
|
|
213
239
|
} else {
|
|
214
240
|
const { stats } = report.scan;
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
const check = report.scan.
|
|
241
|
+
const space = report.scan.space ?? null;
|
|
242
|
+
if (space !== null) {
|
|
243
|
+
const check = report.scan.spaceCheck;
|
|
218
244
|
process.stdout.write(
|
|
219
|
-
`
|
|
245
|
+
` 空间 ${space.id} ${space.title}(登记 ${check.declared} · 在库 ${check.present} · ` +
|
|
220
246
|
`缺 ${check.missing.length} · 未登记 ${check.undeclared.length})\n`,
|
|
221
247
|
);
|
|
222
248
|
}
|
|
@@ -593,3 +619,56 @@ function runMigrate(options) {
|
|
|
593
619
|
}
|
|
594
620
|
return rc;
|
|
595
621
|
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* `clone`:把一个远端空间按原布局拉成本地副本。
|
|
625
|
+
*
|
|
626
|
+
* 落点默认是全局档案库根(`$DSH_HOME/agent-roster`),可以用 `--dir` 指到别处。
|
|
627
|
+
* 目录里有同名空间时**不覆盖**——覆盖别人正在用的副本不该是顺手发生的事;
|
|
628
|
+
* 要重来做 `rm -rf` 或换个 `--space` 名字。
|
|
629
|
+
*
|
|
630
|
+
* @param {object} options
|
|
631
|
+
* @returns {Promise<number>}
|
|
632
|
+
*/
|
|
633
|
+
async function runClone(options) {
|
|
634
|
+
if (options.repo === null) {
|
|
635
|
+
process.stderr.write(`clone 需要 --repo <owner/repo>\n\n${USAGE}`);
|
|
636
|
+
return 2;
|
|
637
|
+
}
|
|
638
|
+
const rootDir = options.dirs.length > 0 ? nodePath.resolve(options.dirs[0]) : globalRosterRoot();
|
|
639
|
+
const spaceId = options.space ?? options.repo.split("/")[1];
|
|
640
|
+
const target = nodePath.join(rootDir, spaceId);
|
|
641
|
+
if (existsSync(target) && options.write !== true) {
|
|
642
|
+
process.stderr.write(
|
|
643
|
+
`目标已存在:${target}\n` +
|
|
644
|
+
"不覆盖已有副本(那可能正被使用)。要重来请先删掉它,或用 --space 换个名字。\n" +
|
|
645
|
+
"确认要就地重写就加 --write(只重写本次拉到的文件)。\n",
|
|
646
|
+
);
|
|
647
|
+
return 1;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const result = await cloneSpace({
|
|
651
|
+
rootDir,
|
|
652
|
+
spaceId,
|
|
653
|
+
repo: options.repo,
|
|
654
|
+
ref: options.ref ?? "main",
|
|
655
|
+
path: options.path ?? "",
|
|
656
|
+
fetchImpl: fetch,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (options.json) {
|
|
660
|
+
process.stdout.write(`${JSON.stringify({ plugin: pkg.name, version: pkg.version, rootDir, ...result }, null, 2)}\n`);
|
|
661
|
+
} else {
|
|
662
|
+
const where = options.path === null ? "" : `/${options.path}`;
|
|
663
|
+
process.stdout.write(
|
|
664
|
+
`空间 ${spaceId} <- ${options.repo}@${options.ref ?? "main"}${where} 布局 ${result.layout} ` +
|
|
665
|
+
`写入 ${result.written.length} 个文件${result.skipped.length > 0 ? ` · 跳过 ${result.skipped.length}` : ""}\n`,
|
|
666
|
+
);
|
|
667
|
+
process.stdout.write(` 落点 ${result.dir}\n`);
|
|
668
|
+
for (const item of result.diagnostics) process.stdout.write(`${formatDiagnostic(item)}\n`);
|
|
669
|
+
if (result.written.length > 0) {
|
|
670
|
+
process.stdout.write(` 文件 ${result.written.slice(0, 8).join(" · ")}${result.written.length > 8 ? " …" : ""}\n`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return result.ok ? 0 : 1;
|
|
674
|
+
}
|
package/client.js
CHANGED
|
@@ -32,6 +32,7 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
const DOCUMENT_ROUTE = "/plugins/dsh-agent-roster/document";
|
|
33
33
|
const ROSTERS_ROUTE = "/plugins/dsh-agent-roster/rosters";
|
|
34
34
|
const INSTALL_ROUTE = "/plugins/dsh-agent-roster/install";
|
|
35
|
+
const INSTALL_SPACE_ROUTE = "/plugins/dsh-agent-roster/install-space";
|
|
35
36
|
const COLLECTION_ROUTE = "/plugins/dsh-agent-roster/collection";
|
|
36
37
|
const APPLY_ROUTE = "/plugins/dsh-agent-roster/apply";
|
|
37
38
|
const SEARCH_ROUTE = "/plugins/dsh-agent-roster/search";
|
|
@@ -1285,7 +1286,7 @@ window.__ModuleLoader__.load({
|
|
|
1285
1286
|
* 查询走后端排序(`/search`):前端再写一套打分,两处迟早给出不同顺序。
|
|
1286
1287
|
* 标签筛选是「且」,与档案页签的标签池同一语义。
|
|
1287
1288
|
*/
|
|
1288
|
-
function CatalogTab({ onOpen, onPull }) {
|
|
1289
|
+
function CatalogTab({ onOpen, onPull, onPullSpace }) {
|
|
1289
1290
|
useTranslation();
|
|
1290
1291
|
const [mode, setMode] = useState("source");
|
|
1291
1292
|
const [query, setQuery] = useState("");
|
|
@@ -1348,6 +1349,13 @@ window.__ModuleLoader__.load({
|
|
|
1348
1349
|
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
1349
1350
|
}, [catalog.entries]);
|
|
1350
1351
|
|
|
1352
|
+
// 哪些远端档案本机已经有了:按 id 对齐(同一个空间里 id 唯一)。
|
|
1353
|
+
const installed = useMemo(() => {
|
|
1354
|
+
const set = new Set();
|
|
1355
|
+
for (const entry of catalog.entries) if (entry.scope === "local") set.add(entry.rosterId);
|
|
1356
|
+
return set;
|
|
1357
|
+
}, [catalog.entries]);
|
|
1358
|
+
|
|
1351
1359
|
const entries = hits !== null && hits.status === "ready" ? hits.matches : catalog.entries;
|
|
1352
1360
|
const filtered = useMemo(() => {
|
|
1353
1361
|
return entries.filter((entry) => {
|
|
@@ -1365,14 +1373,19 @@ window.__ModuleLoader__.load({
|
|
|
1365
1373
|
map.set(key, {
|
|
1366
1374
|
key,
|
|
1367
1375
|
scope: entry.scope,
|
|
1368
|
-
|
|
1376
|
+
id: entry.scope === "local" ? t("catalog.local") : entry.collectionId ?? "",
|
|
1377
|
+
title: entry.scope === "local" ? "" : entry.collectionTitle ?? "",
|
|
1369
1378
|
entries: [],
|
|
1370
1379
|
});
|
|
1371
1380
|
}
|
|
1372
1381
|
map.get(key).entries.push(entry);
|
|
1373
1382
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1383
|
+
// 空间头上只放两件能当场核对的事实:有几份、其中几份本机已经有。
|
|
1384
|
+
return [...map.values()].map((group) => ({
|
|
1385
|
+
...group,
|
|
1386
|
+
installed: group.entries.filter((entry) => installed.has(entry.rosterId)).length,
|
|
1387
|
+
}));
|
|
1388
|
+
}, [filtered, mode, installed, t]);
|
|
1376
1389
|
|
|
1377
1390
|
function toggleTag(tag) {
|
|
1378
1391
|
setSelected((current) => {
|
|
@@ -1410,13 +1423,17 @@ window.__ModuleLoader__.load({
|
|
|
1410
1423
|
"div",
|
|
1411
1424
|
{ className: "rstr-row-stats" },
|
|
1412
1425
|
h("span", { className: "rstr-stat" }, t("catalog.members", { count: String(entry.members ?? 0) })),
|
|
1413
|
-
|
|
1414
|
-
"span",
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1426
|
+
entry.scope === "remote" && installed.has(entry.rosterId)
|
|
1427
|
+
? h("span", { className: "rstr-row-here" }, t("catalog.here"))
|
|
1428
|
+
: null,
|
|
1429
|
+
// 本机组里每行再写一次「本机」是废话:所在的分组头已经说了。
|
|
1430
|
+
entry.scope === "local"
|
|
1431
|
+
? null
|
|
1432
|
+
: h(
|
|
1433
|
+
"span",
|
|
1434
|
+
{ className: "rstr-stat" },
|
|
1435
|
+
t("catalog.remoteFrom", { collection: entry.collectionTitle ?? entry.collectionId ?? "" }),
|
|
1436
|
+
),
|
|
1420
1437
|
),
|
|
1421
1438
|
h(
|
|
1422
1439
|
"div",
|
|
@@ -1428,7 +1445,8 @@ window.__ModuleLoader__.load({
|
|
|
1428
1445
|
className: "rstr-btn rstr-btn-sm",
|
|
1429
1446
|
disabled: busy,
|
|
1430
1447
|
onClick: async () => {
|
|
1431
|
-
|
|
1448
|
+
// 已经在本机的那份,按钮就该是「打开」——不能让人去拉一份自己已经有的。
|
|
1449
|
+
if (entry.scope === "local" || installed.has(entry.rosterId)) {
|
|
1432
1450
|
onOpen(entry.rosterId);
|
|
1433
1451
|
return;
|
|
1434
1452
|
}
|
|
@@ -1441,7 +1459,7 @@ window.__ModuleLoader__.load({
|
|
|
1441
1459
|
}
|
|
1442
1460
|
},
|
|
1443
1461
|
},
|
|
1444
|
-
entry.scope === "local" ? t("catalog.open") : t("catalog.pull"),
|
|
1462
|
+
entry.scope === "local" || installed.has(entry.rosterId) ? t("catalog.open") : t("catalog.pull"),
|
|
1445
1463
|
),
|
|
1446
1464
|
),
|
|
1447
1465
|
),
|
|
@@ -1538,14 +1556,42 @@ window.__ModuleLoader__.load({
|
|
|
1538
1556
|
: groups.map((group) =>
|
|
1539
1557
|
h(
|
|
1540
1558
|
"div",
|
|
1541
|
-
{ key: group.key, className: "rstr-
|
|
1542
|
-
mode === "flat"
|
|
1559
|
+
{ key: group.key, className: "rstr-space" },
|
|
1560
|
+
mode === "flat"
|
|
1543
1561
|
? null
|
|
1544
1562
|
: h(
|
|
1545
1563
|
"div",
|
|
1546
|
-
{ className: "rstr-
|
|
1547
|
-
h("
|
|
1548
|
-
h("span", { className: "rstr-
|
|
1564
|
+
{ className: "rstr-space-head" },
|
|
1565
|
+
h("code", { className: "rstr-space-id", title: group.id }, group.id),
|
|
1566
|
+
h("span", { className: "rstr-space-title" }, group.title),
|
|
1567
|
+
h(
|
|
1568
|
+
"span",
|
|
1569
|
+
{ className: "rstr-space-facts" },
|
|
1570
|
+
t("catalog.facts", {
|
|
1571
|
+
count: String(group.entries.length),
|
|
1572
|
+
installed: String(group.installed),
|
|
1573
|
+
}),
|
|
1574
|
+
),
|
|
1575
|
+
group.scope === "remote" && typeof onPullSpace === "function"
|
|
1576
|
+
? h(
|
|
1577
|
+
"button",
|
|
1578
|
+
{
|
|
1579
|
+
type: "button",
|
|
1580
|
+
className: "rstr-btn rstr-btn-sm rstr-btn-ghost",
|
|
1581
|
+
disabled: pulling === `space:${group.key}`,
|
|
1582
|
+
onClick: async () => {
|
|
1583
|
+
setPulling(`space:${group.key}`);
|
|
1584
|
+
try {
|
|
1585
|
+
await onPullSpace(group.key.slice(2));
|
|
1586
|
+
setNonce((value) => value + 1);
|
|
1587
|
+
} finally {
|
|
1588
|
+
setPulling("");
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
},
|
|
1592
|
+
t("catalog.pullSpace"),
|
|
1593
|
+
)
|
|
1594
|
+
: null,
|
|
1549
1595
|
),
|
|
1550
1596
|
group.entries.map(row),
|
|
1551
1597
|
),
|
|
@@ -1654,7 +1700,7 @@ window.__ModuleLoader__.load({
|
|
|
1654
1700
|
}
|
|
1655
1701
|
}
|
|
1656
1702
|
|
|
1657
|
-
const
|
|
1703
|
+
const space = (snapshot?.workspaces ?? []).map((workspace) => workspace.space).find((item) => item && item.id)?.id ?? "";
|
|
1658
1704
|
|
|
1659
1705
|
const header = h(
|
|
1660
1706
|
"header",
|
|
@@ -1670,7 +1716,7 @@ window.__ModuleLoader__.load({
|
|
|
1670
1716
|
h("span", null, h("b", null, String(snapshot?.summary?.rosters ?? 0)), " ", t("panel.metaRosters")),
|
|
1671
1717
|
h("span", null, h("b", null, String(snapshot?.summary?.members ?? 0)), " ", t("panel.metaMembers")),
|
|
1672
1718
|
h("span", null, h("b", null, String(snapshot?.summary?.teams ?? 0)), " ", t("panel.metaTeams")),
|
|
1673
|
-
|
|
1719
|
+
space === "" ? null : h("span", null, t("panel.metaSpace", { name: space })),
|
|
1674
1720
|
),
|
|
1675
1721
|
),
|
|
1676
1722
|
h(
|
|
@@ -1766,6 +1812,28 @@ window.__ModuleLoader__.load({
|
|
|
1766
1812
|
setToast(t("misc.failed"));
|
|
1767
1813
|
}
|
|
1768
1814
|
},
|
|
1815
|
+
onPullSpace: async (collectionId) => {
|
|
1816
|
+
try {
|
|
1817
|
+
const result = await readJson(INSTALL_SPACE_ROUTE, {
|
|
1818
|
+
method: "POST",
|
|
1819
|
+
headers: { "content-type": "application/json" },
|
|
1820
|
+
body: JSON.stringify({ collection: collectionId }),
|
|
1821
|
+
});
|
|
1822
|
+
if (!result.ok || result.body === null) {
|
|
1823
|
+
setToast(`${t("misc.failed")} (${String(result.status)})`);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const body = result.body;
|
|
1827
|
+
const summary = t("catalog.pulledSpace", {
|
|
1828
|
+
files: String((body.written ?? []).length),
|
|
1829
|
+
skipped: String((body.skipped ?? []).length),
|
|
1830
|
+
});
|
|
1831
|
+
setToast(summary);
|
|
1832
|
+
library.reload();
|
|
1833
|
+
} catch {
|
|
1834
|
+
setToast(t("misc.failed"));
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1769
1837
|
});
|
|
1770
1838
|
}
|
|
1771
1839
|
|
|
@@ -2682,13 +2750,37 @@ window.__ModuleLoader__.load({
|
|
|
2682
2750
|
}
|
|
2683
2751
|
.rstr-catalog .rstr-search { max-width: 560px; }
|
|
2684
2752
|
|
|
2685
|
-
.rstr-
|
|
2753
|
+
.rstr-space { display: flex; flex-direction: column; min-width: 0; }
|
|
2754
|
+
.rstr-space + .rstr-space { margin-top: 24px; }
|
|
2755
|
+
.rstr-space-head {
|
|
2756
|
+
display: grid;
|
|
2757
|
+
grid-template-columns: minmax(110px, 176px) minmax(0, 1fr) auto auto;
|
|
2758
|
+
align-items: baseline;
|
|
2759
|
+
gap: 8px 16px;
|
|
2760
|
+
padding: 16px 8px 6px 0;
|
|
2761
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
|
2762
|
+
min-width: 0;
|
|
2763
|
+
}
|
|
2764
|
+
.rstr-space-id {
|
|
2765
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2766
|
+
font-size: 13px;
|
|
2767
|
+
font-weight: 600;
|
|
2768
|
+
color: var(--dsw-alias-label-primary);
|
|
2769
|
+
overflow: hidden;
|
|
2770
|
+
text-overflow: ellipsis;
|
|
2771
|
+
white-space: nowrap;
|
|
2772
|
+
}
|
|
2773
|
+
.rstr-space-title { font-size: 13px; color: var(--dsw-alias-label-secondary); min-width: 0; }
|
|
2774
|
+
.rstr-space-facts {
|
|
2775
|
+
font-size: 11px;
|
|
2776
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2777
|
+
font-variant-numeric: tabular-nums;
|
|
2778
|
+
white-space: nowrap;
|
|
2779
|
+
}
|
|
2780
|
+
.rstr-row-here { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
2686
2781
|
|
|
2687
|
-
.rstr-group-head { display: flex; align-items: baseline; gap: 8px; padding: 10px 0 4px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
2688
2782
|
|
|
2689
|
-
.rstr-group-title { font-size: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--dsw-alias-label-secondary); }
|
|
2690
2783
|
|
|
2691
|
-
.rstr-group-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
|
|
2692
2784
|
|
|
2693
2785
|
.rstr-results { display: flex; flex-direction: column; min-width: 0; padding: 0 28px 28px; border-top: 1px solid var(--dsw-alias-border-l2); }
|
|
2694
2786
|
|
|
@@ -3027,6 +3119,16 @@ window.__ModuleLoader__.load({
|
|
|
3027
3119
|
.rstr-tag { border-left: none; border-bottom: 2px solid transparent; border-radius: 0; white-space: nowrap; }
|
|
3028
3120
|
.rstr-tag-on { border-left: none; border-bottom-color: var(--dsw-alias-state-business-primary); background: transparent; }
|
|
3029
3121
|
.rstr-tag-clear { margin-top: 0; flex: 0 0 auto; }
|
|
3122
|
+
/* 空间头:窄屏由一列 id/标题/事实/按钮的四列网格折成两行 */
|
|
3123
|
+
.rstr-space-head {
|
|
3124
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
3125
|
+
row-gap: 2px;
|
|
3126
|
+
padding-top: 14px;
|
|
3127
|
+
}
|
|
3128
|
+
.rstr-space-id { grid-area: 1 / 1; }
|
|
3129
|
+
.rstr-space-head > .rstr-btn { grid-area: 1 / 2; }
|
|
3130
|
+
.rstr-space-title { grid-area: 2 / 1; }
|
|
3131
|
+
.rstr-space-facts { grid-area: 2 / 2; text-align: right; }
|
|
3030
3132
|
.rstr-row-body { grid-template-columns: minmax(0, 1fr) auto; }
|
|
3031
3133
|
.rstr-row-id { grid-column: 1; }
|
|
3032
3134
|
.rstr-row-text { grid-column: 1; }
|
package/docs/roster-v2.md
CHANGED
|
@@ -209,7 +209,7 @@ gates:
|
|
|
209
209
|
| 层 | 文件 | 回答的问题 |
|
|
210
210
|
|---|---|---|
|
|
211
211
|
| 能力 | `capabilities.yaml` | 这份档案要求部署提供什么(有证据源) |
|
|
212
|
-
|
|
|
212
|
+
| 空间 | `space.yaml` | 这座库收了哪些档案、什么布局(flat / monorepo)、本地副本从哪来 |
|
|
213
213
|
| 角色/组装 | `AGENT.md` + `roster.yaml` | 谁是谁、怎么拼、编排放哪 |
|
|
214
214
|
|
|
215
215
|
三者正交:`requires` 说"必须有",`capabilities.yaml` 说"这份组装必须有",`organization.yaml` 说"这座库有哪些"。
|