@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
@@ -13,74 +13,102 @@ import { createLogger } from "../shared/lib/logger";
13
13
  const log = createLogger("gateway");
14
14
 
15
15
  function safeEqual(a: string, b: string): boolean {
16
- const bufA = Buffer.from(a);
17
- const bufB = Buffer.from(b);
18
- if (bufA.length !== bufB.length) return false;
19
- return timingSafeEqual(bufA, bufB);
16
+ const bufA = Buffer.from(a);
17
+ const bufB = Buffer.from(b);
18
+ if (bufA.length !== bufB.length) return false;
19
+ return timingSafeEqual(bufA, bufB);
20
20
  }
21
21
 
22
22
  export interface WsHandlerDeps {
23
- config: { readonly port: number; readonly authToken: string; readonly maxUploadSize: number };
23
+ config: { readonly port: number; readonly authToken: string; readonly maxUploadSize: number };
24
24
  }
25
25
 
26
26
  export function createWsHandler(httpServer: Server, deps: WsHandlerDeps): WebSocketServer {
27
- const { config: cfg } = deps;
28
- const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
27
+ const { config: cfg } = deps;
28
+ const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
29
29
 
30
- wss.on("connection", (ws: WebSocket, req) => {
31
- // Token 验证
32
- const url = req.url ? new URL(req.url, "http://localhost") : null;
33
- const token = url?.searchParams.get("token");
34
- if (!safeEqual(token || "", cfg.authToken)) {
35
- log.warn("Connection rejected: invalid token");
36
- ws.close(4001, "Unauthorized");
37
- return;
38
- }
30
+ wss.on("connection", (ws: WebSocket, req) => {
31
+ // Token 验证(header 优先,query fallback)
32
+ const url = req.url ? new URL(req.url, "http://localhost") : null;
33
+ const headerToken = (req.headers["sec-websocket-protocol"] as string) || undefined;
34
+ const queryToken = url?.searchParams.get("token");
35
+ const token = headerToken || queryToken;
36
+ if (!safeEqual(token || "", cfg.authToken)) {
37
+ log.warn("Connection rejected: invalid token", {
38
+ providedPrefix: token ? `${token.substring(0, 4)}***` : "(none)",
39
+ });
40
+ ws.close(4001, "Unauthorized");
41
+ return;
42
+ }
39
43
 
40
- log.info("Client connected", { total: wss.clients.size });
44
+ log.info("Client connected", { total: wss.clients.size });
41
45
 
42
- const wsTransport = {
43
- send: async (message: unknown): Promise<void> => {
44
- if (ws.readyState === WebSocket.OPEN) {
45
- ws.send(JSON.stringify(message));
46
- }
47
- },
48
- onMessage: (handler: (message: unknown) => void): (() => void) => {
49
- const listener = (data: Buffer) => {
50
- try {
51
- const msg = JSON.parse(data.toString());
52
- handler(msg);
53
- } catch (err) {
54
- log.error("Failed to parse message", { error: err instanceof Error ? err.message : String(err) });
55
- }
56
- };
57
- ws.on("message", listener);
58
- return () => ws.off("message", listener);
59
- },
60
- onError: (): (() => void) => {
61
- return () => {};
62
- },
63
- onDisconnect: (): (() => void) => {
64
- return () => {};
65
- },
66
- isConnected: (): boolean => ws.readyState === WebSocket.OPEN,
67
- close: (): void => {},
68
- };
46
+ const PING_INTERVAL = 30000;
47
+ let pongReceived = true;
48
+ const pingTimer = setInterval(() => {
49
+ if (ws.readyState !== WebSocket.OPEN) {
50
+ clearInterval(pingTimer);
51
+ return;
52
+ }
53
+ if (!pongReceived) {
54
+ log.warn("Client heartbeat timeout, closing connection");
55
+ clearInterval(pingTimer);
56
+ ws.terminate();
57
+ return;
58
+ }
59
+ pongReceived = false;
60
+ ws.ping();
61
+ }, PING_INTERVAL);
69
62
 
70
- const rpcServer = new RPCServer(wsTransport as Transport);
63
+ ws.on("pong", () => {
64
+ pongReceived = true;
65
+ });
71
66
 
72
- // 自动导入并注册所有 handlers
73
- registerAllHandlers(rpcServer, { platform: "web" });
67
+ const wsTransport = {
68
+ send: async (message: unknown): Promise<void> => {
69
+ if (ws.readyState === WebSocket.OPEN) {
70
+ ws.send(JSON.stringify(message));
71
+ }
72
+ },
73
+ onMessage: (handler: (message: unknown) => void): (() => void) => {
74
+ const listener = (data: Buffer) => {
75
+ try {
76
+ const msg = JSON.parse(data.toString());
77
+ handler(msg);
78
+ } catch (err) {
79
+ log.error("Failed to parse message", {
80
+ error: err instanceof Error ? err.message : String(err),
81
+ });
82
+ }
83
+ };
84
+ ws.on("message", listener);
85
+ return () => ws.off("message", listener);
86
+ },
87
+ onError: (): (() => void) => {
88
+ return () => {};
89
+ },
90
+ onDisconnect: (): (() => void) => {
91
+ return () => {};
92
+ },
93
+ isConnected: (): boolean => ws.readyState === WebSocket.OPEN,
94
+ close: (): void => {},
95
+ };
74
96
 
75
- ws.on("close", () => {
76
- log.info("Client disconnected", { total: wss.clients.size });
77
- rpcServer.close();
78
- });
97
+ const rpcServer = new RPCServer(wsTransport as Transport);
79
98
 
80
- ws.on("error", (err: Error) => {
81
- log.error("Client error", { error: err.message });
82
- });
83
- });
99
+ // 自动导入并注册所有 handlers
100
+ registerAllHandlers(rpcServer, { platform: "web" });
84
101
 
85
- return wss;
102
+ ws.on("close", () => {
103
+ if (pingTimer) clearInterval(pingTimer);
104
+ log.info("Client disconnected", { total: wss.clients.size });
105
+ rpcServer.close();
106
+ });
107
+
108
+ ws.on("error", (err: Error) => {
109
+ log.error("Client error", { error: err.message });
110
+ });
111
+ });
112
+
113
+ return wss;
86
114
  }
