@dyyz1993/create-agent 2.0.0 → 2.1.0

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 (165) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +24 -1
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -1,130 +1,152 @@
1
- import { appendFile, mkdir as mkdirAsync, readdir as readdirAsync, unlink as unlinkAsync, stat as statAsync } from "fs/promises";
1
+ import {
2
+ appendFile,
3
+ mkdir as mkdirAsync,
4
+ readdir as readdirAsync,
5
+ unlink as unlinkAsync,
6
+ stat as statAsync,
7
+ } from "fs/promises";
2
8
  import { mkdirSync, existsSync } from "fs";
3
9
  import { join } from "path";
4
10
 
5
- export type LogModule = "server" | "gateway" | "system" | "chat" | "file" | "timer" | "git" | "feed";
11
+ export type LogModule =
12
+ | "server"
13
+ | "gateway"
14
+ | "system"
15
+ | "chat"
16
+ | "file"
17
+ | "timer"
18
+ | "git"
19
+ | "feed"
20
+ | "web-server";
6
21
  type LogLevel = "debug" | "info" | "warn" | "error";
7
22
 
8
23
  interface LogEntry {
9
- timestamp: string;
10
- level: LogLevel;
11
- module: LogModule;
12
- message: string;
13
- data?: Record<string, unknown>;
24
+ timestamp: string;
25
+ level: LogLevel;
26
+ module: LogModule;
27
+ message: string;
28
+ data?: Record<string, unknown>;
14
29
  }
15
30
 
16
31
  let _logDir: string | null = null;
17
32
  let _maxAgeDays = 30;
18
33
 
19
- export async function configureLogDir(dir: string, options?: { maxAgeDays?: number }): Promise<void> {
20
- _logDir = dir;
21
- if (options?.maxAgeDays !== undefined) {
22
- _maxAgeDays = options.maxAgeDays;
23
- }
24
- if (!existsSync(dir)) {
25
- mkdirSync(dir, { recursive: true });
26
- }
27
- await cleanOldLogs(dir, _maxAgeDays);
34
+ export async function configureLogDir(
35
+ dir: string,
36
+ options?: { maxAgeDays?: number }
37
+ ): Promise<void> {
38
+ _logDir = dir;
39
+ if (options?.maxAgeDays !== undefined) {
40
+ _maxAgeDays = options.maxAgeDays;
41
+ }
42
+ if (!existsSync(dir)) {
43
+ mkdirSync(dir, { recursive: true });
44
+ }
45
+ await cleanOldLogs(dir, _maxAgeDays);
28
46
  }
29
47
 
30
48
  function getLogDir(): string {
31
- if (!_logDir) {
32
- _logDir = "logs";
33
- if (!existsSync(_logDir)) {
34
- mkdirSync(_logDir, { recursive: true });
35
- }
36
- }
37
- return _logDir;
49
+ if (!_logDir) {
50
+ _logDir = "logs";
51
+ if (!existsSync(_logDir)) {
52
+ mkdirSync(_logDir, { recursive: true });
53
+ }
54
+ }
55
+ return _logDir;
38
56
  }
39
57
 
40
58
  async function cleanOldLogs(dir: string, maxAgeDays: number): Promise<void> {
41
- try {
42
- if (!existsSync(dir)) return;
43
- const files = await readdirAsync(dir);
44
- const cutoff = Date.now() - maxAgeDays * 86400000;
45
- for (const f of files) {
46
- if (!f.endsWith(".log")) continue;
47
- const filePath = join(dir, f);
48
- const s = await statAsync(filePath);
49
- if (s.mtimeMs < cutoff) {
50
- await unlinkAsync(filePath);
51
- }
52
- }
53
- } catch { /* ignore */ }
59
+ try {
60
+ if (!existsSync(dir)) return;
61
+ const files = await readdirAsync(dir);
62
+ const cutoff = Date.now() - maxAgeDays * 86400000;
63
+ for (const f of files) {
64
+ if (!f.endsWith(".log")) continue;
65
+ const filePath = join(dir, f);
66
+ const s = await statAsync(filePath);
67
+ if (s.mtimeMs < cutoff) {
68
+ await unlinkAsync(filePath);
69
+ }
70
+ }
71
+ } catch {
72
+ /* ignore */
73
+ }
54
74
  }
55
75
 
56
76
  function formatLine(entry: LogEntry): string {
57
- const base = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.module}] ${entry.message}`;
58
- return entry.data ? `${base} ${JSON.stringify(entry.data)}` : base;
77
+ const base = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.module}] ${entry.message}`;
78
+ return entry.data ? `${base} ${JSON.stringify(entry.data)}` : base;
59
79
  }
