@wingsky-1/dsh-worktree-sidebar 0.2.5

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +186 -0
  3. package/README.md +190 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/client/bindings.d.ts +16 -0
  6. package/lib/client/index.d.ts +23 -0
  7. package/lib/client/inject.d.ts +21 -0
  8. package/lib/client/shared/ports.d.ts +141 -0
  9. package/lib/client/source.d.ts +22 -0
  10. package/lib/client/takeover.d.ts +51 -0
  11. package/lib/client.js +394 -0
  12. package/lib/index.d.ts +28 -0
  13. package/lib/index.js +1614 -0
  14. package/lib/server/api/deps.d.ts +35 -0
  15. package/lib/server/api/impl/handlers/index.d.ts +24 -0
  16. package/lib/server/api/impl/route/index.d.ts +25 -0
  17. package/lib/server/api/impl/service/index.d.ts +31 -0
  18. package/lib/server/api/interface.d.ts +12 -0
  19. package/lib/server/binding/deps.d.ts +9 -0
  20. package/lib/server/binding/impl/model/index.d.ts +16 -0
  21. package/lib/server/binding/impl/model/type.d.ts +37 -0
  22. package/lib/server/binding/impl/service/index.d.ts +67 -0
  23. package/lib/server/binding/impl/store/index.d.ts +6 -0
  24. package/lib/server/binding/interface.d.ts +24 -0
  25. package/lib/server/git/deps.d.ts +59 -0
  26. package/lib/server/git/impl/exec/index.d.ts +3 -0
  27. package/lib/server/git/impl/inspect/index.d.ts +47 -0
  28. package/lib/server/git/impl/service/index.d.ts +70 -0
  29. package/lib/server/git/interface.d.ts +41 -0
  30. package/lib/server/host/agents.d.ts +39 -0
  31. package/lib/server/host/sessions.d.ts +41 -0
  32. package/lib/server/host/typert.d.ts +11 -0
  33. package/lib/server/scope/deps.d.ts +110 -0
  34. package/lib/server/scope/impl/inherit/index.d.ts +25 -0
  35. package/lib/server/scope/impl/own/index.d.ts +21 -0
  36. package/lib/server/scope/impl/resolve/index.d.ts +40 -0
  37. package/lib/server/scope/impl/service/index.d.ts +115 -0
  38. package/lib/server/scope/interface.d.ts +40 -0
  39. package/lib/server/shared/file-io.d.ts +19 -0
  40. package/lib/server/shared/interface.d.ts +11 -0
  41. package/lib/server/shared/paths.d.ts +2 -0
  42. package/lib/server/shared/type.d.ts +6 -0
  43. package/lib/server/tools/deps.d.ts +52 -0
  44. package/lib/server/tools/impl/bind/index.d.ts +62 -0
  45. package/lib/server/tools/impl/create/index.d.ts +4 -0
  46. package/lib/server/tools/impl/protocol/index.d.ts +36 -0
  47. package/lib/server/tools/impl/register/index.d.ts +4 -0
  48. package/lib/server/tools/impl/remove/index.d.ts +12 -0
  49. package/lib/server/tools/impl/service/index.d.ts +28 -0
  50. package/lib/server/tools/impl/session/index.d.ts +18 -0
  51. package/lib/server/tools/interface.d.ts +14 -0
  52. package/lib/shared/contract.d.ts +26 -0
  53. package/lib/shared/interface.d.ts +8 -0
  54. package/package.json +103 -0
  55. package/shared/client/ensure-style.d.ts +21 -0
  56. package/shared/client/i18n.d.ts +15 -0
  57. package/shared/dsh-home.d.ts +15 -0
  58. package/shared/host-utils.d.ts +65 -0
  59. package/shared/loopback.d.ts +21 -0
  60. package/shared/settings-namespace.d.ts +46 -0
  61. package/shared/sse-hub.d.ts +76 -0