@@ -44,5 +44,6 @@ describe("i18n configuration", () => {
44
44
  const i18n = await import("../../lib/i18n");
45
45
  await i18n.default.changeLanguage("zh");
46
46
  expect(i18n.default.t("app.title")).toBe("Pi Agent");
47
+ expect(i18n.default.t("sidebar.explorer")).toBe("资源管理器");
47
48
  });
48
49
  });
@@ -1,37 +1,40 @@
1
- import "@testing-library/jest-dom/vitest";
1
+ import { expect } from "vitest";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+
4
+ expect.extend(matchers);
2
5
 
3
6
  Object.defineProperty(window, "matchMedia", {
4
- writable: true,
5
- value: (query: string) => ({
6
- matches: false,
7
- media: query,
8
- onchange: null,
9
- addListener: () => {},
10
- removeListener: () => {},
11
- addEventListener: () => {},
12
- removeEventListener: () => {},
13
- dispatchEvent: () => false,
14
- }),
7
+ writable: true,
8
+ value: (query: string) => ({
9
+ matches: false,
10
+ media: query,
11
+ onchange: null,
12
+ addListener: () => {},
13
+ removeListener: () => {},
14
+ addEventListener: () => {},
15
+ removeEventListener: () => {},
16
+ dispatchEvent: () => false,
17
+ }),
15
18
  });
16
19
 
17
20
  const localStorageMock = (() => {
18
- let store: Record<string, string> = {};
19
- return {
20
- getItem: (key: string) => store[key] ?? null,
21
- setItem: (key: string, value: string) => {
22
- store[key] = String(value);
23
- },
24
- removeItem: (key: string) => {
25
- delete store[key];
26
- },
27
- clear: () => {
28
- store = {};
29
- },
30
- get length() {
31
- return Object.keys(store).length;
32
- },
33
- key: (i: number) => Object.keys(store)[i] ?? null,
34
- };
21
+ let store: Record<string, string> = {};
22
+ return {
23
+ getItem: (key: string) => store[key] ?? null,
24
+ setItem: (key: string, value: string) => {
25
+ store[key] = String(value);
26
+ },
27
+ removeItem: (key: string) => {
28
+ delete store[key];
29
+ },
30
+ clear: () => {
31
+ store = {};
32
+ },
33
+ get length() {
34
+ return Object.keys(store).length;
35
+ },
36
+ key: (i: number) => Object.keys(store)[i] ?? null,
37
+ };
35
38
  })();
36
39
 