60
80
 
61
81
  let writeQueue: Promise<void> = Promise.resolve();
62
82
 
63
83
  function writeToFile(line: string): void {
64
- writeQueue = writeQueue.then(async () => {
65
- try {
66
- const date = new Date().toISOString().slice(0, 10);
67
- const dir = getLogDir();
68
- if (!existsSync(dir)) {
69
- await mkdirAsync(dir, { recursive: true });
70
- }
71
- await appendFile(join(dir, `${date}.log`), `${line}\n`);
72
- } catch { /* ignore */ }
73
- });
84
+ writeQueue = writeQueue.then(async () => {
85
+ try {
86
+ const date = new Date().toISOString().slice(0, 10);
87
+ const dir = getLogDir();
88
+ if (!existsSync(dir)) {
89
+ await mkdirAsync(dir, { recursive: true });
90
+ }
91
+ await appendFile(join(dir, `${date}.log`), `${line}\n`);
92
+ } catch {
93
+ /* ignore */
94
+ }
95
+ });
74
96
  }
75
97
 
76
98
  export function flushLogs(): Promise<void> {
77
- return writeQueue;
99
+ return writeQueue;
78
100
  }
79
101
 
80
102
  export class Logger {
81
- private readonly module: LogModule;
82
-
83
- constructor(module: LogModule) {
84
- this.module = module;
85
- }
86
-
87
- debug(message: string, data?: Record<string, unknown>): void {
88
- this.write("debug", message, data);
89
- }
90
-
91
- info(message: string, data?: Record<string, unknown>): void {
92
- this.write("info", message, data);
93
- }
94
-
95
- warn(message: string, data?: Record<string, unknown>): void {
96
- this.write("warn", message, data);
97
- }
98
-
99
- error(message: string, data?: Record<string, unknown>): void {
100
- this.write("error", message, data);
101
- }
102
-
103
- private write(level: LogLevel, message: string, data?: Record<string, unknown>): void {
104
- const entry: LogEntry = {
105
- timestamp: new Date().toISOString(),
106
- level,
107
- module: this.module,
108
- message,
109
- ...(data ? { data } : {}),
110
- };
111
-
112
- const line = formatLine(entry);
113
-
114
- if (level === "error") {
115
- console.error(line);
116
- } else if (level === "warn") {
117
- console.warn(line);
118
- } else {
119
- console.log(line);
120
- }
121
-
122
- if (process.env.NODE_ENV !== "production" || level !== "debug") {
123
- writeToFile(line);
124
- }
125
- }
103
+ private readonly module: LogModule;
104
+
105
+ constructor(module: LogModule) {
106
+ this.module = module;
107
+ }
108
+
109
+ debug(message: string, data?: Record<string, unknown>): void {
110
+ this.write("debug", message, data);
111
+ }
112
+
113
+ info(message: string, data?: Record<string, unknown>): void {
114
+ this.write("info", message, data);
115
+ }
116
+
117
+ warn(message: string, data?: Record<string, unknown>): void {
118
+ this.write("warn", message, data);
119
+ }
120
+
121
+ error(message: string, data?: Record<string, unknown>): void {
122
+ this.write("error", message, data);
123
+ }
124
+
125
+ private write(level: LogLevel, message: string, data?: Record<string, unknown>): void {
126
+ const entry: LogEntry = {
127
+ timestamp: new Date().toISOString(),
128
+ level,
129
+ module: this.module,
130
+ message,
131
+ ...(data ? { data } : {}),
132
+ };
133
+
134
+ const line = formatLine(entry);
135
+
136
+ if (level === "error") {
137
+ console.error(line);
138
+ } else if (level === "warn") {
139
+ console.warn(line);
140
+ } else {
141
+ console.log(line);
142
+ }
143
+
144
+ if (process.env.NODE_ENV !== "production" || level !== "debug") {
145
+ writeToFile(line);
146
+ }
147
+ }
126
148
  }
127
149
 
128
150
  export function createLogger(module: LogModule): Logger {
129
- return new Logger(module);
151
+ return new Logger(module);
130
152
  }
