dsh-sidebar-row-menu 1.4.0 → 1.4.2

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/lib/host/route.js CHANGED
@@ -1,20 +1,23 @@
1
1
  /**
2
- * HTTP 边界:/open 与 /session/delete。
2
+ * HTTP 边界:/open、/session/delete 与 /workspace/delete。
3
3
  *
4
- * 浏览器端不能自己打开系统应用,也不该直接碰会话日志,所以这两件事都放在 host:
5
- * POST /dsh-sidebar-row-menu/open 打开目录(VS Code / 资源管理器)
6
- * POST /dsh-sidebar-row-menu/session/delete 永久删除会话(仅已归档,由 UI 把关)
4
+ * 浏览器端不能自己打开系统应用,也不该直接碰会话日志,所以这几件事都放在 host:
5
+ * POST /dsh-sidebar-row-menu/open 打开目录(VS Code / 资源管理器)
6
+ * POST /dsh-sidebar-row-menu/session/delete 永久删除会话(归档范围由 UI 把关)
7
+ * POST /dsh-sidebar-row-menu/workspace/delete 永久删除工作区及其全部会话
7
8
  */
8
9
 
9
10
  import { inspectOpenPath } from './paths.js';
10
11
  import { isLoopbackRequest } from './security.js';
11
12
  import { openInVscode, revealDirectory } from './launch.js';
12
13
  import { DELETE_BATCH_LIMIT, removeSessions } from './session-delete.js';
14
+ import { removeWorkspaceWithSessions } from './workspace-delete.js';
13
15
 
14
16
  /** 路由前缀;客户端半边硬编码同一个值。 */
15
17
  export const ROUTE_PREFIX = '/dsh-sidebar-row-menu';
16
18
  export const ROUTE_OPEN = `${ROUTE_PREFIX}/open`;
17
19
  export const ROUTE_DELETE = `${ROUTE_PREFIX}/session/delete`;
20
+ export const ROUTE_WORKSPACE_DELETE = `${ROUTE_PREFIX}/workspace/delete`;
18
21
  /** 请求体上限(只传路径或会话 id 列表,64KB 足够)。 */
19
22
  const MAX_BODY_BYTES = 64 * 1024;
20
23
 
@@ -171,8 +174,65 @@ export async function handleSessionDeleteRequest(request, response, ctx) {
171
174
  }
172
175
  }
173
176
 
