@dyyz1993/create-agent 2.0.0 → 2.0.1

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 (141) hide show
  1. package/package.json +1 -1
  2. package/src/lib/copy.ts +23 -1
  3. package/templates/agent/eslint.config.mjs +15 -2
  4. package/templates/agent/package.json +10 -2
  5. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  6. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  7. package/templates/agent/src/bun/index.ts +96 -41
  8. package/templates/agent/src/bun/three.d.ts +1 -0
  9. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  10. package/templates/agent/src/gateway/http-routes.ts +1 -1
  11. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  12. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  13. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  14. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  15. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  16. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  17. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  18. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  19. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  20. package/templates/agent/src/mainview/lib/api-client.ts +182 -172
  21. package/templates/agent/src/server-config.ts +34 -32
  22. package/templates/agent/src/server.ts +43 -54
  23. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  24. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  25. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  26. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  27. package/templates/agent/src/shared/handlers/debug.ts +8 -0
  28. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  29. package/templates/agent/src/shared/handlers/index.ts +1 -0
  30. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  31. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  32. package/templates/agent/src/shared/http-routes.ts +250 -0
  33. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  34. package/templates/agent/src/shared/lib/logger.ts +117 -95
  35. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  36. package/templates/agent/src/shared/modules/debug.ts +8 -0
  37. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  38. package/templates/agent/test-upload.txt +0 -0
  39. package/templates/agent/tsconfig.ipc.json +10 -0
  40. package/templates/agent/tsconfig.json +10 -2
  41. package/templates/chat/eslint.config.mjs +15 -2
  42. package/templates/chat/package.json +10 -2
  43. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  44. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  45. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  46. package/templates/chat/src/bun/index.ts +94 -44
  47. package/templates/chat/src/bun/three.d.ts +1 -0
  48. package/templates/chat/src/gateway/http-routes.ts +1 -1
  49. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  50. package/templates/chat/src/mainview/App.tsx +6 -0
  51. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  52. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  53. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  54. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  55. package/templates/chat/src/mainview/lib/api-client.ts +183 -156
  56. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  57. package/templates/chat/src/mainview/main.tsx +4 -3
  58. package/templates/chat/src/server-config.ts +33 -31
  59. package/templates/chat/src/server.ts +43 -60
  60. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  61. package/templates/chat/src/shared/handlers/debug.ts +8 -0
  62. package/templates/chat/src/shared/handlers/index.ts +1 -0
  63. package/templates/chat/src/shared/http-routes.ts +250 -0
  64. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  65. package/templates/chat/src/shared/lib/logger.ts +108 -95
  66. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  67. package/templates/chat/src/shared/modules/debug.ts +8 -0
  68. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  69. package/templates/chat/test-upload.txt +0 -0
  70. package/templates/chat/tsconfig.ipc.json +10 -0
  71. package/templates/general/eslint.config.mjs +15 -2
  72. package/templates/general/package.json +10 -2
  73. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  74. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  75. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  76. package/templates/general/src/bun/index.ts +94 -44
  77. package/templates/general/src/bun/three.d.ts +1 -0
  78. package/templates/general/src/gateway/http-routes.ts +1 -1
  79. package/templates/general/src/gateway/ws-handler.ts +84 -56
  80. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  81. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  82. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  83. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  84. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  85. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  86. package/templates/general/src/mainview/lib/api-client.ts +183 -156
  87. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  88. package/templates/general/src/mainview/main.tsx +4 -3
  89. package/templates/general/src/server-config.ts +33 -31
  90. package/templates/general/src/server.ts +43 -60
  91. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  92. package/templates/general/src/shared/handlers/chat.ts +189 -155
  93. package/templates/general/src/shared/handlers/debug.ts +8 -0
  94. package/templates/general/src/shared/handlers/feed.ts +38 -40
  95. package/templates/general/src/shared/handlers/index.ts +1 -0
  96. package/templates/general/src/shared/http-routes.ts +250 -0
  97. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  98. package/templates/general/src/shared/lib/logger.ts +117 -95
  99. package/templates/general/src/shared/lib/web-server.ts +128 -0
  100. package/templates/general/src/shared/modules/debug.ts +8 -0
  101. package/templates/general/src/shared/rpc-schema.ts +12 -2
  102. package/templates/general/test-upload.txt +0 -0
  103. package/templates/general/tsconfig.ipc.json +10 -0
  104. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  105. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  106. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  107. package/templates/shared/__tests__/logger.test.ts +186 -0
  108. package/templates/shared/components/ErrorBoundary.tsx +58 -0
  109. package/templates/shared/env.d.ts +11 -0
  110. package/templates/shared/http-routes.ts +230 -0
  111. package/templates/shared/logger.ts +129 -0
  112. package/templates/shared/test-upload.txt +0 -0
  113. package/templates/shared/vite-base.config.ts +92 -0
  114. package/templates/shared/vitest-base.config.ts +42 -0
  115. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  116. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  117. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  118. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  119. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  120. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  121. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  122. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  123. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  124. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  125. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  126. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  127. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  128. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  129. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  130. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  131. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  132. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  133. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  134. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  135. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  136. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  137. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  138. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  139. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  140. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  141. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { unlink } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+