@@ -0,0 +1,128 @@
1
+ import { createServer, type Server } from "http";
2
+ import { networkInterfaces } from "os";
3
+ import { createHttpHandler } from "../http-routes";
4
+ import { createWsHandler } from "../../gateway/ws-handler";
5
+ import { createLogger } from "./logger";
6
+
7
+ const log = createLogger("web-server");
8
+
9
+ export interface WebServerConfig {
10
+ port: number;
11
+ authToken: string;
12
+ maxUploadSize: number;
13
+ corsOrigin: string;
14
+ }
15
+
16
+ export interface WebServerResult {
17
+ httpServer: Server;
18
+ wss: ReturnType<typeof createWsHandler>;
19
+ port: number;
20
+ authToken: string;
21
+ close: () => Promise<void>;
22
+ }
23
+
24
+ export function getLocalIP(): string {
25
+ const nets = networkInterfaces();
26
+ for (const name of Object.keys(nets)) {
27
+ const interfaces = nets[name];
28
+ if (!interfaces) continue;
29
+ for (const net of interfaces) {
30
+ if (net.family === "IPv4" && !net.internal) {
31
+ return net.address;
32
+ }
33
+ }
34
+ }
35
+ return "127.0.0.1";
36
+ }
37
+
38
+ function checkPort(port: number): Promise<number> {
39
+ return new Promise((resolve, reject) => {
40
+ const testServer = createServer();
41
+ testServer.once("error", (err: NodeJS.ErrnoException) => {
42
+ testServer.close();
43
+ reject(err);
44
+ });
45
+ testServer.once("listening", () => {
46
+ testServer.close();
47
+ resolve(port);
48
+ });
49
+ testServer.listen(port);
50
+ });
51
+ }
52
+
53
+ export async function findAvailablePort(startPort: number, maxRetries = 10): Promise<number> {
54
+ for (let port = startPort; port < startPort + maxRetries; port++) {
55
+ try {
56
+ await checkPort(port);
57
+ return port;
58
+ } catch {
59
+ continue;
60
+ }
61
+ }
62
+ throw new Error(`No available port found after ${maxRetries} retries starting from ${startPort}`);
63
+ }
64
+
65
+ function getFreePort(): Promise<number> {
66
+ return new Promise((resolve, reject) => {
67
+ const s = createServer();
68
+ s.listen(0, () => {
69
+ const addr = s.address();
70
+ s.close(() => {
71
+ if (typeof addr === "object" && addr) {
72
+ resolve(addr.port);
73
+ } else {
74
+ reject(new Error("Failed to allocate port"));
75
+ }
76
+ });
77
+ });
78
+ s.on("error", reject);
79
+ });
80
+ }
81
+
82
+ export async function createWebServer(config: WebServerConfig): Promise<WebServerResult> {
83
+ const port = config.port === 0 ? await getFreePort() : await findAvailablePort(config.port);
84
+
85
+ const httpServer = createServer();
86
+
87
+ const wss = createWsHandler(httpServer, {
88
+ config: {
89
+ port,
90
+ authToken: config.authToken,
91
+ maxUploadSize: config.maxUploadSize,
92
+ },
93
+ });
94
+
95
+ httpServer.on(
96
+ "request",
97
+ createHttpHandler({
98
+ config: {
99
+ port,
100
+ authToken: config.authToken,
101
+ maxUploadSize: config.maxUploadSize,
102
+ corsOrigin: config.corsOrigin,
103
+ },
104
+ getWebSocketClientCount: () => wss.clients.size,
105
+ })
106
+ );
107
+
108
+ return new Promise((resolve, reject) => {
109
+ httpServer.listen(port, () => {
110
+ log.info("Web server started", { port, corsOrigin: config.corsOrigin });
111
+
112
+ resolve({
113
+ httpServer,
114
+ wss,
115
+ port,
116
+ authToken: config.authToken,
117
+ close: () =>
118
+ new Promise<void>((res) => {
119
+ wss.clients.forEach((ws) => ws.close());
120
+ wss.close(() => {
121
+ httpServer.close(() => res());
122
+ });
123
+ }),
124
+ });
125
+ });
126
+ httpServer.on("error", reject);
127
+ });
128
+ }
@@ -0,0 +1,8 @@
1
+ export interface DebugMethods {
2
+ "debug.subscriptions": {
3
+ params: {};
4
+ result: {
5
+ subscriptions: Array<{ id: string; eventType: string; filter: Record<string, unknown> }>;
6
+ };
7
+ };
8
+ }
@@ -8,6 +8,7 @@ import type { FeedMethods, FeedEvents } from "./modules/feed";
8
8
  import type { BashMethods, BashEvents } from "./modules/bash";
