@xiaozhi-client/cli 1.9.4-beta.10
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.
- package/README.md +98 -0
- package/package.json +31 -0
- package/project.json +75 -0
- package/src/Constants.ts +105 -0
- package/src/Container.ts +212 -0
- package/src/Types.ts +79 -0
- package/src/commands/CommandHandlerFactory.ts +98 -0
- package/src/commands/ConfigCommandHandler.ts +279 -0
- package/src/commands/EndpointCommandHandler.ts +158 -0
- package/src/commands/McpCommandHandler.ts +778 -0
- package/src/commands/ProjectCommandHandler.ts +254 -0
- package/src/commands/ServiceCommandHandler.ts +182 -0
- package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
- package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
- package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
- package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
- package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
- package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
- package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
- package/src/commands/index.ts +351 -0
- package/src/errors/ErrorHandlers.ts +141 -0
- package/src/errors/ErrorMessages.ts +121 -0
- package/src/errors/__tests__/index.test.ts +186 -0
- package/src/errors/index.ts +163 -0
- package/src/global.d.ts +19 -0
- package/src/index.ts +53 -0
- package/src/interfaces/Command.ts +128 -0
- package/src/interfaces/CommandTypes.ts +95 -0
- package/src/interfaces/Config.ts +25 -0
- package/src/interfaces/Service.ts +99 -0
- package/src/services/DaemonManager.ts +318 -0
- package/src/services/ProcessManager.ts +235 -0
- package/src/services/ServiceManager.ts +319 -0
- package/src/services/TemplateManager.ts +382 -0
- package/src/services/__tests__/DaemonManager.test.ts +378 -0
- package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
- package/src/services/__tests__/ProcessManager.test.ts +296 -0
- package/src/services/__tests__/ServiceManager.test.ts +774 -0
- package/src/services/__tests__/TemplateManager.test.ts +337 -0
- package/src/types/backend.d.ts +48 -0
- package/src/utils/FileUtils.ts +320 -0
- package/src/utils/FormatUtils.ts +198 -0
- package/src/utils/PathUtils.ts +255 -0
- package/src/utils/PlatformUtils.ts +217 -0
- package/src/utils/Validation.ts +274 -0
- package/src/utils/VersionUtils.ts +141 -0
- package/src/utils/__tests__/FileUtils.test.ts +728 -0
- package/src/utils/__tests__/FormatUtils.test.ts +243 -0
- package/src/utils/__tests__/PathUtils.test.ts +1165 -0
- package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
- package/src/utils/__tests__/Validation.test.ts +560 -0
- package/src/utils/__tests__/VersionUtils.test.ts +410 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +100 -0
- package/vitest.config.ts +97 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板管理服务单元测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { FileError, ValidationError } from "@cli/errors/index.js";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { TemplateCreateOptions } from "../TemplateManager";
|
|
8
|
+
import { TemplateManagerImpl } from "../TemplateManager";
|
|
9
|
+
|
|
10
|
+
// Mock 依赖
|
|
11
|
+
vi.mock("@cli/utils/PathUtils.js", () => ({
|
|
12
|
+
PathUtils: {
|
|
13
|
+
findTemplatesDir: vi.fn(),
|
|
14
|
+
getTemplatePath: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@cli/utils/FileUtils.js", () => ({
|
|
19
|
+
FileUtils: {
|
|
20
|
+
exists: vi.fn(),
|
|
21
|
+
readFile: vi.fn(),
|
|
22
|
+
writeFile: vi.fn(),
|
|
23
|
+
ensureDir: vi.fn(),
|
|
24
|
+
copyDirectory: vi.fn(),
|
|
25
|
+
listDirectory: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("@cli/utils/Validation.js", () => ({
|
|
30
|
+
Validation: {
|
|
31
|
+
validateTemplateName: vi.fn(),
|
|
32
|
+
validateRequired: vi.fn(),
|
|
33
|
+
validateProjectName: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("node:fs", () => ({
|
|
38
|
+
default: {
|
|
39
|
+
readdirSync: vi.fn(),
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("node:path", () => ({
|
|
44
|
+
default: {
|
|
45
|
+
join: vi.fn((...args) => args.join("/")),
|
|
46
|
+
resolve: vi.fn((p) => `/resolved/${p}`),
|
|
47
|
+
relative: vi.fn((from, to) => String(to).replace(`${String(from)}/`, "")),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const { PathUtils } = await import("@cli/utils/PathUtils.js");
|
|
52
|
+
const { FileUtils } = await import("@cli/utils/FileUtils.js");
|
|
53
|
+
const { Validation } = await import("@cli/utils/Validation.js");
|
|
54
|
+
const fs = await import("node:fs");
|
|
55
|
+
|
|
56
|
+
describe("TemplateManagerImpl", () => {
|
|
57
|
+
let templateManager: TemplateManagerImpl;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
templateManager = new TemplateManagerImpl();
|
|
61
|
+
|
|
62
|
+
// 重置所有 mock
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
vi.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("getAvailableTemplates", () => {
|
|
71
|
+
it("should return empty array if templates directory not found", async () => {
|
|
72
|
+
(PathUtils.findTemplatesDir as any).mockReturnValue(null);
|
|
73
|
+
|
|
74
|
+
const templates = await templateManager.getAvailableTemplates();
|
|
75
|
+
|
|
76
|
+
expect(templates).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return available templates", async () => {
|
|
80
|
+
(PathUtils.findTemplatesDir as any).mockReturnValue("/templates");
|
|
81
|
+
(fs.default.readdirSync as any).mockReturnValue([
|
|
82
|
+
{ name: "template1", isDirectory: () => true },
|
|
83
|
+
{ name: "template2", isDirectory: () => true },
|
|
84
|
+
{ name: "file.txt", isDirectory: () => false },
|
|
85
|
+
]);
|
|
86
|
+
(PathUtils.getTemplatePath as any).mockImplementation(
|
|
87
|
+
(name: string) => `/templates/${name}`
|
|
88
|
+
);
|
|
89
|
+
(FileUtils.exists as any).mockReturnValue(false); // No template.json
|
|
90
|
+
(FileUtils.listDirectory as any).mockReturnValue([
|
|
91
|
+
"/templates/template1/index.js",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const templates = await templateManager.getAvailableTemplates();
|
|
95
|
+
|
|
96
|
+
expect(templates).toHaveLength(2);
|
|
97
|
+
expect(templates[0].name).toBe("template1");
|
|
98
|
+
expect(templates[1].name).toBe("template2");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle template directory read errors", async () => {
|
|
102
|
+
(PathUtils.findTemplatesDir as any).mockReturnValue("/templates");
|
|
103
|
+
(fs.default.readdirSync as any).mockImplementation(() => {
|
|
104
|
+
throw new Error("Permission denied");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(templateManager.getAvailableTemplates()).rejects.toThrow(
|
|
108
|
+
FileError
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("getTemplateInfo", () => {
|
|
114
|
+
it("should validate template name", async () => {
|
|
115
|
+
(Validation.validateTemplateName as any).mockImplementation(() => {
|
|
116
|
+
throw new ValidationError("Invalid template name", "templateName");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
templateManager.getTemplateInfo("invalid-name")
|
|
121
|
+
).rejects.toThrow(ValidationError);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should return null if template path not found", async () => {
|
|
125
|
+
(PathUtils.getTemplatePath as any).mockReturnValue(null);
|
|
126
|
+
|
|
127
|
+
const result = await templateManager.getTemplateInfo("nonexistent");
|
|
128
|
+
|
|
129
|
+
expect(result).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should return template info without config file", async () => {
|
|
133
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
134
|
+
(FileUtils.exists as any).mockReturnValue(false);
|
|
135
|
+
(FileUtils.listDirectory as any).mockReturnValue([
|
|
136
|
+
"/templates/test/index.js",
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const result = await templateManager.getTemplateInfo("test");
|
|
140
|
+
|
|
141
|
+
expect(result).toEqual({
|
|
142
|
+
name: "test",
|
|
143
|
+
path: "/templates/test",
|
|
144
|
+
description: "test 模板",
|
|
145
|
+
version: "1.0.0",
|
|
146
|
+
author: undefined,
|
|
147
|
+
files: ["/templates/test/index.js"],
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should return template info with config file", async () => {
|
|
152
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
153
|
+
(FileUtils.exists as any).mockReturnValue(true);
|
|
154
|
+
(FileUtils.readFile as any).mockReturnValue(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
description: "Test template",
|
|
157
|
+
version: "2.0.0",
|
|
158
|
+
author: "Test Author",
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
(FileUtils.listDirectory as any).mockReturnValue([
|
|
162
|
+
"/templates/test/index.js",
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const result = await templateManager.getTemplateInfo("test");
|
|
166
|
+
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
name: "test",
|
|
169
|
+
path: "/templates/test",
|
|
170
|
+
description: "Test template",
|
|
171
|
+
version: "2.0.0",
|
|
172
|
+
author: "Test Author",
|
|
173
|
+
files: ["/templates/test/index.js"],
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should handle invalid config file gracefully", async () => {
|
|
178
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
179
|
+
(FileUtils.exists as any).mockReturnValue(true);
|
|
180
|
+
(FileUtils.readFile as any).mockReturnValue("invalid json");
|
|
181
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
182
|
+
|
|
183
|
+
const result = await templateManager.getTemplateInfo("test");
|
|
184
|
+
|
|
185
|
+
expect(result?.description).toBe("test 模板");
|
|
186
|
+
expect(result?.version).toBe("1.0.0");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should cache template info", async () => {
|
|
190
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
191
|
+
(FileUtils.exists as any).mockReturnValue(false);
|
|
192
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
193
|
+
|
|
194
|
+
// First call
|
|
195
|
+
await templateManager.getTemplateInfo("test");
|
|
196
|
+
// Second call
|
|
197
|
+
await templateManager.getTemplateInfo("test");
|
|
198
|
+
|
|
199
|
+
expect(PathUtils.getTemplatePath).toHaveBeenCalledTimes(1);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("createProject", () => {
|
|
204
|
+
const defaultOptions: TemplateCreateOptions = {
|
|
205
|
+
targetPath: "my-project",
|
|
206
|
+
projectName: "MyProject",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
it("should validate create options", async () => {
|
|
210
|
+
(Validation.validateRequired as any).mockImplementation(() => {
|
|
211
|
+
throw new ValidationError("Required field missing", "field");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
templateManager.createProject(defaultOptions)
|
|
216
|
+
).rejects.toThrow(ValidationError);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should throw error if template not found", async () => {
|
|
220
|
+
(PathUtils.getTemplatePath as any).mockReturnValue(null);
|
|
221
|
+
|
|
222
|
+
await expect(
|
|
223
|
+
templateManager.createProject({
|
|
224
|
+
...defaultOptions,
|
|
225
|
+
templateName: "nonexistent",
|
|
226
|
+
})
|
|
227
|
+
).rejects.toThrow(FileError);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should throw error if target path already exists", async () => {
|
|
231
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/default");
|
|
232
|
+
(FileUtils.exists as any).mockImplementation((path: string) => {
|
|
233
|
+
return path === "/resolved/my-project";
|
|
234
|
+
});
|
|
235
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
templateManager.createProject(defaultOptions)
|
|
239
|
+
).rejects.toThrow(FileError);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should create project successfully", async () => {
|
|
243
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/default");
|
|
244
|
+
(FileUtils.exists as any).mockImplementation((path: string) => {
|
|
245
|
+
return path !== "/resolved/my-project"; // Target doesn't exist
|
|
246
|
+
});
|
|
247
|
+
(FileUtils.listDirectory as any).mockReturnValue([
|
|
248
|
+
"/templates/default/package.json",
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
await templateManager.createProject(defaultOptions);
|
|
252
|
+
|
|
253
|
+
expect(FileUtils.ensureDir).toHaveBeenCalledWith("/resolved/my-project");
|
|
254
|
+
expect(FileUtils.copyDirectory).toHaveBeenCalledWith(
|
|
255
|
+
"/templates/default",
|
|
256
|
+
"/resolved/my-project",
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
exclude: ["template.json", ".git", "node_modules"],
|
|
259
|
+
overwrite: false,
|
|
260
|
+
recursive: true,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should process template variables", async () => {
|
|
266
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/default");
|
|
267
|
+
(FileUtils.exists as any).mockImplementation((path: string) => {
|
|
268
|
+
return path !== "/resolved/my-project" && path.includes("package.json");
|
|
269
|
+
});
|
|
270
|
+
(FileUtils.listDirectory as any).mockReturnValue([
|
|
271
|
+
"/resolved/my-project/package.json",
|
|
272
|
+
]);
|
|
273
|
+
(FileUtils.readFile as any).mockReturnValue(
|
|
274
|
+
'{"name": "{{PROJECT_NAME}}"}'
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await templateManager.createProject({
|
|
278
|
+
...defaultOptions,
|
|
279
|
+
variables: { CUSTOM_VAR: "custom_value" },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(FileUtils.writeFile).toHaveBeenCalledWith(
|
|
283
|
+
"/resolved/my-project/package.json",
|
|
284
|
+
'{"name": "MyProject"}',
|
|
285
|
+
{ overwrite: true }
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("validateTemplate", () => {
|
|
291
|
+
it("should return false if template not found", async () => {
|
|
292
|
+
(PathUtils.getTemplatePath as any).mockReturnValue(null);
|
|
293
|
+
|
|
294
|
+
const isValid = await templateManager.validateTemplate("nonexistent");
|
|
295
|
+
|
|
296
|
+
expect(isValid).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should return false if required files missing", async () => {
|
|
300
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
301
|
+
(FileUtils.exists as any).mockImplementation((path: string) => {
|
|
302
|
+
return !path.includes("package.json");
|
|
303
|
+
});
|
|
304
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
305
|
+
|
|
306
|
+
const isValid = await templateManager.validateTemplate("test");
|
|
307
|
+
|
|
308
|
+
expect(isValid).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should return true if template is valid", async () => {
|
|
312
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
313
|
+
(FileUtils.exists as any).mockReturnValue(true);
|
|
314
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
315
|
+
|
|
316
|
+
const isValid = await templateManager.validateTemplate("test");
|
|
317
|
+
|
|
318
|
+
expect(isValid).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("clearCache", () => {
|
|
323
|
+
it("should clear template cache", async () => {
|
|
324
|
+
// Add something to cache first
|
|
325
|
+
(PathUtils.getTemplatePath as any).mockReturnValue("/templates/test");
|
|
326
|
+
(FileUtils.exists as any).mockReturnValue(false);
|
|
327
|
+
(FileUtils.listDirectory as any).mockReturnValue([]);
|
|
328
|
+
|
|
329
|
+
await templateManager.getTemplateInfo("test");
|
|
330
|
+
templateManager.clearCache();
|
|
331
|
+
|
|
332
|
+
// Should call getTemplatePath again after cache clear
|
|
333
|
+
await templateManager.getTemplateInfo("test");
|
|
334
|
+
expect(PathUtils.getTemplatePath).toHaveBeenCalledTimes(2);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend 模块类型声明
|
|
3
|
+
* 使用 any 类型避免递归解析 backend 代码
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module "@/lib/config/manager" {
|
|
7
|
+
export interface LocalMCPServerConfig {
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MCPServerConfig {
|
|
14
|
+
type?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const configManager: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module "@/lib/config/manager.js" {
|
|
25
|
+
export interface LocalMCPServerConfig {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MCPServerConfig {
|
|
32
|
+
type?: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
command?: string;
|
|
35
|
+
args?: string[];
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const configManager: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module "@root/WebServer" {
|
|
43
|
+
export class WebServer {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare module "@root/WebServer.js" {
|
|
47
|
+
export class WebServer {}
|
|
48
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件操作工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { FileOperationOptions } from "../Types";
|
|
9
|
+
import { FileError } from "../errors/index";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 文件工具类
|
|
13
|
+
*/
|
|
14
|
+
export class FileUtils {
|
|
15
|
+
/**
|
|
16
|
+
* 检查文件是否存在
|
|
17
|
+
*/
|
|
18
|
+
static exists(filePath: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return fs.existsSync(filePath);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 确保目录存在
|
|
28
|
+
*/
|
|
29
|
+
static ensureDir(dirPath: string): void {
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.existsSync(dirPath)) {
|
|
32
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new FileError("无法创建目录", dirPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 读取文件内容
|
|
41
|
+
*/
|
|
42
|
+
static readFile(filePath: string, encoding: BufferEncoding = "utf8"): string {
|
|
43
|
+
try {
|
|
44
|
+
if (!FileUtils.exists(filePath)) {
|
|
45
|
+
throw FileError.notFound(filePath);
|
|
46
|
+
}
|
|
47
|
+
return fs.readFileSync(filePath, encoding);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof FileError) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
throw new FileError("无法读取文件", filePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 写入文件内容
|
|
58
|
+
*/
|
|
59
|
+
static writeFile(
|
|
60
|
+
filePath: string,
|
|
61
|
+
content: string,
|
|
62
|
+
options?: { overwrite?: boolean }
|
|
63
|
+
): void {
|
|
64
|
+
try {
|
|
65
|
+
if (!options?.overwrite && FileUtils.exists(filePath)) {
|
|
66
|
+
throw FileError.alreadyExists(filePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 确保目录存在
|
|
70
|
+
const dir = path.dirname(filePath);
|
|
71
|
+
FileUtils.ensureDir(dir);
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof FileError) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
throw new FileError("无法写入文件", filePath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 复制文件
|
|
84
|
+
*/
|
|
85
|
+
static copyFile(
|
|
86
|
+
srcPath: string,
|
|
87
|
+
destPath: string,
|
|
88
|
+
options?: { overwrite?: boolean }
|
|
89
|
+
): void {
|
|
90
|
+
try {
|
|
91
|
+
if (!FileUtils.exists(srcPath)) {
|
|
92
|
+
throw FileError.notFound(srcPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!options?.overwrite && FileUtils.exists(destPath)) {
|
|
96
|
+
throw FileError.alreadyExists(destPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 确保目标目录存在
|
|
100
|
+
const destDir = path.dirname(destPath);
|
|
101
|
+
FileUtils.ensureDir(destDir);
|
|
102
|
+
|
|
103
|
+
fs.copyFileSync(srcPath, destPath);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof FileError) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
throw new FileError("无法复制文件", srcPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 删除文件
|
|
114
|
+
*/
|
|
115
|
+
static deleteFile(filePath: string): void {
|
|
116
|
+
try {
|
|
117
|
+
if (FileUtils.exists(filePath)) {
|
|
118
|
+
fs.unlinkSync(filePath);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new FileError("无法删除文件", filePath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 复制目录
|
|
127
|
+
*/
|
|
128
|
+
static copyDirectory(
|
|
129
|
+
srcDir: string,
|
|
130
|
+
destDir: string,
|
|
131
|
+
options: FileOperationOptions = {}
|
|
132
|
+
): void {
|
|
133
|
+
try {
|
|
134
|
+
if (!FileUtils.exists(srcDir)) {
|
|
135
|
+
throw FileError.notFound(srcDir);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 确保目标目录存在
|
|
139
|
+
FileUtils.ensureDir(destDir);
|
|
140
|
+
|
|
141
|
+
const items = fs.readdirSync(srcDir);
|
|
142
|
+
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
// 检查是否在排除列表中
|
|
145
|
+
if (options.exclude?.includes(item)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const srcPath = path.join(srcDir, item);
|
|
150
|
+
const destPath = path.join(destDir, item);
|
|
151
|
+
const stat = fs.statSync(srcPath);
|
|
152
|
+
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
if (options.recursive !== false) {
|
|
155
|
+
FileUtils.copyDirectory(srcPath, destPath, options);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
FileUtils.copyFile(srcPath, destPath, {
|
|
159
|
+
overwrite: options.overwrite,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof FileError) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
throw new FileError("无法复制目录", srcDir);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 删除目录
|
|
173
|
+
*/
|
|
174
|
+
static deleteDirectory(
|
|
175
|
+
dirPath: string,
|
|
176
|
+
options: { recursive?: boolean } = {}
|
|
177
|
+
): void {
|
|
178
|
+
try {
|
|
179
|
+
if (FileUtils.exists(dirPath)) {
|
|
180
|
+
fs.rmSync(dirPath, {
|
|
181
|
+
recursive: options.recursive ?? true,
|
|
182
|
+
force: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new FileError("无法删除目录", dirPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 获取文件信息
|
|
192
|
+
*/
|
|
193
|
+
static getFileInfo(filePath: string): {
|
|
194
|
+
size: number;
|
|
195
|
+
isFile: boolean;
|
|
196
|
+
isDirectory: boolean;
|
|
197
|
+
mtime: Date;
|
|
198
|
+
ctime: Date;
|
|
199
|
+
} {
|
|
200
|
+
try {
|
|
201
|
+
if (!FileUtils.exists(filePath)) {
|
|
202
|
+
throw FileError.notFound(filePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const stats = fs.statSync(filePath);
|
|
206
|
+
return {
|
|
207
|
+
size: stats.size,
|
|
208
|
+
isFile: stats.isFile(),
|
|
209
|
+
isDirectory: stats.isDirectory(),
|
|
210
|
+
mtime: stats.mtime,
|
|
211
|
+
ctime: stats.ctime,
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof FileError) {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
throw new FileError("无法获取文件信息", filePath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 列出目录内容
|
|
223
|
+
*/
|
|
224
|
+
static listDirectory(
|
|
225
|
+
dirPath: string,
|
|
226
|
+
options: {
|
|
227
|
+
recursive?: boolean;
|
|
228
|
+
includeHidden?: boolean;
|
|
229
|
+
} = {}
|
|
230
|
+
): string[] {
|
|
231
|
+
try {
|
|
232
|
+
if (!FileUtils.exists(dirPath)) {
|
|
233
|
+
throw FileError.notFound(dirPath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const items = fs.readdirSync(dirPath);
|
|
237
|
+
let result: string[] = [];
|
|
238
|
+
|
|
239
|
+
for (const item of items) {
|
|
240
|
+
// 跳过隐藏文件(除非明确要求包含)
|
|
241
|
+
if (!options.includeHidden && item.startsWith(".")) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const itemPath = path.join(dirPath, item);
|
|
246
|
+
result.push(itemPath);
|
|
247
|
+
|
|
248
|
+
// 递归处理子目录
|
|
249
|
+
if (options.recursive && fs.statSync(itemPath).isDirectory()) {
|
|
250
|
+
const subItems = FileUtils.listDirectory(itemPath, options);
|
|
251
|
+
result = result.concat(subItems);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof FileError) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
throw new FileError("无法列出目录内容", dirPath);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 创建临时文件
|
|
266
|
+
*/
|
|
267
|
+
static createTempFile(prefix = "xiaozhi-", suffix = ".tmp"): string {
|
|
268
|
+
const tempDir = process.env.TMPDIR || process.env.TEMP || tmpdir();
|
|
269
|
+
const timestamp = Date.now();
|
|
270
|
+
const random = Math.random().toString(36).substring(2);
|
|
271
|
+
const fileName = `${prefix}${timestamp}-${random}${suffix}`;
|
|
272
|
+
return path.join(tempDir, fileName);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 检查文件权限
|
|
277
|
+
*/
|
|
278
|
+
static checkPermissions(
|
|
279
|
+
filePath: string,
|
|
280
|
+
mode: number = fs.constants.R_OK | fs.constants.W_OK
|
|
281
|
+
): boolean {
|
|
282
|
+
try {
|
|
283
|
+
fs.accessSync(filePath, mode);
|
|
284
|
+
return true;
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 获取文件扩展名
|
|
292
|
+
*/
|
|
293
|
+
static getExtension(filePath: string): string {
|
|
294
|
+
return path.extname(filePath).toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 获取文件名(不含扩展名)
|
|
299
|
+
*/
|
|
300
|
+
static getBaseName(filePath: string): string {
|
|
301
|
+
return path.basename(filePath, path.extname(filePath));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 规范化路径
|
|
306
|
+
*/
|
|
307
|
+
static normalizePath(filePath: string): string {
|
|
308
|
+
return path.normalize(filePath);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 解析相对路径为绝对路径
|
|
313
|
+
*/
|
|
314
|
+
static resolvePath(filePath: string, basePath?: string): string {
|
|
315
|
+
if (basePath) {
|
|
316
|
+
return path.resolve(basePath, filePath);
|
|
317
|
+
}
|
|
318
|
+
return path.resolve(filePath);
|
|
319
|
+
}
|
|
320
|
+
}
|