@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
@@ -3,53 +3,53 @@ import { apiClient } from "../lib/api-client";
3
3
  import type { FeedCategory, FeedPost } from "../../shared/modules/feed";
4
4
 
5
5
  interface FeedState {
6
- posts: FeedPost[];
7
- author: string;
8
- category: FeedCategory;
9
- content: string;
10
- loading: boolean;
11
-
12
- setAuthor: (author: string) => void;
13
- setCategory: (category: FeedCategory) => void;
14
- setContent: (content: string) => void;
15
- createPost: () => Promise<void>;
16
- loadPosts: () => Promise<void>;
17
- addPost: (post: FeedPost) => void;
6
+ posts: FeedPost[];
7
+ author: string;
8
+ category: FeedCategory;
9
+ content: string;
10
+ loading: boolean;
11
+
12
+ setAuthor: (author: string) => void;
13
+ setCategory: (category: FeedCategory) => void;
14
+ setContent: (content: string) => void;
15
+ createPost: () => Promise<void>;
16
+ loadPosts: () => Promise<void>;
17
+ addPost: (post: FeedPost) => void;
18
18
  }
19
19
 
20
20
  export const useFeedStore = create<FeedState>((set, get) => ({
21
- posts: [],
22
- author: "alice",
23
- category: "tech",
24
- content: "",
25
- loading: false,
26
-
27
- setAuthor: (author) => set({ author }),
28
- setCategory: (category) => set({ category }),
29
- setContent: (content) => set({ content }),
30
-
31
- createPost: async () => {
32
- const { content, category, author } = get();
33
- if (!content.trim()) return;
34
- set({ loading: true });
35
- try {
36
- await apiClient.call("feed.post", { content: content.trim(), category, author });
37
- set({ content: "", loading: false });
38
- } catch {
39
- set({ loading: false });
40
- }
41
- },
42
-
43
- loadPosts: async () => {
44
- try {
45
- const res = await apiClient.call("feed.list", { limit: 100 });
46
- set({ posts: res.posts });
47
- } catch {
48
- // ignore
49
- }
50
- },
51
-
52
- addPost: (post) => set((s) => ({ posts: [...s.posts, post] })),
21
+ posts: [],
22
+ author: "alice",
23
+ category: "tech",
24
+ content: "",
25
+ loading: false,
26
+
27
+ setAuthor: (author) => set({ author }),
28
+ setCategory: (category) => set({ category }),
29
+ setContent: (content) => set({ content }),
30
+
31
+ createPost: async () => {
32
+ const { content, category, author } = get();
33
+ if (!content.trim()) return;
34
+ set({ loading: true });
35
+ try {
36
+ await apiClient.call("feed.post", { content: content.trim(), category, author });
37
+ set({ content: "", loading: false });
38
+ } catch {
39
+ set({ loading: false });
40
+ }
41
+ },
42
+
43
+ loadPosts: async () => {
44
+ try {
45
+ const res = await apiClient.call("feed.list", { limit: 100 });
46
+ set({ posts: res.posts });
47
+ } catch {
48
+ // ignore
49
+ }
50
+ },
51
+
52
+ addPost: (post) => set((s) => ({ posts: [...s.posts, post] })),
53
53
  }));
54
54
 
55
55
  // --- Event Stream (订阅面板) ---
