@stofone/dsh-agent-roster 0.13.0 → 1.0.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 +53 -0
- package/README.md +14 -12
- package/README.zh.md +26 -11
- package/bin/cli.mjs +87 -8
- package/client.js +58 -5
- 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 +5 -2
- package/locale/zh.json +5 -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,57 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.0.0] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### A space is the thing a repository holds
|
|
10
|
+
|
|
11
|
+
Until now a pulled roster was a flattened copy: whatever the source repository looked like, it arrived as
|
|
12
|
+
`<id>/roster.yaml` and the editor had no idea which file upstream it corresponded to. That is fine for "I
|
|
13
|
+
only need this one", and wrong for every monorepo — which is what this project's own libraries have been
|
|
14
|
+
since 0.10.0.
|
|
15
|
+
|
|
16
|
+
`space.yaml` (`kind: dsh-agent-roster/space@1`) is the level that was missing. It declares what a
|
|
17
|
+
directory in a repository *is*: which rosters it holds, whether the layout is `flat` (`<id>/roster.yaml`)
|
|
18
|
+
or `monorepo` (`roles/` + `rosters/` + `dist/`), and — only on local copies — where it came from
|
|
19
|
+
(`origin: {repo, ref, path}`). Layout is now part of the format instead of something the puller guesses.
|
|
20
|
+
Clone a space and the local tree is the repository's tree: edit `rosters/<id>/roster.yaml` and you are
|
|
21
|
+
editing the file upstream.
|
|
22
|
+
|
|
23
|
+
### Changed (breaking)
|
|
24
|
+
|
|
25
|
+
- `organization.yaml` is replaced by `space.yaml`. The old file, its `kind`
|
|
26
|
+
(`dsh-agent-roster-organization/v1`), its schema and its diagnostics codes (`organization-*`) are gone;
|
|
27
|
+
the equivalents are `space.yaml`, `dsh-agent-roster/space@1` and `space-*`. Migration: rename the file,
|
|
28
|
+
change `kind`, rename the `organization:` key to `space:`, and add `layout: flat|monorepo`.
|
|
29
|
+
- A space file inside a repository may not declare `origin`; the validator rejects it. `origin` describes
|
|
30
|
+
a local copy, and a repository claiming a source is a second copy of the truth.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `lib/space.js` + `schema/space.v1.schema.json`: declaration, cross-check (missing = error, undeclared =
|
|
35
|
+
warn, archived = info) and strict validation, with `layout` and `origin`.
|
|
36
|
+
- `lib/clone.js` and `dsh-agent-roster clone --repo <owner/repo> [--path] [--ref] [--space]`: pull a whole
|
|
37
|
+
space with its layout intact — rosters, the roles they reference, and each role's `parts/`. The written
|
|
38
|
+
`space.yaml` is the source's cover plus `origin`.
|
|
39
|
+
- `POST /plugins/dsh-agent-roster/install-space` and a "Pull whole space" button on every source group in
|
|
40
|
+
the Library tab; pulled files land in `<library>/<space>/` with the repository's paths.
|
|
41
|
+
- The Library tab marks rosters that are already installed locally, so "everything is pulled" and "what
|
|
42
|
+
do I actually have" stop looking the same.
|
|
43
|
+
- `examples/`, `examples-orchestration/` and `orgs/agent-roster-collective/` each ship a `space.yaml`.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## [0.13.1] - 2026-11-03
|
|
47
|
+
|
|
48
|
+
### The local rosters are the front page
|
|
49
|
+
|
|
50
|
+
0.13.0 opened on the library tab. That got the metaphor right and the priority wrong: the rosters you
|
|
51
|
+
already manage are what you come back to every day, and they were one tab away. The panel now lands on
|
|
52
|
+
**Rosters** — local, searchable, with its tag pool — and the library marketplace is the second tab.
|
|
53
|
+
|
|
54
|
+
- Tab order is `Rosters, Library, Teams, Sources, Health`; the default tab is `rosters` again.
|
|
55
|
+
- Nothing moved between the two: both read the same snapshot, both filter by tag, and the tag pools stay
|
|
56
|
+
independent (local-only vs everything in view) so neither one silently changes the other's counts.
|
|
57
|
+
|
|
7
58
|
## [0.13.0] - 2026-11-03
|
|
8
59
|
|
|
9
60
|
### A tab that behaves like a marketplace, not a filter on a list
|
|
@@ -615,6 +666,8 @@ point is now one search field above the tabs.
|
|
|
615
666
|
且所有返回值先过 `lib/json.js#lossless`。
|
|
616
667
|
|
|
617
668
|
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...main
|
|
669
|
+
[0.13.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.13.0...v0.13.1
|
|
670
|
+
[1.0.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.13.1...v1.0.0
|
|
618
671
|
[0.13.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...v0.13.0
|
|
619
672
|
[0.12.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.11.1...v0.12.0
|
|
620
673
|
[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
|
|
|
@@ -308,8 +309,9 @@ turns into decoration.
|
|
|
308
309
|
|
|
309
310
|
## The library tab: find by capability, not by source
|
|
310
311
|
|
|
311
|
-
The panel opens on **
|
|
312
|
-
|
|
312
|
+
The panel opens on **Rosters** — the local ones: what you work with every day, its tags and members, ready
|
|
313
|
+
to filter or edit. The second tab is the **Library**, meant to read like a plugin marketplace: one search
|
|
314
|
+
field, one row of tags, and one list that spans this machine and every subscribed source.
|
|
313
315
|
|
|
314
316
|
```
|
|
315
317
|
要找什么能力?例:数据质量、发版、代码评审
|
|
@@ -509,7 +511,7 @@ bin/cli.mjs validate/list/compile/migrate/registry for CI
|
|
|
509
511
|
scripts/ host-smoke.mjs, client-smoke.mjs, check-locale.mjs (dev only, not shipped)
|
|
510
512
|
examples/ a v2 library migrated from the v1 archives
|
|
511
513
|
examples-orchestration/ a v2 library with orchestration, gates and a construction plan
|
|
512
|
-
orgs/ a worked example library with its own
|
|
514
|
+
orgs/ a worked example library with its own space.yaml (21 rosters)
|
|
513
515
|
test/ unit tests
|
|
514
516
|
```
|
|
515
517
|
|
|
@@ -517,7 +519,7 @@ test/ unit tests
|
|
|
517
519
|
|
|
518
520
|
Verified on this machine:
|
|
519
521
|
|
|
520
|
-
- `npm test` —
|
|
522
|
+
- `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).
|
|
521
523
|
- `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.
|
|
522
524
|
- CLI: `validate --dir examples` returns exit 0 over both example libraries.
|
|
523
525
|
- 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
|
|
|
@@ -294,7 +308,8 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
294
308
|
|
|
295
309
|
## 档案库:按能力找,而不是按来源翻
|
|
296
310
|
|
|
297
|
-
|
|
311
|
+
面板的**首屏是本机档案**(第一个页签):你每天打交道的那些档案、它们的标签与成员,进来就能看见、能筛选、能改。
|
|
312
|
+
第二个页签是**档案库**,定位接近一个插件市场:一个搜索框、一排标签、一份横跨本机与所有订阅来源的清单。
|
|
298
313
|
|
|
299
314
|
```
|
|
300
315
|
要找什么能力?例:数据质量、发版、代码评审
|
|
@@ -373,8 +388,8 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
373
388
|
|
|
374
389
|
| 页签 | 能做什么 |
|
|
375
390
|
|---|---|
|
|
391
|
+
| 档案 | 浏览每个工作区 × 每座档案库,展开成员;**新建 / 编辑 / 复制 / 删除 / 导入 / 导出**档案。编辑器有表单与源码两种模式(源码就是文件原文);写之前由宿主校验,覆盖已有文件会留一份 `.bak`;顶上有搜索框与标签池 |
|
|
376
392
|
| 档案库 | 按能力找档案:搜索(横跨本机与订阅来源,命中给出处)、标签筛选、按来源聚合或平铺,行内直接打开或拉取 |
|
|
377
|
-
| 档案 | 浏览每个工作区 × 每座档案库,展开成员;**新建 / 编辑 / 复制 / 删除 / 导入 / 导出**档案。编辑器有表单与源码两种模式(源码就是文件原文);写之前由宿主校验,覆盖已有文件会留一份 `.bak` |
|
|
378
393
|
| 团队 | 读一个配置好的状态目录(默认 `.agent-teams`),列出运行时真正建出来的每个团队:档案、成员档案与模型路由、每个持久化成员是否真的与其档案一致,以及该团队的任务 |
|
|
379
394
|
| 档案源 | 列出订阅的公共 registry 目录,每个 collection 一个「拉取到本机」按钮;可展开先看里面有哪些档案 |
|
|
380
395
|
| 体检 | 把「配置有没有指错」集中一处:目录解析到了哪、状态目录在不在、有多少声明未被确认,以及该改哪个设置 |
|
|
@@ -486,7 +501,7 @@ test/ 单测
|
|
|
486
501
|
|
|
487
502
|
本机已验证:
|
|
488
503
|
|
|
489
|
-
- `npm test` ——
|
|
504
|
+
- `npm test` —— 313/313 通过(格式、解析、校验、扫描、路径、桥接、核对四桶、摘要与溯源、档案差异、能力要求与能力闸门、档案库组织、索引 schema、无损 JSON、写入计划与执行、六条路由、快照与提示、两本词典一致、工具契约、客户端加载器契约、包清单、配置视图、样式 token 约束、声明层字段与队长段、schema 与常量对齐、v2 角色/组装/编译三层及其 schema 对齐、v1→v2 迁移、执行回执并入施工图)。
|
|
490
505
|
- `npm run smoke` —— 用真实 DSH bundle 的 `defineTool` 注册六个工具,逐个调用:输出全部通过无损校验、`path` 覆盖生效、释放干净;同一条命令随后用桩模块表加载 `client.js`,断言三处槽位注册、样式只注入一次、effect 可回收。
|
|
491
506
|
- CLI:`validate --dir examples` 对两座示例库退出码 0。
|
|
492
507
|
- 用插件管理器真的装进了本机 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) => {
|
|
@@ -1410,6 +1418,9 @@ window.__ModuleLoader__.load({
|
|
|
1410
1418
|
"div",
|
|
1411
1419
|
{ className: "rstr-row-stats" },
|
|
1412
1420
|
h("span", { className: "rstr-stat" }, t("catalog.members", { count: String(entry.members ?? 0) })),
|
|
1421
|
+
entry.scope === "remote" && installed.has(entry.rosterId)
|
|
1422
|
+
? h(Badge, { tone: "ok" }, t("catalog.installed"))
|
|
1423
|
+
: null,
|
|
1413
1424
|
h(
|
|
1414
1425
|
"span",
|
|
1415
1426
|
{ className: "rstr-stat" },
|
|
@@ -1546,6 +1557,26 @@ window.__ModuleLoader__.load({
|
|
|
1546
1557
|
{ className: "rstr-group-head" },
|
|
1547
1558
|
h("span", { className: "rstr-group-title" }, group.title),
|
|
1548
1559
|
h("span", { className: "rstr-group-count" }, String(group.entries.length)),
|
|
1560
|
+
group.scope === "remote" && typeof onPullSpace === "function"
|
|
1561
|
+
? h(
|
|
1562
|
+
"button",
|
|
1563
|
+
{
|
|
1564
|
+
type: "button",
|
|
1565
|
+
className: "rstr-btn rstr-btn-sm rstr-btn-ghost",
|
|
1566
|
+
disabled: pulling === `space:${group.key}`,
|
|
1567
|
+
onClick: async () => {
|
|
1568
|
+
setPulling(`space:${group.key}`);
|
|
1569
|
+
try {
|
|
1570
|
+
await onPullSpace(group.key.slice(2));
|
|
1571
|
+
setNonce((value) => value + 1);
|
|
1572
|
+
} finally {
|
|
1573
|
+
setPulling("");
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
t("catalog.pullSpace"),
|
|
1578
|
+
)
|
|
1579
|
+
: null,
|
|
1549
1580
|
),
|
|
1550
1581
|
group.entries.map(row),
|
|
1551
1582
|
),
|
|
@@ -1556,7 +1587,7 @@ window.__ModuleLoader__.load({
|
|
|
1556
1587
|
function Panel() {
|
|
1557
1588
|
useTranslation();
|
|
1558
1589
|
const library = useLibrary();
|
|
1559
|
-
const [tab, setTab] = useState("
|
|
1590
|
+
const [tab, setTab] = useState("rosters");
|
|
1560
1591
|
const [query, setQuery] = useState("");
|
|
1561
1592
|
const [selectedTags, setSelectedTags] = useState(() => new Set());
|
|
1562
1593
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
@@ -1654,7 +1685,7 @@ window.__ModuleLoader__.load({
|
|
|
1654
1685
|
}
|
|
1655
1686
|
}
|
|
1656
1687
|
|
|
1657
|
-
const
|
|
1688
|
+
const space = (snapshot?.workspaces ?? []).map((workspace) => workspace.space).find((item) => item && item.id)?.id ?? "";
|
|
1658
1689
|
|
|
1659
1690
|
const header = h(
|
|
1660
1691
|
"header",
|
|
@@ -1670,7 +1701,7 @@ window.__ModuleLoader__.load({
|
|
|
1670
1701
|
h("span", null, h("b", null, String(snapshot?.summary?.rosters ?? 0)), " ", t("panel.metaRosters")),
|
|
1671
1702
|
h("span", null, h("b", null, String(snapshot?.summary?.members ?? 0)), " ", t("panel.metaMembers")),
|
|
1672
1703
|
h("span", null, h("b", null, String(snapshot?.summary?.teams ?? 0)), " ", t("panel.metaTeams")),
|
|
1673
|
-
|
|
1704
|
+
space === "" ? null : h("span", null, t("panel.metaSpace", { name: space })),
|
|
1674
1705
|
),
|
|
1675
1706
|
),
|
|
1676
1707
|
h(
|
|
@@ -1713,8 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1713
1744
|
"div",
|
|
1714
1745
|
{ className: "rstr-tabs", role: "tablist" },
|
|
1715
1746
|
[
|
|
1716
|
-
["catalog", t("panel.tabsCatalog"), null],
|
|
1717
1747
|
["rosters", t("panel.tabsRosters"), rosters.length],
|
|
1748
|
+
["catalog", t("panel.tabsCatalog"), null],
|
|
1718
1749
|
["teams", t("panel.tabsTeams"), teams.length],
|
|
1719
1750
|
["sources", t("panel.tabsSources"), snapshot?.remote?.collections?.length ?? 0],
|
|
1720
1751
|
// 只有真正要用户处理的问题才计数:info 只是说明,不该在页签上挂数字。
|
|
@@ -1766,6 +1797,28 @@ window.__ModuleLoader__.load({
|
|
|
1766
1797
|
setToast(t("misc.failed"));
|
|
1767
1798
|
}
|
|
1768
1799
|
},
|
|
1800
|
+
onPullSpace: async (collectionId) => {
|
|
1801
|
+
try {
|
|
1802
|
+
const result = await readJson(INSTALL_SPACE_ROUTE, {
|
|
1803
|
+
method: "POST",
|
|
1804
|
+
headers: { "content-type": "application/json" },
|
|
1805
|
+
body: JSON.stringify({ collection: collectionId }),
|
|
1806
|
+
});
|
|
1807
|
+
if (!result.ok || result.body === null) {
|
|
1808
|
+
setToast(`${t("misc.failed")} (${String(result.status)})`);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const body = result.body;
|
|
1812
|
+
const summary = t("catalog.pulledSpace", {
|
|
1813
|
+
files: String((body.written ?? []).length),
|
|
1814
|
+
skipped: String((body.skipped ?? []).length),
|
|
1815
|
+
});
|
|
1816
|
+
setToast(summary);
|
|
1817
|
+
library.reload();
|
|
1818
|
+
} catch {
|
|
1819
|
+
setToast(t("misc.failed"));
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1769
1822
|
});
|
|
1770
1823
|
}
|
|
1771
1824
|
|
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` 说"这座库有哪些"。
|
package/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { ROSTER2_KIND } from "./lib/format-v2.js";
|
|
|
24
24
|
import { validateRoster } from "./lib/validate.js";
|
|
25
25
|
import { validateRoster2 } from "./lib/roster2.js";
|
|
26
26
|
import { sendError, sendJson, createAuthGate, readJsonBody, requireMethod } from "./lib/http.js";
|
|
27
|
-
import { buildPanelSnapshot, installCollection, loadRemotePanel, resolveRoots } from "./lib/service.js";
|
|
27
|
+
import { buildPanelSnapshot, installCollection, installSpace, loadRemotePanel, resolveRoots } from "./lib/service.js";
|
|
28
28
|
import { createTtlCache, fetchCollection } from "./lib/remote.js";
|
|
29
29
|
import { searchRosters } from "./lib/search.js";
|
|
30
30
|
import { registerRosterTools } from "./lib/tools.js";
|
|
@@ -382,6 +382,35 @@ export function apply(ctx, config) {
|
|
|
382
382
|
"install route",
|
|
383
383
|
);
|
|
384
384
|
|
|
385
|
+
register(
|
|
386
|
+
"/plugins/dsh-agent-roster/install-space",
|
|
387
|
+
async (req, res) => {
|
|
388
|
+
if (guard(req, res)) return;
|
|
389
|
+
if (!requireMethod(req, res, "POST")) return;
|
|
390
|
+
const body = await readJsonBody(req);
|
|
391
|
+
if (!body.ok) {
|
|
392
|
+
sendError(res, body.status, body.code);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const input = body.value !== null && typeof body.value === "object" ? body.value : {};
|
|
396
|
+
if (typeof input.collection !== "string" || input.collection === "") {
|
|
397
|
+
sendError(res, 400, "missing-collection");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const result = await installSpace(settings, {
|
|
401
|
+
collectionId: input.collection,
|
|
402
|
+
source: typeof input.source === "string" && input.source !== "" ? input.source : undefined,
|
|
403
|
+
spaceId: typeof input.space === "string" && input.space !== "" ? input.space : undefined,
|
|
404
|
+
});
|
|
405
|
+
if (result.code === "collection-not-found") {
|
|
406
|
+
sendJson(res, 404, { ok: false, error: { code: result.code }, known: result.known ?? [], diagnostics: result.diagnostics ?? [] });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
sendJson(res, 200, result);
|
|
410
|
+
},
|
|
411
|
+
"install-space route",
|
|
412
|
+
);
|
|
413
|
+
|
|
385
414
|
register(
|
|
386
415
|
"/plugins/dsh-agent-roster/apply",
|
|
387
416
|
async (req, res) => {
|