@@ -0,0 +1,35 @@
1
+ /** api 域依赖声明:只声明「我需要外部什么」,声明面只有类型。 */
2
+ import type { WebRoute } from "@deepseek-ai/dsh-host-webserver";
3
+ import type * as bindingApi from "../binding/interface.js";
4
+ import type * as scopeApi from "../scope/interface.js";
5
+ import type { LoggerPort } from "../shared/interface.js";
6
+ /** 宿主路由注册口:与宿主契约同源,不在两侧各写一遍。 */
7
+ export type RegisterRoute = (route: WebRoute) => () => void;
8
+ /** binding 域给本域的能力面:客户端以修订号判定「宿主侧是否变了」。 */
9
+ export type RevisionPort = Pick<typeof bindingApi, "revision">;
10
+ /**
11
+ * scope 域给本域的能力面:只读**生效**的文件根。
12
+ *
13
+ * 这里刻意**不**直接读绑定表:绑定表是「用户登记了什么」,而客户端需要的是「宿主现在按哪个根解析」。
14
+ * 两者在失效绑定(目录没了、已不是该仓库的 worktree)上会分叉——分叉的表现是客户端把树指向 worktree、
15
+ * 而宿主按 cwd 解析,于是每次列目录都得到 outside-workspace。所以本域只认 scope 域算出来的**生效值**。
16
+ */
17
+ export type EffectiveWorktreePort = Pick<typeof scopeApi, "effectiveWorktree">;
18
+ /**
19
+ * scope 域给本域的**状态读数**面:health 用它把「文件根没换」的成因分开——
20
+ * 还没等到 provider(waiting)、接管权被别人占了(abandoned)、接管了但该会话没命中绑定。
21
+ * 会话链读数另解一类:**持久面读失败会被静默收口成「到顶」**,功能降级成 live-only 而现场不留痕迹,
22
+ * 这一条读数是它唯一能落地的痕迹(真机上插件的 `logger.warn` 不落盘,见 doc §19.5)。
23
+ */
24
+ export type ScopeStatePort = Pick<typeof scopeApi, "takeoverState" | "chainDiagnostics">;
25
+ /** 装配入参:一个提供方一行,两个提供方互不搭界(一个给修订号,一个给生效根)。 */
26
+ export interface ApiDeps {
27
+ /** 宿主路由注册口:只有组合根够得着 `ctx.webServer`。 */
28
+ readonly register: RegisterRoute;
29
+ /** 失败出口(端点内的异常一律在这里出声,不静默吞)。 */
30
+ readonly logger: LoggerPort;
31
+ /** 绑定表修订号。本域**不能**写任何状态,所以它不含 put/drop。 */
32
+ readonly binding: RevisionPort;
33
+ /** 该会话当前生效的 worktree 根;null 表示按会话 cwd 走。两项读数同域同源。 */
34
+ readonly scope: EffectiveWorktreePort & ScopeStatePort;
35
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * api 域的两个端点。都是只读的:浏览器侧没有写绑定的路径。
3
+ *
4
+ * 两个刻意的取舍:
5
+ * - 绑定查询**不回 repoRoot**。客户端只需要目录根,多回一个字段就多一份「客户端知道主仓库位置」的暴露面。
6
+ * - 缺 `session` 参数判 400 而不是回空。前者是调用方出错(可修),后者会被误读成「该会话没有绑定」。
7
+ *
8
+ * 两个事实来自两个域(修订号来自 binding、生效根来自 scope),故入参是两个窄端口而不是一个拼出来的对象。
9
+ */
10
+ import type { Endpoint } from "../route/index.js";
11
+ import type { EffectiveWorktreePort, RevisionPort, ScopeStatePort } from "../../deps.js";
12
+ /** `GET /api/dsh-worktree-sidebar/bindings?session=<id>` —— 单会话文件根查询。 */
13
+ export declare function bindingsEndpoint(binding: RevisionPort, scope: EffectiveWorktreePort): Endpoint;
14
+ /**
15
+ * `GET /api/dsh-worktree-sidebar/health` —— 存活探针。带回 revision 与接管状态,让它同时是有用的状态查询。
16
+ *
17
+ * `scopeTakeover` 是「文件根为什么没换」的第一手证据:真实启动序里 provider 与插件的先后没有稳定保证,
18
+ * 只报「没换根」会让 waiting / abandoned / 未命中绑定三种成因长得一模一样。
19
+ *
20
+ * `scopeChain` 解的是另一类**无声**故障:已结束会话的父链只能从持久面读,读不出来时本域按「到顶」收口
21
+ * (正确的行为),于是继承悄悄退回 live-only——而真机上插件的 `logger.warn` 不落盘(doc §19.5),
22
+ * 现场除这个读数之外没有任何痕迹。
23
+ */
24
+ export declare function healthEndpoint(binding: RevisionPort, scope: ScopeStatePort): Endpoint;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * api 域路由块:注册端点并给每条请求套围栏。
3
+ *
4
+ * 围栏用仓库共享层那一份(`guardLoopbackMethod`),不在这里重写判定:
5
+ * 安全边界的修复必须传播到所有插件,复制一份就是让它不再传播(shared/loopback.js 的模块注释即此意)。
6
+ * 顺序由共享层保证:非回环 403 先于方法错 405。
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ import type { LoggerPort } from "../../../shared/interface.js";
10
+ import type { RegisterRoute } from "../../deps.js";
11
+ /** 一个端点的处理器。 */
12
+ type RequestHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
13
+ /** 一条路由:路径 + 方法白名单(方法名 → 处理器)。 */
14
+ export interface Endpoint {
15
+ readonly path: string;
16
+ readonly methods: Readonly<Record<string, RequestHandler>>;
17
+ }
18
+ /**
19
+ * 注册端点组,返回摘除器清单。**要么全上、要么全不上**。
20
+ *
21
+ * 中途失败时把已挂的那些摘回去再抛:宿主侧留着一条没有摘除器的路由,比一条都没挂更糟——
22
+ * 它在册、会响应、而下一次装配只会再挂一遍,且没人能摘掉它。
23
+ */
24
+ export declare function registerEndpoints(register: RegisterRoute, endpoints: readonly Endpoint[], logger: LoggerPort): Array<() => void>;
25
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * api 域装配:**浏览器出口**。把宿主端的事实经 HTTP 送到页面;本域不判业务,也不写任何状态。
3
+ *
4
+ * 它是本插件唯一的浏览器入口,所以围栏(回环判定、方法判定、异常收口)也只有一份实现,
5
+ * 少写一处就是多开一个洞。
6
+ *
7
+ * 已挂路由的 disposer 住在实例里;域是**进程内单例**,第二次 `install` 由 `installed` 守卫
8
+ * **显式抛错**(响亮失败优于静默共享/丢数据)。
9
+ */
10
+ import type { ApiDeps } from "../../deps.js";
11
+ /** api 域实例:装配的产物只对外给一个释放面。 */
12
+ export interface ApiInstance {
13
+ /** 摘掉全部路由。幂等。 */
14
+ release(): void;
15
+ }
16
+ /** 浏览器出口:唯一实例。 */
17
+ declare class ApiService implements ApiInstance {
18
+ /** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
19
+ private installed;
20
+ /** 已挂路由的摘除器。释放即逐个调用并清空,所以不会叠成两份。 */
21
+ private disposers;
22
+ /** 路由当前是否在册。释放幂等靠它,也靠它区分「没装过」与「已摘完」。 */
23
+ private live;
24
+ /** 装配浏览器出口(组合根在 apply 期调用一次)。重复装配是编程错误,当场暴露。 */
25
+ install(deps: ApiDeps): void;
26
+ /** 摘掉全部路由并复位装配标记——同进程的下一次 `install` 会重新注册,且只注册一份。 */
27
+ release(): void;
28
+ }
29
+ /** 本域唯一实例:类不外放,外面 `new` 不出第二份注册。 */
30
+ export declare const apiService: ApiService;
31
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * api 域对外契约:**浏览器出口**。把宿主端的事实经 HTTP 送到页面。
3
+ *
4
+ * 本文件只做收口——单例与它的 `ApiInstance` 形状的物理定义都在 `impl/service`(形状不从门面转出:
5
+ * 它是实现细节,转出就成了本域的第二张公开契约),围栏与端点注册在 `impl/route`,端点形状在 `impl/handlers`。
6
+ * 本域不对外提供能力(端点就是它的产物),门面因此只有装配与释放。
7
+ */
8
+ import type { ApiDeps } from "./deps.js";
9
+ /** 装配浏览器出口(组合根在 `apply` 期调用一次)。重复装配是编程错误,当场抛错。 */
10
+ export declare function installApi(deps: ApiDeps): void;
11
+ /** 摘掉全部路由,与 `installApi` 配对(重复调用无害)。 */
12
+ export declare function releaseApi(): void;
@@ -0,0 +1,9 @@
1
+ /** binding 域依赖声明:只声明「我需要外部什么」,声明面只有类型(共享层设施由实现块直接引)。 */
2
+ import type { LoggerPort } from "../shared/interface.js";
3
+ /** 装配入参。本域**拿不到** ctx:落盘路径由组合根给出,域内不反推环境。 */
4
+ export interface BindingDeps {
5
+ /** 失败出口。写盘失败是「保持内存不前移 + 出声」,不静默。 */
6
+ readonly logger: LoggerPort;
7
+ /** bindings.json 的完整路径(组合根决定 DSH_HOME 归属,域内不拼路径)。 */
8
+ readonly file: string;
9
+ }
@@ -0,0 +1,16 @@
1
+ import type { BindingRecord, BindingsFile } from "./type.js";
2
+ /** 空表。revision 从 0 起:它是**内容版本**而不是写入次数,客户端只做相等比较。 */
3
+ export declare function emptyTable(): BindingsFile;
4
+ /** 单条记录的字段校验。任何一项不合格就丢弃该条——半条记录比没有记录更危险。 */
5
+ export declare function validateRecord(value: unknown): BindingRecord | undefined;
6
+ /**
7
+ * 解析绑定表文本。**任何**异常与形状不符都回落空表:这里没有「部分恢复」的选项——
8
+ * 一条来源不明的绑定会把会话的文件树指向一个用户没选过的目录。
9
+ */
10
+ export declare function parseTable(text: string): BindingsFile;
11
+ /** 序列化。带缩进是为了让人能直接看这份文件——它的内容就是「哪个会话指向哪」的全部答案。 */
12
+ export declare function serializeTable(table: BindingsFile): string;
13
+ /** 落一条绑定。revision 递增:客户端以它判定「宿主侧是否已经变了」。 */
14
+ export declare function putBinding(table: BindingsFile, sessionId: string, record: BindingRecord): BindingsFile;
15
+ /** 摘一条绑定。目标不存在时**原样返回**(不涨 revision):否则每次「清理一个本来就没有的会话」都会让全网客户端白刷一次。 */
16
+ export declare function dropBinding(table: BindingsFile, sessionId: string): BindingsFile;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * binding 域的存储形状:`bindings.json` 的版本与记录。
3
+ *
4
+ * 它是域内契约而不是双端契约——宿主与客户端都不直接读这个文件,客户端只经 api 域的
5
+ * 路由读 `revision` 与 `worktreePath`(见 `src/shared/contract.ts` 的 `BindingResponse`)。
6
+ * 物理定义放在这里,域外经 `binding/interface.ts` 取用。
7
+ */
8
+ /**
9
+ * bindings.json 的形状版本。读到其它版本按损坏处理而不是猜着往下读:
10
+ * 别的版本的字段含义未定,猜错会静默把别人的 worktree 挂到本会话上。
11
+ *
12
+ * v2 加了 `sessionCreatedAt`。**没有迁移路径**:本包尚未发布,磁盘上不存在合法的 v1 文件,
13
+ * 而按 v1 读意味着把「会话 id 可能会被复用」这个洞原样留着。
14
+ */
15
+ export declare const BINDINGS_VERSION = 2;
16
+ /** 一次「把某个 git worktree 登记给某个会话」。 */
17
+ export interface BindingRecord {
18
+ /** worktree 所属主仓库根(用于校验 worktree 归属)。 */
19
+ readonly repoRoot: string;
20
+ /** 该会话视图根要指向的 worktree 绝对路径。 */
21
+ readonly worktreeRoot: string;
22
+ /** 登记时的分支名,仅用于展示与排查。 */
23
+ readonly branch: string;
24
+ /** 登记时刻的 ISO 时间戳(人读用)。 */
25
+ readonly createdAt: string;
26
+ /**
27
+ * 登记时会话 header 的 `createdAt`(epoch 毫秒)。会话 id 是**进程内计数器**,
28
+ * 重启后新会话会重新拿到 `session-1`;这一项是「这条登记属于哪个会话」的唯一凭据。
29
+ */
30
+ readonly sessionCreatedAt: number;
31
+ }
32
+ /** bindings.json 的完整形状。 */
33
+ export interface BindingsFile {
34
+ readonly version: number;
35
+ readonly revision: number;
36
+ readonly bindings: Readonly<Record<string, BindingRecord>>;
37
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * binding 域装配:绑定表的**唯一事实源**。
3
+ *
4
+ * 内存快照是刻意的:scope 域的解析器与 api 域的路由读同一个对象,两端 revision 因此不可能
5
+ * 各说各话(降级预案 G7)。落盘只是它的持久化副本——写盘失败时内存不前移,
6
+ * 因为「内存说已登记、磁盘说没有」在重启后会变成一次静默的换根失败。
7
+ *
8
+ * 状态(表快照与写盘串行链)住在实例里;域是**进程内单例**,第二次 `install` 由 `installed`
9
+ * 守卫**显式抛错**(响亮失败优于静默共享/丢数据)。
10
+ */
11
+ import type { BindingRecord } from "../model/type.js";
12
+ import type { FileWrite } from "../../../shared/interface.js";
13
+ import type { BindingDeps } from "../../deps.js";
14
+ /** 绑定域的服务面。 */
15
+ export interface BindingApi {
16
+ /** 当前表版本。与 api 域路由读的是同一快照,故它也是客户端能观察到的版本。 */
17
+ revision(): number;
18
+ /** 按会话取绑定。 */
19
+ get(sessionId: string): BindingRecord | undefined;
20
+ /** 落一条绑定并持久化。 */
21
+ put(sessionId: string, record: BindingRecord): Promise<FileWrite>;
22
+ /** 摘一条绑定并持久化(幂等:本来就没有不算失败)。 */
23
+ drop(sessionId: string): Promise<FileWrite>;
24
+ }
25
+ /** 绑定表:唯一实例持有内存快照与写盘串行链。 */
26
+ declare class BindingService implements BindingApi {
27
+ /** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
28
+ private installed;
29
+ private deps;
30
+ private table;
31
+ /**
32
+ * 写盘串行链:两次并发修改都基于同一份旧表时,后写的那次会静默吞掉前一次的绑定。
33
+ * 链本身不承载结果,失败也不该断链,故 catch 掉、语义归返回值。
34
+ */
35
+ private writes;
36
+ /**
37
+ * 装配代数。`release` 的尾部清理要 `await` 在飞的写盘,那一小段时间里**新的一个
38
+ * `install` 可能已经装上并从磁盘读回了新表**;无脑清表会抹掉它,紧接着一次 `put`
39
+ * 就把空表写回磁盘、丢掉所有会话的登记。
40
+ */
41
+ private generation;
42
+ /** 装配绑定域:把 bindings.json 读进内存(损坏回落空表,见 model 块)。重复装配是编程错误。 */
43
+ install(deps: BindingDeps): void;
44
+ /**
45
+ * 卸载:**先等在飞的写盘链落定**,再放开入参、丢掉内存快照、复位装配标记。
46
+ * 不等的话,同进程的下一次装配会读到比已提交内容更旧的磁盘状态——表现为「刚写的绑定在重装后消失」。
47
+ * 重复调用无害。
48
+ */
49
+ release(): Promise<void>;
50
+ revision(): number;
51
+ get(sessionId: string): BindingRecord | undefined;
52
+ put(sessionId: string, record: BindingRecord): Promise<FileWrite>;
53
+ drop(sessionId: string): Promise<FileWrite>;
54
+ /**
55
+ * 应用一次表变换并持久化。写盘成功才算成功——半成功的内存状态是最坏结果:
56
+ * 内存说有、磁盘说没有,重启后就是一次静默的换根失败。
57
+ *
58
+ * 入参在**调用当刻**取出:在飞的写盘链要活过 `release`(它等的就是这一步落定),
59
+ * 而那一刻 `this.deps` 已经被放开了。
60
+ */
61
+ private commit;
62
+ /** 未装配时能力面当场失败,不拿旧 deps 出结果。 */
63
+ private requireInstalled;
64
+ }
65
+ /** 本域唯一实例:类不外放,外面 `new` 不出第二份绑定表。 */
66
+ export declare const bindingService: BindingService;
67
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { FileWrite } from "../../../shared/interface.js";
2
+ import type { BindingsFile } from "../model/type.js";
3
+ /** 读绑定表。文件缺失、不可读、内容损坏一律回落空表(parseTable 自己兜底)。 */
4
+ export declare function loadTable(file: string): BindingsFile;
5
+ /** 原子写绑定表。返回值而不是抛异常——调用方对失败的处置是统一的「内存不前移 + 出声」。 */
6
+ export declare function saveTable(file: string, table: BindingsFile): Promise<FileWrite>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * binding 域对外契约:绑定表的**唯一事实源**。
3
+ *
4
+ * 本文件只做收口——`BindingApi` 与单例的物理定义在 `impl/service`,表的读写形状在 `impl/model`,
5
+ * 落盘在 `impl/store`。单例本身不出这道门:它一旦被转出就成了本域的第二张公开契约,
6
+ * 调用方还能持有它、绕过释放。
7
+ */
8
+ import type { BindingRecord } from "./impl/model/type.js";
9
+ import type { FileWrite } from "../shared/interface.js";
10
+ import type { BindingDeps } from "./deps.js";
11
+ export { BINDINGS_VERSION } from "./impl/model/type.js";
12
+ export type { BindingRecord } from "./impl/model/type.js";
13
+ /** 装配 binding 域(组合根在 `apply` 期调用一次)。重复装配是编程错误,当场抛错。 */
14
+ export declare function installBinding(deps: BindingDeps): void;
15
+ /** 卸载 binding 域,与 `installBinding` 配对:等在飞的写盘落定后丢掉内存快照。此后能力面当场失败。 */
16
+ export declare function releaseBinding(): Promise<void>;
17
+ /** 当前表版本。与 api 域路由读的是同一快照,故它也是客户端能观察到的版本。 */
18
+ export declare function revision(): number;
19
+ /** 按会话取绑定。 */
20
+ export declare function get(sessionId: string): BindingRecord | undefined;
21
+ /** 落一条绑定并持久化。 */
22
+ export declare function put(sessionId: string, record: BindingRecord): Promise<FileWrite>;
23
+ /** 摘一条绑定并持久化(幂等:本来就没有不算失败)。 */
24
+ export declare function drop(sessionId: string): Promise<FileWrite>;
@@ -0,0 +1,59 @@
1
+ /** git 域依赖声明:exec 面由组合根注入——域内不起子进程,测试才能断言 argv 而不真跑 git。 */
2
+ /**
3
+ * 一次 git 调用的结果。非零退出不是异常:`check-ref-format` 就是靠退出码回答问题的。
4
+ *
5
+ * `code` 把「git 回答了 no」与「git 没答上来」分开:进程根本没起来(spawn ENOENT)或被超时杀掉时
6
+ * 它是 null。这两件事在调用方的处置**相反**——前者是答案,后者必须保住上一次的结论。
7
+ */
8
+ export interface GitRunResult {
9
+ readonly ok: boolean;
10
+ readonly stdout: string;
11
+ readonly stderr: string;
12
+ /** 退出码;没有退出码(spawn 失败 / 超时被杀)时是 null。 */
13
+ readonly code: number | null;
14
+ }
15
+ /** git 执行面。argv 是数组而不是命令行字符串:路径里的空格与 `-` 开头不会被 shell 重新解释。 */
16
+ export interface GitExecPort {
17
+ run(args: readonly string[]): Promise<GitRunResult>;
18
+ }
19
+ /** 装配入参。 */
20
+ export interface GitDeps {
21
+ readonly exec: GitExecPort;
22
+ }
23
+ /**
24
+ * 一次「这个目录属于哪个仓库」的读数。
25
+ *
26
+ * `not-repo` 与 `failed` 都是「没拿到公共 git 目录」,但对**写绑定**的调用方意义不同:
27
+ * 前者是 git 的回答(这个目录不是工作树),后者是没问出结果(spawn 失败 / 权限 / 超时)。
28
+ */
29
+ export type CommonDirReading = {
30
+ readonly kind: "repo";
31
+ readonly dir: string;
32
+ } | {
33
+ readonly kind: "not-repo";
34
+ } | {
35
+ readonly kind: "failed";
36
+ readonly reason: string;
37
+ };
38
+ /**
39
+ * 一次归属判定的读数。**三态而不是布尔**:
40
+ *
41
+ * `same` 与 `different` 都要求两侧**确实**各自返回了一个公共 git 目录——只有那时才敢摘掉用户的登记。
42
+ * 只要有一侧读不出来就归 `unknown`,调用方对它的处置是「保住已有登记 + 出声」:
43
+ * 把权限或 IO 抖动当成「换了仓库」会**永久摘掉**用户的绑定,而同一条纪律在目录存在性判定上早已写明
44
+ * (`scope/impl/own` 的 `directoryExists`)。
45
+ */
46
+ export type BelongsToReading = {
47
+ readonly kind: "same";
48
+ } | {
49
+ readonly kind: "different";
50
+ } | {
51
+ readonly kind: "unknown";
52
+ readonly reason: string;
53
+ /**
54
+ * 至少有一侧**明确**回了「不是 git 工作树」(git 给了否定答案)。
55
+ * 它不是「不同」,所以摘除判定不看它;写路径用它把失败文案说准(「不是工作树」比「验证不了」有用),
56
+ * 而 `chmod 000` 这类权限失败在读数上与它同形,因此写路径也仍然是 fail-closed。
57
+ */
58
+ readonly notRepo: boolean;
59
+ };
@@ -0,0 +1,3 @@
1
+ import type { GitExecPort } from "../../deps.js";
2
+ /** 真实的 git 执行面。它没有任何状态,故是常量而不是工厂——组合根没得选,测试换的是装配入参。 */
3
+ export declare const gitExec: GitExecPort;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * git 查询的 argv 构造与输出解析:全是纯函数。
3
+ *
4
+ * 执行被隔离在装配层之后,所以「branch 名是否经过校验」「positional 前是否加了 `--`」
5
+ * 这类安全属性可以被单测逐字断言,而不需要真仓库。
6
+ *
7
+ * 一律用 `-C <dir>` 而不是依赖进程 cwd:cwd 是全局状态,并发调用会互相污染。
8
+ */
9
+ /** 一个 worktree 在 `git worktree list --porcelain` 里的样子。 */
10
+ export interface WorktreeEntry {
11
+ readonly path: string;
12
+ readonly branch: string | undefined;
13
+ readonly detached: boolean;
14
+ }
15
+ /** 问某目录所属仓库的公共 git 目录(同一仓库的所有 worktree 共享它,故它标识「属于哪个仓库」)。 */
16
+ export declare function commonDirArgs(dir: string): readonly string[];
17
+ /** 列出某目录所属仓库的全部 worktree。 */
18
+ export declare function worktreeListArgs(dir: string): readonly string[];
19
+ /** 读某 worktree 当前 HEAD 的分支名(`detached` 时 git 返回 `HEAD`)。 */
20
+ export declare function headBranchArgs(dir: string): readonly string[];
21
+ /** 让 git 自己校验分支名。`--branch` 还会拒绝 `-` 开头的名字,这是把参数注入挡在 argv 之外的第一道。 */
22
+ export declare function checkRefFormatArgs(branch: string): readonly string[];
23
+ /**
24
+ * 新建 worktree。`--` 结束选项解析,路径即使形如选项也不会被当成标志。
25
+ *
26
+ * 省略 branch 时必须显式 `--detach`:`git worktree add <path>` 的默认行为是**新建一个以目录
27
+ * basename 命名的分支**并 checkout(实测 `Preparing worktree (new branch 'wtA')`),而不是
28
+ * 「check out the repository HEAD」。那条默认路径还有两个后果——basename 含空格时 git 直接
29
+ * `fatal: 'wt spaceB' is not a valid branch name`(macOS 的家目录常见空格),basename 以 `-` 开头时
30
+ * `--` 只挡住了 worktree add 自己的选项解析、挡不住它把 basename 当分支名后的二次解析(`unknown switch`)。
31
+ * `--detach` 让「省略 branch」的语义与文档一致,也把这条二次解析链路整个绕开。
32
+ *
33
+ * **`<path>` 之后的位置参数仍会被重新解析成选项**:实测 git 2.34.1 下
34
+ * `worktree add -b br -- <path> --force` 与 `-f` 都 rc=0,但**起点被静默忽略、从 HEAD 建**;
35
+ * `-badref` 则被当成 `git branch` 的选项报 usage。`--` 与 `--end-of-options` 都挡不住这一处。
36
+ * 所以起点必须由调用方先做形态 guard(拒绝 `-` 开头)并归一化成 commit SHA 再递进来:
37
+ * SHA 不会以 `-` 开头,这条二次解析链路就没有入口。
38
+ */
39
+ export declare function addWorktreeArgs(repoRoot: string, path: string, branch: string | undefined, base: string | undefined): readonly string[];
40
+ /** 把起点(分支 / tag / SHA / 相对 rev)解析成 commit SHA。`^{commit}` 让 annotated tag 也落到提交上。 */
41
+ export declare function revParseCommitArgs(dir: string, rev: string): readonly string[];
42
+ /** 删除 worktree。`force` 只在调用方显式要求时出现——默认可丢未提交改动的删除不该是默认值。 */
43
+ export declare function removeWorktreeArgs(repoRoot: string, path: string, force: boolean): readonly string[];
44
+ /** 取 `rev-parse` 的单行输出。相对路径返回值(`--git-common-dir` 可能是相对路径)由调用方补全。 */
45
+ export declare function parseSingleLine(stdout: string): string | undefined;
46
+ /** 解析 `worktree list --porcelain`:空行分块,每块首行是路径,其余是属性。 */
47
+ export declare function parseWorktreeList(stdout: string): readonly WorktreeEntry[];
@@ -0,0 +1,70 @@
1
+ import type { BelongsToReading, GitDeps } from "../../deps.js";
2
+ import type { WorktreeEntry } from "../inspect/index.js";
3
+ /** git 域的服务面。 */
4
+ export interface GitApi {
5
+ /** 该目录是否在某个仓库里;是则给公共 git 目录的**绝对**路径(带 TTL 缓存)。 */
6
+ commonDir(dir: string): Promise<string | undefined>;
7
+ /** 同一主仓库下的全部 worktree。 */
8
+ listWorktrees(dir: string): Promise<readonly WorktreeEntry[]>;
9
+ /**
10
+ * `dir` 与 `repoRoot` 是否属于同一仓库(带 TTL 缓存)。
11
+ *
12
+ * 回三态而不是布尔:`unknown`(有一侧读不出公共 git 目录)与 `different`(两侧都读出来了、值不同)
13
+ * 对调用方的处置相反——前者要保住用户已有的登记,后者才是「确实换了仓库」。
14
+ */
15
+ belongsTo(dir: string, repoRoot: string): Promise<BelongsToReading>;
16
+ /** 某 worktree 的当前分支显示名。 */
17
+ headBranch(dir: string): Promise<string | undefined>;
18
+ /** 让 git 校验分支名。 */
19
+ checkRefFormat(branch: string): Promise<boolean>;
20
+ /**
21
+ * 新建 worktree。`base` 必须是**已经归一化的 commit SHA**——argv 里 `<path>` 之后的
22
+ * 参数会被 git 重新解析成选项,起点直接传用户输入就等于把注入面留在那里(见 `impl/inspect`)。
23
+ */
24
+ addWorktree(repoRoot: string, path: string, branch: string | undefined, base: string | undefined): Promise<GitMutation>;
25
+ /** 把起点解析成 commit SHA;解析不出来回 undefined(调用方据此给一句可操作的失败)。 */
26
+ resolveCommit(dir: string, rev: string): Promise<string | undefined>;
27
+ /** 删除 worktree。 */
28
+ removeWorktree(repoRoot: string, path: string, force: boolean): Promise<GitMutation>;
29
+ }
30
+ /** git 写操作的结果:`reason` 是 git 的原文,直接给模型看。 */
31
+ export type GitMutation = {
32
+ readonly ok: true;
33
+ } | {
34
+ readonly ok: false;
35
+ readonly reason: string;
36
+ };
37
+ /** git 域:唯一实例持有归属缓存与那一次 git 调用面。 */
38
+ declare class GitService implements GitApi {
39
+ /** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
40
+ private installed;
41
+ /** 装配入参。释放即放开,能力面随之当场失败。 */
42
+ private deps;
43
+ private readonly belongsCache;
44
+ /** 只放正结果,所以值不是 `string | undefined`——负结果没有条目。 */
45
+ private readonly commonDirCache;
46
+ /** 装配 git 域。重复装配是编程错误,当场暴露。 */
47
+ install(deps: GitDeps): void;
48
+ /** 卸载:丢掉缓存与装配入参,复位装配标记——同进程的下一次 `install` 不该撞上「只能装配一次」。 */
49
+ release(): void;
50
+ commonDir(dir: string): Promise<string | undefined>;
51
+ /**
52
+ * 带缓存的仓库判定读数。缓存只认正结果(见 `COMMON_DIR_TTL_MS`),负结果每次都真问一次 git。
53
+ */
54
+ private reading;
55
+ /** 真起一次 git 求公共目录;缓存命中不走这里。 */
56
+ private computeCommonDir;
57
+ listWorktrees(dir: string): Promise<readonly WorktreeEntry[]>;
58
+ belongsTo(dir: string, repoRoot: string): Promise<BelongsToReading>;
59
+ headBranch(dir: string): Promise<string | undefined>;
60
+ checkRefFormat(branch: string): Promise<boolean>;
61
+ addWorktree(repoRoot: string, path: string, branch: string | undefined, base: string | undefined): Promise<GitMutation>;
62
+ /** 解析起点。`--quiet` 让「不是有效 rev」走退出码而不是 stderr,失败在这里就收成 undefined。 */
63
+ resolveCommit(dir: string, rev: string): Promise<string | undefined>;
64
+ removeWorktree(repoRoot: string, path: string, force: boolean): Promise<GitMutation>;
65
+ /** 取执行面:未装配时当场失败,而不是拿一份空执行面跑出「git 说没有」这种假答案。 */
66
+ private exec;
67
+ }
68
+ /** 本域唯一实例:类不外放,外面 `new` 不出第二份缓存。 */
69
+ export declare const gitService: GitService;
70
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * git 域对外契约:worktree 归属查询与增删。
3
+ *
4
+ * 本文件只做收口——`GitApi` 与单例的物理定义在 `impl/service`(`GitApi` 形状不从门面转出),argv 构造在 `impl/inspect`。
5
+ * `gitExec` 也从这里出去:组合根只允许引 `interface.ts`(verify-dir-imports 规则 1/2),
6
+ * 把它藏进 impl 会让组合根要么直引实现、要么自己重写一份 exec。
7
+ * 单例本身不出这道门:它一旦被转出就成了本域的第二张公开契约,调用方还能持有它、绕过释放。
8
+ */
9
+ import type { BelongsToReading, GitDeps } from "./deps.js";
10
+ import type { WorktreeEntry } from "./impl/inspect/index.js";
11
+ import type { GitMutation } from "./impl/service/index.js";
12
+ /** 真实的 git 执行面:它没有状态,故按常量转出而不是工厂。 */
13
+ export { gitExec } from "./impl/exec/index.js";
14
+ /** 装配 git 域(组合根在 `apply` 期调用一次)。重复装配是编程错误,当场抛错。 */
15
+ export declare function installGit(deps: GitDeps): void;
16
+ /** 卸载 git 域,与 `installGit` 配对:丢掉归属缓存与装配入参。此后能力面当场失败。 */
17
+ export declare function releaseGit(): void;
18
+ /** 该目录是否在某个仓库里;是则给公共 git 目录的**绝对**路径。 */
19
+ export declare function commonDir(dir: string): Promise<string | undefined>;
20
+ /** 同一主仓库下的全部 worktree。 */
21
+ export declare function listWorktrees(dir: string): Promise<readonly WorktreeEntry[]>;
22
+ /** `dir` 与 `repoRoot` 是否属于同一仓库。三态:`unknown` 不是「不同」。 */
23
+ export declare function belongsTo(dir: string, repoRoot: string): Promise<BelongsToReading>;
24
+ /** 某 worktree 的当前分支显示名。 */
25
+ export declare function headBranch(dir: string): Promise<string | undefined>;
26
+ /** 让 git 校验分支名。 */
27
+ export declare function checkRefFormat(branch: string): Promise<boolean>;
28
+ /**
29
+ * 新建 worktree。`base` 必须已经被调用方归一化成 commit SHA(理由见 `resolveCommit`)。
30
+ */
31
+ export declare function addWorktree(repoRoot: string, path: string, branch: string | undefined, base: string | undefined): Promise<GitMutation>;
32
+ /**
33
+ * 把起点(分支 / tag / SHA / 相对 rev)解析成 commit SHA;解析不出来回 undefined。
34
+ *
35
+ * 这一层是**安全边界**而不是便利:`git worktree add` 在 `<path>` 之后会重新开始选项解析,
36
+ * 起点位置的 `-` 开头值会被当选项(实测 `-f` / `--force` 会让起点被静默忽略、从 HEAD 建)。
37
+ * 递进 argv 的必须是这里产出的 SHA。
38
+ */
39
+ export declare function resolveCommit(dir: string, rev: string): Promise<string | undefined>;
40
+ /** 删除 worktree。 */
41
+ export declare function removeWorktree(repoRoot: string, path: string, force: boolean): Promise<GitMutation>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 宿主 agent 适配层:把官方 `Agent` 面收窄成 tools 域认得的注册面。
3
+ *
4
+ * 「装给**所有** agent(含子 agent)」与「装进去的效应随 agent 一起释放」这两条判断住在这里。
5
+ * 它只认下面这个窄端口,因此不需要起 cordis 就能被白盒驱动。
6
+ */
7
+ import type { ToolDefinition } from "@deepseek-ai/dsh-tools";
8
+ import type { AgentPort } from "../tools/deps.js";
9
+ /**
10
+ * 适配器真正用到的宿主 agent 面。刻意与官方 `Agent` 解耦:公开的 `Agent` 面要求一个真的
11
+ * `Context`,而本适配器只用「身份 / 会话 cwd / 往这个作用域装工具 / 效应随作用域释放」四件事。
12
+ * 官方 `Agent` 结构上可赋值给它。
13
+ */
14
+ export interface HostAgentLike {
15
+ readonly id: string;
16
+ readonly session: {
17
+ readonly header: {
18
+ readonly cwd?: string;
19
+ };
20
+ };
21
+ readonly ctx: {
22
+ readonly tools: {
23
+ register(definition: ToolDefinition): () => void;
24
+ };
25
+ /** 返回该效应的释放器;官方实现是「可等待的」,域内只需能调用它。 */
26
+ effect(execute: () => () => void): () => unknown;
27
+ };
28
+ }
29
+ /** 宿主事件面与 agent 枚举面:只开本适配器要的两样。 */
30
+ export interface AgentHostPort {
31
+ /** 订阅 agent 发布。返回退订函数。 */
32
+ on(event: "agent/created", handler: (payload: {
33
+ agent: HostAgentLike;
34
+ }) => void): () => void;
35
+ /** 当前**所有存活** agent 快照(含子 agent)。 */
36
+ all(): readonly HostAgentLike[];
37
+ }
38
+ /** agent 注册面:工具域只看到「一个 agent 有 id、有 cwd、可以往里装工具」。 */
39
+ export declare function bindAgents(host: AgentHostPort): AgentPort;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 会话链适配层:只回答「这个会话的父会话是谁」。
3
+ *
4
+ * 官方把父链记在会话 header 上(`parentSession?: SessionId`,dsh-session/lib/types/types.d.ts:71)。它有两个来源,
5
+ * **两个都必要**:活会话走 `ctx.sessions.get`(同步),已结束会话只能走持久面 `sessionPersistence.stat`
6
+ * (单会话定位,不读事件日志)——官方 `dsh-session/lib/index.js:1550-1557` 的 `get` 契约明写是
7
+ * "Look up a live session",会话被释放/detach 之后 header 就取不到了。而 UI 里能点选的子会话恰恰是
8
+ * **已结束**的那些,少了持久面这一半,继承在用户可见的那个状态上就是空的。
9
+ *
10
+ * 持久面是可选服务(`dsh-base/cordis.patch.yml:110-111` 的 jsonl 后端提供):缺席时如实回 undefined,
11
+ * 继承退回 live-only,功能降级而不是把整条文件根解析拖垮。
12
+ */
13
+ import type { SessionId } from "@deepseek-ai/dsh-session";
14
+ import type { SessionChainPort } from "../scope/deps.js";
15
+ /**
16
+ * 官方会话 header 的窄面。`createdAt` 与 `parentSession` 同样重要:
17
+ * id 是进程内计数器、重启后会重排,绑定要靠 `createdAt` 才认得出「这是不是同一个会话」。
18
+ */
19
+ interface SessionHeaderFace {
20
+ readonly parentSession?: string;
21
+ readonly createdAt: number;
22
+ }
23
+ /** 官方 sessions 服务的窄面:只按 id 取一次**活**会话记录。 */
24
+ export interface SessionsFace {
25
+ get(id: SessionId): {
26
+ readonly header: SessionHeaderFace;
27
+ } | undefined;
28
+ }
29
+ /** 官方持久会话服务的窄面:只取一条已结束会话的 header。 */
30
+ export interface StoredSessionsFace {
31
+ stat(id: SessionId): Promise<{
32
+ readonly header: SessionHeaderFace;
33
+ } | undefined>;
34
+ }
35
+ export declare function bindSessions(sessions: SessionsFace,
36
+ /**
37
+ * 持久面按**调用时刻**取:`ctx.get` 的语义是「取当刻值,未提供回 undefined」
38
+ * (`cordis/lib/index.js:754-771`),在 apply 期取一次会让晚挂的后端永久退化成缺席。
39
+ */
40
+ storedSessions: () => StoredSessionsFace | undefined): SessionChainPort;
41
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { TypertLookupRegistry } from "@deepseek-ai/dsh-typert-protocol";
2
+ import type { TypertPort } from "../scope/deps.js";
3
+ /**
4
+ * 查找表窄面:本插件只读当前描述符、配置自己那一个键、并订阅变化,其余能力(register/definitions)不开。
5
+ *
6
+ * `subscribe` 是「等 provider」那条路径的唯一叫醒源:官方 `register`/`withdraw` 都会 emit
7
+ * `{kind:"lookup", key}`(dsh-typert-registry/lib/index.js:238-251 与 :211-224),
8
+ * 监听器不关心事件载荷,只把「再看一眼」这件事做一次。
9
+ */
10
+ export type TypertLookupsPort = Pick<TypertLookupRegistry, "get" | "configure" | "subscribe">;
11
+ export declare function bindTypert(lookups: TypertLookupsPort): TypertPort;