@xiaozhi-client/cli 2.0.0 → 2.1.0-beta.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.
- package/package.json +2 -2
- package/src/Container.ts +8 -0
- package/src/commands/CommandHandlerFactory.ts +9 -0
- package/src/commands/ConfigCommandHandler.ts +22 -14
- package/src/services/DaemonManager.ts +8 -0
- package/src/services/ServiceManager.ts +8 -0
- package/src/services/TemplateManager.ts +9 -0
- package/src/utils/__tests__/path-utils.config.test.ts +214 -0
- package/src/utils/__tests__/path-utils.executable.test.ts +424 -0
- package/src/utils/__tests__/path-utils.filesystem.test.ts +278 -0
- package/src/utils/__tests__/path-utils.security.test.ts +357 -0
- package/src/utils/__tests__/path-utils.test.ts +0 -1165
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaozhi-client/cli",
|
|
3
|
-
"version": "2.0.0",
|
|
3
|
+
"version": "2.1.0-beta.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"commander": "^14.0.0",
|
|
23
23
|
"consola": "^3.4.2",
|
|
24
24
|
"ora": "^8.2.0",
|
|
25
|
-
"@xiaozhi-client/config": "2.0.0"
|
|
25
|
+
"@xiaozhi-client/config": "2.1.0-beta.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.3.0",
|
package/src/Container.ts
CHANGED
|
@@ -110,6 +110,23 @@ export class ConfigCommandHandler extends BaseCommandHandler {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* 确保配置文件存在,如果不存在则显示提示并返回 false
|
|
115
|
+
*/
|
|
116
|
+
private async ensureConfigExists(spinner: ora.Ora): Promise<boolean> {
|
|
117
|
+
const configManager = this.getService<any>("configManager");
|
|
118
|
+
|
|
119
|
+
if (!configManager.configExists()) {
|
|
120
|
+
spinner.fail("配置文件不存在");
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.yellow('💡 提示: 请先运行 "xiaozhi config init" 初始化配置')
|
|
123
|
+
);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
/**
|
|
114
131
|
* 处理获取配置命令
|
|
115
132
|
*/
|
|
@@ -117,16 +134,11 @@ export class ConfigCommandHandler extends BaseCommandHandler {
|
|
|
117
134
|
const spinner = ora("读取配置...").start();
|
|
118
135
|
|
|
119
136
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!configManager.configExists()) {
|
|
123
|
-
spinner.fail("配置文件不存在");
|
|
124
|
-
console.log(
|
|
125
|
-
chalk.yellow('💡 提示: 请先运行 "xiaozhi config init" 初始化配置')
|
|
126
|
-
);
|
|
137
|
+
if (!(await this.ensureConfigExists(spinner))) {
|
|
127
138
|
return;
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
const configManager = this.getService<any>("configManager");
|
|
130
142
|
const config = configManager.getConfig();
|
|
131
143
|
|
|
132
144
|
switch (key) {
|
|
@@ -226,16 +238,12 @@ export class ConfigCommandHandler extends BaseCommandHandler {
|
|
|
226
238
|
const spinner = ora("更新配置...").start();
|
|
227
239
|
|
|
228
240
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (!configManager.configExists()) {
|
|
232
|
-
spinner.fail("配置文件不存在");
|
|
233
|
-
console.log(
|
|
234
|
-
chalk.yellow('💡 提示: 请先运行 "xiaozhi config init" 初始化配置')
|
|
235
|
-
);
|
|
241
|
+
if (!(await this.ensureConfigExists(spinner))) {
|
|
236
242
|
return;
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
const configManager = this.getService<any>("configManager");
|
|
246
|
+
|
|
239
247
|
switch (key) {
|
|
240
248
|
case "mcpEndpoint":
|
|
241
249
|
configManager.updateMcpEndpoint(value);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { PathUtils } from "../PathUtils.js";
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock("../FileUtils.js", () => ({
|
|
8
|
+
FileUtils: {
|
|
9
|
+
exists: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("node:url", () => ({
|
|
14
|
+
fileURLToPath: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock environment variables
|
|
18
|
+
const originalEnv = process.env;
|
|
19
|
+
|
|
20
|
+
describe("PathUtils - 配置路径", () => {
|
|
21
|
+
let mockFileExists: any;
|
|
22
|
+
let mockFileURLToPath: any;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
|
|
27
|
+
// Setup mocks
|
|
28
|
+
const { FileUtils } = await import("../FileUtils.js");
|
|
29
|
+
mockFileExists = vi.mocked(FileUtils.exists);
|
|
30
|
+
mockFileURLToPath = vi.mocked(fileURLToPath);
|
|
31
|
+
|
|
32
|
+
// Default mock implementations
|
|
33
|
+
mockFileExists.mockReturnValue(true);
|
|
34
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
35
|
+
|
|
36
|
+
// Reset environment variables
|
|
37
|
+
process.env = { ...originalEnv };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
process.env = originalEnv;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getConfigDir 获取配置目录路径", () => {
|
|
45
|
+
it("应该返回环境变量中的配置目录", () => {
|
|
46
|
+
process.env.XIAOZHI_CONFIG_DIR = "/env/config";
|
|
47
|
+
|
|
48
|
+
const result = PathUtils.getConfigDir();
|
|
49
|
+
|
|
50
|
+
expect(result).toBe("/env/config");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("应该在没有环境变量时返回当前工作目录", () => {
|
|
54
|
+
process.env.XIAOZHI_CONFIG_DIR = undefined;
|
|
55
|
+
const originalCwd = process.cwd();
|
|
56
|
+
|
|
57
|
+
const result = PathUtils.getConfigDir();
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(originalCwd);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("getWorkDir 获取工作目录路径", () => {
|
|
64
|
+
it("应该基于配置目录返回工作目录", () => {
|
|
65
|
+
process.env.XIAOZHI_CONFIG_DIR = "/custom/config";
|
|
66
|
+
|
|
67
|
+
const result = PathUtils.getWorkDir();
|
|
68
|
+
const expected = path.join("/custom/config", ".xiaozhi");
|
|
69
|
+
|
|
70
|
+
expect(result).toBe(expected);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("应该在没有配置目录时基于当前工作目录", () => {
|
|
74
|
+
process.env.XIAOZHI_CONFIG_DIR = undefined;
|
|
75
|
+
const originalCwd = process.cwd();
|
|
76
|
+
|
|
77
|
+
const result = PathUtils.getWorkDir();
|
|
78
|
+
const expected = path.join(originalCwd, ".xiaozhi");
|
|
79
|
+
|
|
80
|
+
expect(result).toBe(expected);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("resolveConfigPath 解析配置文件路径", () => {
|
|
85
|
+
it("应该返回指定格式的配置文件路径", () => {
|
|
86
|
+
process.env.XIAOZHI_CONFIG_DIR = "/config";
|
|
87
|
+
|
|
88
|
+
const result = PathUtils.resolveConfigPath("json5");
|
|
89
|
+
const expected = path.join("/config", "xiaozhi.config.json5");
|
|
90
|
+
|
|
91
|
+
expect(result).toBe(expected);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("应该按优先级查找存在的配置文件", () => {
|
|
95
|
+
process.env.XIAOZHI_CONFIG_DIR = "/config";
|
|
96
|
+
mockFileExists
|
|
97
|
+
.mockReturnValueOnce(false) // json5 不存在
|
|
98
|
+
.mockReturnValueOnce(true); // jsonc 存在
|
|
99
|
+
|
|
100
|
+
const result = PathUtils.resolveConfigPath();
|
|
101
|
+
const expected = path.join("/config", "xiaozhi.config.jsonc");
|
|
102
|
+
|
|
103
|
+
expect(result).toBe(expected);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("应该在没有找到配置文件时返回默认路径", () => {
|
|
107
|
+
process.env.XIAOZHI_CONFIG_DIR = "/config";
|
|
108
|
+
mockFileExists.mockReturnValue(false);
|
|
109
|
+
|
|
110
|
+
const result = PathUtils.resolveConfigPath();
|
|
111
|
+
const expected = path.join("/config", "xiaozhi.config.json");
|
|
112
|
+
|
|
113
|
+
expect(result).toBe(expected);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("应该处理所有支持的配置文件格式", () => {
|
|
117
|
+
process.env.XIAOZHI_CONFIG_DIR = "/config";
|
|
118
|
+
|
|
119
|
+
const formats = ["json", "json5", "jsonc"] as const;
|
|
120
|
+
|
|
121
|
+
for (const format of formats) {
|
|
122
|
+
const result = PathUtils.resolveConfigPath(format);
|
|
123
|
+
const expected = path.join("/config", `xiaozhi.config.${format}`);
|
|
124
|
+
expect(result).toBe(expected);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("getDefaultConfigPath 获取默认配置文件路径", () => {
|
|
130
|
+
it("应该返回项目根目录下的默认配置文件路径", () => {
|
|
131
|
+
mockFileURLToPath.mockReturnValue("/project/src/cli/utils/PathUtils.js");
|
|
132
|
+
|
|
133
|
+
const result = PathUtils.getDefaultConfigPath();
|
|
134
|
+
const expected = path.join("/project", "xiaozhi.config.default.json");
|
|
135
|
+
|
|
136
|
+
expect(result).toBe(expected);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("getScriptDir 获取脚本目录路径", () => {
|
|
141
|
+
it("应该返回脚本所在目录", () => {
|
|
142
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
143
|
+
|
|
144
|
+
const result = PathUtils.getScriptDir();
|
|
145
|
+
|
|
146
|
+
expect(result).toBe("/test/src/cli/utils");
|
|
147
|
+
expect(mockFileURLToPath).toHaveBeenCalledWith(expect.any(String));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("应该处理不同的脚本路径", () => {
|
|
151
|
+
mockFileURLToPath.mockReturnValue("/different/path/script.js");
|
|
152
|
+
|
|
153
|
+
const result = PathUtils.getScriptDir();
|
|
154
|
+
|
|
155
|
+
expect(result).toBe("/different/path");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("getProjectRoot 获取项目根目录路径", () => {
|
|
160
|
+
it("应该从脚本目录计算项目根目录", () => {
|
|
161
|
+
mockFileURLToPath.mockReturnValue("/project/src/cli/utils/PathUtils.js");
|
|
162
|
+
|
|
163
|
+
const result = PathUtils.getProjectRoot();
|
|
164
|
+
// 使用跨平台的路径比较
|
|
165
|
+
const expected = path.normalize("/project");
|
|
166
|
+
|
|
167
|
+
expect(result).toBe(expected);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("应该处理不同深度的脚本路径", () => {
|
|
171
|
+
mockFileURLToPath.mockReturnValue(
|
|
172
|
+
"/deep/nested/src/cli/utils/PathUtils.js"
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = PathUtils.getProjectRoot();
|
|
176
|
+
// 使用跨平台的路径比较
|
|
177
|
+
const expected = path.normalize("/deep/nested");
|
|
178
|
+
|
|
179
|
+
expect(result).toBe(expected);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("getDistDir 获取构建输出目录路径", () => {
|
|
184
|
+
it("应该返回项目根目录下的 dist 目录", () => {
|
|
185
|
+
mockFileURLToPath.mockReturnValue("/project/src/cli/utils/PathUtils.js");
|
|
186
|
+
|
|
187
|
+
const result = PathUtils.getDistDir();
|
|
188
|
+
const expected = path.join("/project", "dist");
|
|
189
|
+
|
|
190
|
+
expect(result).toBe(expected);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("getRelativePath 获取相对路径", () => {
|
|
195
|
+
it("应该返回相对于项目根目录的路径", () => {
|
|
196
|
+
mockFileURLToPath.mockReturnValue("/project/src/cli/utils/PathUtils.js");
|
|
197
|
+
const filePath = "/project/src/test/file.js";
|
|
198
|
+
|
|
199
|
+
const result = PathUtils.getRelativePath(filePath);
|
|
200
|
+
const expected = path.relative("/project", filePath);
|
|
201
|
+
|
|
202
|
+
expect(result).toBe(expected);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("应该处理项目外的文件路径", () => {
|
|
206
|
+
mockFileURLToPath.mockReturnValue("/project/src/cli/utils/PathUtils.js");
|
|
207
|
+
const filePath = "/outside/project/file.js";
|
|
208
|
+
|
|
209
|
+
const result = PathUtils.getRelativePath(filePath);
|
|
210
|
+
|
|
211
|
+
expect(result).toContain("..");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|