@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.11-beta.3

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 (68) hide show
  1. package/clients/dsh-remote/agent/README.md +412 -0
  2. package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  3. package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  4. package/clients/dsh-remote/agent/codex.mjs +736 -0
  5. package/clients/dsh-remote/agent/detect.mjs +399 -0
  6. package/clients/dsh-remote/agent/events.mjs +511 -0
  7. package/clients/dsh-remote/agent/gemini.mjs +503 -0
  8. package/clients/dsh-remote/agent/index.mjs +339 -0
  9. package/clients/dsh-remote/agent/opencode.mjs +628 -0
  10. package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  11. package/clients/dsh-remote/agent/proc.mjs +179 -0
  12. package/clients/dsh-remote/agent/version.mjs +148 -0
  13. package/clients/dsh-remote/approval-coordinator.mjs +150 -0
  14. package/clients/dsh-remote/decision-poller.mjs +235 -0
  15. package/clients/dsh-remote/dsh-bridge.mjs +214 -1
  16. package/clients/dsh-remote/push-reporter.mjs +54 -0
  17. package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
  18. package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
  19. package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
  20. package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
  21. package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
  22. package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
  23. package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
  24. package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
  25. package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
  26. package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
  27. package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
  28. package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
  29. package/clients/dsh-remote/test/wecom-bot.test.mjs +1438 -0
  30. package/clients/dsh-remote/wecom-bot.mjs +2444 -0
  31. package/dsh-setup.mjs +18 -3
  32. package/package.json +2 -2
  33. package/packages/dsh-remote-web/lib/client.js +1048 -223
  34. package/packages/dsh-remote-web/lib/index.js +299 -2
  35. package/packages/dsh-remote-web/package.json +1 -1
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  39. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
  40. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
  41. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
  42. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
  43. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
  44. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
  45. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  46. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
  47. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
  48. package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
  49. package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
  50. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
  51. package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
  52. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
  53. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
  54. package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
  55. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
  56. package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
  57. package/packages/dsh-remote-web/test/im-bots.test.mjs +359 -0
  58. package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
  59. package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
  60. package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -0
  61. package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
  62. package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
  63. package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +10 -1
  64. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
  65. package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
  66. package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
  67. package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
  68. package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 事件映射最小集契约测试(M2 第 3 周 B 项)。