9
9
  import type { RulesMethods } from "./modules/rules";
10
10
  import type { TodoMethods } from "./modules/todo";
11
+ import type { DebugMethods } from "./modules/debug";
11
12
 
12
13
  /**
13
14
  * RPC 方法定义 — 前后端共享的唯一类型入口
@@ -19,7 +20,19 @@ import type { TodoMethods } from "./modules/todo";
19
20
  * handlers/ 自动发现,无需修改 register-all-handlers.ts
20
21
  * 入口文件(bun/index.ts, server.ts)无需修改
21
22
  */
22
- export interface RPCMethods extends AnyMethods, SystemMethods, FileMethods, TimerMethods, ChatMethods, GitMethods, FeedMethods, BashMethods, RulesMethods, TodoMethods {}
23
+ export interface RPCMethods
24
+ extends
25
+ AnyMethods,
26
+ SystemMethods,
27
+ FileMethods,
28
+ TimerMethods,
29
+ ChatMethods,
30
+ GitMethods,
31
+ FeedMethods,
32
+ BashMethods,
33
+ RulesMethods,
34
+ TodoMethods,
35
+ DebugMethods {}
23
36
 
24
37
  /**
25
38
  * RPC 事件定义 — 合并所有模块事件
@@ -30,6 +43,6 @@ export interface RPCEvents extends TimerEvents, ChatEvents, FeedEvents, BashEven
30
43
  * Handler 注册选项
31
44
  */
32
45
  export interface HandlerOptions {
33
- platform: "desktop" | "web";
34
- enableBash?: boolean;
46
+ platform: "desktop" | "web";
47
+ enableBash?: boolean;
35
48
  }
File without changes
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noUnusedLocals": false,
5
+ "noUnusedParameters": false,
6
+ "skipLibCheck": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "noImplicitReturns": true,
9
+ "noFallthroughCasesInSwitch": true,
10
+ "strict": true
11
+ },
12
+ "include": ["src/bun", "src/gateway/ipc-transport.ts", "src/shared"],
13
+ "exclude": ["node_modules", "dist", "build", "**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
14
+ }
@@ -17,10 +17,20 @@
17
17
  "noFallthroughCasesInSwitch": true,
18
18
  "baseUrl": ".",
19
19
  "paths": {
20
- "@dyyz1993/rpc-core": ["../packages/rpc-core/src/index.ts"],
20
+ "@dyyz1993/rpc-core": ["../../packages/rpc-core/src/index.ts"],
21
21
  "@shared/*": ["../shared/*"]
22
- }
22
+ },
23
+ "noUncheckedIndexedAccess": true,
24
+ "noImplicitReturns": true
23
25
  },
24
26
  "include": ["src", "../shared"],
25
- "exclude": ["node_modules", "dist", "build", "../../package/dist", "**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
27
+ "exclude": [
28
+ "node_modules",
29
+ "dist",
30
+ "build",
31
+ "../../package/dist",
32
+ "**/__tests__/**",
33
+ "**/*.test.ts",
34
+ "**/*.test.tsx"
35
+ ]
26
36
  }
@@ -1,6 +1,9 @@
1
1
  import js from '@eslint/js';
2
2
  import tseslint from 'typescript-eslint';
3
- import rpcPlugin from './eslint-plugin-rpc/index.js';
3
+ import rpcPlugin from '@dyyz1993/eslint-plugin-rpc';
4
+ import reactHooks from 'eslint-plugin-react-hooks';
5
+ import reactRefresh from 'eslint-plugin-react-refresh';
6
+ import jsxA11y from 'eslint-plugin-jsx-a11y';
4
7
 