174
- /** 构造 webServer 路由对象(kind=prefix)。 */
175
- export function createOpenRoute(ctx) {
177
+ /**
178
+ * 处理一次「删除工作区及其全部会话」请求。
179
+ * 请求体:{ workspaceId: string }。
180
+ * 200 返回 { ok, sessions, deleted, failed, workspaceDeleted, error?, code? }。
181
+ * 会话删除有失败时不会删工作区,返回 ok:false 让客户端保留工作区并提示重试。
182
+ */
183
+ export async function handleWorkspaceDeleteRequest(request, response, ctx) {
184
+ if (!isLoopbackRequest(request)) {
185
+ writeJson(response, 403, { ok: false, error: 'forbidden' });
186
+ return;
187
+ }
188
+ if (request.method !== 'POST') {
189
+ writeJson(response, 405, { ok: false, error: 'method-not-allowed' });
190
+ return;
191
+ }
192
+ if (pathnameOf(request) !== ROUTE_WORKSPACE_DELETE) {
193
+ writeJson(response, 404, { ok: false, error: 'not-found' });
194
+ return;
195
+ }
196
+
197
+ let payload;
198
+ try {
199
+ payload = await readJsonBody(request);
200
+ } catch {
201
+ writeJson(response, 400, { ok: false, error: 'bad-request' });
202
+ return;
203
+ }
204
+ const workspaceId = payload?.workspaceId;
205
+ if (typeof workspaceId !== 'string' || workspaceId.trim() === '') {
206
+ writeJson(response, 400, { ok: false, error: 'bad-request' });
207
+ return;
208
+ }
209
+
210
+ if (ctx === undefined || ctx === null || typeof ctx.get !== 'function') {
211
+ writeJson(response, 503, { ok: false, error: 'service-unavailable' });
212
+ return;
213
+ }
214
+ for (const name of ['sessions', 'sessionQuery', 'sessionPersistence', 'workspaceRegistry']) {
215
+ let available;
216
+ try {
217
+ available = ctx.get(name) !== undefined && ctx.get(name) !== null;
218
+ } catch {
219
+ available = false;
220
+ }
221
+ if (!available) {
222
+ writeJson(response, 503, { ok: false, error: 'service-unavailable' });
223
+ return;
224
+ }
225
+ }
226
+ try {
227
+ const outcome = await removeWorkspaceWithSessions(ctx, workspaceId);
228
+ writeJson(response, 200, outcome);
229
+ } catch (error) {
230
+ ctx.logger?.warn?.('[dsh-sidebar-row-menu] workspace delete failed:', error);
231
+ writeJson(response, 500, { ok: false, error: 'delete-failed' });
232
+ }
233
+ }
234
+
235
+ /** 构造 webServer 路由对象(kind=prefix)。 */export function createOpenRoute(ctx) {
176
236
  return {
177
237
  kind: 'prefix',
178
238
  path: ROUTE_PREFIX,
@@ -180,6 +240,7 @@ export function createOpenRoute(ctx) {
180
240
  const pathname = pathnameOf(request);
181
241
  if (pathname === ROUTE_OPEN) return handleOpenRequest(request, response);
182
242
  if (pathname === ROUTE_DELETE) return handleSessionDeleteRequest(request, response, ctx);
243
+ if (pathname === ROUTE_WORKSPACE_DELETE) return handleWorkspaceDeleteRequest(request, response, ctx);
183
244
  if (!isLoopbackRequest(request)) {
184
245
  writeJson(response, 403, { ok: false, error: 'forbidden' });
185
246
  return undefined;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 删除工作区及其下所有会话。
3
+ *
4
+ * 内置 `workspaces.delete(id)`(registry.delete)只摘掉工作区登记,会话会被
5
+ * 重新归到「未分组」。这里先按工作区的 sessionIds 永久删除每个会话日志,
6
+ * 全部成功后再删工作区登记;任何一个会话删除失败就保留工作区,避免留下
7
+ * 一堆没人管的「未分组」会话。
8
+ *
9
+ * 工作区目录本身不删(和内置行为一致),只删会话日志与工作区记录。
10
+ */
11
+
12
+ import { removeSessions } from './session-delete.js';
13
+
14
+ function service(ctx, name) {
15
+ try {
16
+ return ctx.get(name);
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 永久删除一个工作区及其全部会话。
24
+ * @param ctx - 宿主插件上下文。
25
+ * @param workspaceId - 工作区 id。
26
+ * @returns {Promise<{ok:boolean,sessions:number,deleted:number,failed:number,
27
+ * workspaceDeleted:boolean,missing?:boolean,code?:string,error:string}>}
28
+ */
29
+ export async function removeWorkspaceWithSessions(ctx, workspaceId) {
30
+ const registry = service(ctx, 'workspaceRegistry');
31
+ if (
32
+ registry === null
33
+ || registry === undefined
34
+ || typeof registry.get !== 'function'
35
+ || typeof registry.delete !== 'function'
36
+ ) {
37
+ return {
38
+ ok: false,
39
+ sessions: 0,
40
+ deleted: 0,
41
+ failed: 0,
42
+ workspaceDeleted: false,
43
+ code: 'unavailable',
44
+ error: 'workspace-registry-unavailable',
45
+ };
46
+ }
47
+
48
+ let workspace;
49
+ try {
50
+ workspace = registry.get(workspaceId);
51
+ } catch (error) {
52
+ return {
53
+ ok: false,
54
+ sessions: 0,
55
+ deleted: 0,
56
+ failed: 0,
57
+ workspaceDeleted: false,
58
+ code: 'lookup-failed',
59
+ error: error instanceof Error ? error.message : String(error),
60
+ };
61
+ }
62
+ if (workspace === null || workspace === undefined) {
63
+ return { ok: true, sessions: 0, deleted: 0, failed: 0, workspaceDeleted: false, missing: true, error: '' };
64
+ }
65
+
66
+ const ids = Array.isArray(workspace.sessionIds)
67
+ ? workspace.sessionIds.filter((id) => typeof id === 'string' && id.trim() !== '')
68
+ : [];
69
+ let deleted = 0;
70
+ if (ids.length > 0) {
71
+ const outcome = await removeSessions(ctx, ids);
72
+ deleted = outcome.deleted ?? 0;
73
+ if (outcome.ok !== true) {
74
+ return {
75
+ ok: false,
76
+ sessions: ids.length,
77
+ deleted,
78
+ failed: outcome.failed ?? Math.max(0, ids.length - deleted),
79
+ workspaceDeleted: false,
80
+ code: typeof outcome.code === 'string' ? outcome.code : 'session-delete-failed',
81
+ error: typeof outcome.error === 'string' ? outcome.error : '',
82
+ };
83
+ }
84
+ }
85
+
86
+ let workspaceDeleted = false;
87
+ try {
88
+ workspaceDeleted = (await registry.delete(workspaceId)) !== false;
89
+ } catch (error) {
90
+ return {
91
+ ok: false,
92
+ sessions: ids.length,
93
+ deleted,
94
+ failed: 0,
95
+ workspaceDeleted: false,
96
+ code: 'workspace-delete-failed',
97
+ error: error instanceof Error ? error.message : String(error),
98
+ };
99
+ }
100
+ return { ok: true, sessions: ids.length, deleted, failed: 0, workspaceDeleted, error: '' };
101
+ }
package/lib/index.js CHANGED
@@ -3,19 +3,22 @@
3
3
  *
4
4
  * 两件事:
5
5
  * 1. `/open` 让浏览器请宿主用系统命令打开目录;
6
- * 2. `/session/delete` 永久删除会话日志(仅 UI 对已归档会话开放)。
6
+ * 2. `/session/delete` 永久删除会话日志(默认由 UI 对已归档会话开放;
7
+ * 3. `/workspace/delete` 永久删除工作区及其下全部会话。
7
8
  *
8
- * 具体实现拆成四个模块:
9
+ * 具体实现拆成六个模块:
9
10
  *
10
- * lib/host/security.js loopback 请求围栏
11
- * lib/host/paths.js 路径校验与 vscode:// URL 编码
12
- * lib/host/launch.js VS Code / 资源管理器启动
13
- * lib/host/session-delete.js 停跑、摘运行时、删日志、清归档记录
14
- * lib/host/route.js HTTP 边界与状态码
11
+ * lib/host/security.js loopback 请求围栏
12
+ * lib/host/paths.js 路径校验与 vscode:// URL 编码
13
+ * lib/host/launch.js VS Code / 资源管理器启动
14
+ * lib/host/session-delete.js 停跑、摘运行时、删日志、清归档记录
15
+ * lib/host/workspace-delete.js 先删会话再摘工作区登记
16
+ * lib/host/route.js HTTP 边界与状态码
15
17
  *
16
18
  * 浏览器自己不能打开资源管理器,也不该直接删会话日志,所以这些必须放在 host。
17
19
  * 打开请求体:{ target: 'vscode' | 'explorer', path: '<绝对目录路径>' }
18
- * 删除请求体:{ sessionIds: ['<会话 id>', ...] }
20
+ * 删除会话请求体:{ sessionIds: ['<会话 id>', ...] }
21
+ * 删除工作区请求体:{ workspaceId: '<工作区 id>' }
19
22
  *
20
23
  * 删除流程参考 dsh-session-menu v0.1.3(MIT, big-head-pigeon),不引入 Typert 依赖。
21
24
  */
@@ -29,8 +32,9 @@ export const inject = ['webServer', 'sessions', 'sessionPersistence', 'sessionQu
29
32
  export { isLoopbackRequest } from './host/security.js';
30
33
  export { inspectOpenPath, toVscodeFileUrl } from './host/paths.js';
31
34
  export { resolveVscodeLauncher } from './host/launch.js';
32
- export { createOpenRoute, handleSessionDeleteRequest } from './host/route.js';
35
+ export { createOpenRoute, handleSessionDeleteRequest, handleWorkspaceDeleteRequest } from './host/route.js';
33
36
  export { DELETE_BATCH_LIMIT, removeSession, removeSessions } from './host/session-delete.js';
37
+ export { removeWorkspaceWithSessions } from './host/workspace-delete.js';
34
38
 
35
39
  /**
36
40
  * 注册路由。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidebar-row-menu",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "description": "DSH sidebar right-click menus: a self-composed menu for workspace rows, and the native slot menu opened at the pointer for session rows, with open-with and archived-session cleanup.",
6
6
  "keywords": [
@@ -60,7 +60,7 @@
60
60
  "scripts": {
61
61
  "test": "node --test test/host.test.mjs test/client.test.mjs",
62
62
  "test:local": "node --test --test-isolation=none test/host.test.mjs test/client.test.mjs",
63
- "check": "node --check lib/index.js && node --check lib/client.js && node --check lib/host/paths.js && node --check lib/host/security.js && node --check lib/host/launch.js && node --check lib/host/route.js && node --check lib/host/session-delete.js",
63
+ "check": "node --check lib/index.js && node --check lib/client.js && node --check lib/host/paths.js && node --check lib/host/security.js && node --check lib/host/launch.js && node --check lib/host/route.js && node --check lib/host/session-delete.js && node --check lib/host/workspace-delete.js",
64
64
  "build": "npm pack",
65
65
  "marketplace:check": "node scripts/marketplace-preflight.mjs",
66
66
  "prepack": "npm run check",