@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.
- package/LICENSE +21 -0
- package/README.en.md +186 -0
- package/README.md +190 -0
- package/cordis.patch.yml +9 -0
- package/lib/client/bindings.d.ts +16 -0
- package/lib/client/index.d.ts +23 -0
- package/lib/client/inject.d.ts +21 -0
- package/lib/client/shared/ports.d.ts +141 -0
- package/lib/client/source.d.ts +22 -0
- package/lib/client/takeover.d.ts +51 -0
- package/lib/client.js +394 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +1614 -0
- package/lib/server/api/deps.d.ts +35 -0
- package/lib/server/api/impl/handlers/index.d.ts +24 -0
- package/lib/server/api/impl/route/index.d.ts +25 -0
- package/lib/server/api/impl/service/index.d.ts +31 -0
- package/lib/server/api/interface.d.ts +12 -0
- package/lib/server/binding/deps.d.ts +9 -0
- package/lib/server/binding/impl/model/index.d.ts +16 -0
- package/lib/server/binding/impl/model/type.d.ts +37 -0
- package/lib/server/binding/impl/service/index.d.ts +67 -0
- package/lib/server/binding/impl/store/index.d.ts +6 -0
- package/lib/server/binding/interface.d.ts +24 -0
- package/lib/server/git/deps.d.ts +59 -0
- package/lib/server/git/impl/exec/index.d.ts +3 -0
- package/lib/server/git/impl/inspect/index.d.ts +47 -0
- package/lib/server/git/impl/service/index.d.ts +70 -0
- package/lib/server/git/interface.d.ts +41 -0
- package/lib/server/host/agents.d.ts +39 -0
- package/lib/server/host/sessions.d.ts +41 -0
- package/lib/server/host/typert.d.ts +11 -0
- package/lib/server/scope/deps.d.ts +110 -0
- package/lib/server/scope/impl/inherit/index.d.ts +25 -0
- package/lib/server/scope/impl/own/index.d.ts +21 -0
- package/lib/server/scope/impl/resolve/index.d.ts +40 -0
- package/lib/server/scope/impl/service/index.d.ts +115 -0
- package/lib/server/scope/interface.d.ts +40 -0
- package/lib/server/shared/file-io.d.ts +19 -0
- package/lib/server/shared/interface.d.ts +11 -0
- package/lib/server/shared/paths.d.ts +2 -0
- package/lib/server/shared/type.d.ts +6 -0
- package/lib/server/tools/deps.d.ts +52 -0
- package/lib/server/tools/impl/bind/index.d.ts +62 -0
- package/lib/server/tools/impl/create/index.d.ts +4 -0
- package/lib/server/tools/impl/protocol/index.d.ts +36 -0
- package/lib/server/tools/impl/register/index.d.ts +4 -0
- package/lib/server/tools/impl/remove/index.d.ts +12 -0
- package/lib/server/tools/impl/service/index.d.ts +28 -0
- package/lib/server/tools/impl/session/index.d.ts +18 -0
- package/lib/server/tools/interface.d.ts +14 -0
- package/lib/shared/contract.d.ts +26 -0
- package/lib/shared/interface.d.ts +8 -0
- package/package.json +103 -0
- package/shared/client/ensure-style.d.ts +21 -0
- package/shared/client/i18n.d.ts +15 -0
- package/shared/dsh-home.d.ts +15 -0
- package/shared/host-utils.d.ts +65 -0
- package/shared/loopback.d.ts +21 -0
- package/shared/settings-namespace.d.ts +46 -0
- package/shared/sse-hub.d.ts +76 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
import { createRequire as __worktreeSidebarRequire } from "node:module";
|
|
2
|
+
const require = __worktreeSidebarRequire(import.meta.url);
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name2 in all)
|
|
6
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// lib/shared/contract.js
|
|
10
|
+
var ROUTES = {
|
|
11
|
+
bindings: "/api/dsh-worktree-sidebar/bindings",
|
|
12
|
+
health: "/api/dsh-worktree-sidebar/health"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// lib/server/host/agents.js
|
|
16
|
+
function bindAgents(host) {
|
|
17
|
+
const faceOf = (agent) => ({
|
|
18
|
+
id: agent.id,
|
|
19
|
+
cwd: agent.session.header.cwd
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
subscribe: (handler) => host.on("agent/created", ({ agent }) => {
|
|
23
|
+
handler(faceOf(agent));
|
|
24
|
+
}),
|
|
25
|
+
list: () => host.all().map(faceOf),
|
|
26
|
+
publish: (face, definitions) => {
|
|
27
|
+
const agent = host.all().find((candidate) => candidate.id === face.id);
|
|
28
|
+
if (agent === void 0) {
|
|
29
|
+
throw new Error(`dsh-worktree-sidebar: agent ${face.id} 未注册,拒绝把工具装进未知作用域`);
|
|
30
|
+
}
|
|
31
|
+
const disposers = definitions.map((definition) => agent.ctx.tools.register(definition));
|
|
32
|
+
const disposeTools = () => {
|
|
33
|
+
for (const dispose2 of disposers) {
|
|
34
|
+
try {
|
|
35
|
+
dispose2();
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const dispose = agent.ctx.effect(() => disposeTools);
|
|
41
|
+
return () => {
|
|
42
|
+
void Promise.resolve(dispose()).catch(() => void 0);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// lib/server/host/sessions.js
|
|
49
|
+
function identityFrom(createdAt) {
|
|
50
|
+
return Number.isFinite(createdAt) ? { createdAt } : void 0;
|
|
51
|
+
}
|
|
52
|
+
function bindSessions(sessions, storedSessions) {
|
|
53
|
+
return {
|
|
54
|
+
liveParentOf: (sessionId) => {
|
|
55
|
+
const session = sessions.get(sessionId);
|
|
56
|
+
if (session === void 0)
|
|
57
|
+
return { kind: "not-live" };
|
|
58
|
+
const parent = session.header.parentSession;
|
|
59
|
+
return parent === void 0 ? { kind: "root" } : { kind: "parent", id: parent };
|
|
60
|
+
},
|
|
61
|
+
storedParentOf: async (sessionId) => {
|
|
62
|
+
const stored = storedSessions();
|
|
63
|
+
if (stored === void 0)
|
|
64
|
+
return void 0;
|
|
65
|
+
const snapshot = await stored.stat(sessionId);
|
|
66
|
+
return snapshot?.header.parentSession;
|
|
67
|
+
},
|
|
68
|
+
liveIdentityOf: (sessionId) => {
|
|
69
|
+
const live = sessions.get(sessionId);
|
|
70
|
+
return live === void 0 ? void 0 : identityFrom(live.header.createdAt);
|
|
71
|
+
},
|
|
72
|
+
storedIdentityOf: async (sessionId) => {
|
|
73
|
+
const stored = storedSessions();
|
|
74
|
+
if (stored === void 0)
|
|
75
|
+
return void 0;
|
|
76
|
+
const snapshot = await stored.stat(sessionId);
|
|
77
|
+
return snapshot === void 0 ? void 0 : identityFrom(snapshot.header.createdAt);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// lib/server/host/typert.js
|
|
83
|
+
function bindTypert(lookups) {
|
|
84
|
+
return {
|
|
85
|
+
current: () => {
|
|
86
|
+
const descriptor = lookups.get("workspaceFileScope");
|
|
87
|
+
if (descriptor === void 0)
|
|
88
|
+
return void 0;
|
|
89
|
+
const resolve3 = descriptor.resolve.bind(descriptor);
|
|
90
|
+
return {
|
|
91
|
+
resolve: async (sessionId) => {
|
|
92
|
+
const scope = await resolve3(sessionId);
|
|
93
|
+
if (scope === void 0)
|
|
94
|
+
return void 0;
|
|
95
|
+
return { sessionId: scope.sessionId, workspaceRoot: scope.workspaceRoot };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
subscribe: (listener) => lookups.subscribe(() => listener()),
|
|
100
|
+
configure: (resolver) => lookups.configure("workspaceFileScope", async (sessionId) => {
|
|
101
|
+
const scope = await resolver(sessionId);
|
|
102
|
+
if (scope === void 0)
|
|
103
|
+
return void 0;
|
|
104
|
+
return { sessionId, workspaceRoot: scope.workspaceRoot };
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ../../shared/loopback.js
|
|
110
|
+
function isLoopbackRequest(request, options = {}) {
|
|
111
|
+
const address = request.socket?.remoteAddress;
|
|
112
|
+
if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
|
|
113
|
+
const host = request.headers.host;
|
|
114
|
+
if (typeof host !== "string") return false;
|
|
115
|
+
let hostUrl;
|
|
116
|
+
try {
|
|
117
|
+
hostUrl = new URL(`http://${host}`);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (hostUrl.hostname !== "127.0.0.1" && hostUrl.hostname !== "localhost" && hostUrl.hostname !== "[::1]")
|
|
122
|
+
return false;
|
|
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
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const origin = request.headers.origin;
|
|
129
|
+
if (origin === void 0) return true;
|
|
130
|
+
try {
|
|
131
|
+
return new URL(origin).host === hostUrl.host;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ../../shared/host-utils.js
|
|
138
|
+
function guardLoopbackMethod(req, res, methods, loopbackOptions) {
|
|
139
|
+
if (!isLoopbackRequest(req, loopbackOptions)) {
|
|
140
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (!methods.includes(req.method)) {
|
|
144
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
function writeJson(res, status, payload) {
|
|
150
|
+
res.writeHead(status, {
|
|
151
|
+
"content-type": "application/json; charset=utf-8",
|
|
152
|
+
"referrer-policy": "no-referrer"
|
|
153
|
+
});
|
|
154
|
+
res.end(JSON.stringify(payload));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// lib/server/api/impl/handlers/index.js
|
|
158
|
+
function queryParam(url, name2) {
|
|
159
|
+
if (url === void 0)
|
|
160
|
+
return void 0;
|
|
161
|
+
const value = new URL(url, "http://placeholder.invalid").searchParams.get(name2);
|
|
162
|
+
return value === null || value.length === 0 ? void 0 : value;
|
|
163
|
+
}
|
|
164
|
+
function bindingsEndpoint(binding, scope) {
|
|
165
|
+
return {
|
|
166
|
+
path: ROUTES.bindings,
|
|
167
|
+
methods: {
|
|
168
|
+
GET: async (req, res) => {
|
|
169
|
+
const session = queryParam(req.url, "session");
|
|
170
|
+
if (session === void 0) {
|
|
171
|
+
writeJson(res, 400, { ok: false, error: { message: "缺少 session 参数" } });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const worktreePath = await scope.effectiveWorktree(session);
|
|
175
|
+
const body = { revision: binding.revision(), worktreePath };
|
|
176
|
+
writeJson(res, 200, body);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function healthEndpoint(binding, scope) {
|
|
182
|
+
return {
|
|
183
|
+
path: ROUTES.health,
|
|
184
|
+
methods: {
|
|
185
|
+
GET: (_req, res) => {
|
|
186
|
+
writeJson(res, 200, {
|
|
187
|
+
ok: true,
|
|
188
|
+
revision: binding.revision(),
|
|
189
|
+
scopeTakeover: scope.takeoverState(),
|
|
190
|
+
scopeChain: scope.chainDiagnostics()
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// lib/server/api/impl/route/index.js
|
|
198
|
+
function registerEndpoints(register, endpoints, logger) {
|
|
199
|
+
const disposers = [];
|
|
200
|
+
for (const endpoint of endpoints) {
|
|
201
|
+
try {
|
|
202
|
+
disposers.push(register({
|
|
203
|
+
kind: "exact",
|
|
204
|
+
path: endpoint.path,
|
|
205
|
+
handler: (req, res) => {
|
|
206
|
+
if (!guardLoopbackMethod(req, res, Object.keys(endpoint.methods)))
|
|
207
|
+
return;
|
|
208
|
+
const handle = endpoint.methods[req.method ?? ""];
|
|
209
|
+
try {
|
|
210
|
+
const done = handle(req, res);
|
|
211
|
+
if (done instanceof Promise) {
|
|
212
|
+
done.catch((cause) => reportFailure(res, logger, cause));
|
|
213
|
+
}
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
reportFailure(res, logger, cause);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
219
|
+
} catch (cause) {
|
|
220
|
+
for (const dispose of [...disposers].reverse()) {
|
|
221
|
+
try {
|
|
222
|
+
dispose();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw cause;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return disposers;
|
|
230
|
+
}
|
|
231
|
+
function reportFailure(res, logger, cause) {
|
|
232
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
233
|
+
logger.warn("dsh-worktree-sidebar: 浏览器端点处理失败 — " + reason);
|
|
234
|
+
if (!res.headersSent)
|
|
235
|
+
writeJson(res, 500, { ok: false, error: { message: reason } });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// lib/server/api/impl/service/index.js
|
|
239
|
+
var ApiService = class {
|
|
240
|
+
/** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
|
|
241
|
+
installed = false;
|
|
242
|
+
/** 已挂路由的摘除器。释放即逐个调用并清空,所以不会叠成两份。 */
|
|
243
|
+
disposers = [];
|
|
244
|
+
/** 路由当前是否在册。释放幂等靠它,也靠它区分「没装过」与「已摘完」。 */
|
|
245
|
+
live = false;
|
|
246
|
+
/** 装配浏览器出口(组合根在 apply 期调用一次)。重复装配是编程错误,当场暴露。 */
|
|
247
|
+
install(deps) {
|
|
248
|
+
if (this.installed)
|
|
249
|
+
throw new Error("dsh-worktree-sidebar: api 域只能装配一次");
|
|
250
|
+
const disposers = registerEndpoints(deps.register, [bindingsEndpoint(deps.binding, deps.scope), healthEndpoint(deps.binding, deps.scope)], deps.logger);
|
|
251
|
+
this.installed = true;
|
|
252
|
+
this.disposers = disposers;
|
|
253
|
+
this.live = true;
|
|
254
|
+
}
|
|
255
|
+
/** 摘掉全部路由并复位装配标记——同进程的下一次 `install` 会重新注册,且只注册一份。 */
|
|
256
|
+
release() {
|
|
257
|
+
this.installed = false;
|
|
258
|
+
if (!this.live)
|
|
259
|
+
return;
|
|
260
|
+
this.live = false;
|
|
261
|
+
for (const dispose of this.disposers) {
|
|
262
|
+
try {
|
|
263
|
+
dispose();
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.disposers = [];
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var apiService = new ApiService();
|
|
271
|
+
|
|
272
|
+
// lib/server/api/interface.js
|
|
273
|
+
function installApi(deps) {
|
|
274
|
+
apiService.install(deps);
|
|
275
|
+
}
|
|
276
|
+
function releaseApi() {
|
|
277
|
+
apiService.release();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// lib/server/binding/interface.js
|
|
281
|
+
var interface_exports = {};
|
|
282
|
+
__export(interface_exports, {
|
|
283
|
+
BINDINGS_VERSION: () => BINDINGS_VERSION,
|
|
284
|
+
drop: () => drop,
|
|
285
|
+
get: () => get,
|
|
286
|
+
installBinding: () => installBinding,
|
|
287
|
+
put: () => put,
|
|
288
|
+
releaseBinding: () => releaseBinding,
|
|
289
|
+
revision: () => revision
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// lib/server/binding/impl/model/type.js
|
|
293
|
+
var BINDINGS_VERSION = 2;
|
|
294
|
+
|
|
295
|
+
// lib/server/binding/impl/model/index.js
|
|
296
|
+
function emptyTable() {
|
|
297
|
+
return { version: BINDINGS_VERSION, revision: 0, bindings: {} };
|
|
298
|
+
}
|
|
299
|
+
function validateRecord(value) {
|
|
300
|
+
if (typeof value !== "object" || value === null)
|
|
301
|
+
return void 0;
|
|
302
|
+
const raw = value;
|
|
303
|
+
const repoRoot = raw["repoRoot"];
|
|
304
|
+
const worktreeRoot = raw["worktreeRoot"];
|
|
305
|
+
const branch = raw["branch"];
|
|
306
|
+
const createdAt = raw["createdAt"];
|
|
307
|
+
const sessionCreatedAt = raw["sessionCreatedAt"];
|
|
308
|
+
if (typeof repoRoot !== "string" || repoRoot.length === 0)
|
|
309
|
+
return void 0;
|
|
310
|
+
if (typeof worktreeRoot !== "string" || worktreeRoot.length === 0)
|
|
311
|
+
return void 0;
|
|
312
|
+
if (typeof branch !== "string")
|
|
313
|
+
return void 0;
|
|
314
|
+
if (typeof createdAt !== "string")
|
|
315
|
+
return void 0;
|
|
316
|
+
if (typeof sessionCreatedAt !== "number" || !Number.isFinite(sessionCreatedAt))
|
|
317
|
+
return void 0;
|
|
318
|
+
return { repoRoot, worktreeRoot, branch, createdAt, sessionCreatedAt };
|
|
319
|
+
}
|
|
320
|
+
function parseTable(text) {
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(text);
|
|
324
|
+
} catch {
|
|
325
|
+
return emptyTable();
|
|
326
|
+
}
|
|
327
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
328
|
+
return emptyTable();
|
|
329
|
+
const raw = parsed;
|
|
330
|
+
if (raw["version"] !== BINDINGS_VERSION)
|
|
331
|
+
return emptyTable();
|
|
332
|
+
const revision2 = raw["revision"];
|
|
333
|
+
if (typeof revision2 !== "number" || !Number.isFinite(revision2) || revision2 < 0)
|
|
334
|
+
return emptyTable();
|
|
335
|
+
const entries = raw["bindings"];
|
|
336
|
+
if (typeof entries !== "object" || entries === null)
|
|
337
|
+
return emptyTable();
|
|
338
|
+
const bindings = {};
|
|
339
|
+
for (const [sessionId, value] of Object.entries(entries)) {
|
|
340
|
+
if (sessionId.length === 0)
|
|
341
|
+
continue;
|
|
342
|
+
const record = validateRecord(value);
|
|
343
|
+
if (record !== void 0)
|
|
344
|
+
bindings[sessionId] = record;
|
|
345
|
+
}
|
|
346
|
+
return { version: BINDINGS_VERSION, revision: revision2, bindings };
|
|
347
|
+
}
|
|
348
|
+
function serializeTable(table) {
|
|
349
|
+
return JSON.stringify(table, null, 2) + "\n";
|
|
350
|
+
}
|
|
351
|
+
function putBinding(table, sessionId, record) {
|
|
352
|
+
return {
|
|
353
|
+
version: table.version,
|
|
354
|
+
revision: table.revision + 1,
|
|
355
|
+
bindings: { ...table.bindings, [sessionId]: record }
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function dropBinding(table, sessionId) {
|
|
359
|
+
if (!(sessionId in table.bindings))
|
|
360
|
+
return table;
|
|
361
|
+
const bindings = { ...table.bindings };
|
|
362
|
+
delete bindings[sessionId];
|
|
363
|
+
return { version: table.version, revision: table.revision + 1, bindings };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// lib/server/shared/file-io.js
|
|
367
|
+
import { readFileSync } from "node:fs";
|
|
368
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
369
|
+
import { dirname } from "node:path";
|
|
370
|
+
function readTextFileSync(file) {
|
|
371
|
+
try {
|
|
372
|
+
return { ok: true, text: readFileSync(file, "utf8") };
|
|
373
|
+
} catch {
|
|
374
|
+
return { ok: false };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function writeTextAtomic(file, text) {
|
|
378
|
+
const temporary = `${file}.tmp-${process.pid}`;
|
|
379
|
+
try {
|
|
380
|
+
await mkdir(dirname(file), { recursive: true });
|
|
381
|
+
await writeFile(temporary, text, "utf8");
|
|
382
|
+
await rename(temporary, file);
|
|
383
|
+
return { ok: true };
|
|
384
|
+
} catch (cause) {
|
|
385
|
+
return { ok: false, reason: cause instanceof Error ? cause.message : String(cause) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// lib/server/shared/paths.js
|
|
390
|
+
import { join as join2 } from "node:path";
|
|
391
|
+
|
|
392
|
+
// ../../shared/dsh-home.js
|
|
393
|
+
import { homedir } from "node:os";
|
|
394
|
+
import { join } from "node:path";
|
|
395
|
+
function userHome() {
|
|
396
|
+
const env = process.platform === "win32" ? process.env.USERPROFILE ?? process.env.HOME : process.env.HOME;
|
|
397
|
+
return env !== void 0 && env.trim().length > 0 ? env : homedir();
|
|
398
|
+
}
|
|
399
|
+
function dshHome() {
|
|
400
|
+
const env = process.env.DSH_HOME;
|
|
401
|
+
return env !== void 0 && env.trim().length > 0 ? env : join(userHome(), ".dsh");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// lib/server/shared/paths.js
|
|
405
|
+
var PACKAGE_DIR = "@wingsky-1/dsh-worktree-sidebar";
|
|
406
|
+
var BINDINGS_FILE_NAME = "bindings.json";
|
|
407
|
+
function bindingsFile() {
|
|
408
|
+
return join2(dshHome(), PACKAGE_DIR, BINDINGS_FILE_NAME);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// lib/server/binding/impl/store/index.js
|
|
412
|
+
function loadTable(file) {
|
|
413
|
+
const read = readTextFileSync(file);
|
|
414
|
+
if (!read.ok)
|
|
415
|
+
return parseTable("");
|
|
416
|
+
return parseTable(read.text);
|
|
417
|
+
}
|
|
418
|
+
function saveTable(file, table) {
|
|
419
|
+
return writeTextAtomic(file, serializeTable(table));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// lib/server/binding/impl/service/index.js
|
|
423
|
+
var NOT_INSTALLED = "dsh-worktree-sidebar: binding 域尚未装配";
|
|
424
|
+
var BindingService = class {
|
|
425
|
+
/** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
|
|
426
|
+
installed = false;
|
|
427
|
+
deps;
|
|
428
|
+
table = emptyTable();
|
|
429
|
+
/**
|
|
430
|
+
* 写盘串行链:两次并发修改都基于同一份旧表时,后写的那次会静默吞掉前一次的绑定。
|
|
431
|
+
* 链本身不承载结果,失败也不该断链,故 catch 掉、语义归返回值。
|
|
432
|
+
*/
|
|
433
|
+
writes = Promise.resolve();
|
|
434
|
+
/**
|
|
435
|
+
* 装配代数。`release` 的尾部清理要 `await` 在飞的写盘,那一小段时间里**新的一个
|
|
436
|
+
* `install` 可能已经装上并从磁盘读回了新表**;无脑清表会抹掉它,紧接着一次 `put`
|
|
437
|
+
* 就把空表写回磁盘、丢掉所有会话的登记。
|
|
438
|
+
*/
|
|
439
|
+
generation = 0;
|
|
440
|
+
/** 装配绑定域:把 bindings.json 读进内存(损坏回落空表,见 model 块)。重复装配是编程错误。 */
|
|
441
|
+
install(deps) {
|
|
442
|
+
if (this.installed)
|
|
443
|
+
throw new Error("dsh-worktree-sidebar: binding 域只能装配一次");
|
|
444
|
+
this.installed = true;
|
|
445
|
+
this.generation += 1;
|
|
446
|
+
this.deps = deps;
|
|
447
|
+
this.table = loadTable(deps.file);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 卸载:**先等在飞的写盘链落定**,再放开入参、丢掉内存快照、复位装配标记。
|
|
451
|
+
* 不等的话,同进程的下一次装配会读到比已提交内容更旧的磁盘状态——表现为「刚写的绑定在重装后消失」。
|
|
452
|
+
* 重复调用无害。
|
|
453
|
+
*/
|
|
454
|
+
async release() {
|
|
455
|
+
const generation = ++this.generation;
|
|
456
|
+
const writes = this.writes;
|
|
457
|
+
this.installed = false;
|
|
458
|
+
this.deps = void 0;
|
|
459
|
+
await writes;
|
|
460
|
+
if (this.generation !== generation)
|
|
461
|
+
return;
|
|
462
|
+
this.writes = Promise.resolve();
|
|
463
|
+
this.table = emptyTable();
|
|
464
|
+
}
|
|
465
|
+
revision() {
|
|
466
|
+
this.requireInstalled();
|
|
467
|
+
return this.table.revision;
|
|
468
|
+
}
|
|
469
|
+
get(sessionId) {
|
|
470
|
+
this.requireInstalled();
|
|
471
|
+
return this.table.bindings[sessionId];
|
|
472
|
+
}
|
|
473
|
+
put(sessionId, record) {
|
|
474
|
+
this.requireInstalled();
|
|
475
|
+
if (sessionId.length === 0)
|
|
476
|
+
return Promise.resolve({ ok: false, reason: "空 sessionId" });
|
|
477
|
+
return this.commit((current) => putBinding(current, sessionId, record));
|
|
478
|
+
}
|
|
479
|
+
drop(sessionId) {
|
|
480
|
+
this.requireInstalled();
|
|
481
|
+
return this.commit((current) => dropBinding(current, sessionId));
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* 应用一次表变换并持久化。写盘成功才算成功——半成功的内存状态是最坏结果:
|
|
485
|
+
* 内存说有、磁盘说没有,重启后就是一次静默的换根失败。
|
|
486
|
+
*
|
|
487
|
+
* 入参在**调用当刻**取出:在飞的写盘链要活过 `release`(它等的就是这一步落定),
|
|
488
|
+
* 而那一刻 `this.deps` 已经被放开了。
|
|
489
|
+
*/
|
|
490
|
+
commit(reduce) {
|
|
491
|
+
const deps = this.deps;
|
|
492
|
+
if (deps === void 0)
|
|
493
|
+
throw new Error(NOT_INSTALLED);
|
|
494
|
+
const run = async () => {
|
|
495
|
+
const next = reduce(this.table);
|
|
496
|
+
if (next === this.table)
|
|
497
|
+
return { ok: true };
|
|
498
|
+
const written = await saveTable(deps.file, next);
|
|
499
|
+
if (written.ok)
|
|
500
|
+
this.table = next;
|
|
501
|
+
else
|
|
502
|
+
deps.logger.warn("dsh-worktree-sidebar: 绑定表写盘失败 — " + written.reason);
|
|
503
|
+
return written;
|
|
504
|
+
};
|
|
505
|
+
const queued = this.writes.then(run, run);
|
|
506
|
+
this.writes = queued.catch(() => void 0);
|
|
507
|
+
return queued;
|
|
508
|
+
}
|
|
509
|
+
/** 未装配时能力面当场失败,不拿旧 deps 出结果。 */
|
|
510
|
+
requireInstalled() {
|
|
511
|
+
if (this.deps === void 0)
|
|
512
|
+
throw new Error(NOT_INSTALLED);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
var bindingService = new BindingService();
|
|
516
|
+
|
|
517
|
+
// lib/server/binding/interface.js
|
|
518
|
+
function installBinding(deps) {
|
|
519
|
+
bindingService.install(deps);
|
|
520
|
+
}
|
|
521
|
+
function releaseBinding() {
|
|
522
|
+
return bindingService.release();
|
|
523
|
+
}
|
|
524
|
+
function revision() {
|
|
525
|
+
return bindingService.revision();
|
|
526
|
+
}
|
|
527
|
+
function get(sessionId) {
|
|
528
|
+
return bindingService.get(sessionId);
|
|
529
|
+
}
|
|
530
|
+
function put(sessionId, record) {
|
|
531
|
+
return bindingService.put(sessionId, record);
|
|
532
|
+
}
|
|
533
|
+
function drop(sessionId) {
|
|
534
|
+
return bindingService.drop(sessionId);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// lib/server/git/interface.js
|
|
538
|
+
var interface_exports2 = {};
|
|
539
|
+
__export(interface_exports2, {
|
|
540
|
+
addWorktree: () => addWorktree,
|
|
541
|
+
belongsTo: () => belongsTo,
|
|
542
|
+
checkRefFormat: () => checkRefFormat,
|
|
543
|
+
commonDir: () => commonDir,
|
|
544
|
+
gitExec: () => gitExec,
|
|
545
|
+
headBranch: () => headBranch,
|
|
546
|
+
installGit: () => installGit,
|
|
547
|
+
listWorktrees: () => listWorktrees,
|
|
548
|
+
releaseGit: () => releaseGit,
|
|
549
|
+
removeWorktree: () => removeWorktree,
|
|
550
|
+
resolveCommit: () => resolveCommit
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// lib/server/git/impl/service/index.js
|
|
554
|
+
import { resolve } from "node:path";
|
|
555
|
+
|
|
556
|
+
// lib/server/git/impl/inspect/index.js
|
|
557
|
+
function commonDirArgs(dir) {
|
|
558
|
+
return ["-C", dir, "rev-parse", "--git-common-dir"];
|
|
559
|
+
}
|
|
560
|
+
function worktreeListArgs(dir) {
|
|
561
|
+
return ["-C", dir, "worktree", "list", "--porcelain"];
|
|
562
|
+
}
|
|
563
|
+
function headBranchArgs(dir) {
|
|
564
|
+
return ["-C", dir, "rev-parse", "--abbrev-ref", "HEAD"];
|
|
565
|
+
}
|
|
566
|
+
function checkRefFormatArgs(branch) {
|
|
567
|
+
return ["check-ref-format", "--branch", branch];
|
|
568
|
+
}
|
|
569
|
+
function addWorktreeArgs(repoRoot, path, branch, base) {
|
|
570
|
+
const flags = branch === void 0 ? ["--detach"] : ["-b", branch];
|
|
571
|
+
const startPoint = base === void 0 ? [] : [base];
|
|
572
|
+
return ["-C", repoRoot, "worktree", "add", ...flags, "--", path, ...startPoint];
|
|
573
|
+
}
|
|
574
|
+
function revParseCommitArgs(dir, rev) {
|
|
575
|
+
return ["-C", dir, "rev-parse", "--verify", "--quiet", rev + "^{commit}"];
|
|
576
|
+
}
|
|
577
|
+
function removeWorktreeArgs(repoRoot, path, force) {
|
|
578
|
+
const flags = force ? ["--force"] : [];
|
|
579
|
+
return ["-C", repoRoot, "worktree", "remove", ...flags, "--", path];
|
|
580
|
+
}
|
|
581
|
+
function parseSingleLine(stdout) {
|
|
582
|
+
const line = stdout.split("\n")[0]?.trim() ?? "";
|
|
583
|
+
return line.length === 0 ? void 0 : line;
|
|
584
|
+
}
|
|
585
|
+
function parseWorktreeList(stdout) {
|
|
586
|
+
const entries = [];
|
|
587
|
+
for (const block of stdout.split(/\r?\n\r?\n/)) {
|
|
588
|
+
let path;
|
|
589
|
+
let branch;
|
|
590
|
+
let detached = false;
|
|
591
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
592
|
+
const line = rawLine.trim();
|
|
593
|
+
if (line.startsWith("worktree "))
|
|
594
|
+
path = line.slice("worktree ".length);
|
|
595
|
+
else if (line.startsWith("branch "))
|
|
596
|
+
branch = line.slice("branch ".length);
|
|
597
|
+
else if (line === "detached")
|
|
598
|
+
detached = true;
|
|
599
|
+
}
|
|
600
|
+
if (path !== void 0 && path.length > 0)
|
|
601
|
+
entries.push({ path, branch, detached });
|
|
602
|
+
}
|
|
603
|
+
return entries;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// lib/server/git/impl/service/index.js
|
|
607
|
+
var BELONGS_TTL_MS = 3e4;
|
|
608
|
+
var BELONGS_CACHE_MAX = 256;
|
|
609
|
+
var COMMON_DIR_TTL_MS = 3e4;
|
|
610
|
+
var COMMON_DIR_CACHE_MAX = 256;
|
|
611
|
+
var NOT_INSTALLED2 = "dsh-worktree-sidebar: git 域尚未装配";
|
|
612
|
+
var GitService = class {
|
|
613
|
+
/** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
|
|
614
|
+
installed = false;
|
|
615
|
+
/** 装配入参。释放即放开,能力面随之当场失败。 */
|
|
616
|
+
deps;
|
|
617
|
+
belongsCache = /* @__PURE__ */ new Map();
|
|
618
|
+
/** 只放正结果,所以值不是 `string | undefined`——负结果没有条目。 */
|
|
619
|
+
commonDirCache = /* @__PURE__ */ new Map();
|
|
620
|
+
/** 装配 git 域。重复装配是编程错误,当场暴露。 */
|
|
621
|
+
install(deps) {
|
|
622
|
+
if (this.installed)
|
|
623
|
+
throw new Error("dsh-worktree-sidebar: git 域只能装配一次");
|
|
624
|
+
this.installed = true;
|
|
625
|
+
this.deps = deps;
|
|
626
|
+
}
|
|
627
|
+
/** 卸载:丢掉缓存与装配入参,复位装配标记——同进程的下一次 `install` 不该撞上「只能装配一次」。 */
|
|
628
|
+
release() {
|
|
629
|
+
this.installed = false;
|
|
630
|
+
this.belongsCache.clear();
|
|
631
|
+
this.commonDirCache.clear();
|
|
632
|
+
this.deps = void 0;
|
|
633
|
+
}
|
|
634
|
+
async commonDir(dir) {
|
|
635
|
+
const reading = await this.reading(dir);
|
|
636
|
+
return reading.kind === "repo" ? reading.dir : void 0;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 带缓存的仓库判定读数。缓存只认正结果(见 `COMMON_DIR_TTL_MS`),负结果每次都真问一次 git。
|
|
640
|
+
*/
|
|
641
|
+
async reading(dir) {
|
|
642
|
+
const hit = this.commonDirCache.get(dir);
|
|
643
|
+
if (hit !== void 0 && Date.now() - hit.at < COMMON_DIR_TTL_MS) {
|
|
644
|
+
return { kind: "repo", dir: hit.dir };
|
|
645
|
+
}
|
|
646
|
+
const reading = await this.computeCommonDir(dir);
|
|
647
|
+
if (reading.kind === "repo") {
|
|
648
|
+
if (this.commonDirCache.size >= COMMON_DIR_CACHE_MAX)
|
|
649
|
+
this.commonDirCache.clear();
|
|
650
|
+
this.commonDirCache.set(dir, { at: Date.now(), dir: reading.dir });
|
|
651
|
+
}
|
|
652
|
+
return reading;
|
|
653
|
+
}
|
|
654
|
+
/** 真起一次 git 求公共目录;缓存命中不走这里。 */
|
|
655
|
+
async computeCommonDir(dir) {
|
|
656
|
+
const result = await this.exec().run(commonDirArgs(dir));
|
|
657
|
+
if (!result.ok) {
|
|
658
|
+
return result.code === null ? { kind: "failed", reason: reasonOf(result) } : { kind: "not-repo" };
|
|
659
|
+
}
|
|
660
|
+
const value = parseSingleLine(result.stdout);
|
|
661
|
+
if (value === void 0)
|
|
662
|
+
return { kind: "failed", reason: "git 未返回公共 git 目录" };
|
|
663
|
+
return { kind: "repo", dir: resolve(dir, value) };
|
|
664
|
+
}
|
|
665
|
+
async listWorktrees(dir) {
|
|
666
|
+
const result = await this.exec().run(worktreeListArgs(dir));
|
|
667
|
+
if (!result.ok)
|
|
668
|
+
return [];
|
|
669
|
+
return parseWorktreeList(result.stdout);
|
|
670
|
+
}
|
|
671
|
+
async belongsTo(dir, repoRoot) {
|
|
672
|
+
const key = dir + "\0" + repoRoot;
|
|
673
|
+
const hit = this.belongsCache.get(key);
|
|
674
|
+
if (hit !== void 0 && Date.now() - hit.at < BELONGS_TTL_MS)
|
|
675
|
+
return hit.reading;
|
|
676
|
+
const [left, right] = await Promise.all([this.reading(dir), this.reading(repoRoot)]);
|
|
677
|
+
const reading = left.kind === "repo" && right.kind === "repo" ? left.dir === right.dir ? { kind: "same" } : { kind: "different" } : {
|
|
678
|
+
kind: "unknown",
|
|
679
|
+
reason: unreadableReason(dir, left, repoRoot, right),
|
|
680
|
+
notRepo: left.kind === "not-repo" || right.kind === "not-repo"
|
|
681
|
+
};
|
|
682
|
+
if (reading.kind !== "unknown") {
|
|
683
|
+
if (this.belongsCache.size >= BELONGS_CACHE_MAX)
|
|
684
|
+
this.belongsCache.clear();
|
|
685
|
+
this.belongsCache.set(key, { at: Date.now(), reading });
|
|
686
|
+
}
|
|
687
|
+
return reading;
|
|
688
|
+
}
|
|
689
|
+
async headBranch(dir) {
|
|
690
|
+
const result = await this.exec().run(headBranchArgs(dir));
|
|
691
|
+
if (!result.ok)
|
|
692
|
+
return void 0;
|
|
693
|
+
const value = parseSingleLine(result.stdout);
|
|
694
|
+
if (value === void 0 || value === "HEAD")
|
|
695
|
+
return void 0;
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
async checkRefFormat(branch) {
|
|
699
|
+
const result = await this.exec().run(checkRefFormatArgs(branch));
|
|
700
|
+
return result.ok;
|
|
701
|
+
}
|
|
702
|
+
async addWorktree(repoRoot, path, branch, base) {
|
|
703
|
+
const result = await this.exec().run(addWorktreeArgs(repoRoot, path, branch, base));
|
|
704
|
+
return result.ok ? { ok: true } : { ok: false, reason: reasonOf(result) };
|
|
705
|
+
}
|
|
706
|
+
/** 解析起点。`--quiet` 让「不是有效 rev」走退出码而不是 stderr,失败在这里就收成 undefined。 */
|
|
707
|
+
async resolveCommit(dir, rev) {
|
|
708
|
+
const result = await this.exec().run(revParseCommitArgs(dir, rev));
|
|
709
|
+
if (!result.ok)
|
|
710
|
+
return void 0;
|
|
711
|
+
return parseSingleLine(result.stdout);
|
|
712
|
+
}
|
|
713
|
+
async removeWorktree(repoRoot, path, force) {
|
|
714
|
+
const result = await this.exec().run(removeWorktreeArgs(repoRoot, path, force));
|
|
715
|
+
return result.ok ? { ok: true } : { ok: false, reason: reasonOf(result) };
|
|
716
|
+
}
|
|
717
|
+
/** 取执行面:未装配时当场失败,而不是拿一份空执行面跑出「git 说没有」这种假答案。 */
|
|
718
|
+
exec() {
|
|
719
|
+
const deps = this.deps;
|
|
720
|
+
if (deps === void 0)
|
|
721
|
+
throw new Error(NOT_INSTALLED2);
|
|
722
|
+
return deps.exec;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var gitService = new GitService();
|
|
726
|
+
function reasonOf(result) {
|
|
727
|
+
const text = result.stderr.trim();
|
|
728
|
+
return text.length > 0 ? text : "git 退出码非零";
|
|
729
|
+
}
|
|
730
|
+
function unreadableReason(dir, left, repoRoot, right) {
|
|
731
|
+
const sides = [];
|
|
732
|
+
const describe = (label, path, reading) => {
|
|
733
|
+
if (reading.kind === "repo")
|
|
734
|
+
return;
|
|
735
|
+
const why = reading.kind === "failed" ? "git 执行失败(" + reading.reason + ")" : "不是 git 工作树";
|
|
736
|
+
sides.push(label + " " + path + " " + why);
|
|
737
|
+
};
|
|
738
|
+
describe("worktree 侧", dir, left);
|
|
739
|
+
describe("主仓库侧", repoRoot, right);
|
|
740
|
+
return sides.join(";");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// lib/server/git/impl/exec/index.js
|
|
744
|
+
import { execFile } from "node:child_process";
|
|
745
|
+
var TIMEOUT_MS = 1e4;
|
|
746
|
+
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
747
|
+
var gitExec = {
|
|
748
|
+
run: (args) => new Promise((resolve3) => {
|
|
749
|
+
execFile("git", [...args], { timeout: TIMEOUT_MS, maxBuffer: MAX_BUFFER, windowsHide: true }, (error, stdout, stderr) => {
|
|
750
|
+
const detail = stderr.length > 0 ? stderr : error === null ? "" : error.message;
|
|
751
|
+
const code = error === null ? 0 : typeof error.code === "number" ? error.code : null;
|
|
752
|
+
resolve3({ ok: error === null, stdout, stderr: detail, code });
|
|
753
|
+
});
|
|
754
|
+
})
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// lib/server/git/interface.js
|
|
758
|
+
function installGit(deps) {
|
|
759
|
+
gitService.install(deps);
|
|
760
|
+
}
|
|
761
|
+
function releaseGit() {
|
|
762
|
+
gitService.release();
|
|
763
|
+
}
|
|
764
|
+
function commonDir(dir) {
|
|
765
|
+
return gitService.commonDir(dir);
|
|
766
|
+
}
|
|
767
|
+
function listWorktrees(dir) {
|
|
768
|
+
return gitService.listWorktrees(dir);
|
|
769
|
+
}
|
|
770
|
+
function belongsTo(dir, repoRoot) {
|
|
771
|
+
return gitService.belongsTo(dir, repoRoot);
|
|
772
|
+
}
|
|
773
|
+
function headBranch(dir) {
|
|
774
|
+
return gitService.headBranch(dir);
|
|
775
|
+
}
|
|
776
|
+
function checkRefFormat(branch) {
|
|
777
|
+
return gitService.checkRefFormat(branch);
|
|
778
|
+
}
|
|
779
|
+
function addWorktree(repoRoot, path, branch, base) {
|
|
780
|
+
return gitService.addWorktree(repoRoot, path, branch, base);
|
|
781
|
+
}
|
|
782
|
+
function resolveCommit(dir, rev) {
|
|
783
|
+
return gitService.resolveCommit(dir, rev);
|
|
784
|
+
}
|
|
785
|
+
function removeWorktree(repoRoot, path, force) {
|
|
786
|
+
return gitService.removeWorktree(repoRoot, path, force);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// lib/server/scope/interface.js
|
|
790
|
+
var interface_exports3 = {};
|
|
791
|
+
__export(interface_exports3, {
|
|
792
|
+
chainDiagnostics: () => chainDiagnostics,
|
|
793
|
+
effectiveWorktree: () => effectiveWorktree2,
|
|
794
|
+
installScope: () => installScope,
|
|
795
|
+
releaseScope: () => releaseScope,
|
|
796
|
+
rootOf: () => rootOf,
|
|
797
|
+
takeoverState: () => takeoverState,
|
|
798
|
+
worktreeOrigin: () => worktreeOrigin
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// lib/server/scope/impl/own/index.js
|
|
802
|
+
import { statSync } from "node:fs";
|
|
803
|
+
function realStat(path) {
|
|
804
|
+
return statSync(path, { throwIfNoEntry: false });
|
|
805
|
+
}
|
|
806
|
+
function directoryExists(path, stat = realStat) {
|
|
807
|
+
try {
|
|
808
|
+
return stat(path)?.isDirectory() === true;
|
|
809
|
+
} catch {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function ownRecord(deps, sessionId) {
|
|
814
|
+
const record = deps.binding.get(sessionId);
|
|
815
|
+
if (record === void 0)
|
|
816
|
+
return void 0;
|
|
817
|
+
const exists = deps.existsDirectory ?? directoryExists;
|
|
818
|
+
if (!exists(record.worktreeRoot)) {
|
|
819
|
+
await drop2(deps, sessionId, "worktree 目录不存在:" + record.worktreeRoot);
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
const identity = await identityOf(deps, sessionId);
|
|
823
|
+
if (identity !== void 0 && identity.createdAt !== record.sessionCreatedAt) {
|
|
824
|
+
await drop2(deps, sessionId, "会话 id 已被另一个会话复用(登记属于 createdAt=" + record.sessionCreatedAt + ",当前会话是 " + identity.createdAt + "):" + record.worktreeRoot);
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
827
|
+
const belongs = await deps.git.belongsTo(record.worktreeRoot, record.repoRoot);
|
|
828
|
+
if (belongs.kind === "different") {
|
|
829
|
+
await drop2(deps, sessionId, "已不是该仓库的 worktree:" + record.worktreeRoot);
|
|
830
|
+
return void 0;
|
|
831
|
+
}
|
|
832
|
+
if (belongs.kind === "unknown") {
|
|
833
|
+
deps.logger.warn("dsh-worktree-sidebar: 无法确认 worktree 归属,保留该会话的登记 — " + belongs.reason);
|
|
834
|
+
}
|
|
835
|
+
return record;
|
|
836
|
+
}
|
|
837
|
+
async function identityOf(deps, sessionId) {
|
|
838
|
+
const live = deps.sessions.liveIdentityOf(sessionId);
|
|
839
|
+
if (live !== void 0)
|
|
840
|
+
return live;
|
|
841
|
+
try {
|
|
842
|
+
return await deps.sessions.storedIdentityOf(sessionId);
|
|
843
|
+
} catch (cause) {
|
|
844
|
+
deps.logger.warn("dsh-worktree-sidebar: 无法核对会话身份,保留该会话的登记 — " + (cause instanceof Error ? cause.message : String(cause)));
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function drop2(deps, sessionId, reason) {
|
|
849
|
+
deps.logger.warn("dsh-worktree-sidebar: " + reason + " —— 已摘掉该会话的登记");
|
|
850
|
+
try {
|
|
851
|
+
const dropped = await deps.binding.drop(sessionId);
|
|
852
|
+
if (!dropped.ok) {
|
|
853
|
+
deps.logger.warn("dsh-worktree-sidebar: 摘除失效绑定失败 — " + dropped.reason);
|
|
854
|
+
}
|
|
855
|
+
} catch (cause) {
|
|
856
|
+
deps.logger.warn("dsh-worktree-sidebar: 摘除失效绑定抛出 — " + (cause instanceof Error ? cause.message : String(cause)));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// lib/server/scope/impl/inherit/index.js
|
|
861
|
+
async function inheritedOrigin(deps, sessionId) {
|
|
862
|
+
const seen = /* @__PURE__ */ new Set([sessionId]);
|
|
863
|
+
let current = sessionId;
|
|
864
|
+
for (; ; ) {
|
|
865
|
+
const parent = await parentOf(deps, current);
|
|
866
|
+
if (parent === void 0 || seen.has(parent))
|
|
867
|
+
return void 0;
|
|
868
|
+
seen.add(parent);
|
|
869
|
+
const inherited = await ownRecord(deps, parent);
|
|
870
|
+
if (inherited !== void 0)
|
|
871
|
+
return { record: inherited, ownerSessionId: parent };
|
|
872
|
+
current = parent;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function parentOf(deps, sessionId) {
|
|
876
|
+
const live = deps.sessions.liveParentOf(sessionId);
|
|
877
|
+
if (live.kind === "parent")
|
|
878
|
+
return live.id;
|
|
879
|
+
if (live.kind === "root")
|
|
880
|
+
return void 0;
|
|
881
|
+
try {
|
|
882
|
+
return await deps.sessions.storedParentOf(sessionId);
|
|
883
|
+
} catch {
|
|
884
|
+
return void 0;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// lib/server/scope/impl/resolve/index.js
|
|
889
|
+
function rootOf(origin) {
|
|
890
|
+
return origin.kind === "none" ? null : origin.record.worktreeRoot;
|
|
891
|
+
}
|
|
892
|
+
async function bindingOrigin(deps, sessionId) {
|
|
893
|
+
const own = await ownRecord(deps, sessionId);
|
|
894
|
+
if (own !== void 0)
|
|
895
|
+
return { kind: "own", record: own };
|
|
896
|
+
const inherited = await inheritedOrigin(deps, sessionId);
|
|
897
|
+
if (inherited !== void 0) {
|
|
898
|
+
return {
|
|
899
|
+
kind: "inherited",
|
|
900
|
+
record: inherited.record,
|
|
901
|
+
ownerSessionId: inherited.ownerSessionId
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
return { kind: "none" };
|
|
905
|
+
}
|
|
906
|
+
async function effectiveWorktree(deps, sessionId) {
|
|
907
|
+
return rootOf(await bindingOrigin(deps, sessionId));
|
|
908
|
+
}
|
|
909
|
+
async function resolveScope(deps, delegate, sessionId) {
|
|
910
|
+
try {
|
|
911
|
+
const root = await effectiveWorktree(deps, sessionId);
|
|
912
|
+
if (root !== null)
|
|
913
|
+
return { sessionId, workspaceRoot: root };
|
|
914
|
+
} catch (cause) {
|
|
915
|
+
deps.logger.warn("dsh-worktree-sidebar: 解析文件根失败,回退官方语义 — " + (cause instanceof Error ? cause.message : String(cause)));
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
return await delegate(sessionId);
|
|
919
|
+
} catch (cause) {
|
|
920
|
+
deps.logger.warn("dsh-worktree-sidebar: 官方默认解析失败 — " + (cause instanceof Error ? cause.message : String(cause)));
|
|
921
|
+
return void 0;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// lib/server/scope/impl/service/index.js
|
|
926
|
+
var NOT_INSTALLED3 = "dsh-worktree-sidebar: scope 域尚未装配";
|
|
927
|
+
var ScopeService = class {
|
|
928
|
+
state = "idle";
|
|
929
|
+
deps;
|
|
930
|
+
/** 捕获到的委托对象。释放即丢掉,不留在实例上供下一次装配复用。 */
|
|
931
|
+
delegate;
|
|
932
|
+
/** configure 交回的释放面:调用它官方默认解析即恢复。 */
|
|
933
|
+
dispose;
|
|
934
|
+
/** 查找表订阅的退订面。等待期需要它,进入终态后它只是空转。 */
|
|
935
|
+
unsubscribe;
|
|
936
|
+
/** 等待期只出一次声:通知可能来很多次,每次都报会把日志淹掉。 */
|
|
937
|
+
warnedWaiting = false;
|
|
938
|
+
/** 持久面读数。跨调用存活,所以只能住在这里,不能住在 `inherit`(那是无状态函数)。 */
|
|
939
|
+
chain = { storedReads: 0, storedFailures: 0, lastFailure: void 0 };
|
|
940
|
+
/** 装配 scope 域。重复装配是编程错误,当场暴露。 */
|
|
941
|
+
install(deps) {
|
|
942
|
+
if (this.state !== "idle")
|
|
943
|
+
throw new Error("dsh-worktree-sidebar: scope 域只能装配一次");
|
|
944
|
+
this.chain = { storedReads: 0, storedFailures: 0, lastFailure: void 0 };
|
|
945
|
+
this.deps = { ...deps, sessions: this.observed(deps.sessions) };
|
|
946
|
+
this.state = "waiting";
|
|
947
|
+
this.unsubscribe = deps.typert.subscribe(() => this.attempt());
|
|
948
|
+
this.attempt();
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* 卸载:把 resolver 交还官方、退订、丢掉捕获到的委托与装配入参,复位状态。重复调用无害。
|
|
952
|
+
* 此后到达的解析请求回 undefined(gateway 走它自己的 lookup-not-found),不会拿旧 deps 继续服务。
|
|
953
|
+
*/
|
|
954
|
+
release() {
|
|
955
|
+
const unsubscribe = this.unsubscribe;
|
|
956
|
+
const dispose = this.dispose;
|
|
957
|
+
this.state = "idle";
|
|
958
|
+
this.deps = void 0;
|
|
959
|
+
this.delegate = void 0;
|
|
960
|
+
this.dispose = void 0;
|
|
961
|
+
this.unsubscribe = void 0;
|
|
962
|
+
this.warnedWaiting = false;
|
|
963
|
+
this.chain = { storedReads: 0, storedFailures: 0, lastFailure: void 0 };
|
|
964
|
+
if (unsubscribe !== void 0) {
|
|
965
|
+
try {
|
|
966
|
+
unsubscribe();
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (dispose === void 0)
|
|
971
|
+
return;
|
|
972
|
+
try {
|
|
973
|
+
dispose();
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
takeoverState() {
|
|
978
|
+
return this.state;
|
|
979
|
+
}
|
|
980
|
+
/** 读数是快照:调用方拿不到本域内部那个对象,改不动它。 */
|
|
981
|
+
chainDiagnostics() {
|
|
982
|
+
return { ...this.chain };
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* 给会话链的持久面加一圈读数。失败**照原样抛回**,收口仍在调用方——
|
|
986
|
+
* 这里只负责记账,不改变任何判定。
|
|
987
|
+
*
|
|
988
|
+
* 身份核对也走同一条持久面,所以它同样计入读数:只统计父链会让「身份读不出来」这类
|
|
989
|
+
* 故障在 health 上完全不可见。
|
|
990
|
+
*/
|
|
991
|
+
observed(inner) {
|
|
992
|
+
return {
|
|
993
|
+
liveParentOf: (sessionId) => inner.liveParentOf(sessionId),
|
|
994
|
+
storedParentOf: (sessionId) => this.counted(() => inner.storedParentOf(sessionId)),
|
|
995
|
+
liveIdentityOf: (sessionId) => inner.liveIdentityOf(sessionId),
|
|
996
|
+
storedIdentityOf: (sessionId) => this.counted(() => inner.storedIdentityOf(sessionId))
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/** 记一次持久面读取:无论成败都算一次读,抛错另计一次失败并留下最后原因。 */
|
|
1000
|
+
async counted(read) {
|
|
1001
|
+
this.chain.storedReads += 1;
|
|
1002
|
+
try {
|
|
1003
|
+
return await read();
|
|
1004
|
+
} catch (cause) {
|
|
1005
|
+
this.chain.storedFailures += 1;
|
|
1006
|
+
this.chain.lastFailure = cause instanceof Error ? cause.message : String(cause);
|
|
1007
|
+
throw cause;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async effectiveWorktree(sessionId) {
|
|
1011
|
+
const deps = this.deps;
|
|
1012
|
+
if (deps === void 0)
|
|
1013
|
+
throw new Error(NOT_INSTALLED3);
|
|
1014
|
+
if (this.state !== "live")
|
|
1015
|
+
return null;
|
|
1016
|
+
return effectiveWorktree(deps, sessionId);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* 绑定来源。**不设 takeover 门**:登记是否存在与「文件根是否已经换成它」是两件事,
|
|
1020
|
+
* 工具面(register / create / remove)要的是前者的完整答案——它据此决定能不能摘、
|
|
1021
|
+
* 该不该报「继承自哪个会话」。把工具面也卡在 `live` 上,接管冲突时连一条已确认失效的
|
|
1022
|
+
* 登记都清理不掉。
|
|
1023
|
+
*/
|
|
1024
|
+
async worktreeOrigin(sessionId) {
|
|
1025
|
+
const deps = this.deps;
|
|
1026
|
+
if (deps === void 0)
|
|
1027
|
+
throw new Error(NOT_INSTALLED3);
|
|
1028
|
+
return bindingOrigin(deps, sessionId);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* 试一次接管。provider 没出现就退回等待态,等待由查找表的变更通知再次叫醒。
|
|
1032
|
+
*/
|
|
1033
|
+
attempt() {
|
|
1034
|
+
if (this.state !== "waiting")
|
|
1035
|
+
return;
|
|
1036
|
+
const deps = this.deps;
|
|
1037
|
+
if (deps === void 0)
|
|
1038
|
+
return;
|
|
1039
|
+
const descriptor = deps.typert.current();
|
|
1040
|
+
if (descriptor === void 0) {
|
|
1041
|
+
if (!this.warnedWaiting) {
|
|
1042
|
+
this.warnedWaiting = true;
|
|
1043
|
+
deps.logger.warn("dsh-worktree-sidebar: workspaceFileScope 的 provider 尚未注册,先等它出现(文件根暂按官方语义)");
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const delegate = descriptor.resolve;
|
|
1048
|
+
this.delegate = delegate;
|
|
1049
|
+
this.state = "live";
|
|
1050
|
+
try {
|
|
1051
|
+
this.dispose = deps.typert.configure((sessionId) => this.resolve(sessionId));
|
|
1052
|
+
} catch (cause) {
|
|
1053
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1054
|
+
this.state = "abandoned";
|
|
1055
|
+
this.delegate = void 0;
|
|
1056
|
+
this.dispose = void 0;
|
|
1057
|
+
deps.logger.warn("dsh-worktree-sidebar: workspaceFileScope 已有解析器,放弃接管(文件根保持官方语义)— " + reason);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* 交给官方 typert 的解析器。委托对象与装配入参都按**调用当刻**取:释放之后这个闭包只剩
|
|
1062
|
+
* undefined,官方拿到的是它自己的 lookup-not-found,而不是一份过期绑定。
|
|
1063
|
+
*/
|
|
1064
|
+
async resolve(sessionId) {
|
|
1065
|
+
const deps = this.deps;
|
|
1066
|
+
const delegate = this.delegate;
|
|
1067
|
+
if (deps === void 0 || delegate === void 0)
|
|
1068
|
+
return void 0;
|
|
1069
|
+
return resolveScope(deps, delegate, sessionId);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
var scopeService = new ScopeService();
|
|
1073
|
+
|
|
1074
|
+
// lib/server/scope/interface.js
|
|
1075
|
+
function installScope(deps) {
|
|
1076
|
+
scopeService.install(deps);
|
|
1077
|
+
}
|
|
1078
|
+
function releaseScope() {
|
|
1079
|
+
scopeService.release();
|
|
1080
|
+
}
|
|
1081
|
+
function effectiveWorktree2(sessionId) {
|
|
1082
|
+
return scopeService.effectiveWorktree(sessionId);
|
|
1083
|
+
}
|
|
1084
|
+
function worktreeOrigin(sessionId) {
|
|
1085
|
+
return scopeService.worktreeOrigin(sessionId);
|
|
1086
|
+
}
|
|
1087
|
+
function takeoverState() {
|
|
1088
|
+
return scopeService.takeoverState();
|
|
1089
|
+
}
|
|
1090
|
+
function chainDiagnostics() {
|
|
1091
|
+
return scopeService.chainDiagnostics();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// lib/server/tools/impl/bind/index.js
|
|
1095
|
+
import { statSync as statSync2 } from "node:fs";
|
|
1096
|
+
import { isAbsolute, resolve as resolve2 } from "node:path";
|
|
1097
|
+
var NO_ORIGIN = { kind: "none" };
|
|
1098
|
+
async function readOrigin(deps, sessionId) {
|
|
1099
|
+
try {
|
|
1100
|
+
return { origin: await deps.scope.worktreeOrigin(sessionId), problem: void 0 };
|
|
1101
|
+
} catch (cause) {
|
|
1102
|
+
deps.logger.warn("dsh-worktree-sidebar: 读取绑定来源失败 —— " + (cause instanceof Error ? cause.message : String(cause)));
|
|
1103
|
+
return {
|
|
1104
|
+
origin: NO_ORIGIN,
|
|
1105
|
+
problem: "Could not read this session's binding state. Retry once the extension is fully loaded."
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function stateOf(origin) {
|
|
1110
|
+
if (origin.kind === "none")
|
|
1111
|
+
return { bound: false, worktree: "", branch: "" };
|
|
1112
|
+
return { bound: true, worktree: origin.record.worktreeRoot, branch: origin.record.branch };
|
|
1113
|
+
}
|
|
1114
|
+
function resultOf(origin, ok, detail) {
|
|
1115
|
+
const state = stateOf(origin);
|
|
1116
|
+
const inherited = origin.kind === "inherited" ? "This root is inherited from session " + origin.ownerSessionId + "; this session has no binding of its own. " : "";
|
|
1117
|
+
return {
|
|
1118
|
+
ok,
|
|
1119
|
+
bound: state.bound,
|
|
1120
|
+
worktree: state.worktree,
|
|
1121
|
+
branch: state.branch,
|
|
1122
|
+
detail: inherited + detail
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function resolveTarget(cwd, raw) {
|
|
1126
|
+
return isAbsolute(raw) ? raw : resolve2(cwd, raw);
|
|
1127
|
+
}
|
|
1128
|
+
function directoryProblem(target) {
|
|
1129
|
+
const info = statSync2(target, { throwIfNoEntry: false });
|
|
1130
|
+
if (info === void 0)
|
|
1131
|
+
return "No such directory: " + target;
|
|
1132
|
+
if (!info.isDirectory())
|
|
1133
|
+
return "Not a directory: " + target;
|
|
1134
|
+
return void 0;
|
|
1135
|
+
}
|
|
1136
|
+
async function availableWorktrees(deps, repo) {
|
|
1137
|
+
const entries = await deps.git.listWorktrees(repo);
|
|
1138
|
+
if (entries.length === 0)
|
|
1139
|
+
return "";
|
|
1140
|
+
return "\nAvailable worktrees:\n" + entries.map((entry) => "- " + entry.path).join("\n");
|
|
1141
|
+
}
|
|
1142
|
+
async function bindWorktree(deps, session, repo, target, registeredAt, before) {
|
|
1143
|
+
if (session.createdAt === void 0) {
|
|
1144
|
+
return resultOf(before, false, "This session exposes no creation timestamp, so the binding could not be told apart from a different session that reuses the same id after a restart. Refusing to bind.");
|
|
1145
|
+
}
|
|
1146
|
+
const belongs = await deps.git.belongsTo(target, repo);
|
|
1147
|
+
if (belongs.kind !== "same") {
|
|
1148
|
+
const why = belongs.kind === "different" || belongs.kind === "unknown" && belongs.notRepo ? "That directory is not a worktree of this session's repository (" + repo + "), so binding it would point the file tree outside this repository." : "Could not verify that the directory belongs to this session's repository (" + repo + "): " + (belongs.kind === "unknown" ? belongs.reason : "") + ". Retry once the directory is readable.";
|
|
1149
|
+
return resultOf(before, false, why + await availableWorktrees(deps, repo));
|
|
1150
|
+
}
|
|
1151
|
+
const branch = await deps.git.headBranch(target) ?? "";
|
|
1152
|
+
const record = {
|
|
1153
|
+
repoRoot: repo,
|
|
1154
|
+
worktreeRoot: target,
|
|
1155
|
+
branch,
|
|
1156
|
+
createdAt: registeredAt,
|
|
1157
|
+
sessionCreatedAt: session.createdAt
|
|
1158
|
+
};
|
|
1159
|
+
const written = await deps.binding.put(session.id, record);
|
|
1160
|
+
if (!written.ok) {
|
|
1161
|
+
return resultOf(before, false, "Could not persist the binding: " + written.reason);
|
|
1162
|
+
}
|
|
1163
|
+
return resultOf({ kind: "own", record }, true, "Bound this session to the worktree. Open or refresh the right-sidebar Files tab to see it; the session cwd is unchanged, and @ references / present targets still resolve against the cwd.");
|
|
1164
|
+
}
|
|
1165
|
+
async function repoOf(deps, agent) {
|
|
1166
|
+
if (agent.cwd === void 0)
|
|
1167
|
+
return void 0;
|
|
1168
|
+
const common = await deps.git.commonDir(agent.cwd);
|
|
1169
|
+
return common === void 0 ? void 0 : agent.cwd;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// lib/server/tools/impl/protocol/index.js
|
|
1173
|
+
var RESULT_SCHEMA = {
|
|
1174
|
+
type: "object",
|
|
1175
|
+
properties: {
|
|
1176
|
+
ok: { type: "boolean", description: "Whether the requested operation succeeded." },
|
|
1177
|
+
bound: {
|
|
1178
|
+
type: "boolean",
|
|
1179
|
+
description: "Whether a worktree is bound to this session or inherited from a parent session after the call."
|
|
1180
|
+
},
|
|
1181
|
+
worktree: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
description: "Absolute path of the bound worktree; empty when bound is false."
|
|
1184
|
+
},
|
|
1185
|
+
branch: {
|
|
1186
|
+
type: "string",
|
|
1187
|
+
description: "Bound branch name; empty when bound is false or the worktree is detached."
|
|
1188
|
+
},
|
|
1189
|
+
detail: {
|
|
1190
|
+
type: "string",
|
|
1191
|
+
description: "What happened: for failures, the reason and the next thing to try; for successes, the effect."
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
required: ["ok", "bound", "worktree", "branch", "detail"],
|
|
1195
|
+
additionalProperties: false
|
|
1196
|
+
};
|
|
1197
|
+
function renderResult(value) {
|
|
1198
|
+
const state = value.bound ? "bound worktree: " + value.worktree + (value.branch === "" ? "" : " [" + value.branch + "]") : "no worktree bound to this session";
|
|
1199
|
+
return [{ type: "text", text: state + "\n" + value.detail }];
|
|
1200
|
+
}
|
|
1201
|
+
function argString(args, key) {
|
|
1202
|
+
if (typeof args !== "object" || args === null)
|
|
1203
|
+
return void 0;
|
|
1204
|
+
const value = args[key];
|
|
1205
|
+
if (typeof value !== "string")
|
|
1206
|
+
return void 0;
|
|
1207
|
+
const trimmed = value.trim();
|
|
1208
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
1209
|
+
}
|
|
1210
|
+
function argBool(args, key) {
|
|
1211
|
+
if (typeof args !== "object" || args === null)
|
|
1212
|
+
return false;
|
|
1213
|
+
return args[key] === true;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// lib/server/tools/impl/session/index.js
|
|
1217
|
+
function sessionOf(exec) {
|
|
1218
|
+
if (typeof exec !== "object" || exec === null)
|
|
1219
|
+
return void 0;
|
|
1220
|
+
const session = readObject(readObject(exec, "agent"), "session");
|
|
1221
|
+
if (session === void 0)
|
|
1222
|
+
return void 0;
|
|
1223
|
+
const id = session.id;
|
|
1224
|
+
if (typeof id !== "string" || id.length === 0)
|
|
1225
|
+
return void 0;
|
|
1226
|
+
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
|
+
return {
|
|
1230
|
+
id,
|
|
1231
|
+
cwd: typeof cwd === "string" && cwd.length > 0 ? cwd : void 0,
|
|
1232
|
+
createdAt: typeof createdAt === "number" && Number.isFinite(createdAt) ? createdAt : void 0
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function readObject(source, key) {
|
|
1236
|
+
if (typeof source !== "object" || source === null)
|
|
1237
|
+
return void 0;
|
|
1238
|
+
const value = source[key];
|
|
1239
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// lib/server/tools/impl/create/index.js
|
|
1243
|
+
function buildCreateTool(deps) {
|
|
1244
|
+
return {
|
|
1245
|
+
name: "ws_worktree_create",
|
|
1246
|
+
description: "Create a git worktree from this session's repository with git worktree add, then bind it to THIS session so the right-sidebar Files tab is rooted at it (open or refresh that tab to see the new root). The session cwd does not change. Prefer this over a bare git worktree add when the Files tab should show the new worktree: a bare git command creates the directory but leaves the Files tab rooted at the session cwd (for a worktree you created that way, call ws_worktree_register on it instead). Path and branch are yours to choose; the plugin imposes no layout convention. base defaults to the current HEAD of the repository the session working directory is in, so pass it explicitly when you need a different start point. If git worktree add succeeds but binding fails, the directory is left in place and the result says so. Whether the Files tab then actually follows the binding is reported by GET /api/dsh-worktree-sidebar/health as scopeTakeover.",
|
|
1247
|
+
parameters: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
properties: {
|
|
1250
|
+
path: {
|
|
1251
|
+
type: "string",
|
|
1252
|
+
description: "Where to create the worktree: an absolute path, or a path relative to the session working directory."
|
|
1253
|
+
},
|
|
1254
|
+
branch: {
|
|
1255
|
+
type: "string",
|
|
1256
|
+
description: "Optional branch to create for the worktree (git worktree add -b). Validated with git check-ref-format --branch before git is called. Omit to check out the start point detached (git worktree add --detach): that is base, or the repository HEAD when base is omitted too. Creating no branch also works for paths whose last segment is not a valid branch name."
|
|
1257
|
+
},
|
|
1258
|
+
base: {
|
|
1259
|
+
type: "string",
|
|
1260
|
+
description: "Optional start point (commit-ish: branch, tag, or SHA, e.g. origin/main). Defaults to the current HEAD of the repository the session working directory is in."
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
required: ["path"],
|
|
1264
|
+
additionalProperties: false
|
|
1265
|
+
},
|
|
1266
|
+
output: {
|
|
1267
|
+
schema: RESULT_SCHEMA,
|
|
1268
|
+
render: (_args, value) => renderResult(value)
|
|
1269
|
+
},
|
|
1270
|
+
execute: async (args, exec) => {
|
|
1271
|
+
const session = sessionOf(exec);
|
|
1272
|
+
if (session === void 0) {
|
|
1273
|
+
return resultOf(NO_ORIGIN, false, "This tool needs an agent session, and the call carries none. Run it as an agent tool call.");
|
|
1274
|
+
}
|
|
1275
|
+
if (session.cwd === void 0) {
|
|
1276
|
+
return resultOf(NO_ORIGIN, false, "This session has no working directory, so it cannot be checked against a git repository.");
|
|
1277
|
+
}
|
|
1278
|
+
const repo = session.cwd;
|
|
1279
|
+
if (await deps.git.commonDir(repo) === void 0) {
|
|
1280
|
+
return resultOf(NO_ORIGIN, false, "This session is not inside a git repository.");
|
|
1281
|
+
}
|
|
1282
|
+
const raw = argString(args, "path");
|
|
1283
|
+
if (raw === void 0) {
|
|
1284
|
+
return resultOf(NO_ORIGIN, false, "Missing required parameter: path.");
|
|
1285
|
+
}
|
|
1286
|
+
const read = await readOrigin(deps, session.id);
|
|
1287
|
+
if (read.problem !== void 0)
|
|
1288
|
+
return resultOf(read.origin, false, read.problem);
|
|
1289
|
+
const origin = read.origin;
|
|
1290
|
+
const target = resolveTarget(repo, raw);
|
|
1291
|
+
const branch = argString(args, "branch");
|
|
1292
|
+
if (branch !== void 0 && !await deps.git.checkRefFormat(branch)) {
|
|
1293
|
+
return resultOf(origin, false, "Not a valid git branch name: " + branch + " (git check-ref-format --branch rejected it).");
|
|
1294
|
+
}
|
|
1295
|
+
const base = argString(args, "base");
|
|
1296
|
+
if (base !== void 0 && base.startsWith("-")) {
|
|
1297
|
+
return resultOf(origin, false, "Not a valid start point: " + base + ' (a start point must not begin with "-").');
|
|
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
|
+
}
|
|
1306
|
+
const created = await deps.git.addWorktree(repo, target, branch, startPoint);
|
|
1307
|
+
if (!created.ok) {
|
|
1308
|
+
return resultOf(origin, false, "git worktree add failed: " + created.reason);
|
|
1309
|
+
}
|
|
1310
|
+
const bound = await bindWorktree(deps, session, repo, target, deps.now(), origin);
|
|
1311
|
+
if (!bound.ok) {
|
|
1312
|
+
return {
|
|
1313
|
+
...bound,
|
|
1314
|
+
detail: "The worktree was created at " + target + ", but binding it to this session failed: " + bound.detail + " The directory is left in place: bind it with ws_worktree_register, or remove it with git worktree remove -- " + target
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
...bound,
|
|
1319
|
+
detail: "Created the worktree and bound this session to it. Open or refresh the right-sidebar Files tab to see it; the session cwd is unchanged."
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// lib/server/tools/impl/remove/index.js
|
|
1326
|
+
function inheritedHelp(ownerSessionId, cwd) {
|
|
1327
|
+
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
|
+
if (cwd === void 0)
|
|
1329
|
+
return main;
|
|
1330
|
+
return main + ' A third option, when that session is gone: bind this session to its own working directory with ws_worktree_register({ worktree: "' + cwd + '" }), which makes this session stop inheriting.';
|
|
1331
|
+
}
|
|
1332
|
+
function buildRemoveTool(deps) {
|
|
1333
|
+
return {
|
|
1334
|
+
name: "ws_worktree_remove",
|
|
1335
|
+
description: "Unbind the worktree from THIS session; the right-sidebar Files tab returns to the session cwd on its next open or refresh. This only touches this session's own binding: when the Files tab follows a root inherited from a parent session instead, the tool refuses (it never unbinds or removes another session's worktree) and reports which session owns that root plus the ways to stop following it. By default the worktree directory is left in place - pass removeDirectory: true to also run git worktree remove, and force: true as well when it has uncommitted changes you accept losing. The plugin never deletes directories itself.",
|
|
1336
|
+
parameters: {
|
|
1337
|
+
type: "object",
|
|
1338
|
+
properties: {
|
|
1339
|
+
removeDirectory: {
|
|
1340
|
+
type: "boolean",
|
|
1341
|
+
description: "Also remove the worktree directory with git worktree remove. Defaults to false: only the binding is dropped."
|
|
1342
|
+
},
|
|
1343
|
+
force: {
|
|
1344
|
+
type: "boolean",
|
|
1345
|
+
description: "Adds --force to git worktree remove, discarding uncommitted changes. Only valid together with removeDirectory: true."
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
additionalProperties: false
|
|
1349
|
+
},
|
|
1350
|
+
output: {
|
|
1351
|
+
schema: RESULT_SCHEMA,
|
|
1352
|
+
render: (_args, value) => renderResult(value)
|
|
1353
|
+
},
|
|
1354
|
+
execute: async (args, exec) => {
|
|
1355
|
+
const session = sessionOf(exec);
|
|
1356
|
+
if (session === void 0) {
|
|
1357
|
+
return resultOf(NO_ORIGIN, false, "This tool needs an agent session, and the call carries none. Run it as an agent tool call.");
|
|
1358
|
+
}
|
|
1359
|
+
const read = await readOrigin(deps, session.id);
|
|
1360
|
+
if (read.problem !== void 0)
|
|
1361
|
+
return resultOf(read.origin, false, read.problem);
|
|
1362
|
+
const origin = read.origin;
|
|
1363
|
+
if (origin.kind === "inherited") {
|
|
1364
|
+
return resultOf(origin, false, inheritedHelp(origin.ownerSessionId, session.cwd));
|
|
1365
|
+
}
|
|
1366
|
+
if (origin.kind === "none") {
|
|
1367
|
+
return resultOf(origin, false, "No worktree is bound to this session, so there is nothing to unbind.");
|
|
1368
|
+
}
|
|
1369
|
+
const removeDirectory = argBool(args, "removeDirectory");
|
|
1370
|
+
const force = argBool(args, "force");
|
|
1371
|
+
if (force && !removeDirectory) {
|
|
1372
|
+
return resultOf(origin, false, "force only applies together with removeDirectory: true; the binding is unchanged.");
|
|
1373
|
+
}
|
|
1374
|
+
if (!removeDirectory) {
|
|
1375
|
+
const dropped2 = await deps.binding.drop(session.id);
|
|
1376
|
+
if (!dropped2.ok) {
|
|
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.");
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// lib/server/tools/impl/register/index.js
|
|
1403
|
+
function buildRegisterTool(deps) {
|
|
1404
|
+
return {
|
|
1405
|
+
name: "ws_worktree_register",
|
|
1406
|
+
description: "Bind an existing git worktree to THIS session, so the right-sidebar Files tab is rooted at that worktree once you open or refresh it. The session cwd does not change. Use this when the directory already exists - including a worktree you created with a bare git worktree add, which creates the directory but leaves the Files tab on the session cwd; use ws_worktree_create when the worktree still has to be created. Only the Files tab follows the binding: @ file references, present targets and the skill catalog still resolve against the session cwd, and under the workspace-write file policy the agent cannot write into the worktree. If this session inherits a root from a parent session, binding here overrides that inherited root for this session only. Whether the Files tab then actually follows the binding is reported by GET /api/dsh-worktree-sidebar/health as scopeTakeover.",
|
|
1407
|
+
parameters: {
|
|
1408
|
+
type: "object",
|
|
1409
|
+
properties: {
|
|
1410
|
+
worktree: {
|
|
1411
|
+
type: "string",
|
|
1412
|
+
description: "The worktree to bind: an absolute path, or a path relative to the session working directory."
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
required: ["worktree"],
|
|
1416
|
+
additionalProperties: false
|
|
1417
|
+
},
|
|
1418
|
+
output: {
|
|
1419
|
+
schema: RESULT_SCHEMA,
|
|
1420
|
+
render: (_args, value) => renderResult(value)
|
|
1421
|
+
},
|
|
1422
|
+
execute: async (args, exec) => {
|
|
1423
|
+
const session = sessionOf(exec);
|
|
1424
|
+
if (session === void 0) {
|
|
1425
|
+
return resultOf(NO_ORIGIN, false, "This tool needs an agent session, and the call carries none. Run it as an agent tool call.");
|
|
1426
|
+
}
|
|
1427
|
+
if (session.cwd === void 0) {
|
|
1428
|
+
return resultOf(NO_ORIGIN, false, "This session has no working directory, so it cannot be checked against a git repository.");
|
|
1429
|
+
}
|
|
1430
|
+
if (await deps.git.commonDir(session.cwd) === void 0) {
|
|
1431
|
+
return resultOf(NO_ORIGIN, false, "This session is not inside a git repository.");
|
|
1432
|
+
}
|
|
1433
|
+
const raw = argString(args, "worktree");
|
|
1434
|
+
if (raw === void 0) {
|
|
1435
|
+
return resultOf(NO_ORIGIN, false, "Missing required parameter: worktree.");
|
|
1436
|
+
}
|
|
1437
|
+
const read = await readOrigin(deps, session.id);
|
|
1438
|
+
if (read.problem !== void 0)
|
|
1439
|
+
return resultOf(read.origin, false, read.problem);
|
|
1440
|
+
const origin = read.origin;
|
|
1441
|
+
const target = resolveTarget(session.cwd, raw);
|
|
1442
|
+
const problem = directoryProblem(target);
|
|
1443
|
+
if (problem !== void 0) {
|
|
1444
|
+
return resultOf(origin, false, problem + await availableWorktrees(deps, session.cwd));
|
|
1445
|
+
}
|
|
1446
|
+
return bindWorktree(deps, session, session.cwd, target, deps.now(), origin);
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// lib/server/tools/impl/service/index.js
|
|
1452
|
+
var ToolsService = class {
|
|
1453
|
+
/** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */
|
|
1454
|
+
installed = false;
|
|
1455
|
+
deps;
|
|
1456
|
+
/** 每个 agent 的工具摘除器。释放时逐个调用并清空,下一次装配才装得进同一个 agent。 */
|
|
1457
|
+
perAgent = /* @__PURE__ */ new Map();
|
|
1458
|
+
unsubscribe;
|
|
1459
|
+
// 判定要走一次 git,是异步的;不串行化的话两个同时发布的 agent 会各自看到「尚未注册」,
|
|
1460
|
+
// 于是同一个 agent 被装两遍。
|
|
1461
|
+
chain = Promise.resolve();
|
|
1462
|
+
/**
|
|
1463
|
+
* 装配代数。在飞的异步判定跨过一次 release 就作废——否则它会把上一代的 deps 装进新一代,
|
|
1464
|
+
* 而那种串味的症状是「工具装了但绑的是上一个装配体的表」。
|
|
1465
|
+
*/
|
|
1466
|
+
generation = 0;
|
|
1467
|
+
/** 装配工具域。重复装配是编程错误,当场暴露。 */
|
|
1468
|
+
install(deps) {
|
|
1469
|
+
if (this.installed)
|
|
1470
|
+
throw new Error("dsh-worktree-sidebar: tools 域只能装配一次");
|
|
1471
|
+
this.installed = true;
|
|
1472
|
+
this.deps = deps;
|
|
1473
|
+
const generation = this.generation;
|
|
1474
|
+
const consider = (agent) => this.consider(generation, agent);
|
|
1475
|
+
this.unsubscribe = deps.agents.subscribe(consider);
|
|
1476
|
+
for (const agent of deps.agents.list())
|
|
1477
|
+
consider(agent);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* 卸载:退订、逐个摘掉每个 agent 的工具、丢掉映射与在飞的异步链,复位装配标记。重复调用无害。
|
|
1481
|
+
* 映射必须清空——留着它下一次装配会把同一个 agent 当成「已装过」直接跳过,工具永远装不上。
|
|
1482
|
+
*/
|
|
1483
|
+
release() {
|
|
1484
|
+
this.installed = false;
|
|
1485
|
+
this.generation += 1;
|
|
1486
|
+
this.deps = void 0;
|
|
1487
|
+
const unsubscribe = this.unsubscribe;
|
|
1488
|
+
this.unsubscribe = void 0;
|
|
1489
|
+
if (unsubscribe !== void 0) {
|
|
1490
|
+
try {
|
|
1491
|
+
unsubscribe();
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
for (const dispose of [...this.perAgent.values()].reverse()) {
|
|
1496
|
+
try {
|
|
1497
|
+
dispose();
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
this.perAgent.clear();
|
|
1502
|
+
this.chain = Promise.resolve();
|
|
1503
|
+
}
|
|
1504
|
+
/** 判定一个 agent 是否该装工具,该装就装。同一个 agent 只处理一次。 */
|
|
1505
|
+
consider(generation, agent) {
|
|
1506
|
+
this.chain = this.chain.then(async () => {
|
|
1507
|
+
const deps = this.deps;
|
|
1508
|
+
if (deps === void 0)
|
|
1509
|
+
return;
|
|
1510
|
+
if (generation !== this.generation || this.perAgent.has(agent.id))
|
|
1511
|
+
return;
|
|
1512
|
+
if (await repoOf(deps, agent) === void 0)
|
|
1513
|
+
return;
|
|
1514
|
+
if (generation !== this.generation || this.perAgent.has(agent.id))
|
|
1515
|
+
return;
|
|
1516
|
+
const definitions = [
|
|
1517
|
+
buildRegisterTool(deps),
|
|
1518
|
+
buildCreateTool(deps),
|
|
1519
|
+
buildRemoveTool(deps)
|
|
1520
|
+
];
|
|
1521
|
+
this.perAgent.set(agent.id, deps.agents.publish(agent, definitions));
|
|
1522
|
+
}).catch((cause) => {
|
|
1523
|
+
const deps = this.deps;
|
|
1524
|
+
if (deps === void 0 || generation !== this.generation)
|
|
1525
|
+
return;
|
|
1526
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1527
|
+
deps.logger.warn("dsh-worktree-sidebar: 工具注册失败 — " + reason);
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
var toolsService = new ToolsService();
|
|
1532
|
+
|
|
1533
|
+
// lib/server/tools/interface.js
|
|
1534
|
+
function installTools(deps) {
|
|
1535
|
+
toolsService.install(deps);
|
|
1536
|
+
}
|
|
1537
|
+
function releaseTools() {
|
|
1538
|
+
toolsService.release();
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// lib/index.js
|
|
1542
|
+
var name = "worktree-sidebar";
|
|
1543
|
+
var inject = ["webServer", "agents", "typert", "sessions"];
|
|
1544
|
+
async function apply(ctx, config = {}) {
|
|
1545
|
+
const host = {
|
|
1546
|
+
logger: ctx.logger,
|
|
1547
|
+
register: (route) => ctx.webServer.register(route),
|
|
1548
|
+
agents: bindAgents({
|
|
1549
|
+
on: (event, handler) => ctx.on(event, handler),
|
|
1550
|
+
all: () => ctx.agents.list()
|
|
1551
|
+
}),
|
|
1552
|
+
typert: bindTypert(ctx.typert.lookups),
|
|
1553
|
+
sessions: bindSessions(ctx.sessions, () => storedSessionsOf(ctx)),
|
|
1554
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
1555
|
+
};
|
|
1556
|
+
const disposers = await assemble(host, config);
|
|
1557
|
+
ctx.effect(() => () => safeDisposeAll(disposers));
|
|
1558
|
+
}
|
|
1559
|
+
function storedSessionsOf(ctx) {
|
|
1560
|
+
return ctx.get("sessionPersistence");
|
|
1561
|
+
}
|
|
1562
|
+
async function assemble(host, config) {
|
|
1563
|
+
const disposers = [];
|
|
1564
|
+
if (config.enabled === false)
|
|
1565
|
+
return disposers;
|
|
1566
|
+
try {
|
|
1567
|
+
installBinding({ logger: host.logger, file: bindingsFile() });
|
|
1568
|
+
disposers.push(releaseBinding);
|
|
1569
|
+
installGit({ exec: gitExec });
|
|
1570
|
+
disposers.push(releaseGit);
|
|
1571
|
+
installScope({
|
|
1572
|
+
logger: host.logger,
|
|
1573
|
+
binding: interface_exports,
|
|
1574
|
+
git: interface_exports2,
|
|
1575
|
+
typert: host.typert,
|
|
1576
|
+
sessions: host.sessions
|
|
1577
|
+
});
|
|
1578
|
+
disposers.push(releaseScope);
|
|
1579
|
+
installTools({
|
|
1580
|
+
logger: host.logger,
|
|
1581
|
+
binding: interface_exports,
|
|
1582
|
+
git: interface_exports2,
|
|
1583
|
+
scope: interface_exports3,
|
|
1584
|
+
agents: host.agents,
|
|
1585
|
+
now: host.now
|
|
1586
|
+
});
|
|
1587
|
+
disposers.push(releaseTools);
|
|
1588
|
+
installApi({
|
|
1589
|
+
register: host.register,
|
|
1590
|
+
logger: host.logger,
|
|
1591
|
+
binding: interface_exports,
|
|
1592
|
+
scope: interface_exports3
|
|
1593
|
+
});
|
|
1594
|
+
disposers.push(releaseApi);
|
|
1595
|
+
} catch (cause) {
|
|
1596
|
+
await safeDisposeAll(disposers);
|
|
1597
|
+
throw cause;
|
|
1598
|
+
}
|
|
1599
|
+
return disposers;
|
|
1600
|
+
}
|
|
1601
|
+
async function safeDisposeAll(disposers) {
|
|
1602
|
+
for (const dispose of [...disposers].reverse()) {
|
|
1603
|
+
try {
|
|
1604
|
+
await dispose();
|
|
1605
|
+
} catch {
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
export {
|
|
1610
|
+
ROUTES,
|
|
1611
|
+
apply,
|
|
1612
|
+
inject,
|
|
1613
|
+
name
|
|
1614
|
+
};
|