@wingsky-1/dsh-worktree-sidebar 0.2.5 → 0.2.6

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.en.md CHANGED
@@ -7,6 +7,14 @@ Three tools that let the agent bind a **git worktree** to the current session, s
7
7
  This is a **transitional adapter**: if the official product ships native worktree sessions, this plugin retires.
8
8
  How it works (five host domains, host/client chains, self-healing rules, the six things to remember) is documented in [docs/architecture/dsh-worktree-sidebar.md](../../docs/architecture/dsh-worktree-sidebar.md).
9
9
 
10
+ ## Quick install
11
+
12
+ ```sh
13
+ dsh plugin --profile web add @wingsky-1/dsh-worktree-sidebar
14
+ ```
15
+
16
+ > After install / uninstall / update, **restart `dsh web` once** (bundle layers are only composed at startup) for changes to take effect.
17
+
10
18
  ## What it does
11
19
 
12
20
  The right sidebar's file tree is always rooted at `session.header.cwd`, and that field is immutable once the session is created (adopting a session with a different cwd throws `ApiSessionCwdConflict`). So when a session runs in the main checkout while the actual work happens in a worktree, the tree shows the wrong place.
@@ -140,7 +148,9 @@ Four **UI semantics** cannot be covered by any automated gate (there is no brows
140
148
 
141
149
  ## Compatibility (read-only coupling)
142
150
 
143
- The plugin does not modify official sources, but it **reads** these contracts (baseline `@deepseek-ai/dsh 0.1.5-rc.1`):
151
+ The plugin does not modify official sources, but it **reads** these contracts from the sole
152
+ supported target runtime, dsh `0.1.7-rc.2`. No compatibility promise is made for any other
153
+ runtime version:
144
154
 
145
155
  - the host's `typert` `workspaceFileScope` lookup: the plugin registers its resolver via `lookups.configure` and delegates to the **official resolve captured before configuring** on a miss;
146
156
  - the client `sidebarRightTabs` type registry and the keyed seat `sidebar.right.pane.tab` (including the `StoredEntry` shape: `component/inject/store/locale`);
package/README.md CHANGED
@@ -7,6 +7,14 @@
7
7
  这是一个**过渡适配层**:官方若推出原生 worktree 会话能力,本插件即退役。
8
8
  原理与运行机制(五域装配、宿主/客户端链路、自愈判据、命门清单)见 [架构图解](../../docs/architecture/dsh-worktree-sidebar.md)。
9
9
 
10
+ ## 一键安装
11
+
12
+ ```sh
13
+ dsh plugin --profile web add @wingsky-1/dsh-worktree-sidebar
14
+ ```
15
+
16
+ > 安装 / 卸载 / 更新后都需**重启一次** `dsh web`(bundle 层只在启动时组合)生效。
17
+
10
18
  ## 它做什么
11
19
 
12
20
  dsh 的右侧栏文件树根固定取 `session.header.cwd`,且该字段创建后不可变(显式收养不同 cwd 的会话会抛 `ApiSessionCwdConflict`)。于是在「会话开在主 checkout、实际改动在 worktree」这一常见工作方式下,树看的是主 checkout,看错了地方。
@@ -24,7 +32,7 @@ dsh 的右侧栏文件树根固定取 `session.header.cwd`,且该字段创建
24
32
 
25
33
  子 agent 的会话与**用户 fork 出来的会话**都继承父会话的登记:自己没有登记时,右侧栏的 Files 页签指向父链上**第一个持有登记**的那个会话所绑定的 worktree(父链到顶、那条登记被摘除即回退到自己的 cwd)。判据同在会话 header 的 `parentSession`,本插件刻意不区分这两种形态——fork 的 header 同样拷了父的 cwd,视图根跟着一起继承才与「文件根是会话 cwd 的改写」自洽。
26
34
 
27
- 工具对 **git 仓库内的所有 agent(含子 agent)**暴露:每个 agent 注册时按其会话目录判断,不在 git 仓库里就不注册。执行期还有一次兜底校验,环境在两次之间变了也不会按错误前提动作。
35
+ 工具对 **git 仓库内的所有 agent(含子 agent)**暴露:每个 agent 注册时按其会话目录判断,不在 git 仓库里就不注册。插件装载前已存在的 agent 会在装载时补注册;后续 `agent/created` 事件会等待本插件返回的 Promise,确保工具注册完成后事件才继续。执行期还有一次兜底校验,环境在两次之间变了也不会按错误前提动作。
28
36
 
29
37
  三个工具同时带一个可见的返回文本,写明当前指向哪个 worktree、分支是什么 —— 让模型不必额外调一次工具就能确认状态。
30
38
 
@@ -144,7 +152,7 @@ pnpm gate:pr # 开 PR 前;新增包与 catalog 条目另需 pn
144
152
 
145
153
  ## 兼容性(只读耦合点)
146
154
 
147
- 插件不改官方源码,但**读取**以下官方契约(基线 `@deepseek-ai/dsh 0.1.5-rc.1`);官方改版时这些点是唯一的失效面:
155
+ 插件不改官方源码,但**读取**以下官方契约(基线 `@deepseek-ai/dsh 0.1.7-rc.2`);官方改版时这些点是唯一的失效面:
148
156
 
149
157
  - 宿主 `typert` 的 `workspaceFileScope` 查表:本插件用 `lookups.configure` 注册自己的解析器,并在 miss 时委托**配置前捕获到的官方 resolve**;
150
158
  - 客户端 `sidebarRightTabs` 类型注册表与键控座位 `sidebar.right.pane.tab`(含 `StoredEntry` 的 `component/inject/store/locale` 形状);
package/cordis.patch.yml CHANGED
@@ -5,5 +5,5 @@
5
5
  # 浏览器端(exports "./client")在官方封装就绪后接管 files 页签的目录根。
6
6
  # 配置走 schema 默认值(enabled),行内不带 config。
7
7
  - insert:
8
- - id: ui-dsh-worktree-sidebar
8
+ - id: dsh-worktree-sidebar
9
9
  name: "@wingsky-1/dsh-worktree-sidebar"
@@ -36,17 +36,21 @@ export interface StoredEntryLike {
36
36
  readonly locale?: string | undefined;
37
37
  }
38
38
  /**
39
- * tab 类型定义,逐字对齐官方 SidebarRightTabDefinition。
39
+ * tab 类型定义,语义对齐官方 SidebarRightTabDefinition(只读,不改写类型表)。
40
40
  *
41
- * 这里**必须**把所有字段都列全(尤其 `guide`):类型表用的是官方定义的那一份(本插件只遮蔽
41
+ * 这里**必须**把在册定义的判据字段列全(尤其 `guide`):类型表用的是官方定义的那一份(本插件只遮蔽
42
42
  * 正文,从不改写类型表),官方注册表 refresh 后会用**在册定义**重算 guide 条目。丢掉 `guide` 的后果不是少一块文案,
43
43
  * 而是 guide 归零、所有会话(含从未登记的)默认页签从 Files 变成空的 Guide、文件树打不开。
44
+ * `multiple`/`keepMounted` 仅作读面声明(本包不写类型表):可选即双基线可编译;
45
+ * 隐藏页签无 abort 时仍在册,binding 刷新仍需 reseed(见 inject keepMounted 用例)。
44
46
  */
45
47
  export interface TabDefinitionLike {
46
48
  readonly id: string;
47
49
  readonly kind: string;
48
50
  readonly patterns?: readonly string[];
49
- readonly priority?: string;
51
+ readonly priority?: "extension" | "builtin" | "fallback";
52
+ readonly multiple?: boolean;
53
+ readonly keepMounted?: boolean;
50
54
  readonly canOpen?: (address: string) => boolean;
51
55
  readonly title: (address: string) => string;
52
56
  readonly guide?: readonly unknown[];
@@ -4,12 +4,13 @@
4
4
  * 接管只做一件事:以**官方 id** 为 key、**更低的 priority** 再登记一条正文,组件 / store /
5
5
  * locale 全部复用官方那条(inject 面由 `inject.ts` 包一层)。官方 ui-slots 的每个 cell 取
6
6
  * 「优先级最低的那条存活项」——它的注册表原话是 register at a different priority to shadow it
7
- * (lowest renders)(`@deepseek-ai/dsh-client-ui-slots` 的 `lib/index.js:77`;排序 `:130`;取当值项 `:187-200`)。
7
+ * (lowest renders)(`@deepseek-ai/dsh-client-ui-slots` 的遮蔽语义:同 key 不同 priority 方可并存、
8
+ * 按 priority 升序、当值取每 cell 首条存活项)。
8
9
  *
9
10
  * **那个包不在 dsh 安装树里**:它在构建期被内联进 `dsh-web-frontend/dist/assets/index-*.js`,
10
11
  * 所以要照注释去读原文时,可读副本在同仓 catalog 锁版处
11
- * (`node_modules/.pnpm/@deepseek-ai+dsh-client-ui-slots@0.1.5-rc.1_...` 下的
12
- * `node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js`)。
12
+ * (`node_modules/.pnpm/@deepseek-ai+dsh-client-ui-slots@<catalog 锁版>_...` 下的
13
+ * `node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js`,版本号见 `pnpm-workspace.yaml`,不硬编码)。
13
14
  * 树内可读的两处是类型面与运行时入口:`dsh-client-ui-renderer/lib/types/client/registry.d.ts:46/84/154/164`
14
15
  * (`class SlotRegistry`、`register`、`entries`、`entriesOfSlot`)与同一包 `lib/client.js:953/1191-1198/1388`。
15
16
  *
package/lib/client.js CHANGED
@@ -42,7 +42,7 @@
42
42
  // src/client/inject.ts
43
43
  var liveSeedings = /* @__PURE__ */ new Set();
44
44
  function releaseAllSeedings() {
45
- for (const release of [...liveSeedings]) release();
45
+ for (const release of [...liveSeedings].reverse()) release();
46
46
  }
47
47
  function createInjectWrapper(viewFor) {
48
48
  return (official) => {
@@ -73,48 +73,54 @@
73
73
  const reseed = () => {
74
74
  const next = view.root.getSnapshot();
75
75
  for (const [tabId, tab] of [...watched]) {
76
- if (tab.signal?.aborted === true) continue;
76
+ if (tab.abort?.signal.aborted === true) continue;
77
77
  const root = next ?? tab.fallback;
78
78
  if (root === tab.seeded) continue;
79
79
  tab.seeded = root;
80
- officialStart(tabId, root, tab.signal);
80
+ officialStart(tabId, root, tab.abort?.signal);
81
81
  }
82
82
  };
83
+ const detachAbort = (tab) => {
84
+ if (tab?.abort === void 0) return;
85
+ tab.abort.signal.removeEventListener("abort", tab.abort.listener);
86
+ };
83
87
  const release = () => {
84
- watched.clear();
85
- liveSeedings.delete(release);
86
- unsubscribe?.();
87
- unsubscribe = void 0;
88
+ for (const [, tab] of [...watched].reverse()) detachAbort(tab);
88
89
  detachVisible?.();
89
90
  detachVisible = void 0;
90
- };
91
- const watchAbort = (tabId, signal) => {
92
- signal?.addEventListener(
93
- "abort",
94
- () => {
95
- watched.delete(tabId);
96
- if (watched.size === 0) release();
97
- },
98
- { once: true }
99
- );
91
+ unsubscribe?.();
92
+ unsubscribe = void 0;
93
+ watched.clear();
94
+ liveSeedings.delete(release);
100
95
  };
101
96
  const attach = (tabId, fallback, seeded, signal) => {
102
97
  liveSeedings.add(release);
103
- watched.set(tabId, { seeded, fallback, signal });
98
+ detachAbort(watched.get(tabId));
99
+ const onAbort = () => {
100
+ const current = watched.get(tabId);
101
+ if (current === void 0 || current.abort !== abort) return;
102
+ detachAbort(current);
103
+ watched.delete(tabId);
104
+ if (watched.size === 0) release();
105
+ };
106
+ const abort = signal === void 0 ? void 0 : { signal, listener: onAbort };
107
+ const tab = { seeded, fallback, abort };
108
+ watched.set(tabId, tab);
104
109
  unsubscribe ?? (unsubscribe = view.root.subscribe(reseed));
105
110
  detachVisible ?? (detachVisible = refreshWhenVisible(refresh));
106
- watchAbort(tabId, signal);
111
+ if (abort !== void 0) abort.signal.addEventListener("abort", abort.listener, { once: true });
112
+ return tab;
107
113
  };
108
114
  const start = (tabId, root, signal) => {
109
- attach(tabId, root, root, signal);
115
+ if (signal?.aborted === true) return;
116
+ const tab = attach(tabId, root, root, signal);
110
117
  officialStart(tabId, root, signal);
111
118
  void view.root.refresh().then(() => {
112
- if (signal?.aborted === true) return;
119
+ if (watched.get(tabId) !== tab || tab.abort?.signal.aborted === true) return;
113
120
  const seeded = view.root.getSnapshot() ?? root;
114
- const tab = watched.get(tabId);
115
- if (tab === void 0 || tab.seeded === seeded) return;
121
+ if (tab.seeded === seeded) return;
116
122
  tab.seeded = seeded;
117
- officialStart(tabId, seeded, tab.signal);
123
+ officialStart(tabId, seeded, tab.abort?.signal);
118
124
  });
119
125
  };
120
126
  const load = (tabId, path, signal) => {
package/lib/index.js CHANGED
@@ -19,8 +19,9 @@ function bindAgents(host) {
19
19
  cwd: agent.session.header.cwd
20
20
  });
21
21
  return {
22
- subscribe: (handler) => host.on("agent/created", ({ agent }) => {
23
- handler(faceOf(agent));
22
+ subscribe: (handler) => host.on("agent/created", async ({ agent }) => {
23
+ await handler(faceOf(agent));
24
+ return void 0;
24
25
  }),
25
26
  list: () => host.all().map(faceOf),
26
27
  publish: (face, definitions) => {
@@ -364,9 +365,13 @@ function dropBinding(table, sessionId) {
364
365
  }
365
366
 
366
367
  // lib/server/shared/file-io.js
368
+ import { randomBytes } from "node:crypto";
367
369
  import { readFileSync } from "node:fs";
368
370
  import { mkdir, rename, writeFile } from "node:fs/promises";
369
371
  import { dirname } from "node:path";
372
+ function temporaryNameFor(file) {
373
+ return `${file}.tmp-${process.pid}.${Date.now().toString(36)}.${randomBytes(6).toString("hex")}.tmp`;
374
+ }
370
375
  function readTextFileSync(file) {
371
376
  try {
372
377
  return { ok: true, text: readFileSync(file, "utf8") };
@@ -375,7 +380,7 @@ function readTextFileSync(file) {
375
380
  }
376
381
  }
377
382
  async function writeTextAtomic(file, text) {
378
- const temporary = `${file}.tmp-${process.pid}`;
383
+ const temporary = temporaryNameFor(file);
379
384
  try {
380
385
  await mkdir(dirname(file), { recursive: true });
381
386
  await writeFile(temporary, text, "utf8");
@@ -386,9 +391,6 @@ async function writeTextAtomic(file, text) {
386
391
  }
387
392
  }
388
393
 
389
- // lib/server/shared/paths.js
390
- import { join as join2 } from "node:path";
391
-
392
394
  // ../../shared/dsh-home.js
393
395
  import { homedir } from "node:os";
394
396
  import { join } from "node:path";
@@ -401,11 +403,17 @@ function dshHome() {
401
403
  return env !== void 0 && env.trim().length > 0 ? env : join(userHome(), ".dsh");
402
404
  }
403
405
 
406
+ // ../../shared/paths.js
407
+ import { join as join2 } from "node:path";
408
+ function pluginHome(base, ...segments) {
409
+ return join2(base, ...segments);
410
+ }
411
+
404
412
  // lib/server/shared/paths.js
405
413
  var PACKAGE_DIR = "@wingsky-1/dsh-worktree-sidebar";
406
414
  var BINDINGS_FILE_NAME = "bindings.json";
407
415
  function bindingsFile() {
408
- return join2(dshHome(), PACKAGE_DIR, BINDINGS_FILE_NAME);
416
+ return pluginHome(dshHome(), PACKAGE_DIR, BINDINGS_FILE_NAME);
409
417
  }
410
418
 
411
419
  // lib/server/binding/impl/store/index.js
@@ -1474,7 +1482,7 @@ var ToolsService = class {
1474
1482
  const consider = (agent) => this.consider(generation, agent);
1475
1483
  this.unsubscribe = deps.agents.subscribe(consider);
1476
1484
  for (const agent of deps.agents.list())
1477
- consider(agent);
1485
+ void consider(agent);
1478
1486
  }
1479
1487
  /**
1480
1488
  * 卸载:退订、逐个摘掉每个 agent 的工具、丢掉映射与在飞的异步链,复位装配标记。重复调用无害。
@@ -1503,7 +1511,7 @@ var ToolsService = class {
1503
1511
  }
1504
1512
  /** 判定一个 agent 是否该装工具,该装就装。同一个 agent 只处理一次。 */
1505
1513
  consider(generation, agent) {
1506
- this.chain = this.chain.then(async () => {
1514
+ const chain = this.chain.then(async () => {
1507
1515
  const deps = this.deps;
1508
1516
  if (deps === void 0)
1509
1517
  return;
@@ -1526,6 +1534,8 @@ var ToolsService = class {
1526
1534
  const reason = cause instanceof Error ? cause.message : String(cause);
1527
1535
  deps.logger.warn("dsh-worktree-sidebar: 工具注册失败 — " + reason);
1528
1536
  });
1537
+ this.chain = chain;
1538
+ return chain;
1529
1539
  }
1530
1540
  };
1531
1541
  var toolsService = new ToolsService();
@@ -1546,7 +1556,11 @@ async function apply(ctx, config = {}) {
1546
1556
  logger: ctx.logger,
1547
1557
  register: (route) => ctx.webServer.register(route),
1548
1558
  agents: bindAgents({
1549
- on: (event, handler) => ctx.on(event, handler),
1559
+ // 0.1.7-rc.2 serial 会等待 listener:完整等待 tools 域的注册链,再让 agent 创建继续。
1560
+ on: (event, handler) => ctx.on(event, async (payload) => {
1561
+ await handler({ agent: payload.agent });
1562
+ return void 0;
1563
+ }),
1550
1564
  all: () => ctx.agents.list()
1551
1565
  }),
1552
1566
  typert: bindTypert(ctx.typert.lookups),
@@ -28,10 +28,15 @@ export interface HostAgentLike {
28
28
  }
29
29
  /** 宿主事件面与 agent 枚举面:只开本适配器要的两样。 */
30
30
  export interface AgentHostPort {
31
- /** 订阅 agent 发布。返回退订函数。 */
31
+ /**
32
+ * 订阅 agent 发布。返回退订函数。
33
+ *
34
+ * 0.1.7-rc.2 用 serial 派发并等待 listener;因此适配器把 tools 域的 thenable 留在返回链上,
35
+ * 不能在取出 `agent` 后把它截成 `undefined`。
36
+ */
32
37
  on(event: "agent/created", handler: (payload: {
33
38
  agent: HostAgentLike;
34
- }) => void): () => void;
39
+ }) => undefined | PromiseLike<undefined>): () => void;
35
40
  /** 当前**所有存活** agent 快照(含子 agent)。 */
36
41
  all(): readonly HostAgentLike[];
37
42
  }
@@ -33,8 +33,8 @@ export interface AgentFace {
33
33
  * 谁订事件、从哪枚举、怎么把工具装进某个 agent 的作用域,都是组合根的知识。
34
34
  */
35
35
  export interface AgentPort {
36
- /** 订阅「新 agent 发布」。返回退订函数。 */
37
- subscribe(handler: (agent: AgentFace) => void): () => void;
36
+ /** 订阅「新 agent 发布」。返回退订函数;listener 的 thenable 属于宿主 serial 等待链。 */
37
+ subscribe(handler: (agent: AgentFace) => void | PromiseLike<void>): () => void;
38
38
  /** 当前存活的所有 agent 快照(含子 agent;插件加载前就存在的那些都在里面)。 */
39
39
  list(): readonly AgentFace[];
40
40
  /** 把工具装进该 agent 的作用域,返回释放函数。 */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wingsky-1/dsh-worktree-sidebar",
3
- "version": "0.2.5",
4
- "description": "把某个 git worktree 登记给当前会话,使该会话的官方右侧栏文件树根指向该 worktree;会话 cwd 不变。官方提供原生 worktree 会话能力即退役的过渡适配层。",
3
+ "version": "0.2.6",
4
+ "description": "Point the current session's right-sidebar file tree at a git worktree without changing the session cwd. 把某个 git worktree 登记给当前会话,使该会话的官方右侧栏文件树根指向该 worktree;会话 cwd 不变。官方提供原生 worktree 会话能力即退役的过渡适配层。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -20,6 +20,16 @@
20
20
  "patch": "./cordis.patch.yml",
21
21
  "bannerJs": "import { createRequire as __worktreeSidebarRequire } from \"node:module\";\nconst require = __worktreeSidebarRequire(import.meta.url);"
22
22
  },
23
+ "catalog": {
24
+ "category": "ui",
25
+ "summary": {
26
+ "en": "Point the current session's right-sidebar file tree at a git worktree without changing the session cwd.",
27
+ "zh": "将会话右侧栏文件树根指向 git worktree,会话 cwd 不变,原生能力上线即退役"
28
+ },
29
+ "capabilities": [
30
+ "ui-sidebar-right"
31
+ ]
32
+ },
23
33
  "client": {
24
34
  "inject": [
25
35
  "@deepseek-ai/dsh-client-ui-sidebar-right"
@@ -35,21 +45,21 @@
35
45
  "LICENSE"
36
46
  ],
37
47
  "devDependencies": {
38
- "@deepseek-ai/cordis": "4.0.2",
39
- "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
40
- "@deepseek-ai/dsh-api-workspace-files": "0.1.5-rc.1",
41
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
42
- "@deepseek-ai/dsh-session": "0.1.5-rc.1",
43
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
44
- "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1"
48
+ "@deepseek-ai/cordis": "4.0.4",
49
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-api-workspace-files": "0.1.7-rc.2",
51
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
52
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
53
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
54
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2"
45
55
  },
46
56
  "peerDependencies": {
47
- "@deepseek-ai/cordis": "4.0.2",
48
- "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
49
- "@deepseek-ai/dsh-api-workspace-files": "0.1.5-rc.1",
50
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
51
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
52
- "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1"
57
+ "@deepseek-ai/cordis": "4.0.4",
58
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
59
+ "@deepseek-ai/dsh-api-workspace-files": "0.1.7-rc.2",
60
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2"
53
63
  },
54
64
  "peerDependenciesMeta": {
55
65
  "@deepseek-ai/cordis": {
@@ -86,6 +96,7 @@
86
96
  "keywords": [
87
97
  "dsh",
88
98
  "deepseek-harness",
99
+ "dsh-plugin",
89
100
  "plugin",
90
101
  "worktree",
91
102
  "git",
@@ -97,7 +108,7 @@
97
108
  },
98
109
  "scripts": {
99
110
  "build": "node ../../scripts/build/clean-lib.ts && tsc -p tsconfig.json && node ../../scripts/build/bundle-host.ts .",
100
- "test": "node ../../scripts/test/run-vitest.mjs --min 19",
111
+ "test": "node ../../scripts/test/run-vitest.mjs --min 21",
101
112
  "typecheck": "tsc -p tsconfig.json --noEmit"
102
113
  }
103
114
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 插件主目录拼装(单一事实源,实现见 `./paths.js`):等同
3
+ * `join(base, ...segments)`,默认形态路径逐字节不变。
4
+ *
5
+ * 只收敛「包主目录」直拼;以下 7 类排除:legacy/旧根(旧版迁移读面)、
6
+ * settings 文档(宿主文档读面)、resolve 对比(比较/候选命中顺序是语义)、
7
+ * 用户输入解析(`~`/相对路径展开)、credentials-userHome(DSH_HOME 域外凭据)、
8
+ * 展示脱敏(字符串替换不落盘)、包内反推(随包分发不在 DSH home 下)。
9
+ * provider-registry 的旧 `pluginHome` 保留为包内 facade(公开签名不变)。
10
+ * 完整理由见 `./paths.js` 函数 JSDoc。
11
+ */
12
+ export declare function pluginHome(base: string, ...segments: string[]): string;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * 宿主端「设置命名空间」注册(单一事实源)。
3
3
  *
4
- * 背景与语义见 settings-namespace.js 顶部注释。要点:DSH rc.7 起设置卡槽
5
- * `settings.plugin.item` 为 keyed,只有在「宿主 serve 的命名空间 ∩ 卡片 key」
6
- * 非空时渲染;为让卡片显示,宿主端需用此函数把命名空间注册进 settings 服务。
7
- * 零包依赖,服务面注入,等值复刻官方 `installSettingsSection`(dsh-settings)。
4
+ * 背景与语义见 settings-namespace.js 顶部注释。要点:宿主 settings 服务按 `ns`
5
+ * 定位描述项并合成 `base + user`(无层字段时兼容取 `value`);写经
6
+ * update/replace/mutate;热更新经 document-updated 订阅(内部直连,不对外暴露 watch 面)。
7
+ * 本文件只收窄能力面(全 unknown 结构面,无官方包导入)。
8
8
  */
9
9
 
10
10
  /** `installSettingsNamespace` 的 hooks 面。 */
@@ -13,15 +13,51 @@ export interface SettingsNamespaceHooks {
13
13
  setSource(source: () => unknown): void;
14
14
  /** 来源切换或命名空间值变化时触发,插件据此刷新/落盘。 */
15
15
  onChange(): void;
16
- /** 可选自定义校验,透传给 settings.register。 */
17
- validate?: unknown;
18
16
  /**
19
- * 可选;register 返回 owner scope 后立即回调(先于 setSource)。
20
- * 供存量配置迁移 / 写路径装配使用;settings 服务缺失时不触发。
17
+ * 可选;owning fiber ACTIVE 且 canonical namespace 被 settings 服务描述后回调一次。
18
+ * 供存量配置迁移 / 写路径装配使用;fiber / 服务 / namespace 未就绪时不触发。
21
19
  */
22
20
  onScope?(scope: unknown, service: unknown): void;
23
21
  }
24
22
 
23
+ /** 描述项窄面(按 ns 定位,合成 base + user,兼容 value 回退)。 */
24
+ export interface SettingsFormsDescriptor {
25
+ /** 命名空间键。 */
26
+ ns?: unknown;
27
+ /** 运行时解析值(缺少分层字段时的兼容回退)。 */
28
+ value?: unknown;
29
+ /** 基础配置层。 */
30
+ base?: unknown;
31
+ /** 原始 user 覆盖层。 */
32
+ user?: unknown;
33
+ /** 乐观并发修订号。 */
34
+ revision?: unknown;
35
+ }
36
+
37
+ /** owner scope 窄面(describe 定位读+写委托;订阅由接缝内部直连)。 */
38
+ export interface SettingsFormsScope {
39
+ /** 当前有效值(describe base + user,缺席回落 entry)。 */
40
+ get(): unknown;
41
+ /** 委托 settings.update(ns, …)。 */
42
+ update(patch: object, expectedRevision?: number): Promise<unknown>;
43
+ /** 委托 settings.replace(ns, …)。 */
44
+ replace(section: object, expectedRevision?: number): Promise<unknown>;
45
+ /** 委托 settings.mutate(ns, …);服务缺失时返回拒绝。 */
46
+ mutate(ops: readonly unknown[], expectedRevision?: number): Promise<unknown>;
47
+ }
48
+
49
+ /** settings 服务窄面(describe/写)。 */
50
+ export interface SettingsFormsService {
51
+ /** 按 entry id 定位描述项。 */
52
+ describe(options?: { redactSecrets?: boolean }): SettingsFormsDescriptor[];
53
+ /** 合并写。 */
54
+ update?(ns: string, patch: object, expectedRevision?: number): Promise<unknown>;
55
+ /** 整节写。 */
56
+ replace?(ns: string, section: object, expectedRevision?: number): Promise<unknown>;
57
+ /** 路径写(可选)。 */
58
+ mutate?(ns: string, ops: readonly unknown[], expectedRevision?: number): Promise<unknown>;
59
+ }
60
+
25
61
  /**
26
62
  * 日志兜底:logger 可能确实没有(极端降级),全部可选调用。
27
63
  * 单一事实源(#436):notifier / lan-proxy 曾各复刻一份,现统一引用本导出。
@@ -30,11 +66,11 @@ export declare function warnLog(ctx: unknown, message: string): void;
30
66
 
31
67
  /**
32
68
  * 注册插件自有 settings 命名空间(服务面注入,无包依赖)。
33
- * 等值语义参考官方 `installSettingsSection`(@deepseek-ai/dsh-settings@0.1.0-rc.7)。
69
+ * 对外签名保持不变(调用方零改)。
34
70
  * @param ctx - 插件宿主端 apply 收到的 cordis 上下文。
35
71
  * @param ns - 插件自有命名空间(小写 kebab,须唯一)。
36
- * @param schema - schemastery schema,解析该命名空间的值(通常为插件 Config)。
37
- * @param entry - 组合层配置,作为命名空间的 `base` 层。
72
+ * @param schema - 占位:schema 由宿主持有,本函数不注册。
73
+ * @param entry - 组合层配置(describe 缺席时的回落值)。
38
74
  * @param hooks - source 收藏与变更通知。
39
75
  */
40
76
  export declare function installSettingsNamespace(
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 空步:立即完成,不碰任何存储。调用方把版本号写在自己的步骤表里,run 统一指到本函数。
3
+ *
4
+ * @returns {Promise<void>} 恒为完成态的 Promise。
5
+ */
6
+ export function tickUpgradeVersion(): Promise<void>;
7
+
8
+ /**
9
+ * 空步(同步链用):什么都不做,立即返回。
10
+ */
11
+ export function tickUpgradeVersionSync(): void;