@xiaozhi-client/cli 2.0.0-beta.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaozhi-client/cli",
3
- "version": "2.0.0-beta.7",
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-beta.7"
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
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * 依赖注入容器
3
+ *
4
+ * 负责管理 CLI 的依赖注入,包括:
5
+ * - 服务注册(工厂、单例、实例)
6
+ * - 服务获取
7
+ * - 单例模式管理
8
+ * - 默认服务配置
9
+ *
10
+ * @module packages/cli/src/Container
3
11
  */
4
12
 
5
13
  import { configManager } from "@xiaozhi-client/config";
@@ -1,5 +1,14 @@
1
1
  /**
2
2
  * 命令处理器工厂
3
+ *
4
+ * 负责创建和管理命令处理器,包括:
5
+ * - 服务命令处理器
6
+ * - 配置命令处理器
7
+ * - 项目命令处理器
8
+ * - MCP 命令处理器
9
+ * - 端点命令处理器
10
+ *
11
+ * @module packages/cli/src/commands/CommandHandlerFactory
3
12
  */
4
13
 
5
14
  import type {
@@ -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
- const configManager = this.getService<any>("configManager");
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
- const configManager = this.getService<any>("configManager");
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);
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * 守护进程管理服务
3
+ *
4
+ * 负责管理 xiaozhi 服务的守护进程,包括:
5
+ * - 守护进程启动和停止
6
+ * - 日志文件管理
7
+ * - 进程状态监控
8
+ * - 支持手动重启
9
+ *
10
+ * @module packages/cli/src/services/DaemonManager
3
11
  */
4
12
 
5
13
  import type { ChildProcess } from "node:child_process";
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * 服务管理服务
3
+ *
4
+ * 负责管理 xiaozhi 服务的生命周期,包括:
5
+ * - 服务启动和停止
6
+ * - 进程状态查询
7
+ * - 配置验证和初始化
8
+ * - 多种运行模式支持(normal、mcp-server)
9
+ *
10
+ * @module packages/cli/src/services/ServiceManager
3
11
  */
4
12
 
5
13
  import type { ConfigManager } from "@xiaozhi-client/config";
@@ -1,5 +1,14 @@
1
1
  /**
2
2
  * 模板管理服务
3
+ *
4
+ * 负责管理项目模板,包括:
5
+ * - 模板列表查询
6
+ * - 模板信息获取
7
+ * - 项目创建和复制
8
+ * - 模板变量替换
9
+ * - 模板验证
10
+ *
11
+ * @module packages/cli/src/services/TemplateManager
3
12
  */
4
13
 
5
14
  import fs from "node:fs";
@@ -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
+ });