@@ -57,73 +57,73 @@ export const useFeedStore = create<FeedState>((set, get) => ({
57
57
  export type SubEventType = "feed.update" | "chat.message" | "timer.tick";
58
58
 
59
59
  interface EventEntry {
60
- id: string;
61
- eventType: SubEventType;
62
- payload: unknown;
63
- filter: Record<string, unknown>;
64
- timestamp: number;
60
+ id: string;
61
+ eventType: SubEventType;
62
+ payload: unknown;
63
+ filter: Record<string, unknown>;
64
+ timestamp: number;
65
65
  }
66
66
 
67
67
  interface EventStreamState {
68
- entries: EventEntry[];
69
- activeEventType: SubEventType;
70
- activeFilter: string; // JSON string for the filter
71
- subscriptionId: string | null;
72
- subscribed: boolean;
73
-
74
- setActiveEventType: (t: SubEventType) => void;
75
- setActiveFilter: (f: string) => void;
76
- handleSubscribe: () => Promise<void>;
77
- handleUnsubscribe: () => void;
78
- addEntry: (eventType: SubEventType, payload: unknown, filter: Record<string, unknown>) => void;
68
+ entries: EventEntry[];
69
+ activeEventType: SubEventType;
70
+ activeFilter: string; // JSON string for the filter
71
+ subscriptionId: string | null;
72
+ subscribed: boolean;
73
+
74
+ setActiveEventType: (t: SubEventType) => void;
75
+ setActiveFilter: (f: string) => void;
76
+ handleSubscribe: () => Promise<void>;
77
+ handleUnsubscribe: () => void;
78
+ addEntry: (eventType: SubEventType, payload: unknown, filter: Record<string, unknown>) => void;
79
79
  }
80
80
 
81
81
  export const useEventStreamStore = create<EventStreamState>((set, get) => ({
82
- entries: [],
83
- activeEventType: "feed.update",
84
- activeFilter: "",
85
- subscriptionId: null,
86
- subscribed: false,
87
-
88
- setActiveEventType: (t) => set({ activeEventType: t }),
89
- setActiveFilter: (f) => set({ activeFilter: f }),
90
-
91
- handleSubscribe: async () => {
92
- const { activeEventType, activeFilter } = get();
93
- try {
94
- const filter = activeFilter.trim() ? JSON.parse(activeFilter) : {};
95
- const subId = await apiClient.subscribe(
96
- activeEventType,
97
- (payload: unknown) => {
98
- get().addEntry(activeEventType, payload, filter);
99
- },
100
- filter,
101
- );
102
- set({ subscriptionId: subId, subscribed: true });
103
- } catch (err) {
104
- console.error("Subscribe error:", err);
105
- }
106
- },
107
-
108
- handleUnsubscribe: () => {
109
- const { subscriptionId } = get();
110
- if (subscriptionId) {
111
- apiClient.unsubscribe(subscriptionId);
112
- }
113
- set({ subscriptionId: null, subscribed: false });
114
- },
115
-
116
- addEntry: (eventType, payload, filter) =>
117
- set((s) => ({
118
- entries: [
119
- ...s.entries.slice(-49),
120
- {
121
- id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
122
- eventType,
123
- payload,
124
- filter,
125
- timestamp: Date.now(),
126
- },
127
- ],
128
- })),
82
+ entries: [],
83
+ activeEventType: "feed.update",
84
+ activeFilter: "",
85
+ subscriptionId: null,
86
+ subscribed: false,
87
+
88
+ setActiveEventType: (t) => set({ activeEventType: t }),
89
+ setActiveFilter: (f) => set({ activeFilter: f }),
90
+
91
+ handleSubscribe: async () => {
92
+ const { activeEventType, activeFilter } = get();
93
+ try {
94
+ const filter = activeFilter.trim() ? JSON.parse(activeFilter) : {};
95
+ const subId = await apiClient.subscribe(
96
+ activeEventType,
97
+ (payload: unknown) => {
98
+ get().addEntry(activeEventType, payload, filter);
99
+ },
100
+ filter
101
+ );
102
+ set({ subscriptionId: subId, subscribed: true });
103
+ } catch {
104
+ // subscription failed — UI stays unsubscribed
105
+ }
106
+ },
107
+
108
+ handleUnsubscribe: () => {
109
+ const { subscriptionId } = get();
110
+ if (subscriptionId) {
111
+ apiClient.unsubscribe(subscriptionId);
112
+ }
113
+ set({ subscriptionId: null, subscribed: false });
114
+ },
115
+
116
+ addEntry: (eventType, payload, filter) =>
117
+ set((s) => ({
118
+ entries: [
119
+ ...s.entries.slice(-49),
120
+ {
121
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
122
+ eventType,
123
+ payload,
124
+ filter,
125
+ timestamp: Date.now(),
126
+ },
127
+ ],
128
+ })),
129
129
  }));
@@ -1,150 +1,149 @@
1
1
  import { apiClient } from "../lib/api-client";
2
2
 
3
3
  export interface DropEntry {
4
- name: string;
5
- relativePath: string;
6
- file?: File;
7
- isDirectory: boolean;
8
- children?: DropEntry[];
4
+ name: string;
5
+ relativePath: string;
6
+ file?: File;
7
+ isDirectory: boolean;
8
+ children?: DropEntry[];
9
9
  }
10
10
 
11
11
  /**
12
12
  * 递归读取 webkitGetAsEntry,返回扁平文件列表 + 目录结构
13
13
  */
14
- function readEntry(
15
- entry: FileSystemEntry,
16
- path: string,
17
- ): Promise<DropEntry> {
18
- if (entry.isFile) {
19
- return new Promise((resolve) => {
20
- (entry as FileSystemFileEntry).file((file) => {
21
- resolve({ name: entry.name, relativePath: path, file, isDirectory: false });
22
- });
23
- });
24
- }
25
- // Directory
26
- return new Promise((resolve) => {
27
- const reader = (entry as FileSystemDirectoryEntry).createReader();
28
- const children: DropEntry[] = [];
14
+ function readEntry(entry: FileSystemEntry, path: string): Promise<DropEntry> {
15
+ if (entry.isFile) {
16
+ return new Promise((resolve) => {
17
+ (entry as FileSystemFileEntry).file((file) => {
18
+ resolve({ name: entry.name, relativePath: path, file, isDirectory: false });
19
+ });
20
+ });
21
+ }
22
+ // Directory
23
+ return new Promise((resolve) => {
24
+ const reader = (entry as FileSystemDirectoryEntry).createReader();
25
+ const children: DropEntry[] = [];
29
26
 
30
- const readBatch = () => {
31
- reader.readEntries(async (entries) => {
32
- if (entries.length === 0) {
33
- resolve({ name: entry.name, relativePath: path, isDirectory: true, children });
34
- return;
35
- }
36
- for (const e of entries) {
37
- children.push(await readEntry(e, `${path}/${e.name}`));
38
- }
39
- readBatch(); // readEntries may not return all entries in one call
40
- });
41
- };
42
- readBatch();
43
- });
27
+ const readBatch = () => {
28
+ reader.readEntries(async (entries) => {
29
+ if (entries.length === 0) {
30
+ resolve({ name: entry.name, relativePath: path, isDirectory: true, children });
31
+ return;
32
+ }
33
+ for (const e of entries) {
34
+ children.push(await readEntry(e, `${path}/${e.name}`));
35
+ }
36
+ readBatch(); // readEntries may not return all entries in one call
37
+ });
38
+ };
39
+ readBatch();
40
+ });
44
41
  }
45
42
 
46
43
  /**
47
44
  * 从 DataTransfer 递归读取所有文件和目录
48
45
  */
49
46
  export async function readDropItems(dataTransfer: DataTransfer): Promise<DropEntry[]> {
50
- const items = dataTransfer.items;
51
- if (!items) return [];
47
+ const items = dataTransfer.items;
48
+ if (!items) return [];
52
49
 
53
- const entries: DropEntry[] = [];
54
- const tasks: Promise<void>[] = [];
50
+ const entries: DropEntry[] = [];
51
+ const tasks: Promise<void>[] = [];
55
52
 
56
- for (let i = 0; i < items.length; i++) {
57
- const item = items[i];
58
- // Try webkitGetAsEntry (Chrome, Edge, Safari)
59
- const entry = item.webkitGetAsEntry?.();
60
- if (entry) {
61
- tasks.push(
62
- readEntry(entry, entry.name).then<void>((e) => { entries.push(e); }),
63
- );
64
- }
65
- }
53
+ for (let i = 0; i < items.length; i++) {
54
+ const item = items[i]!;
55
+ // Try webkitGetAsEntry (Chrome, Edge, Safari)
56
+ const entry = item.webkitGetAsEntry?.();
57
+ if (entry) {
58
+ tasks.push(
59
+ readEntry(entry, entry.name).then<void>((e) => {
60
+ entries.push(e);
61
+ })
62
+ );
63
+ }
64
+ }
66
65
 
67
- await Promise.all(tasks);
68
- return entries;
66
+ await Promise.all(tasks);
67
+ return entries;
69
68
  }
70
69
 
71
70
  /**
72
71
  * Web 端:递归上传 entries 到目标目录
73
72
  */
74
73
  export async function uploadEntriesWeb(entries: DropEntry[], destDir: string): Promise<number> {
75
- let count = 0;
74
+ let count = 0;
76
75
 
77
- async function process(entry: DropEntry, currentDir: string): Promise<void> {
78
- if (entry.isDirectory) {
79
- // Create directory
80
- const dirPath = `${currentDir}/${entry.name}`;
81
- await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
82
- count++;
83
- if (entry.children) {
84
- for (const child of entry.children) {
85
- await process(child, dirPath);
86
- }
87
- }
88
- } else if (entry.file) {
89
- // Upload file via HTTP
90
- const filePath = `${currentDir}/${entry.name}`;
91
- const arrayBuffer = await entry.file.arrayBuffer();
92
- const baseUrl = apiClient.getBaseUrl();
93
- const token = apiClient.getAuthToken();
94
- const res = await fetch(
95
- `${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
96
- { method: "POST", body: arrayBuffer },
97
- );
98
- if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
99
- count++;
100
- }
101
- }
76
+ async function process(entry: DropEntry, currentDir: string): Promise<void> {
77
+ if (entry.isDirectory) {
78
+ // Create directory
79
+ const dirPath = `${currentDir}/${entry.name}`;
80
+ await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
81
+ count++;
82
+ if (entry.children) {
83
+ for (const child of entry.children) {
84
+ await process(child, dirPath);
85
+ }
86
+ }
87
+ } else if (entry.file) {
88
+ // Upload file via HTTP
89
+ const filePath = `${currentDir}/${entry.name}`;
90
+ const arrayBuffer = await entry.file.arrayBuffer();
91
+ const baseUrl = apiClient.getBaseUrl();
92
+ const token = apiClient.getAuthToken();
93
+ const res = await fetch(
94
+ `${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
95
+ { method: "POST", body: arrayBuffer }
96
+ );
97
+ if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
98
+ count++;
99
+ }
100
+ }
102
101
 
103
- for (const entry of entries) {
104
- await process(entry, destDir);
105
- }
106
- return count;
102
+ for (const entry of entries) {
103
+ await process(entry, destDir);
104
+ }
105
+ return count;
107
106
  }
108
107
 
109
108
  /**
110
109
  * 桌面端:通过 RPC file.copy 直接复制
111
110
  */
112
111
  export async function importFilesDesktop(entries: DropEntry[], destDir: string): Promise<number> {
113
- let count = 0;
112
+ let count = 0;
114
113
 
115
- async function process(entry: DropEntry, currentDir: string): Promise<void> {
116
- if (entry.isDirectory) {
117
- const dirPath = `${currentDir}/${entry.name}`;
118
- await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
119
- count++;
120
- if (entry.children) {
121
- for (const child of entry.children) {
122
- await process(child, dirPath);
123
- }
124
- }
125
- } else if (entry.file) {
126
- // Desktop: File object has .path property (Electron/Electrobun)
127
- const srcPath = (entry.file as File & { path?: string }).path;
128
- if (srcPath) {
129
- await apiClient.call("file.copy", { srcPath, destDir: currentDir });
130
- } else {
131
- // Fallback: no path available, upload via HTTP
132
- const filePath = `${currentDir}/${entry.name}`;
133
- const arrayBuffer = await entry.file.arrayBuffer();
134
- const baseUrl = apiClient.getBaseUrl();
135
- const token = apiClient.getAuthToken();
136
- const res = await fetch(
137
- `${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
138
- { method: "POST", body: arrayBuffer },
139
- );
140
- if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
141
- }
142
- count++;
143
- }
144
- }
114
+ async function process(entry: DropEntry, currentDir: string): Promise<void> {
115
+ if (entry.isDirectory) {
116
+ const dirPath = `${currentDir}/${entry.name}`;
117
+ await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
118
+ count++;
119
+ if (entry.children) {
120
+ for (const child of entry.children) {
121
+ await process(child, dirPath);
122
+ }
123
+ }
124
+ } else if (entry.file) {
125
+ // Desktop: File object has .path property (Electron/Electrobun)
126
+ const srcPath = (entry.file as File & { path?: string }).path;
127
+ if (srcPath) {
128
+ await apiClient.call("file.copy", { srcPath, destDir: currentDir });
129
+ } else {
130
+ // Fallback: no path available, upload via HTTP
131
+ const filePath = `${currentDir}/${entry.name}`;
132
+ const arrayBuffer = await entry.file.arrayBuffer();
133
+ const baseUrl = apiClient.getBaseUrl();
134
+ const token = apiClient.getAuthToken();
135
+ const res = await fetch(
136
+ `${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
137
+ { method: "POST", body: arrayBuffer }
138
+ );
139
+ if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
140
+ }
141
+ count++;
142
+ }
143
+ }
145
144
 
146
- for (const entry of entries) {
147
- await process(entry, destDir);
148
- }
149
- return count;
145
+ for (const entry of entries) {
146
+ await process(entry, destDir);
147
+ }
148
+ return count;
150
149
  }
@@ -3,40 +3,42 @@
3
3
  * Values are read from environment variables with sensible defaults.
4
4
  */
5
5
 
6
+ import { randomUUID } from "crypto";
7
+
8
+ if (process.env.NODE_ENV === "production" && !process.env.AUTH_TOKEN) {
9
+ throw new Error(
10
+ "[FATAL] AUTH_TOKEN environment variable must be set in production mode. " +
11
+ "Example: AUTH_TOKEN=$(openssl rand -hex 32) bun src/server.ts"
12
+ );
13
+ }
14
+
6
15
  export function parseEnvInt(
7
- key: string,
8
- value: string | undefined,
9
- defaultValue: number,
10
- min: number,
11
- max: number,
16
+ key: string,
17
+ value: string | undefined,
18
+ defaultValue: number,
19
+ min: number,
20
+ max: number
12
21
  ): number {
13
- if (value === undefined || value === "") return defaultValue;
14
- const parsed = parseInt(value, 10);
15
- if (isNaN(parsed) || parsed < min || parsed > max) {
16
- console.warn(
17
- `[config] Invalid ${key}: "${value}", using default: ${defaultValue}`,
18
- );
19
- return defaultValue;
20
- }
21
- return parsed;
22
+ if (value === undefined || value === "") return defaultValue;
23
+ const parsed = parseInt(value, 10);
24
+ if (isNaN(parsed) || parsed < min || parsed > max) {
25
+ process.stderr.write(`[config] Invalid ${key}: "${value}", using default: ${defaultValue}\n`);
26
+ return defaultValue;
27
+ }
28
+ return parsed;
22
29
  }
23
30
 
24
31
  export const config = {
25
- port: parseEnvInt("PORT", process.env.PORT, 3100, 1024, 65535),
26
- authToken: process.env.AUTH_TOKEN || "pi-agent-template-token",
27
- maxUploadSize: parseEnvInt(
28
- "MAX_UPLOAD_SIZE",
29
- process.env.MAX_UPLOAD_SIZE,
30
- 50 * 1024 * 1024,
31
- 0,
32
- 1024 * 1024 * 1024,
33
- ),
34
- logDir: process.env.LOG_DIR || "logs",
35
- corsOrigin: process.env.CORS_ORIGIN || "http://localhost:5173",
32
+ port: parseEnvInt("PORT", process.env.PORT, 3100, 1024, 65535),
33
+ authToken: process.env.AUTH_TOKEN || `dev-${randomUUID()}`,
34
+ maxUploadSize: parseEnvInt(
35
+ "MAX_UPLOAD_SIZE",
36
+ process.env.MAX_UPLOAD_SIZE,
37
+ 50 * 1024 * 1024,
38
+ 0,
39
+ 1024 * 1024 * 1024
40
+ ),
41
+ logDir: process.env.LOG_DIR || "logs",
42
+ enableWebService: process.env.ENABLE_WEB_SERVICE === "true",
43
+ corsOrigin: process.env.CORS_ORIGIN || "http://localhost:5173",
36
44
  } as const;
37
-
38
- if (process.env.NODE_ENV === "production" && !process.env.AUTH_TOKEN) {
39
- console.warn(
40
- "[security] WARNING: Using default AUTH_TOKEN in production. Set AUTH_TOKEN environment variable.",
41
- );
42
- }