7
+ vi.mock("../../lib/logger", () => ({
8
+ createLogger: () => ({
9
+ info: vi.fn(),
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ debug: vi.fn(),
13
+ }),
14
+ }));
15
+
16
+ function createMockServer() {
17
+ const handlers = new Map<string, (params: unknown) => Promise<unknown>>();
18
+ return {
19
+ register: vi.fn((method: string, handler: (params: unknown) => Promise<unknown>) => {
20
+ handlers.set(method, handler);
21
+ }),
22
+ emitEvent: vi.fn(),
23
+ handlers,
24
+ async call(method: string, params: unknown) {
25
+ const h = handlers.get(method);
26
+ if (!h) throw new Error(`No handler for ${method}`);
27
+ return h(params);
28
+ },
29
+ };
30
+ }
31
+
32
+ const DESKTOP_HISTORY_PATH = join(homedir(), ".pi-agent", "chat-history-desktop.json");
33
+
34
+ describe("Chat Handler 并发安全", () => {
35
+ beforeEach(() => {
36
+ vi.resetModules();
37
+ });
38
+
39
+ afterEach(async () => {
40
+ if (existsSync(DESKTOP_HISTORY_PATH)) {
41
+ await unlink(DESKTOP_HISTORY_PATH).catch(() => {});
42
+ }
43
+ });
44
+
45
+ it("两个独立的 RPCServer 应注册各自的 handler", async () => {
46
+ const { register } = await import("../chat");
47
+ const serverA = createMockServer();
48
+ const serverB = createMockServer();
49
+
50
+ register(serverA as any, { platform: "desktop" });
51
+ register(serverB as any, { platform: "web" });
52
+
53
+ expect(serverA.register).toHaveBeenCalledWith("chat.list", expect.any(Function));
54
+ expect(serverA.register).toHaveBeenCalledWith("chat.send", expect.any(Function));
55
+ expect(serverB.register).toHaveBeenCalledWith("chat.list", expect.any(Function));
56
+ expect(serverB.register).toHaveBeenCalledWith("chat.send", expect.any(Function));
57
+ });
58
+
59
+ it("server A 的消息不影响 server B 的消息列表", async () => {
60
+ const { register } = await import("../chat");
61
+ const serverA = createMockServer();
62
+ const serverB = createMockServer();
63
+
64
+ register(serverA as any, { platform: "desktop" });
65
+ register(serverB as any, { platform: "web" });
66
+
67
+ await serverA.call("chat.send", { content: "hello from desktop" });
68
+ await serverB.call("chat.send", { content: "hello from web" });
69
+
70
+ const resultA = (await serverA.call("chat.list", { limit: 50 })) as {
71
+ messages: { content: string }[];
72
+ };
73
+ const resultB = (await serverB.call("chat.list", { limit: 50 })) as {
74
+ messages: { content: string }[];
75
+ };
76
+
77
+ const desktopContents = resultA.messages.map((m) => m.content);
78
+ const webContents = resultB.messages.map((m) => m.content);
79
+
80
+ expect(desktopContents).toContain("hello from desktop");
81
+ expect(webContents).toContain("hello from web");
82
+ expect(desktopContents).not.toContain("hello from web");
83
+ expect(webContents).not.toContain("hello from desktop");
84
+ });
85
+
86
+ it("同一 server 内的消息按顺序写入", async () => {
87
+ const { register } = await import("../chat");
88
+ const server = createMockServer();
89
+ register(server as any, { platform: "desktop" });
90
+
91
+ await server.call("chat.send", { content: "msg1" });
92
+ await server.call("chat.send", { content: "msg2" });
93
+ await server.call("chat.send", { content: "msg3" });
94
+
95
+ const result = (await server.call("chat.list", { limit: 50 })) as {
96
+ messages: { role: string; content: string }[];
97
+ };
98
+
99
+ const userMsgs = result.messages.filter((m) => m.role === "user").map((m) => m.content);
100
+ expect(userMsgs).toEqual(["msg1", "msg2", "msg3"]);
101
+ });
102
+
103
+ it("存储路径应包含平台标识(desktop/web)或唯一 ID", async () => {
104
+ const { getStoragePathFor } = await import("../chat");
105
+ const desktopPath = getStoragePathFor("desktop");
106
+ const webPath = getStoragePathFor("web");
107
+
108
+ expect(desktopPath).toContain("desktop");
109
+ expect(webPath).not.toBe(desktopPath);
110
+ expect(webPath).toMatch(/web/);
111
+ });
112
+
113
+ it("desktop 使用固定路径保留历史", async () => {
114
+ const { getStoragePathFor } = await import("../chat");
115
+ const path1 = getStoragePathFor("desktop");
116
+ const path2 = getStoragePathFor("desktop");
117
+ expect(path1).toBe(path2);
118
+ expect(path1).toContain("chat-history-desktop.json");
119
+ });
120
+
121
+ it("web 每次生成不同的路径", async () => {
122
+ const { getStoragePathFor } = await import("../chat");
123
+ const path1 = getStoragePathFor("web");
124
+ const path2 = getStoragePathFor("web");
125
+ expect(path1).not.toBe(path2);
126
+ });
127
+ });
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type { RPCServer } from "@dyyz1993/rpc-core";
3
+
4
+ vi.mock("../../lib/logger", () => ({
5
+ createLogger: () => ({
6
+ info: vi.fn(),
7
+ error: vi.fn(),
8
+ warn: vi.fn(),
9
+ debug: vi.fn(),
10
+ }),
11
+ }));
12
+
13
+ function createMockServer() {
14
+ const handlers: Record<string, Function> = {};
15
+ return {
16
+ handlers,
17
+ server: {
18
+ register: vi.fn((method: string, handler: Function) => {
19
+ handlers[method] = handler;
20
+ }),
21
+ emitEvent: vi.fn(),
22
+ } as unknown as RPCServer,
23
+ };
24
+ }
25
+
26
+ // --- Timer ---
27
+ describe("Handler 隔离性 - timer", () => {
28
+ beforeEach(() => {
29
+ vi.useFakeTimers();
30
+ vi.resetModules();
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.useRealTimers();
35
+ });
36
+
37
+ it("两个独立的 RPCServer 各自维护独立的定时器状态", async () => {
38
+ const a = createMockServer();
39
+ const b = createMockServer();
40
+
41
+ const { register } = await import("../timer");
42
+ register(a.server, { platform: "web" });
43
+ register(b.server, { platform: "web" });
44
+
45
+ const resultA = await a.handlers["timer.start"]({});
46
+ expect(resultA).toEqual({ started: true });
47
+
48
+ const resultB = await b.handlers["timer.start"]({});
49
+ expect(resultB).toEqual({ started: true });
50
+
51
+ await a.handlers["timer.stop"]({});
52
+ const resultB2 = await b.handlers["timer.start"]({});
53
+ expect(resultB2).toEqual({ alreadyRunning: true });
54
+ });
55
+
56
+ it("server A 停止定时器不影响 server B 的定时器", async () => {
57
+ const a = createMockServer();
58
+ const b = createMockServer();
59
+
60
+ const { register } = await import("../timer");
61
+ register(a.server, { platform: "web" });
62
+ register(b.server, { platform: "web" });
63
+
64
+ await a.handlers["timer.start"]({});
65
+ await b.handlers["timer.start"]({});
66
+
67
+ await a.handlers["timer.stop"]({});
68
+
69
+ vi.advanceTimersByTime(2000);
70
+ expect(b.server.emitEvent).toHaveBeenCalled();
71
+ });
72
+ });
73
+
74
+ // --- Bash ---
75
+ describe("Handler 隔离性 - bash", () => {
76
+ beforeEach(() => {
77
+ vi.resetModules();
78
+ vi.stubGlobal("Bun", { spawn: vi.fn() });
79
+ vi.stubGlobal(
80
+ "Response",
81
+ class {
82
+ private src: any;
83
+ constructor(src: any) {
84
+ this.src = src;
85
+ }
86
+ async text() {
87
+ if (this.src && typeof this.src.text === "function") return this.src.text();
88
+ return String(this.src);
89
+ }
90
+ }
91
+ );
92
+ });
93
+
94
+ it("两个独立的 RPCServer 各自维护独立的进程列表", async () => {
95
+ const a = createMockServer();
96
+ const b = createMockServer();
97
+
98
+ const { register } = await import("../bash");
99
+ register(a.server, { platform: "web" });
100
+ register(b.server, { platform: "web" });
101
+
102
+ const mockSub = {
103
+ pid: 100,
104
+ stdout: { text: () => Promise.resolve("ok") },
105
+ stderr: { text: () => Promise.resolve("") },
106
+ exited: Promise.resolve(0),
107
+ };
108
+ (Bun.spawn as ReturnType<typeof vi.fn>).mockReturnValue(mockSub);
109
+
110
+ const resultA = await a.handlers["bash.execute"]({ command: "echo A" });
111
+ const resultB = await b.handlers["bash.execute"]({ command: "echo B" });
112
+
113
+ expect(resultA.pid).toBe(1);
114
+ expect(resultB.pid).toBe(1);
115
+ });
116
+
117
+ it("server A 的 pidCounter 不影响 server B", async () => {
118
+ const a = createMockServer();
119
+ const b = createMockServer();
120
+
121
+ const { register } = await import("../bash");
122
+ register(a.server, { platform: "web" });
123
+ register(b.server, { platform: "web" });
124
+
125
+ const mockSub = {
126
+ pid: 100,
127
+ stdout: { text: () => Promise.resolve("ok") },
128
+ stderr: { text: () => Promise.resolve("") },
129
+ exited: Promise.resolve(0),
130
+ };
131
+ (Bun.spawn as ReturnType<typeof vi.fn>).mockReturnValue(mockSub);
132
+
133
+ await a.handlers["bash.execute"]({ command: "ls" });
134
+ await a.handlers["bash.execute"]({ command: "ls" });
135
+ await a.handlers["bash.execute"]({ command: "ls" });
136
+
137
+ const resultB = await b.handlers["bash.execute"]({ command: "ls" });
138
+ expect(resultB.pid).toBe(1);
139
+ });
140
+ });
141
+
142
+ // --- Feed ---
143
+ describe("Handler 隔离性 - feed", () => {
144
+ beforeEach(() => {
145
+ vi.resetModules();
146
+ });
147
+
148
+ it("两个独立的 RPCServer 各自维护独立的帖子列表", async () => {
149
+ const a = createMockServer();
150
+ const b = createMockServer();
151
+
152
+ const { register } = await import("../feed");
153
+ register(a.server, { platform: "web" });
154
+ register(b.server, { platform: "web" });
155
+
156
+ await a.handlers["feed.post"]({ content: "post from A", category: "tech" });
157
+
158
+ const listB = await b.handlers["feed.list"]({});
159
+ expect(listB.posts).toHaveLength(0);
160
+
161
+ const listA = await a.handlers["feed.list"]({});
162
+ expect(listA.posts).toHaveLength(1);
163
+ expect(listA.posts[0].content).toBe("post from A");
164
+ });
165
+ });
166
+
167
+ // --- Todo ---
168
+ describe("Handler 隔离性 - todo", () => {
169
+ beforeEach(() => {
170
+ vi.resetModules();
171
+ });
172
+
173
+ it("两个独立的 RPCServer 各自维护独立的 todo 列表", async () => {
174
+ const a = createMockServer();
175
+ const b = createMockServer();
176
+
177
+ const { register } = await import("../todo");
178
+ register(a.server, { platform: "web" });
179
+ register(b.server, { platform: "web" });
180
+
181
+ await a.handlers["todo.add"]({ content: "task from A" });
182
+
183
+ const listB = await b.handlers["todo.list"]({});
184
+ expect(listB.items).toHaveLength(0);
185
+
186
+ const listA = await a.handlers["todo.list"]({});
187
+ expect(listA.items).toHaveLength(1);
188
+ });
189
+
190
+ it("server A 的 todoIdCounter 不影响 server B", async () => {
191
+ const a = createMockServer();
192
+ const b = createMockServer();
193
+
194
+ const { register } = await import("../todo");
195
+ register(a.server, { platform: "web" });
196
+ register(b.server, { platform: "web" });
197
+
198
+ await a.handlers["todo.add"]({ content: "A1" });
199
+ await a.handlers["todo.add"]({ content: "A2" });
200
+ await a.handlers["todo.add"]({ content: "A3" });
201
+
202
+ const resultB = await b.handlers["todo.add"]({ content: "B1" });
203
+ expect(resultB.item.id).toBe("todo-1");
204
+ });
205
+ });
206
+
207
+ // --- Rules ---
208
+ describe("Handler 隔离性 - rules", () => {
209
+ beforeEach(() => {
210
+ vi.resetModules();
211
+ });
212
+
213
+ it("两个独立的 RPCServer 各自维护独立的规则列表", async () => {
214
+ const a = createMockServer();
215
+ const b = createMockServer();
216
+
217
+ const { register } = await import("../rules");
218
+ register(a.server, { platform: "web" });
219
+ register(b.server, { platform: "web" });
220
+
221
+ await a.handlers["rules.add"]({ name: "rule A", pattern: "*.ts" });
222
+
223
+ const listB = await b.handlers["rules.list"]({});
224
+ expect(listB.rules).toHaveLength(0);
225
+
226
+ const listA = await a.handlers["rules.list"]({});
227
+ expect(listA.rules).toHaveLength(1);
228
+ });
229
+
230
+ it("server A 的 ruleIdCounter 不影响 server B", async () => {
231
+ const a = createMockServer();
232
+ const b = createMockServer();
233
+
234
+ const { register } = await import("../rules");
235
+ register(a.server, { platform: "web" });
236
+ register(b.server, { platform: "web" });
237
+
238
+ await a.handlers["rules.add"]({ name: "A1", pattern: "*.ts" });
239
+ await a.handlers["rules.add"]({ name: "A2", pattern: "*.js" });
240
+
241
+ const resultB = await b.handlers["rules.add"]({ name: "B1", pattern: "*.css" });
242
+ expect(resultB.rule.id).toBe("rule-1");
243
+ });
244
+ });
@@ -4,86 +4,86 @@ import type { RPCMethods, HandlerOptions } from "../rpc-schema";
4
4
  import { validateCommand, setCommandPolicy } from "../lib/bash-security";
