@wingsky-1/dsh-worktree-sidebar 0.2.5 → 0.2.7
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 +11 -1
- package/README.md +10 -2
- package/cordis.patch.yml +1 -1
- package/lib/client/shared/ports.d.ts +7 -3
- package/lib/client/takeover.d.ts +4 -3
- package/lib/client.js +30 -24
- package/lib/index.js +145 -76
- package/lib/server/host/agents.d.ts +7 -2
- package/lib/server/tools/deps.d.ts +2 -2
- package/lib/server/tools/impl/bind/index.d.ts +5 -0
- package/lib/server/tools/impl/remove/index.d.ts +0 -9
- package/package.json +31 -16
- package/shared/paths.d.ts +12 -0
- package/shared/settings-namespace.d.ts +47 -11
- package/shared/upgrade-tick.d.ts +11 -0
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
|
|
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.
|
|
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
|
@@ -36,17 +36,21 @@ export interface StoredEntryLike {
|
|
|
36
36
|
readonly locale?: string | undefined;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* tab
|
|
39
|
+
* tab 类型定义,语义对齐官方 SidebarRightTabDefinition(只读,不改写类型表)。
|
|
40
40
|
*
|
|
41
|
-
*
|
|
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?:
|
|
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[];
|
package/lib/client/takeover.d.ts
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
119
|
+
if (watched.get(tabId) !== tab || tab.abort?.signal.aborted === true) return;
|
|
113
120
|
const seeded = view.root.getSnapshot() ?? root;
|
|
114
|
-
|
|
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) => {
|
|
@@ -107,24 +108,33 @@ function bindTypert(lookups) {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
// ../../shared/loopback.js
|
|
110
|
-
function
|
|
111
|
+
function isLoopbackPeerAddress(request) {
|
|
111
112
|
const address = request.socket?.remoteAddress;
|
|
112
|
-
|
|
113
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
114
|
+
}
|
|
115
|
+
function loopbackHostUrlOf(request) {
|
|
113
116
|
const host = request.headers.host;
|
|
114
|
-
if (typeof host !== "string") return
|
|
117
|
+
if (typeof host !== "string") return void 0;
|
|
115
118
|
let hostUrl;
|
|
116
119
|
try {
|
|
117
120
|
hostUrl = new URL(`http://${host}`);
|
|
118
121
|
} catch {
|
|
119
|
-
return
|
|
122
|
+
return void 0;
|
|
120
123
|
}
|
|
121
|
-
if (hostUrl.hostname !== "127.0.0.1" && hostUrl.hostname !== "localhost" && hostUrl.hostname !== "[::1]")
|
|
122
|
-
return
|
|
123
|
-
if (request.headers["sec-fetch-site"] === "cross-site") {
|
|
124
|
-
if (!(options.allowCrossSiteNoCors === true && request.headers["sec-fetch-mode"] === "no-cors")) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
124
|
+
if (hostUrl.hostname !== "127.0.0.1" && hostUrl.hostname !== "localhost" && hostUrl.hostname !== "[::1]") {
|
|
125
|
+
return void 0;
|
|
127
126
|
}
|
|
127
|
+
return hostUrl;
|
|
128
|
+
}
|
|
129
|
+
function crossSiteRequestAllowed(request, options) {
|
|
130
|
+
if (request.headers["sec-fetch-site"] !== "cross-site") return true;
|
|
131
|
+
return options.allowCrossSiteNoCors === true && request.headers["sec-fetch-mode"] === "no-cors";
|
|
132
|
+
}
|
|
133
|
+
function isLoopbackRequest(request, options = {}) {
|
|
134
|
+
if (!isLoopbackPeerAddress(request)) return false;
|
|
135
|
+
const hostUrl = loopbackHostUrlOf(request);
|
|
136
|
+
if (hostUrl === void 0) return false;
|
|
137
|
+
if (!crossSiteRequestAllowed(request, options)) return false;
|
|
128
138
|
const origin = request.headers.origin;
|
|
129
139
|
if (origin === void 0) return true;
|
|
130
140
|
try {
|
|
@@ -305,9 +315,9 @@ function validateRecord(value) {
|
|
|
305
315
|
const branch = raw["branch"];
|
|
306
316
|
const createdAt = raw["createdAt"];
|
|
307
317
|
const sessionCreatedAt = raw["sessionCreatedAt"];
|
|
308
|
-
if (
|
|
318
|
+
if (!isNonEmptyString(repoRoot))
|
|
309
319
|
return void 0;
|
|
310
|
-
if (
|
|
320
|
+
if (!isNonEmptyString(worktreeRoot))
|
|
311
321
|
return void 0;
|
|
312
322
|
if (typeof branch !== "string")
|
|
313
323
|
return void 0;
|
|
@@ -317,33 +327,49 @@ function validateRecord(value) {
|
|
|
317
327
|
return void 0;
|
|
318
328
|
return { repoRoot, worktreeRoot, branch, createdAt, sessionCreatedAt };
|
|
319
329
|
}
|
|
330
|
+
function isNonEmptyString(value) {
|
|
331
|
+
return typeof value === "string" && value.length > 0;
|
|
332
|
+
}
|
|
320
333
|
function parseTable(text) {
|
|
321
|
-
|
|
334
|
+
const parsed = parseJsonOrUndefined(text);
|
|
335
|
+
if (parsed === void 0)
|
|
336
|
+
return emptyTable();
|
|
337
|
+
const header = tableHeaderOf(parsed);
|
|
338
|
+
if (header === void 0)
|
|
339
|
+
return emptyTable();
|
|
340
|
+
return { version: BINDINGS_VERSION, revision: header.revision, bindings: recordsOf(header) };
|
|
341
|
+
}
|
|
342
|
+
function parseJsonOrUndefined(text) {
|
|
322
343
|
try {
|
|
323
|
-
|
|
344
|
+
return JSON.parse(text);
|
|
324
345
|
} catch {
|
|
325
|
-
return
|
|
346
|
+
return void 0;
|
|
326
347
|
}
|
|
348
|
+
}
|
|
349
|
+
function tableHeaderOf(parsed) {
|
|
327
350
|
if (typeof parsed !== "object" || parsed === null)
|
|
328
|
-
return
|
|
351
|
+
return void 0;
|
|
329
352
|
const raw = parsed;
|
|
330
353
|
if (raw["version"] !== BINDINGS_VERSION)
|
|
331
|
-
return
|
|
354
|
+
return void 0;
|
|
332
355
|
const revision2 = raw["revision"];
|
|
333
356
|
if (typeof revision2 !== "number" || !Number.isFinite(revision2) || revision2 < 0)
|
|
334
|
-
return
|
|
357
|
+
return void 0;
|
|
335
358
|
const entries = raw["bindings"];
|
|
336
359
|
if (typeof entries !== "object" || entries === null)
|
|
337
|
-
return
|
|
360
|
+
return void 0;
|
|
361
|
+
return { revision: revision2, entries };
|
|
362
|
+
}
|
|
363
|
+
function recordsOf(header) {
|
|
338
364
|
const bindings = {};
|
|
339
|
-
for (const [sessionId, value] of Object.entries(entries)) {
|
|
365
|
+
for (const [sessionId, value] of Object.entries(header.entries)) {
|
|
340
366
|
if (sessionId.length === 0)
|
|
341
367
|
continue;
|
|
342
368
|
const record = validateRecord(value);
|
|
343
369
|
if (record !== void 0)
|
|
344
370
|
bindings[sessionId] = record;
|
|
345
371
|
}
|
|
346
|
-
return
|
|
372
|
+
return bindings;
|
|
347
373
|
}
|
|
348
374
|
function serializeTable(table) {
|
|
349
375
|
return JSON.stringify(table, null, 2) + "\n";
|
|
@@ -364,9 +390,13 @@ function dropBinding(table, sessionId) {
|
|
|
364
390
|
}
|
|
365
391
|
|
|
366
392
|
// lib/server/shared/file-io.js
|
|
393
|
+
import { randomBytes } from "node:crypto";
|
|
367
394
|
import { readFileSync } from "node:fs";
|
|
368
395
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
369
396
|
import { dirname } from "node:path";
|
|
397
|
+
function temporaryNameFor(file) {
|
|
398
|
+
return `${file}.tmp-${process.pid}.${Date.now().toString(36)}.${randomBytes(6).toString("hex")}.tmp`;
|
|
399
|
+
}
|
|
370
400
|
function readTextFileSync(file) {
|
|
371
401
|
try {
|
|
372
402
|
return { ok: true, text: readFileSync(file, "utf8") };
|
|
@@ -375,7 +405,7 @@ function readTextFileSync(file) {
|
|
|
375
405
|
}
|
|
376
406
|
}
|
|
377
407
|
async function writeTextAtomic(file, text) {
|
|
378
|
-
const temporary =
|
|
408
|
+
const temporary = temporaryNameFor(file);
|
|
379
409
|
try {
|
|
380
410
|
await mkdir(dirname(file), { recursive: true });
|
|
381
411
|
await writeFile(temporary, text, "utf8");
|
|
@@ -386,9 +416,6 @@ async function writeTextAtomic(file, text) {
|
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
418
|
|
|
389
|
-
// lib/server/shared/paths.js
|
|
390
|
-
import { join as join2 } from "node:path";
|
|
391
|
-
|
|
392
419
|
// ../../shared/dsh-home.js
|
|
393
420
|
import { homedir } from "node:os";
|
|
394
421
|
import { join } from "node:path";
|
|
@@ -401,11 +428,17 @@ function dshHome() {
|
|
|
401
428
|
return env !== void 0 && env.trim().length > 0 ? env : join(userHome(), ".dsh");
|
|
402
429
|
}
|
|
403
430
|
|
|
431
|
+
// ../../shared/paths.js
|
|
432
|
+
import { join as join2 } from "node:path";
|
|
433
|
+
function pluginHome(base, ...segments) {
|
|
434
|
+
return join2(base, ...segments);
|
|
435
|
+
}
|
|
436
|
+
|
|
404
437
|
// lib/server/shared/paths.js
|
|
405
438
|
var PACKAGE_DIR = "@wingsky-1/dsh-worktree-sidebar";
|
|
406
439
|
var BINDINGS_FILE_NAME = "bindings.json";
|
|
407
440
|
function bindingsFile() {
|
|
408
|
-
return
|
|
441
|
+
return pluginHome(dshHome(), PACKAGE_DIR, BINDINGS_FILE_NAME);
|
|
409
442
|
}
|
|
410
443
|
|
|
411
444
|
// lib/server/binding/impl/store/index.js
|
|
@@ -1215,21 +1248,17 @@ function argBool(args, key) {
|
|
|
1215
1248
|
|
|
1216
1249
|
// lib/server/tools/impl/session/index.js
|
|
1217
1250
|
function sessionOf(exec) {
|
|
1218
|
-
if (typeof exec !== "object" || exec === null)
|
|
1219
|
-
return void 0;
|
|
1220
1251
|
const session = readObject(readObject(exec, "agent"), "session");
|
|
1221
1252
|
if (session === void 0)
|
|
1222
1253
|
return void 0;
|
|
1223
|
-
const id = session
|
|
1254
|
+
const id = readField(session, "id");
|
|
1224
1255
|
if (typeof id !== "string" || id.length === 0)
|
|
1225
1256
|
return void 0;
|
|
1226
1257
|
const header = readObject(session, "header");
|
|
1227
|
-
const cwd = header === void 0 ? void 0 : header.cwd;
|
|
1228
|
-
const createdAt = header === void 0 ? void 0 : header.createdAt;
|
|
1229
1258
|
return {
|
|
1230
1259
|
id,
|
|
1231
|
-
cwd:
|
|
1232
|
-
createdAt:
|
|
1260
|
+
cwd: nonEmptyTextOrUndefined(readField(header, "cwd")),
|
|
1261
|
+
createdAt: finiteNumberOrUndefined(readField(header, "createdAt"))
|
|
1233
1262
|
};
|
|
1234
1263
|
}
|
|
1235
1264
|
function readObject(source, key) {
|
|
@@ -1238,8 +1267,44 @@ function readObject(source, key) {
|
|
|
1238
1267
|
const value = source[key];
|
|
1239
1268
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
1240
1269
|
}
|
|
1270
|
+
function readField(source, key) {
|
|
1271
|
+
if (typeof source !== "object" || source === null)
|
|
1272
|
+
return void 0;
|
|
1273
|
+
return source[key];
|
|
1274
|
+
}
|
|
1275
|
+
function nonEmptyTextOrUndefined(value) {
|
|
1276
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1277
|
+
}
|
|
1278
|
+
function finiteNumberOrUndefined(value) {
|
|
1279
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1280
|
+
}
|
|
1241
1281
|
|
|
1242
1282
|
// lib/server/tools/impl/create/index.js
|
|
1283
|
+
async function checkBranch(deps, origin, branch) {
|
|
1284
|
+
if (branch === void 0)
|
|
1285
|
+
return { ok: true };
|
|
1286
|
+
if (await deps.git.checkRefFormat(branch))
|
|
1287
|
+
return { ok: true };
|
|
1288
|
+
return {
|
|
1289
|
+
ok: false,
|
|
1290
|
+
result: resultOf(origin, false, "Not a valid git branch name: " + branch + " (git check-ref-format --branch rejected it).")
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
async function resolveStartPoint(deps, repo, origin, base) {
|
|
1294
|
+
if (base === void 0)
|
|
1295
|
+
return { ok: true };
|
|
1296
|
+
if (base.startsWith("-")) {
|
|
1297
|
+
return {
|
|
1298
|
+
ok: false,
|
|
1299
|
+
result: resultOf(origin, false, "Not a valid start point: " + base + ' (a start point must not begin with "-").')
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const startPoint = await deps.git.resolveCommit(repo, base);
|
|
1303
|
+
if (startPoint === void 0) {
|
|
1304
|
+
return { ok: false, result: resultOf(origin, false, "Not a valid start point: " + base + ".") };
|
|
1305
|
+
}
|
|
1306
|
+
return { ok: true, startPoint };
|
|
1307
|
+
}
|
|
1243
1308
|
function buildCreateTool(deps) {
|
|
1244
1309
|
return {
|
|
1245
1310
|
name: "ws_worktree_create",
|
|
@@ -1289,20 +1354,13 @@ function buildCreateTool(deps) {
|
|
|
1289
1354
|
const origin = read.origin;
|
|
1290
1355
|
const target = resolveTarget(repo, raw);
|
|
1291
1356
|
const branch = argString(args, "branch");
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
if (
|
|
1297
|
-
return
|
|
1298
|
-
|
|
1299
|
-
let startPoint;
|
|
1300
|
-
if (base !== void 0) {
|
|
1301
|
-
startPoint = await deps.git.resolveCommit(repo, base);
|
|
1302
|
-
if (startPoint === void 0) {
|
|
1303
|
-
return resultOf(origin, false, "Not a valid start point: " + base + ".");
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1357
|
+
const branchCheck = await checkBranch(deps, origin, branch);
|
|
1358
|
+
if (!branchCheck.ok)
|
|
1359
|
+
return branchCheck.result;
|
|
1360
|
+
const start = await resolveStartPoint(deps, repo, origin, argString(args, "base"));
|
|
1361
|
+
if (!start.ok)
|
|
1362
|
+
return start.result;
|
|
1363
|
+
const startPoint = start.startPoint;
|
|
1306
1364
|
const created = await deps.git.addWorktree(repo, target, branch, startPoint);
|
|
1307
1365
|
if (!created.ok) {
|
|
1308
1366
|
return resultOf(origin, false, "git worktree add failed: " + created.reason);
|
|
@@ -1323,6 +1381,32 @@ function buildCreateTool(deps) {
|
|
|
1323
1381
|
}
|
|
1324
1382
|
|
|
1325
1383
|
// lib/server/tools/impl/remove/index.js
|
|
1384
|
+
async function unbindAndRemove(deps, sessionId, origin, force) {
|
|
1385
|
+
const removed = await deps.git.removeWorktree(origin.record.repoRoot, origin.record.worktreeRoot, force);
|
|
1386
|
+
if (!removed.ok) {
|
|
1387
|
+
return resultOf(origin, false, "git worktree remove failed: " + removed.reason + " The binding is unchanged.");
|
|
1388
|
+
}
|
|
1389
|
+
const dropped = await deps.binding.drop(sessionId);
|
|
1390
|
+
if (!dropped.ok) {
|
|
1391
|
+
return resultOf(origin, false, "Removed the worktree directory, but the binding could not be dropped: " + dropped.reason + " It is now stale and will be treated as unbound; call this tool again to retry the unbind.");
|
|
1392
|
+
}
|
|
1393
|
+
const after = await readOrigin(deps, sessionId);
|
|
1394
|
+
if (after.problem !== void 0) {
|
|
1395
|
+
return resultOf(after.origin, true, "Unbound this session and removed the worktree directory with git worktree remove. The Files tab root could not be read back: " + after.problem);
|
|
1396
|
+
}
|
|
1397
|
+
return resultOf(after.origin, true, after.origin.kind === "inherited" ? "Unbound this session and removed the worktree directory with git worktree remove. The Files tab now follows the root inherited from session " + after.origin.ownerSessionId + "." : "Unbound this session and removed the worktree directory with git worktree remove.");
|
|
1398
|
+
}
|
|
1399
|
+
async function unbindOnly(deps, sessionId, origin) {
|
|
1400
|
+
const dropped = await deps.binding.drop(sessionId);
|
|
1401
|
+
if (!dropped.ok) {
|
|
1402
|
+
return resultOf(origin, false, "Could not persist the unbind: " + dropped.reason);
|
|
1403
|
+
}
|
|
1404
|
+
const after = await readOrigin(deps, sessionId);
|
|
1405
|
+
if (after.problem !== void 0) {
|
|
1406
|
+
return resultOf(after.origin, true, "Unbound this session's own binding. The Files tab root could not be read back: " + after.problem);
|
|
1407
|
+
}
|
|
1408
|
+
return resultOf(after.origin, true, after.origin.kind === "inherited" ? "Unbound this session's own binding. The Files tab now follows the root inherited from session " + after.origin.ownerSessionId + "." : "Unbound the worktree from this session. The directory was left in place; open or refresh the Files tab to return to the session cwd.");
|
|
1409
|
+
}
|
|
1326
1410
|
function inheritedHelp(ownerSessionId, cwd) {
|
|
1327
1411
|
const main = "Nothing to unbind here: the Files tab root belongs to session " + ownerSessionId + ", not to this session. To stop following it: (1) unbind it in that session with ws_worktree_remove, or (2) bind this session to another worktree with ws_worktree_create / ws_worktree_register.";
|
|
1328
1412
|
if (cwd === void 0)
|
|
@@ -1371,30 +1455,9 @@ function buildRemoveTool(deps) {
|
|
|
1371
1455
|
if (force && !removeDirectory) {
|
|
1372
1456
|
return resultOf(origin, false, "force only applies together with removeDirectory: true; the binding is unchanged.");
|
|
1373
1457
|
}
|
|
1374
|
-
if (!removeDirectory)
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
return resultOf(origin, false, "Could not persist the unbind: " + dropped2.reason);
|
|
1378
|
-
}
|
|
1379
|
-
const after2 = await readOrigin(deps, session.id);
|
|
1380
|
-
if (after2.problem !== void 0) {
|
|
1381
|
-
return resultOf(after2.origin, true, "Unbound this session's own binding. The Files tab root could not be read back: " + after2.problem);
|
|
1382
|
-
}
|
|
1383
|
-
return resultOf(after2.origin, true, after2.origin.kind === "inherited" ? "Unbound this session's own binding. The Files tab now follows the root inherited from session " + after2.origin.ownerSessionId + "." : "Unbound the worktree from this session. The directory was left in place; open or refresh the Files tab to return to the session cwd.");
|
|
1384
|
-
}
|
|
1385
|
-
const removed = await deps.git.removeWorktree(origin.record.repoRoot, origin.record.worktreeRoot, force);
|
|
1386
|
-
if (!removed.ok) {
|
|
1387
|
-
return resultOf(origin, false, "git worktree remove failed: " + removed.reason + " The binding is unchanged.");
|
|
1388
|
-
}
|
|
1389
|
-
const dropped = await deps.binding.drop(session.id);
|
|
1390
|
-
if (!dropped.ok) {
|
|
1391
|
-
return resultOf(origin, false, "Removed the worktree directory, but the binding could not be dropped: " + dropped.reason + " It is now stale and will be treated as unbound; call this tool again to retry the unbind.");
|
|
1392
|
-
}
|
|
1393
|
-
const after = await readOrigin(deps, session.id);
|
|
1394
|
-
if (after.problem !== void 0) {
|
|
1395
|
-
return resultOf(after.origin, true, "Unbound this session and removed the worktree directory with git worktree remove. The Files tab root could not be read back: " + after.problem);
|
|
1396
|
-
}
|
|
1397
|
-
return resultOf(after.origin, true, after.origin.kind === "inherited" ? "Unbound this session and removed the worktree directory with git worktree remove. The Files tab now follows the root inherited from session " + after.origin.ownerSessionId + "." : "Unbound this session and removed the worktree directory with git worktree remove.");
|
|
1458
|
+
if (!removeDirectory)
|
|
1459
|
+
return unbindOnly(deps, session.id, origin);
|
|
1460
|
+
return unbindAndRemove(deps, session.id, origin, force);
|
|
1398
1461
|
}
|
|
1399
1462
|
};
|
|
1400
1463
|
}
|
|
@@ -1474,7 +1537,7 @@ var ToolsService = class {
|
|
|
1474
1537
|
const consider = (agent) => this.consider(generation, agent);
|
|
1475
1538
|
this.unsubscribe = deps.agents.subscribe(consider);
|
|
1476
1539
|
for (const agent of deps.agents.list())
|
|
1477
|
-
consider(agent);
|
|
1540
|
+
void consider(agent);
|
|
1478
1541
|
}
|
|
1479
1542
|
/**
|
|
1480
1543
|
* 卸载:退订、逐个摘掉每个 agent 的工具、丢掉映射与在飞的异步链,复位装配标记。重复调用无害。
|
|
@@ -1503,7 +1566,7 @@ var ToolsService = class {
|
|
|
1503
1566
|
}
|
|
1504
1567
|
/** 判定一个 agent 是否该装工具,该装就装。同一个 agent 只处理一次。 */
|
|
1505
1568
|
consider(generation, agent) {
|
|
1506
|
-
|
|
1569
|
+
const chain = this.chain.then(async () => {
|
|
1507
1570
|
const deps = this.deps;
|
|
1508
1571
|
if (deps === void 0)
|
|
1509
1572
|
return;
|
|
@@ -1526,6 +1589,8 @@ var ToolsService = class {
|
|
|
1526
1589
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1527
1590
|
deps.logger.warn("dsh-worktree-sidebar: 工具注册失败 — " + reason);
|
|
1528
1591
|
});
|
|
1592
|
+
this.chain = chain;
|
|
1593
|
+
return chain;
|
|
1529
1594
|
}
|
|
1530
1595
|
};
|
|
1531
1596
|
var toolsService = new ToolsService();
|
|
@@ -1546,7 +1611,11 @@ async function apply(ctx, config = {}) {
|
|
|
1546
1611
|
logger: ctx.logger,
|
|
1547
1612
|
register: (route) => ctx.webServer.register(route),
|
|
1548
1613
|
agents: bindAgents({
|
|
1549
|
-
|
|
1614
|
+
// 0.1.7-rc.2 serial 会等待 listener:完整等待 tools 域的注册链,再让 agent 创建继续。
|
|
1615
|
+
on: (event, handler) => ctx.on(event, async (payload) => {
|
|
1616
|
+
await handler({ agent: payload.agent });
|
|
1617
|
+
return void 0;
|
|
1618
|
+
}),
|
|
1550
1619
|
all: () => ctx.agents.list()
|
|
1551
1620
|
}),
|
|
1552
1621
|
typert: bindTypert(ctx.typert.lookups),
|
|
@@ -28,10 +28,15 @@ export interface HostAgentLike {
|
|
|
28
28
|
}
|
|
29
29
|
/** 宿主事件面与 agent 枚举面:只开本适配器要的两样。 */
|
|
30
30
|
export interface AgentHostPort {
|
|
31
|
-
/**
|
|
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
|
-
}) =>
|
|
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 的作用域,返回释放函数。 */
|
|
@@ -2,6 +2,11 @@ import type { WorktreeOrigin } from "../../../scope/interface.js";
|
|
|
2
2
|
import type { AgentFace, ToolsDeps } from "../../deps.js";
|
|
3
3
|
import type { SessionFace } from "../session/index.js";
|
|
4
4
|
import type { ToolResultValue } from "../protocol/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* 绑定来源的形态。本域内各工具一律从**这里**取,不各自直引 scope/interface:
|
|
7
|
+
* tools → scope 是域间引用,纪律要求它只出现在一个面上(bind 域已经替本域读过一次)。
|
|
8
|
+
*/
|
|
9
|
+
export type { WorktreeOrigin };
|
|
5
10
|
/** 当前的绑定状态(三个工具的返回信封都要它)。 */
|
|
6
11
|
export interface BindingState {
|
|
7
12
|
readonly bound: boolean;
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ws_worktree_remove`:摘掉登记;只有显式 `removeDirectory` 才连带 `git worktree remove`。
|
|
3
|
-
*
|
|
4
|
-
* 默认不删目录是刻意的:删除可能丢掉未提交改动,而「我只是想换个根」与「我要销毁这个工作区」
|
|
5
|
-
* 是两件事,不该由同一个默认值承担。
|
|
6
|
-
*
|
|
7
|
-
* **继承态不摘任何东西**:本会话没有自己的登记时,右栏的根属于父会话——摘它是改别人的状态,
|
|
8
|
-
* 删目录更是销毁别人的工作区。这一态只如实说明并给出路。
|
|
9
|
-
*/
|
|
10
1
|
import type { ToolDefinition } from "@deepseek-ai/dsh-tools";
|
|
11
2
|
import type { ToolsDeps } from "../../deps.js";
|
|
12
3
|
export declare function buildRemoveTool(deps: ToolsDeps): ToolDefinition;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wingsky-1/dsh-worktree-sidebar",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "把某个 git worktree 登记给当前会话,使该会话的官方右侧栏文件树根指向该 worktree;会话 cwd 不变。官方提供原生 worktree 会话能力即退役的过渡适配层。",
|
|
3
|
+
"version": "0.2.7",
|
|
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,22 @@
|
|
|
35
45
|
"LICENSE"
|
|
36
46
|
],
|
|
37
47
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "4.0.
|
|
39
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
40
|
-
"@deepseek-ai/dsh-api-workspace-files": "0.1.
|
|
41
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
42
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
43
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
44
|
-
"@deepseek-ai/dsh-typert-protocol": "0.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.
|
|
48
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-api-workspace-files": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
51
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
52
|
-
"@deepseek-ai/dsh-typert-protocol": "0.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",
|
|
63
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2"
|
|
53
64
|
},
|
|
54
65
|
"peerDependenciesMeta": {
|
|
55
66
|
"@deepseek-ai/cordis": {
|
|
@@ -69,6 +80,9 @@
|
|
|
69
80
|
},
|
|
70
81
|
"@deepseek-ai/dsh-typert-protocol": {
|
|
71
82
|
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@deepseek-ai/dsh-session": {
|
|
85
|
+
"optional": true
|
|
72
86
|
}
|
|
73
87
|
},
|
|
74
88
|
"engines": {
|
|
@@ -86,6 +100,7 @@
|
|
|
86
100
|
"keywords": [
|
|
87
101
|
"dsh",
|
|
88
102
|
"deepseek-harness",
|
|
103
|
+
"dsh-plugin",
|
|
89
104
|
"plugin",
|
|
90
105
|
"worktree",
|
|
91
106
|
"git",
|
|
@@ -97,7 +112,7 @@
|
|
|
97
112
|
},
|
|
98
113
|
"scripts": {
|
|
99
114
|
"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
|
|
115
|
+
"test": "node ../../scripts/test/run-vitest.mjs --min 21",
|
|
101
116
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
102
117
|
}
|
|
103
118
|
}
|
|
@@ -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
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
* 可选;
|
|
20
|
-
* 供存量配置迁移 / 写路径装配使用;
|
|
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
|
-
*
|
|
69
|
+
* 对外签名保持不变(调用方零改)。
|
|
34
70
|
* @param ctx - 插件宿主端 apply 收到的 cordis 上下文。
|
|
35
71
|
* @param ns - 插件自有命名空间(小写 kebab,须唯一)。
|
|
36
|
-
* @param schema -
|
|
37
|
-
* @param entry -
|
|
72
|
+
* @param schema - 占位:schema 由宿主持有,本函数不注册。
|
|
73
|
+
* @param entry - 组合层配置(describe 缺席时的回落值)。
|
|
38
74
|
* @param hooks - source 收藏与变更通知。
|
|
39
75
|
*/
|
|
40
76
|
export declare function installSettingsNamespace(
|