5
8
  export default tseslint.config(
6
9
  js.configs.recommended,
@@ -10,7 +13,6 @@ export default tseslint.config(
10
13
  'node_modules/**',
11
14
  'build/**',
12
15
  'dist/**',
13
- 'eslint-plugin-rpc/**',
14
16
  'postcss.config.js',
15
17
  'tailwind.config.js',
16
18
  ],
@@ -19,8 +21,19 @@ export default tseslint.config(
19
21
  files: ['src/**/*.{ts,tsx}'],
20
22
  plugins: {
21
23
  rpc: rpcPlugin,
24
+ 'react-hooks': reactHooks,
25
+ 'react-refresh': reactRefresh,
26
+ 'jsx-a11y': jsxA11y,
22
27
  },
23
28
  rules: {
29
+ ...reactHooks.configs.recommended.rules,
30
+ 'react-hooks/rules-of-hooks': 'warn',
31
+ 'react-hooks/exhaustive-deps': 'warn',
32
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
33
+ ...jsxA11y.configs.recommended.rules,
34
+ 'jsx-a11y/click-events-have-key-events': 'warn',
35
+ 'jsx-a11y/no-static-element-interactions': 'warn',
36
+ 'jsx-a11y/no-autofocus': 'warn',
24
37
  // TS 严格规则
25
38
  '@typescript-eslint/no-explicit-any': 'error',
26
39
  '@typescript-eslint/no-empty-object-type': ['error', { allowObjectTypes: 'always', allowInterfaces: 'with-single-extends' }],
@@ -9,6 +9,9 @@
9
9
  "dev:server": "bun src/server.ts",
10
10
  "build": "vite build",
11
11
  "build:canary": "vite build && electrobun build --env=canary",
12
+ "build:mac": "vite build && electrobun build --env=canary",
13
+ "build:win": "vite build && electrobun build --env=canary",
14
+ "build:linux": "vite build && electrobun build --env=canary",
12
15
  "lint": "eslint .",
13
16
  "lint:fix": "eslint . --fix",
14
17
  "prepare": "husky",
@@ -19,7 +22,7 @@
19
22
  "dependencies": {
20
23
  "@dyyz1993/rpc-core": "^1.2.0",
21
24
  "@tanstack/react-virtual": "^3.13.24",
22
- "electrobun": "1.13.1",
25
+ "electrobun": "1.18.1",
23
26
  "i18next": "^24.2.2",
24
27
  "i18next-browser-languagedetector": "^8.0.4",
25
28
  "lucide-react": "^1.9.0",
@@ -32,7 +35,8 @@
32
35
  "zustand": "^5.0.12"
33
36
  },
34
37
  "devDependencies": {
35
- "@eslint/js": "^9.0.0",
38
+ "@dyyz1993/eslint-plugin-rpc": "workspace:*",
39
+ "@eslint/js": "^9.0.0",
36
40
  "@testing-library/jest-dom": "^6.9.1",
37
41
  "@testing-library/react": "^16.3.2",
38
42
  "@testing-library/user-event": "^14.6.1",
@@ -40,10 +44,14 @@
40
44
  "@types/react": "^18.3.12",
41
45
  "@types/react-dom": "^18.3.1",
42
46
  "@types/ws": "^8.18.1",
47
+ "ws": "^8.18.0",
43
48
  "@vitejs/plugin-react": "^4.3.4",
44
49
  "autoprefixer": "^10.4.20",
45
50
  "concurrently": "^9.1.0",
46
51
  "eslint": "^9.0.0",
52
+ "eslint-plugin-jsx-a11y": "^6.10.0",
53
+ "eslint-plugin-react-hooks": "^5.2.0",
54
+ "eslint-plugin-react-refresh": "^0.4.20",
47
55
  "happy-dom": "^20.9.0",
48
56
  "husky": "^9.1.0",
49
57
  "jsdom": "^29.1.1",
@@ -53,5 +61,8 @@
53
61
  "typescript-eslint": "^8.0.0",
54
62
  "vite": "^6.0.3",
55
63
  "vitest": "^4.1.5"
64
+ },
65
+ "overrides": {
66
+ "minimatch": "^10.0.1"
56
67
  }
57
68
  }
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ vi.mock("electrobun/bun", () => ({
4
+ BrowserWindow: {
5
+ defineRPC: vi.fn(() => ({})),
6
+ },
7
+ BrowserView: {
8
+ defineRPC: vi.fn(() => ({
9
+ requests: {},
10
+ messages: {},
11
+ })),
12
+ },
13
+ Updater: {
14
+ localInfo: vi.fn(() => ({ channel: vi.fn(() => "dev") })),
15
+ },
16
+ ApplicationMenu: {
17
+ setApplicationMenu: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ vi.mock("electrobun", () => ({}));
22
+
23
+ describe("混合模式", () => {
24
+ beforeEach(() => {
25
+ vi.resetModules();
26
+ delete process.env.ENABLE_WEB_SERVICE;
27
+ delete process.env.AUTH_TOKEN;
28
+ process.env.NODE_ENV = "test";
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ it("默认不启动 Web 服务(enableWebService 为 false)", async () => {
36
+ const { config } = await import("../server-config");
37
+ expect(config.enableWebService).toBe(false);
38
+ });
39
+
40
+ it("enableWebService=true 时启动 HTTP + WS 服务", async () => {
41
+ process.env.ENABLE_WEB_SERVICE = "true";
42
+ const { config } = await import("../server-config");
43
+ expect(config.enableWebService).toBe(true);
44
+ });
45
+
46
+ it("IPC 和 WS 服务同时工作,状态隔离", async () => {
47
+ const { ElectrobunTransport } = await import("../gateway/ipc-transport");
48
+ const ipcTransport = new ElectrobunTransport();
49
+ expect(ipcTransport.isConnected()).toBe(false);
50
+
51
+ ipcTransport.setBrowserView({ executeJavascript: vi.fn() });
52
+ expect(ipcTransport.isConnected()).toBe(true);
53
+
54
+ const { createWebServer } = await import("../shared/lib/web-server");
55
+ const webServer = await createWebServer({
56
+ port: 0,
57
+ authToken: "test-token",
58
+ maxUploadSize: 1024,
59
+ corsOrigin: "*",
60
+ });
61
+ expect(webServer.port).toBeGreaterThan(0);
62
+ expect(ipcTransport.isConnected()).toBe(true);
63
+ await webServer.close();
64
+ });
65
+
66
+ it("WS 客户端需要 token 才能连接", async () => {
67
+ const { createWebServer } = await import("../shared/lib/web-server");
68
+ const webServer = await createWebServer({
69
+ port: 0,
70
+ authToken: "test-token",
71
+ maxUploadSize: 1024,
72
+ corsOrigin: "*",
73
+ });
74
+ expect(webServer.authToken).toBeTruthy();
75
+ expect(webServer.authToken.length).toBeGreaterThan(0);
76
+ await webServer.close();
77
+ });
78
+
79
+ it("IPC 客户端不需要 token", async () => {
80
+ const { ElectrobunTransport } = await import("../gateway/ipc-transport");
81
+ const transport = new ElectrobunTransport();
82
+ transport.setBrowserView({ executeJavascript: vi.fn() });
83
+ expect(transport.isConnected()).toBe(true);
84
+ });
85
+
86
+ it("动态生成的 token 每次启动不同", async () => {
87
+ const { createWebServer } = await import("../shared/lib/web-server");
88
+ const ws1 = await createWebServer({
89
+ port: 0,
90
+ authToken: `token-${Date.now()}-1`,
91
+ maxUploadSize: 1024,
92
+ corsOrigin: "*",
93
+ });
94
+ const ws2 = await createWebServer({
95
+ port: 0,
96
+ authToken: `token-${Date.now()}-2`,
97
+ maxUploadSize: 1024,
98
+ corsOrigin: "*",
99
+ });
100
+ expect(ws1.authToken).not.toBe(ws2.authToken);
101
+ await ws1.close();
102
+ await ws2.close();
103
+ });
104
+
105
+ it("关闭桌面端时 Web 服务也关闭", async () => {
106
+ const { createWebServer } = await import("../shared/lib/web-server");
107
+ const webServer = await createWebServer({
108
+ port: 0,
109
+ authToken: "test-token",
110
+ maxUploadSize: 1024,
111
+ corsOrigin: "*",
112
+ });
113
+ expect(webServer.httpServer.listening).toBe(true);
114
+ await webServer.close();
115
+ await new Promise((r) => setTimeout(r, 50));
116
+ expect(webServer.httpServer.listening).toBe(false);
117
+ });
118
+
119
+ it("getLocalIP 返回有效 IP 地址", async () => {
120
+ const { getLocalIP } = await import("../shared/lib/web-server");
121
+ const ip = getLocalIP();
122
+ expect(ip).toBeTruthy();
123
+ expect(ip).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
124
+ });
125
+ });