5
5
 
6
6
  type RegisterFn = <K extends keyof RPCMethods & string>(
7
- method: K,
8
- handler: (params: MethodParams<RPCMethods, K>) => Promise<MethodResult<RPCMethods, K>>,
7
+ method: K,
8
+ handler: (params: MethodParams<RPCMethods, K>) => Promise<MethodResult<RPCMethods, K>>
9
9
  ) => void;
10
10
 
11
11
  interface TrackedProcess {
12
- pid: number;
13
- command: string;
14
- running: boolean;
12
+ pid: number;
13
+ command: string;
14
+ running: boolean;
15
15
  }
16
16
 
17
- const processes = new Map<number, TrackedProcess>();
18
- let pidCounter = 1;
19
-
20
17
  export function register(server: RPCServer, _options: HandlerOptions): void {
21
- setCommandPolicy({
22
- enabled: _options.enableBash !== false,
23
- blockedPatterns: [
24
- /rm\s+-rf\s+(.*\s)?\/($|\s)/,
25
- /rm\s+-rf\s+--no-preserve-root/,
26
- /mkfs/,
27
- /dd\s+if=/,
28
- />\s*\/dev\//,
29
- /:()\s*\{.*\|.*&\s*\}/,
30
- /shutdown/,
31
- /reboot/,
32
- ],
33
- allowedCommands: null,
34
- });
18
+ const processes = new Map<number, TrackedProcess>();
19
+ let pidCounter = 1;
20
+
21
+ setCommandPolicy({
22
+ enabled: _options.enableBash !== false,
23
+ blockedPatterns: [
24
+ /rm\s+-rf\s+(.*\s)?\/($|\s)/,
25
+ /rm\s+-rf\s+--no-preserve-root/,
26
+ /mkfs/,
27
+ /dd\s+if=/,
28
+ />\s*\/dev\//,
29
+ /:()\s*\{.*\|.*&\s*\}/,
30
+ /shutdown/,
31
+ /reboot/,
32
+ ],
33
+ allowedCommands: null,
34
+ });
35
35
 
36
- const r: RegisterFn = (method, handler) => {
37
- server.register(method, handler as (params: unknown) => Promise<unknown>);
38
- };
36
+ const r: RegisterFn = (method, handler) => {
37
+ server.register(method, handler as (params: unknown) => Promise<unknown>);
38
+ };
39
39
 
40
- r("bash.execute", async (params) => {
41
- const safeCommand = validateCommand(params.command);
42
- const pid = pidCounter++;
43
- const proc: TrackedProcess = { pid, command: safeCommand, running: true };
44
- processes.set(pid, proc);
40
+ r("bash.execute", async (params) => {
41
+ const safeCommand = validateCommand(params.command);
42
+ const pid = pidCounter++;
43
+ const proc: TrackedProcess = { pid, command: safeCommand, running: true };
44
+ processes.set(pid, proc);
45
45
 
46
- try {
47
- const subprocess = Bun.spawn(safeCommand.split(" "), {
48
- cwd: params.cwd || process.cwd(),
49
- stdout: "pipe",
50
- stderr: "pipe",
51
- });
46
+ try {
47
+ const subprocess = Bun.spawn(safeCommand.split(" "), {
48
+ cwd: params.cwd || process.cwd(),
49
+ stdout: "pipe",
50
+ stderr: "pipe",
51
+ });
52
52
 
53
- proc.pid = subprocess.pid;
53
+ proc.pid = subprocess.pid;
54
54
 
55
- const stdout = await new Response(subprocess.stdout).text();
56
- const stderr = await new Response(subprocess.stderr).text();
57
- const exitCode = await subprocess.exited;
55
+ const stdout = await new Response(subprocess.stdout).text();
56
+ const stderr = await new Response(subprocess.stderr).text();
57
+ const exitCode = await subprocess.exited;
58
58
 
59
- proc.running = false;
59
+ proc.running = false;
60
60
 
61
- const output = stdout + (stderr ? `\n[stderr]\n${stderr}` : "");
61
+ const output = stdout + (stderr ? `\n[stderr]\n${stderr}` : "");
62
62
 
63
- server.emitEvent("bash.output", { pid, data: output, stream: "stdout" }, {});
64
- server.emitEvent("bash.exit", { pid, code: exitCode }, {});
63
+ server.emitEvent("bash.output", { pid, data: output, stream: "stdout" }, {});
64
+ server.emitEvent("bash.exit", { pid, code: exitCode }, {});
65
65
 
66
- return { pid, output };
67
- } catch (err) {
68
- proc.running = false;
69
- const output = `Error: ${err instanceof Error ? err.message : String(err)}`;
70
- server.emitEvent("bash.output", { pid, data: output, stream: "stderr" }, {});
71
- server.emitEvent("bash.exit", { pid, code: 1 }, {});
72
- return { pid, output };
73
- }
74
- });
66
+ return { pid, output };
67
+ } catch (err) {
68
+ proc.running = false;
69
+ const output = `Error: ${err instanceof Error ? err.message : String(err)}`;
70
+ server.emitEvent("bash.output", { pid, data: output, stream: "stderr" }, {});
71
+ server.emitEvent("bash.exit", { pid, code: 1 }, {});
72
+ return { pid, output };
73
+ }
74
+ });
75
75
 
76
- r("bash.kill", async (params) => {
77
- const proc = processes.get(params.pid);
78
- if (proc) {
79
- proc.running = false;
80
- processes.delete(params.pid);
81
- return { success: true };
82
- }
83
- return { success: false };
84
- });
76
+ r("bash.kill", async (params) => {
77
+ const proc = processes.get(params.pid);
78
+ if (proc) {
79
+ proc.running = false;
80
+ processes.delete(params.pid);
81
+ return { success: true };
82
+ }
83
+ return { success: false };
84
+ });
85
85
 
86
- r("bash.listProcesses", async () => ({
87
- processes: Array.from(processes.values()),
88
- }));
86
+ r("bash.listProcesses", async () => ({
87
+ processes: Array.from(processes.values()),
88
+ }));
89
89
  }