37
40
  Object.defineProperty(window, "localStorage", { value: localStorageMock });
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+
5
+ function extractVarNames(css: string, selector: string): string[] {
6
+ const regex = new RegExp(`${selector.replace(".", "\\.")}\\s*\\{([^}]+)\\}`, "s");
7
+ const match = css.match(regex);
8
+ if (!match) return [];
9
+ const vars = match[1].match(/--[\w-]+/g);
10
+ return vars ? [...new Set(vars)].sort() : [];
11
+ }
12
+
13
+ describe("CSS Variables", () => {
14
+ const cssPath = resolve(dirname(import.meta.url.replace("file://", "")), "../index.css");
15
+ const css = readFileSync(cssPath, "utf-8");
16
+
17
+ it("should have matching variables in :root and .dark", () => {
18
+ const rootVars = extractVarNames(css, ":root");
19
+ const darkVars = extractVarNames(css, ".dark");
20
+
21
+ expect(rootVars.length).toBeGreaterThan(0);
22
+ expect(darkVars.length).toBeGreaterThan(0);
23
+ expect(rootVars).toEqual(darkVars);
24
+ });
25
+
26
+ it("should have all required variable categories", () => {
27
+ const allVars = css.match(/--[\w-]+/g) || [];
28
+ const uniqueVars = [...new Set(allVars)];
29
+
30
+ const requiredPrefixes = ["bg-", "text-", "border-", "accent-"];
31
+ for (const prefix of requiredPrefixes) {
32
+ const hasPrefix = uniqueVars.some((v) => v.includes(prefix));
33
+ expect(hasPrefix, `Missing CSS variables with prefix: ${prefix}`).toBe(true);
34
+ }
35
+ });
36
+ });
@@ -1,58 +1 @@
1
- import React, { Component, type ErrorInfo, type ReactNode } from "react";
2
-
3
- interface Props {
4
- children: ReactNode;
5
- fallback?: ReactNode;
6
- }
7
-
8
- interface State {
9
- hasError: boolean;
10
- error: Error | null;
11
- }
12
-
13
- export class ErrorBoundary extends Component<Props, State> {
14
- constructor(props: Props) {
15
- super(props);
16
- this.state = { hasError: false, error: null };
17
- }
18
-
19
- static getDerivedStateFromError(error: Error): State {
20
- return { hasError: true, error };
21
- }
22
-
23
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
24
- console.error("[ErrorBoundary]", error, errorInfo);
25
- }
26
-
27
- handleRetry = (): void => {
28
- this.setState({ hasError: false, error: null });
29
- };
30
-
31
- render(): ReactNode {
32
- if (this.state.hasError) {
33
- if (this.props.fallback) {
34
- return this.props.fallback;
35
- }
36
-
37
- return (
38
- <div className="flex flex-col items-center justify-center h-full p-8 bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
39
- <div className="text-4xl mb-4">⚠️</div>
40
- <h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
41
- <p className="text-sm text-[var(--color-text-secondary)] mb-4 max-w-md text-center">
42
- {this.state.error?.message || "An unexpected error occurred"}
43
- </p>
44
- <button
45
- onClick={this.handleRetry}
46
- className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded-lg transition-colors text-sm"
47
- role="button"
48
- aria-label="Retry"
49
- >
50
- Retry
51
- </button>
52
- </div>
53
- );
54
- }
55
-
56
- return this.props.children;
57
- }
58
- }
1
+ export { ErrorBoundary } from "@shared/components/ErrorBoundary";
@@ -4,6 +4,7 @@ export function LanguageSwitcher() {
4
4
  const { locale, setLocale } = useLocaleStore();
5
5
  return (
6
6
  <button
7
+ data-testid="language-switcher"
7
8
  onClick={() => setLocale(locale === "en" ? "zh" : "en")}
8
9
  className="px-1.5 py-0.5 rounded text-xs hover:bg-[var(--color-bg-hover)] transition-colors text-[var(--color-text-secondary)]"
9
10
  title={locale === "en" ? "切换到中文" : "Switch to English"}
@@ -6,6 +6,7 @@ export function ThemeToggle() {
6
6
 
7
7
  return (
8
8
  <button
9
+ data-testid="theme-toggle"
9
10
  onClick={toggleTheme}
10
11
  className="p-1.5 rounded-md hover:bg-[var(--color-bg-hover)] transition-colors"
11
12
  title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}