4
+ *
5
+ * 守四件事(每条都对应一个真实风险):
6
+ * ① **每类真实 stream-json 行都能映射**(夹具直接抄自第 1 周实测输出形态);
7
+ * ② **未知形状不许崩**:未知 type/subtype、坏 JSON、超长行 → 计数丢弃,不抛错;
8
+ * ③ **不记录命令原文**:tool.call 只带输入字节数;tool.result 只带截断预览;
9
+ * ④ **落盘失败静默**:sink 写盘抛错只计数 + 告警一次,绝不把异常抛回会话。
10
+ *
11
+ * 用法: node --test clients/dsh-remote/test/agent-events.test.mjs
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import fsSync from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import test from "node:test";
18
+
19
+ import {
20
+ DEFAULT_MAX_LINE_BYTES,
21
+ EVENT_TYPES,
22
+ createEventMapper,
23
+ createJsonlSink,
24
+ mapStreamJsonLine,
25
+ turnReasonOf
26
+ } from "../agent/events.mjs";
27
+
28
+ const NOW = 1_700_000_000_000;
29
+ const at = () => NOW;
30
+
31
+ const line = (obj) => JSON.stringify(obj);
32
+
33
+ /** 真实 claude 2.1.318 的 `system/init` 行(裁剪后,保留关键字段)。 */
34
+ const INIT = {
35
+ type: "system",
36
+ subtype: "init",
37
+ cwd: "/tmp/proj",
38
+ session_id: "sess-1",
39
+ tools: ["Bash", "Read", "Write"],
40
+ mcp_servers: [],
41
+ model: "some-model",
42
+ permissionMode: "default"
43
+ };
44
+
45
+ test("status:system/init → status(working) + 会话/权限模式/工具数(不带 model)", () => {
46
+ const r = mapStreamJsonLine(line(INIT), { now: at });
47
+ assert.equal(r.ok, true);
48
+ assert.equal(r.events.length, 1);
49
+ const ev = r.events[0];
50
+ assert.equal(ev.type, EVENT_TYPES.STATUS);
51
+ assert.equal(ev.status, "working");
52
+ assert.equal(ev.sessionId, "sess-1");
53
+ assert.equal(ev.permissionMode, "default");
54
+ assert.equal(ev.toolCount, 3);
55
+ assert.equal(ev.model, undefined, "不落 model(可能暴露网关配置,也不给日志增噪)");
56
+ assert.equal(ev.at, NOW);
57
+ assert.equal(ev.v, 1);
58
+ });
59
+
60
+ test("status:钩子生命周期(hook_started / hook_response)→ status(hook),不带 output 内容", () => {
61
+ const a = mapStreamJsonLine(line({
62
+ type: "system", subtype: "hook_started", hook_event: "PermissionRequest", hook_id: "h1", session_id: "s"
63
+ }), { now: at });
64
+ assert.equal(a.events[0].status, "hook");
65
+ assert.equal(a.events[0].hookPhase, "started");
66
+ assert.equal(a.events[0].hookEvent, "PermissionRequest");
67
+
68
+ const b = mapStreamJsonLine(line({
69
+ type: "system", subtype: "hook_response", hook_event: "PermissionRequest",
70
+ output: "{\"hookSpecificOutput\":{\"decision\":\"allow\"}}", session_id: "s"
71
+ }), { now: at });
72
+ assert.equal(b.events[0].hookPhase, "finished");
73
+ assert.equal(JSON.stringify(b.events[0]).includes("hookSpecificOutput"), false, "钩子原样输出不进事件");
74
+ });
75
+
76
+ test("status:api_retry → status(retrying);thinking_tokens 直接忽略(不算 unknown)", () => {
77
+ const retry = mapStreamJsonLine(line({ type: "system", subtype: "api_retry", attempt: 2, retry_delay_ms: 500 }), { now: at });
78
+ assert.equal(retry.events[0].status, "retrying");
79
+ assert.equal(retry.events[0].attempt, 2);
80
+
81
+ const think = mapStreamJsonLine(line({ type: "system", subtype: "thinking_tokens", tokens: 10 }), { now: at });
82
+ assert.deepEqual(think, { ok: true, events: [] }, "thinking_tokens 一条会话几十条,落盘没价值");
83
+ });
84
+
85
+ test("status:未知 system subtype → ok:false(上层计数丢弃,不崩)", () => {
86
+ const r = mapStreamJsonLine(line({ type: "system", subtype: "brand_new_thing" }), { now: at });
87
+ assert.equal(r.ok, false);
88
+ assert.match(r.reason, /unknown-system-subtype/);
89
+ });
90
+
91
+ test("status:permission_denied(实测 2.1.318 的拒绝回执)→ status(denied) + 理由,不带命令原文", () => {
92
+ // 夹具抄自真实输出(只裁剪了 uuid/时间相关字段)
93
+ const raw = {
94
+ type: "system",
95
+ subtype: "permission_denied",
96
+ tool_name: "Bash",
97
+ tool_use_id: "call_00_x",
98
+ decision_reason_type: "hook",
99
+ decision_reason: "已按拒绝处理",
100
+ message: "已按拒绝处理",
101
+ session_id: "sess-1"
102
+ };
103
+ const r = mapStreamJsonLine(line(raw), { now: at });
104
+ assert.equal(r.ok, true, "这是实测存在的真实事件,不能算 unknown");
105
+ const ev = r.events[0];
106
+ assert.equal(ev.type, EVENT_TYPES.STATUS);
107
+ assert.equal(ev.status, "denied");
108
+ assert.equal(ev.toolName, "Bash");
109
+ assert.equal(ev.toolCallId, "call_00_x");
110
+ assert.equal(ev.reasonType, "hook");
111
+ assert.match(ev.reason, /拒绝/);
112
+ assert.equal(JSON.stringify(ev).includes("rm -rf"), false);
113
+ });
114
+
115
+ test("message.delta:assistant 完整文本 → final:true", () => {
116
+ const r = mapStreamJsonLine(line({
117
+ type: "assistant",
118
+ message: { content: [{ type: "text", text: "命令的原始输出:ok" }] }
119
+ }), { now: at });
120
+ assert.equal(r.events.length, 1);
121
+ assert.equal(r.events[0].type, EVENT_TYPES.MESSAGE_DELTA);
122
+ assert.equal(r.events[0].text, "命令的原始输出:ok");
123
+ assert.equal(r.events[0].final, true);
124
+ });
125
+
126
+ test("message.delta:stream_event 的 text_delta → final:false(流式增量)", () => {
127
+ const r = mapStreamJsonLine(line({
128
+ type: "stream_event",
129
+ event: { type: "content_block_delta", delta: { type: "text_delta", text: "命" } }
130
+ }), { now: at });
131
+ assert.equal(r.events[0].type, EVENT_TYPES.MESSAGE_DELTA);
132
+ assert.equal(r.events[0].final, false);
133
+ assert.equal(r.events[0].text, "命");
134
+
135
+ const other = mapStreamJsonLine(line({ type: "stream_event", event: { type: "message_start" } }), { now: at });
136
+ assert.equal(other.ok, false);
137
+ });
138
+
139
+ test("tool.call:只带 id/name/kind/输入字节数 —— **绝不带命令原文**", () => {
140
+ const command = "curl -H 'Authorization: Bearer sk-secret' http://example.com";
141
+ const r = mapStreamJsonLine(line({
142
+ type: "assistant",
143
+ message: {
144
+ content: [
145
+ { type: "text", text: "我来执行" },
146
+ { type: "tool_use", id: "call_1", name: "Bash", input: { command, description: "fetch" } }
147
+ ]
148
+ }
149
+ }), { now: at });
150
+ assert.equal(r.events.length, 2);
151
+ const call = r.events[1];
152
+ assert.equal(call.type, EVENT_TYPES.TOOL_CALL);
153
+ assert.equal(call.toolCallId, "call_1");
154
+ assert.equal(call.name, "Bash");
155
+ assert.equal(call.kind, "execute");
156
+ assert.ok(call.inputBytes > 0);
157
+ const dump = JSON.stringify(call);
158
+ assert.equal(dump.includes("sk-secret"), false, "命令原文绝不能进事件");
159
+ assert.equal(dump.includes("curl"), false, "命令原文绝不能进事件");
160
+ });
161
+
162
+ test("tool.call:文件类工具 → kind=edit/read/search", () => {
163
+ const mk = (name) =>
164
+ mapStreamJsonLine(line({
165
+ type: "assistant",
166
+ message: { content: [{ type: "tool_use", id: "c", name, input: { file_path: "/tmp/a.txt" } }] }
167
+ }), { now: at }).events[0];
168
+ assert.equal(mk("Write").kind, "edit");
169
+ assert.equal(mk("Read").kind, "read");
170
+ assert.equal(mk("Grep").kind, "search");
171
+ assert.equal(mk("SomeNewTool").kind, "other", "未知工具落 other,不猜");
172
+ });
173
+
174
+ test("tool.result:string / 数组 content 都能取文本;is_error 透传;预览截断", () => {
175
+ const str = mapStreamJsonLine(line({
176
+ type: "user",
177
+ message: { content: [{ type: "tool_result", tool_use_id: "call_1", is_error: false, content: "hello" }] }
178
+ }), { now: at });
179
+ assert.equal(str.events[0].type, EVENT_TYPES.TOOL_RESULT);
180
+ assert.equal(str.events[0].toolCallId, "call_1");
181
+ assert.equal(str.events[0].isError, false);
182
+ assert.equal(str.events[0].outputBytes, 5);
183
+ assert.equal(str.events[0].outputPreview, "hello");
184
+
185
+ const arr = mapStreamJsonLine(line({
186
+ type: "user",
187
+ message: {
188
+ content: [{
189
+ type: "tool_result", tool_use_id: "call_2", is_error: true,
190
+ content: [{ type: "text", text: "hook-denied: 验证用拒绝" }]
191
+ }]
192
+ }
193
+ }), { now: at });
194
+ assert.equal(arr.events[0].isError, true);
195
+ assert.match(arr.events[0].outputPreview, /hook-denied/);
196
+
197
+ const long = mapStreamJsonLine(line({
198
+ type: "user",
199
+ message: { content: [{ type: "tool_result", tool_use_id: "c", content: "x".repeat(5000) }] }
200
+ }), { now: at, maxPreviewChars: 100 });
201
+ assert.equal(long.events[0].outputBytes, 5000);
202
+ assert.equal(long.events[0].outputPreview.length, 100, "预览必须截断");
203
+
204
+ const off = mapStreamJsonLine(line({
205
+ type: "user",
206
+ message: { content: [{ type: "tool_result", tool_use_id: "c", content: "secret-output" }] }
207
+ }), { now: at, maxPreviewChars: 0 });
208
+ assert.equal(off.events[0].outputPreview, "", "maxPreviewChars=0 时完全不带输出内容");
209
+ });
210
+
211
+ test("turn.end:success → completed;is_error → error;max_turns / interrupted / aborted", () => {
212
+ assert.equal(turnReasonOf({ subtype: "success", is_error: false }), "completed");
213
+ assert.equal(turnReasonOf({ subtype: "success", terminal_reason: "aborted" }), "aborted");
214
+ assert.equal(turnReasonOf({ subtype: "success", terminal_reason: "interrupted" }), "interrupted");
215
+ assert.equal(turnReasonOf({ subtype: "error_during_execution", is_error: true }), "error");
216
+ assert.equal(turnReasonOf({ subtype: "error_max_turns", is_error: true }), "error");
217
+ assert.equal(turnReasonOf({ subtype: "x", is_error: false }), "x");
218
+
219
+ const r = mapStreamJsonLine(line({
220
+ type: "result", subtype: "success", is_error: false,
221
+ duration_ms: 1234, num_turns: 2, session_id: "sess-1", result: "完成"
222
+ }), { now: at });
223
+ const ev = r.events[0];
224
+ assert.equal(ev.type, EVENT_TYPES.TURN_END);
225
+ assert.equal(ev.reason, "completed");
226
+ assert.equal(ev.durationMs, 1234);
227
+ assert.equal(ev.numTurns, 2);
228
+ assert.equal(ev.sessionId, "sess-1");
229
+ assert.equal(JSON.stringify(ev).includes("完成"), false, "最终文本靠 message.delta,不重复塞进 turn.end");
230
+ });
231
+
232
+ test("坏输入:未知 type / 坏 JSON / 空行 / 超长行 —— 全部返回 ok:false 或空,不抛错", () => {
233
+ assert.equal(mapStreamJsonLine(line({ type: "brand_new" }), { now: at }).reason, "unknown-type:brand_new");
234
+ assert.equal(mapStreamJsonLine("{not json", { now: at }).reason, "bad-json");
235
+ assert.equal(mapStreamJsonLine(" ", { now: at }).reason, "empty");
236
+ assert.equal(mapStreamJsonLine("42", { now: at }).reason, "not-object");
237
+
238
+ const huge = JSON.stringify({ type: "assistant", message: { content: [{ type: "text", text: "x".repeat(1000) }] } });
239
+ const r = mapStreamJsonLine(huge, { now: at, maxLineBytes: 100 });
240
+ assert.equal(r.reason, "too-long");
241
+ assert.ok(DEFAULT_MAX_LINE_BYTES > 100);
242
+ });
243
+
244
+ test("mapper:seq 自增 + byType/byReason 统计 + onEvent 回调", () => {
245
+ const seen = [];
246
+ const m = createEventMapper({ now: at, onEvent: (e) => seen.push(e) });
247
+ m.writeLine(line(INIT));
248
+ m.writeLine(line({ type: "assistant", message: { content: [{ type: "text", text: "hi" }] } }));
249
+ m.writeLine(line({ type: "result", subtype: "success", is_error: false }));
250
+ m.writeLine("{bad");
251
+ m.writeLine(line({ type: "nope" }));
252
+
253
+ const s = m.stats();
254
+ assert.equal(s.lines, 5);
255
+ assert.equal(s.events, 3);
256
+ assert.equal(s.badJson, 1);
257
+ assert.equal(s.unknown, 1);
258
+ assert.deepEqual(s.unknownReasons, { "unknown-type:nope": 1 }, "未识别原因要能排障");
259
+ assert.equal(s.byType[EVENT_TYPES.STATUS], 1);
260
+ assert.equal(s.byType[EVENT_TYPES.MESSAGE_DELTA], 1);
261
+ assert.equal(s.byType[EVENT_TYPES.TURN_END], 1);
262
+ assert.equal(s.byReason.completed, 1);
263
+ assert.deepEqual(seen.map((e) => e.seq), [1, 2, 3], "seq 连续自增");
264
+ });
265
+
266
+ test("mapper:坏行不中断后续行(一行意外输出不能把整条会话采集带死)", () => {
267
+ const m = createEventMapper({ now: at });
268
+ m.writeLine("not json at all");
269
+ m.writeLine(line({ type: "result", subtype: "success", is_error: false }));
270
+ assert.equal(m.stats().events, 1);
271
+ assert.equal(m.stats().badJson, 1);
272
+ });
273
+
274
+ test("sink:不给 filePath → 默认不启用,什么都不写", () => {
275
+ const sink = createJsonlSink({});
276
+ assert.equal(sink.enabled, false);
277
+ assert.equal(sink.write({ type: "status" }), false);
278
+ assert.equal(sink.stats().events, 0);
279
+ });
280
+
281
+ test("sink:真写 jsonl(append-only,一行一事件,可回读)", () => {
282
+ const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "agent-events-"));
283
+ const file = path.join(dir, "events.jsonl");
284
+ try {
285
+ const sink = createJsonlSink({ filePath: file, now: at });
286
+ assert.equal(sink.enabled, true);
287
+ assert.equal(sink.write({ v: 1, seq: 1, type: "status" }), true);
288
+ assert.equal(sink.write({ v: 1, seq: 2, type: "turn.end" }), true);
289
+ const lines = fsSync.readFileSync(file, "utf8").trim().split("\n");
290
+ assert.equal(lines.length, 2);
291
+ assert.deepEqual(JSON.parse(lines[0]), { v: 1, seq: 1, type: "status" });
292
+ assert.equal(sink.stats().events, 2);
293
+ assert.ok(sink.stats().bytes > 0);
294
+ } finally {
295
+ fsSync.rmSync(dir, { recursive: true, force: true });
296
+ }
297
+ });
298
+
299
+ test("sink:落盘失败**静默**(计数 + 只告警一次,绝不抛回会话)", () => {
300
+ const logs = [];
301
+ let writes = 0;
302
+ const sink = createJsonlSink({
303
+ filePath: "/definitely/not/writable/events.jsonl",
304
+ now: at,
305
+ log: (e) => logs.push(e),
306
+ fs: {
307
+ appendFileSync: () => {
308
+ writes += 1;
309
+ throw Object.assign(new Error("ENOSPC: no space left"), { code: "ENOSPC" });
310
+ }
311
+ }
312
+ });
313
+ assert.doesNotThrow(() => sink.write({ type: "status" }));
314
+ assert.doesNotThrow(() => sink.write({ type: "status" }));
315
+ assert.equal(writes, 2);
316
+ const s = sink.stats();
317
+ assert.equal(s.writeErrors, 2);
318
+ assert.equal(s.lastError, "ENOSPC");
319
+ assert.equal(logs.length, 1, "只告警一次,不刷屏");
320
+ assert.equal(logs[0].event, "events.sink-error");
321
+ });
322
+
323
+ test("mapper + sink 串联:写入文件的事件与返回值一致", () => {
324
+ const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "agent-events2-"));
325
+ const file = path.join(dir, "e.jsonl");
326
+ try {
327
+ const sink = createJsonlSink({ filePath: file, now: at });
328
+ const m = createEventMapper({ sink, now: at });
329
+ const out = m.writeLine(line({
330
+ type: "assistant",
331
+ message: { content: [{ type: "tool_use", id: "c1", name: "Bash", input: { command: "rm -rf x" } }] }
332
+ }));
333
+ assert.equal(out.length, 1);
334
+ const written = JSON.parse(fsSync.readFileSync(file, "utf8").trim());
335
+ assert.equal(written.toolCallId, "c1");
336
+ assert.equal(JSON.stringify(written).includes("rm -rf"), false);
337
+ } finally {
338
+ fsSync.rmSync(dir, { recursive: true, force: true });
339
+ }
340
+ });