@ai-setting/roy-agent-cli 1.0.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 (158) hide show
  1. package/README.md +126 -0
  2. package/dist/bin/roy.js +127297 -0
  3. package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
  4. package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
  5. package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
  6. package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
  7. package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
  8. package/package.json +91 -0
  9. package/src/bin/roy.ts +12 -0
  10. package/src/cli.ts +101 -0
  11. package/src/commands/act.ts +480 -0
  12. package/src/commands/commands-add.ts +110 -0
  13. package/src/commands/commands-dirs.ts +70 -0
  14. package/src/commands/commands-info.ts +90 -0
  15. package/src/commands/commands-list.ts +161 -0
  16. package/src/commands/commands-remove.ts +147 -0
  17. package/src/commands/commands.ts +55 -0
  18. package/src/commands/config/config-service.test.ts +449 -0
  19. package/src/commands/config/config-service.ts +312 -0
  20. package/src/commands/config/deep-merge.test.ts +168 -0
  21. package/src/commands/config/deep-merge.ts +63 -0
  22. package/src/commands/config/export.ts +97 -0
  23. package/src/commands/config/filter-history-e2e.test.ts +141 -0
  24. package/src/commands/config/import-preserve-refs.test.ts +212 -0
  25. package/src/commands/config/import.ts +119 -0
  26. package/src/commands/config/index.ts +35 -0
  27. package/src/commands/config/list.ts +281 -0
  28. package/src/commands/config/roy-config-e2e.test.ts +297 -0
  29. package/src/commands/config/types.ts +54 -0
  30. package/src/commands/debug/index.ts +38 -0
  31. package/src/commands/debug/log.test.ts +233 -0
  32. package/src/commands/debug/log.ts +123 -0
  33. package/src/commands/debug/span.test.ts +297 -0
  34. package/src/commands/debug/span.ts +211 -0
  35. package/src/commands/debug/trace.test.ts +254 -0
  36. package/src/commands/debug/trace.ts +140 -0
  37. package/src/commands/eventsource/add.ts +133 -0
  38. package/src/commands/eventsource/index.ts +48 -0
  39. package/src/commands/eventsource/list.ts +194 -0
  40. package/src/commands/eventsource/remove.ts +95 -0
  41. package/src/commands/eventsource/start.ts +103 -0
  42. package/src/commands/eventsource/status.ts +185 -0
  43. package/src/commands/eventsource/stop.ts +89 -0
  44. package/src/commands/index.ts +22 -0
  45. package/src/commands/input-handler.test.ts +76 -0
  46. package/src/commands/input-handler.ts +43 -0
  47. package/src/commands/interactive-esc.test.ts +254 -0
  48. package/src/commands/interactive.shutdown.test.ts +122 -0
  49. package/src/commands/interactive.test.ts +221 -0
  50. package/src/commands/interactive.ts +1015 -0
  51. package/src/commands/lsp/check.ts +92 -0
  52. package/src/commands/lsp/index.ts +32 -0
  53. package/src/commands/lsp/install.ts +126 -0
  54. package/src/commands/lsp/list.ts +64 -0
  55. package/src/commands/mcp/index.ts +27 -0
  56. package/src/commands/mcp/list.ts +116 -0
  57. package/src/commands/mcp/reload.ts +70 -0
  58. package/src/commands/mcp/tools.ts +121 -0
  59. package/src/commands/memory/extract-e2e.test.ts +388 -0
  60. package/src/commands/memory/index.ts +11 -0
  61. package/src/commands/memory/memory-simplified.test.ts +58 -0
  62. package/src/commands/memory/memory.ts +25 -0
  63. package/src/commands/memory/organize.ts +300 -0
  64. package/src/commands/memory/recall.test.ts +120 -0
  65. package/src/commands/memory/recall.ts +88 -0
  66. package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
  67. package/src/commands/memory/record-prompt-component.test.ts +343 -0
  68. package/src/commands/memory/record.test.ts +92 -0
  69. package/src/commands/memory/record.ts +332 -0
  70. package/src/commands/plugin.test.ts +292 -0
  71. package/src/commands/plugin.ts +267 -0
  72. package/src/commands/sessions/active.ts +96 -0
  73. package/src/commands/sessions/add-message.ts +96 -0
  74. package/src/commands/sessions/checkpoints.ts +154 -0
  75. package/src/commands/sessions/compact.test.ts +215 -0
  76. package/src/commands/sessions/compact.ts +269 -0
  77. package/src/commands/sessions/delete.ts +236 -0
  78. package/src/commands/sessions/get.ts +165 -0
  79. package/src/commands/sessions/grep.ts +233 -0
  80. package/src/commands/sessions/index.ts +95 -0
  81. package/src/commands/sessions/list.ts +210 -0
  82. package/src/commands/sessions/messages.test.ts +333 -0
  83. package/src/commands/sessions/messages.ts +248 -0
  84. package/src/commands/sessions/mock.ts +194 -0
  85. package/src/commands/sessions/new.ts +82 -0
  86. package/src/commands/sessions/rename.ts +98 -0
  87. package/src/commands/shared/event-handler.ts +213 -0
  88. package/src/commands/shared/event-message-formatter.ts +295 -0
  89. package/src/commands/shared/index.ts +11 -0
  90. package/src/commands/shared/query-executor.test.ts +434 -0
  91. package/src/commands/shared/query-executor.ts +324 -0
  92. package/src/commands/shared/repl-engine.test.ts +354 -0
  93. package/src/commands/shared/session-manager.test.ts +212 -0
  94. package/src/commands/shared/session-manager.ts +114 -0
  95. package/src/commands/skills/get.ts +90 -0
  96. package/src/commands/skills/index.ts +39 -0
  97. package/src/commands/skills/list.ts +129 -0
  98. package/src/commands/skills/reload.ts +59 -0
  99. package/src/commands/skills/search.ts +132 -0
  100. package/src/commands/skills/show-config.ts +93 -0
  101. package/src/commands/tasks/complete.ts +92 -0
  102. package/src/commands/tasks/create.ts +118 -0
  103. package/src/commands/tasks/delete.ts +86 -0
  104. package/src/commands/tasks/get.ts +116 -0
  105. package/src/commands/tasks/index.ts +53 -0
  106. package/src/commands/tasks/list.ts +140 -0
  107. package/src/commands/tasks/operations.ts +120 -0
  108. package/src/commands/tasks/update.ts +122 -0
  109. package/src/commands/tools/exec-tool.ts +128 -0
  110. package/src/commands/tools/get.ts +114 -0
  111. package/src/commands/tools/index.ts +35 -0
  112. package/src/commands/tools/list.ts +107 -0
  113. package/src/commands/tools/shared/index.ts +7 -0
  114. package/src/commands/tools/shared/schema-helper.ts +111 -0
  115. package/src/commands/workflow/commands/add.ts +315 -0
  116. package/src/commands/workflow/commands/get.ts +193 -0
  117. package/src/commands/workflow/commands/list.ts +137 -0
  118. package/src/commands/workflow/commands/nodes.ts +528 -0
  119. package/src/commands/workflow/commands/remove.ts +94 -0
  120. package/src/commands/workflow/commands/run.ts +398 -0
  121. package/src/commands/workflow/commands/status.ts +147 -0
  122. package/src/commands/workflow/commands/stop.ts +91 -0
  123. package/src/commands/workflow/commands/update.ts +130 -0
  124. package/src/commands/workflow/commands/validate.ts +139 -0
  125. package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
  126. package/src/commands/workflow/index.ts +65 -0
  127. package/src/commands/workflow/renderers.ts +358 -0
  128. package/src/commands/workflow/validators/index.ts +8 -0
  129. package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
  130. package/src/commands/workflow/validators/node-validator.ts +125 -0
  131. package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
  132. package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
  133. package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
  134. package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
  135. package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
  136. package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
  137. package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
  138. package/src/commands/workflow/validators/types.ts +78 -0
  139. package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
  140. package/src/commands/workflow/validators/workflow-validator.ts +320 -0
  141. package/src/index.ts +19 -0
  142. package/src/plugin/apply.ts +103 -0
  143. package/src/plugin/discover.ts +219 -0
  144. package/src/plugin/index.ts +45 -0
  145. package/src/plugin/registry.ts +272 -0
  146. package/src/plugin/types.ts +165 -0
  147. package/src/services/context-handler.service.test.ts +501 -0
  148. package/src/services/context-handler.service.ts +372 -0
  149. package/src/services/environment.service.commands-prompt.test.ts +167 -0
  150. package/src/services/environment.service.ts +656 -0
  151. package/src/services/output.service.test.ts +92 -0
  152. package/src/services/output.service.ts +122 -0
  153. package/src/services/quiet-mode.service.test.ts +114 -0
  154. package/src/services/quiet-mode.service.ts +81 -0
  155. package/src/services/stream-output.service.test.ts +214 -0
  156. package/src/services/stream-output.service.ts +323 -0
  157. package/src/util/which.test.ts +101 -0
  158. package/src/util/which.ts +55 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @fileoverview ConfigService - 配置服务工具类
3
+ *
4
+ * 提供配置的导出、导入等功能
5
+ */
6
+
7
+ import * as fsSync from "fs";
8
+ import * as fsPromises from "fs/promises";
9
+ import * as path from "path";
10
+ import type { ConfigComponent } from "@ai-setting/roy-agent-core";
11
+ import { parseJSONC, parseJSONCWithEnv } from "@ai-setting/roy-agent-core/config";
12
+
13
+ // ConfigComponent 类型(兼容 BaseComponent 返回的 Component | undefined)
14
+ type AnyConfigComponent = ConfigComponent | undefined;
15
+ import type { OutputService } from "../../services/output.service";
16
+ import { deepMerge } from "./deep-merge";
17
+
18
+ export interface ConfigExportOptions {
19
+ /** 导出到文件路径(不指定则返回内容) */
20
+ filePath?: string;
21
+ /** 格式化输出(默认 true) */
22
+ pretty?: boolean;
23
+ }
24
+
25
+ export interface ConfigImportOptions {
26
+ /** 导入源配置文件 */
27
+ sourceFile: string;
28
+ /** 预览模式,不实际写入 */
29
+ dryRun?: boolean;
30
+ /** 显示详细操作信息 */
31
+ verbose?: boolean;
32
+ }
33
+
34
+ export interface ImportResult {
35
+ success: boolean;
36
+ merged: boolean;
37
+ changes: Array<{
38
+ key: string;
39
+ oldValue: unknown;
40
+ newValue: unknown;
41
+ }>;
42
+ filePath: string;
43
+ message?: string;
44
+ }
45
+
46
+ /**
47
+ * ConfigService - 配置服务
48
+ */
49
+ export class ConfigService {
50
+ constructor(
51
+ private configComponent: ConfigComponent,
52
+ private output: OutputService
53
+ ) {}
54
+
55
+ /**
56
+ * 获取 component 的运行时配置值
57
+ *
58
+ * 从所有配置源读取配置,合并后返回:
59
+ * - memory source(最高优先级,用户运行时修改)
60
+ * - file source(配置文件)
61
+ * - defaults(注册到 memory 的默认值)
62
+ */
63
+ getComponentConfig(componentName: string): Record<string, unknown> {
64
+ const sources = this.configComponent.getSources();
65
+
66
+ // 查找 memory source(最高优先级)
67
+ const memorySource = sources.find((s) => s.name === "memory");
68
+ // 查找 file source
69
+ const fileSource = sources.find((s) => s.name === "file");
70
+
71
+ const prefix = `${componentName}.`;
72
+ const result: Record<string, unknown> = {};
73
+
74
+ // 1. 先从 file source 读取(作为基础)
75
+ if (fileSource) {
76
+ const fileEntries = fileSource.list();
77
+ for (const entry of fileEntries) {
78
+ if (entry.key === componentName) {
79
+ // component 本身就是根键
80
+ result[entry.key] = entry.value;
81
+ } else if (entry.key.startsWith(prefix)) {
82
+ const restKey = entry.key.slice(prefix.length);
83
+ this.setNestedValue(result, restKey, entry.value);
84
+ }
85
+ }
86
+ }
87
+
88
+ // 2. 再从 memory source 读取(覆盖 file source)
89
+ if (memorySource) {
90
+ const memoryEntries = memorySource.list();
91
+ for (const entry of memoryEntries) {
92
+ if (entry.key === componentName) {
93
+ // component 本身就是根键
94
+ result[entry.key] = entry.value;
95
+ } else if (entry.key.startsWith(prefix)) {
96
+ const restKey = entry.key.slice(prefix.length);
97
+ this.setNestedValue(result, restKey, entry.value);
98
+ }
99
+ }
100
+ }
101
+
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * 获取 component 的 file source 物理路径
107
+ */
108
+ getComponentFilePath(componentName: string): string | undefined {
109
+ const sources = this.configComponent.getSources();
110
+ const fileSource = sources.find((s) => s.name === "file");
111
+
112
+ if (fileSource && "filePath" in fileSource) {
113
+ return fileSource.filePath as string;
114
+ }
115
+
116
+ return undefined;
117
+ }
118
+
119
+ /**
120
+ * 导出配置到文件
121
+ */
122
+ async exportToFile(
123
+ componentName: string,
124
+ options: ConfigExportOptions
125
+ ): Promise<string> {
126
+ const config = this.getComponentConfig(componentName);
127
+ const content = JSON.stringify(
128
+ { [componentName]: config },
129
+ null,
130
+ options.pretty !== false ? 2 : 0
131
+ );
132
+
133
+ if (options.filePath) {
134
+ const dir = path.dirname(options.filePath);
135
+ if (!fsSync.existsSync(dir)) {
136
+ await fsPromises.mkdir(dir, { recursive: true });
137
+ }
138
+ await fsPromises.writeFile(options.filePath, content, "utf-8");
139
+ this.output.log(`✅ 已导出 ${componentName} 配置到 ${options.filePath}`);
140
+ }
141
+
142
+ return content;
143
+ }
144
+
145
+ /**
146
+ * 从文件导入配置(只写入 file source)
147
+ */
148
+ async importFromFile(
149
+ componentName: string,
150
+ options: ConfigImportOptions
151
+ ): Promise<ImportResult> {
152
+ const result: ImportResult = {
153
+ success: false,
154
+ merged: false,
155
+ changes: [],
156
+ filePath: "",
157
+ };
158
+
159
+ // 1. 解析源文件
160
+ let sourceData: Record<string, unknown>;
161
+ try {
162
+ const content = await fsPromises.readFile(options.sourceFile, "utf-8");
163
+ // 源文件也可能是 JSONC 格式,使用 parseJSONCWithEnv 解析
164
+ sourceData = parseJSONCWithEnv(content);
165
+ } catch (error) {
166
+ result.message = `无法读取源文件: ${error}`;
167
+ return result;
168
+ }
169
+
170
+ // 提取 component 配置
171
+ const sourceComponentConfig = sourceData[componentName];
172
+ if (!sourceComponentConfig || typeof sourceComponentConfig !== "object") {
173
+ result.message = `源文件中未找到 ${componentName} 配置`;
174
+ return result;
175
+ }
176
+
177
+ // 2. 获取目标文件路径
178
+ const targetFilePath = this.getComponentFilePath(componentName);
179
+ if (!targetFilePath) {
180
+ result.message = `无法获取 ${componentName} 的 file source 路径`;
181
+ return result;
182
+ }
183
+ result.filePath = targetFilePath;
184
+
185
+ // 3. 读取目标文件现有内容
186
+ let targetData: Record<string, unknown> = {} as Record<string, unknown>;
187
+ try {
188
+ if (fsSync.existsSync(targetFilePath)) {
189
+ const content = await fsPromises.readFile(targetFilePath, "utf-8");
190
+ // 使用 parseJSONC 解析目标文件(保留 JSONC 语法包括 $ 引用)
191
+ // 注意:不使用 parseJSONCWithEnv,否则会替换掉 $ 引用
192
+ targetData = parseJSONC(content);
193
+ }
194
+ } catch (error) {
195
+ // 文件不存在或解析失败,使用空对象
196
+ if (options.verbose) {
197
+ this.output.log(`📝 目标文件不存在或无法解析,将创建新文件`);
198
+ }
199
+ }
200
+
201
+ // 4. 深度合并
202
+ const existingComponentConfig = (targetData[componentName] || {}) as Record<string, unknown>;
203
+ const mergedComponentConfig = deepMerge(
204
+ existingComponentConfig,
205
+ sourceComponentConfig as Record<string, unknown>
206
+ );
207
+
208
+ targetData[componentName] = mergedComponentConfig;
209
+
210
+ // 计算变更
211
+ result.changes = this.calculateChanges(
212
+ existingComponentConfig,
213
+ sourceComponentConfig as Record<string, unknown>
214
+ );
215
+ result.merged = result.changes.length > 0;
216
+
217
+ if (options.verbose) {
218
+ this.output.log(`📋 发现 ${result.changes.length} 个配置变更`);
219
+ for (const change of result.changes) {
220
+ this.output.log(` ${change.key}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`);
221
+ }
222
+ }
223
+
224
+ // 5. Dry-run 模式
225
+ if (options.dryRun) {
226
+ this.output.log(`🔍 预览模式:${result.changes.length} 个变更待应用`);
227
+ this.output.log(` 目标文件: ${targetFilePath}`);
228
+ result.success = true;
229
+ return result;
230
+ }
231
+
232
+ // 6. 写入文件
233
+ try {
234
+ const dir = path.dirname(targetFilePath);
235
+ if (!fsSync.existsSync(dir)) {
236
+ await fsPromises.mkdir(dir, { recursive: true });
237
+ }
238
+
239
+ const content = JSON.stringify(targetData, null, 2);
240
+ await fsPromises.writeFile(targetFilePath, content, "utf-8");
241
+
242
+ this.output.log(`✅ 已导入配置到 ${targetFilePath}`);
243
+ this.output.log(` ${result.changes.length} 个配置项已更新`);
244
+ result.success = true;
245
+ } catch (error) {
246
+ result.message = `写入文件失败: ${error}`;
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ /**
253
+ * 计算配置变更
254
+ */
255
+ private calculateChanges(
256
+ existing: Record<string, unknown>,
257
+ source: Record<string, unknown>
258
+ ): Array<{ key: string; oldValue: unknown; newValue: unknown }> {
259
+ const changes: Array<{ key: string; oldValue: unknown; newValue: unknown }> = [];
260
+
261
+ const flatten = (obj: unknown, prefix = ""): Array<{ key: string; value: unknown }> => {
262
+ const result: Array<{ key: string; value: unknown }> = [];
263
+ if (typeof obj !== "object" || obj === null) {
264
+ return result;
265
+ }
266
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
267
+ const key = prefix ? `${prefix}.${k}` : k;
268
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
269
+ result.push(...flatten(v, key));
270
+ } else {
271
+ result.push({ key, value: v });
272
+ }
273
+ }
274
+ return result;
275
+ };
276
+
277
+ const sourceEntries = flatten(source);
278
+ const existingEntries = flatten(existing);
279
+ const existingMap = new Map(existingEntries.map((e) => [e.key, e.value]));
280
+
281
+ for (const entry of sourceEntries) {
282
+ const oldValue = existingMap.get(entry.key);
283
+ if (JSON.stringify(oldValue) !== JSON.stringify(entry.value)) {
284
+ changes.push({
285
+ key: entry.key,
286
+ oldValue,
287
+ newValue: entry.value,
288
+ });
289
+ }
290
+ }
291
+
292
+ return changes;
293
+ }
294
+
295
+ /**
296
+ * 设置嵌套值
297
+ */
298
+ private setNestedValue(obj: Record<string, unknown>, key: string, value: unknown): void {
299
+ const keys = key.split(".");
300
+ let current = obj;
301
+
302
+ for (let i = 0; i < keys.length - 1; i++) {
303
+ const k = keys[i];
304
+ if (!(k in current) || typeof current[k] !== "object" || current[k] === null) {
305
+ current[k] = {};
306
+ }
307
+ current = current[k] as Record<string, unknown>;
308
+ }
309
+
310
+ current[keys[keys.length - 1]] = value;
311
+ }
312
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @fileoverview 深度合并工具测试
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { deepMerge } from "./deep-merge";
7
+
8
+ describe("deepMerge", () => {
9
+ it("should merge two flat objects", () => {
10
+ const target = { a: 1, b: 2 };
11
+ const source = { b: 3, c: 4 };
12
+ const result = deepMerge(target, source);
13
+
14
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
15
+ });
16
+
17
+ it("should recursively merge nested objects", () => {
18
+ const target = {
19
+ agent: {
20
+ maxIterations: 100,
21
+ maxErrorRetries: 3,
22
+ },
23
+ };
24
+ const source = {
25
+ agent: {
26
+ maxIterations: 200,
27
+ doomLoopThreshold: 5,
28
+ },
29
+ };
30
+ const result = deepMerge(target, source);
31
+
32
+ expect(result).toEqual({
33
+ agent: {
34
+ maxIterations: 200,
35
+ maxErrorRetries: 3,
36
+ doomLoopThreshold: 5,
37
+ },
38
+ });
39
+ });
40
+
41
+ it("should preserve fields in target that are not in source", () => {
42
+ const target = {
43
+ agent: {
44
+ maxIterations: 100,
45
+ maxErrorRetries: 3,
46
+ preserveMe: "keep",
47
+ },
48
+ llm: {
49
+ provider: "openai",
50
+ },
51
+ };
52
+ const source = {
53
+ agent: {
54
+ maxIterations: 200,
55
+ },
56
+ };
57
+ const result = deepMerge(target, source);
58
+
59
+ expect(result).toEqual({
60
+ agent: {
61
+ maxIterations: 200,
62
+ maxErrorRetries: 3,
63
+ preserveMe: "keep",
64
+ },
65
+ llm: {
66
+ provider: "openai",
67
+ },
68
+ });
69
+ });
70
+
71
+ it("should replace arrays instead of merging", () => {
72
+ const target = {
73
+ agent: {
74
+ tools: ["tool1", "tool2"],
75
+ },
76
+ };
77
+ const source = {
78
+ agent: {
79
+ tools: ["tool3", "tool4", "tool5"],
80
+ },
81
+ };
82
+ const result = deepMerge(target, source);
83
+
84
+ expect(result).toEqual({
85
+ agent: {
86
+ tools: ["tool3", "tool4", "tool5"],
87
+ },
88
+ });
89
+ });
90
+
91
+ it("should handle null values", () => {
92
+ const target = {
93
+ agent: {
94
+ customField: null,
95
+ keepMe: "value",
96
+ },
97
+ };
98
+ const source = {
99
+ agent: {
100
+ customField: "newValue",
101
+ },
102
+ };
103
+ const result = deepMerge(target, source);
104
+
105
+ expect(result).toEqual({
106
+ agent: {
107
+ customField: "newValue",
108
+ keepMe: "value",
109
+ },
110
+ });
111
+ });
112
+
113
+ it("should handle empty objects", () => {
114
+ const target = {};
115
+ const source = { agent: { maxIterations: 100 } };
116
+ const result = deepMerge(target, source);
117
+
118
+ expect(result).toEqual({ agent: { maxIterations: 100 } });
119
+ });
120
+
121
+ it("should handle deeply nested objects", () => {
122
+ const target = {
123
+ agent: {
124
+ defaultAgent: {
125
+ nested: {
126
+ deep: {
127
+ value: 1,
128
+ },
129
+ },
130
+ },
131
+ },
132
+ };
133
+ const source = {
134
+ agent: {
135
+ defaultAgent: {
136
+ nested: {
137
+ deep: {
138
+ value: 2,
139
+ added: 3,
140
+ },
141
+ },
142
+ },
143
+ },
144
+ };
145
+ const result = deepMerge(target, source);
146
+
147
+ expect(result).toEqual({
148
+ agent: {
149
+ defaultAgent: {
150
+ nested: {
151
+ deep: {
152
+ value: 2,
153
+ added: 3,
154
+ },
155
+ },
156
+ },
157
+ },
158
+ });
159
+ });
160
+
161
+ it("should not mutate original target", () => {
162
+ const target = { agent: { maxIterations: 100, keep: true } };
163
+ const source = { agent: { maxIterations: 200 } };
164
+ deepMerge(target, source);
165
+
166
+ expect(target).toEqual({ agent: { maxIterations: 100, keep: true } });
167
+ });
168
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @fileoverview 深度合并工具
3
+ *
4
+ * 将源对象深度合并到目标对象
5
+ * - target 现有值会被保留
6
+ * - source 中的值会覆盖 target 中的值
7
+ * - 嵌套对象递归合并
8
+ * - 数组会被替换而非合并
9
+ */
10
+
11
+ /**
12
+ * 深度合并两个对象
13
+ *
14
+ * @param target - 目标对象(现有值会被保留)
15
+ * @param source - 源对象(值会覆盖 target)
16
+ * @returns 合并后的新对象
17
+ */
18
+ export function deepMerge<T extends Record<string, unknown>>(
19
+ target: T,
20
+ source: Partial<T>
21
+ ): T;
22
+
23
+ /**
24
+ * 深度合并两个对象(支持任意结构)
25
+ */
26
+ export function deepMerge<T extends object>(
27
+ target: T,
28
+ source: Partial<T>
29
+ ): T;
30
+
31
+ export function deepMerge<T extends Record<string, unknown>>(
32
+ target: T,
33
+ source: Partial<T>
34
+ ): T {
35
+ const result = { ...target } as Record<string, unknown>;
36
+
37
+ for (const key in source) {
38
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
39
+ const sourceValue = source[key];
40
+ const targetValue = result[key];
41
+
42
+ if (
43
+ sourceValue !== null &&
44
+ typeof sourceValue === "object" &&
45
+ !Array.isArray(sourceValue) &&
46
+ targetValue !== null &&
47
+ typeof targetValue === "object" &&
48
+ !Array.isArray(targetValue)
49
+ ) {
50
+ // 两者都是对象,递归合并
51
+ result[key] = deepMerge(
52
+ targetValue as Record<string, unknown>,
53
+ sourceValue as Record<string, unknown>
54
+ );
55
+ } else {
56
+ // 其他情况直接覆盖
57
+ result[key] = sourceValue as unknown;
58
+ }
59
+ }
60
+ }
61
+
62
+ return result as T;
63
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview config export 子命令
3
+ */
4
+
5
+ import { CommandModule } from "yargs";
6
+ import chalk from "chalk";
7
+ import { EnvironmentService } from "../../services/environment.service";
8
+ import { OutputService } from "../../services/output.service";
9
+ import { ConfigService } from "./config-service";
10
+ import { SUPPORTED_COMPONENTS, resolveComponentName } from "./types";
11
+
12
+ export interface ConfigExportOptions {
13
+ component: string;
14
+ file?: string;
15
+ pretty?: boolean;
16
+ config?: string;
17
+ }
18
+
19
+ /**
20
+ * ConfigExportCommand - config export 子命令
21
+ */
22
+ export const ConfigExportCommand: CommandModule<object, ConfigExportOptions> = {
23
+ command: "export <component>",
24
+ describe: "导出组件配置到文件",
25
+
26
+ builder: (yargs) =>
27
+ yargs
28
+ .positional("component", {
29
+ type: "string",
30
+ describe: "Component 名称",
31
+ choices: [...SUPPORTED_COMPONENTS, "sessions"],
32
+ demandOption: true,
33
+ })
34
+ .option("file", {
35
+ type: "string",
36
+ describe: "导出文件路径",
37
+ alias: "f",
38
+ demandOption: true,
39
+ })
40
+ .option("pretty", {
41
+ type: "boolean",
42
+ describe: "格式化 JSON 输出",
43
+ default: true,
44
+ })
45
+ .option("config", {
46
+ type: "string",
47
+ describe: "配置文件路径",
48
+ alias: "c",
49
+ })
50
+ .example("roy config export agent --file agent-config.json", "导出 agent 配置")
51
+ .example("roy config export session --file session-config.json", "导出 session 配置"),
52
+
53
+ async handler(args) {
54
+ const output = new OutputService();
55
+ const envService = new EnvironmentService(output);
56
+
57
+ try {
58
+ await envService.create({ configPath: args.config });
59
+ const env = envService.getEnvironment();
60
+ if (!env) {
61
+ output.error("Failed to get environment");
62
+ process.exit(1);
63
+ }
64
+ const configComponent = env.getComponent("config") as any;
65
+ if (!configComponent) {
66
+ output.error("ConfigComponent not available");
67
+ process.exit(1);
68
+ }
69
+ const configService = new ConfigService(configComponent, output);
70
+
71
+ // 解析 component 名称(处理别名)
72
+ const componentName = resolveComponentName(args.component);
73
+
74
+ // 验证 component
75
+ if (!SUPPORTED_COMPONENTS.includes(componentName as any)) {
76
+ output.error(`Unknown component: ${args.component}`);
77
+ output.log("");
78
+ output.log("Supported components:");
79
+ for (const comp of SUPPORTED_COMPONENTS) {
80
+ output.log(` ${chalk.cyan(comp)}`);
81
+ }
82
+ process.exit(1);
83
+ }
84
+
85
+ // 导出配置
86
+ await configService.exportToFile(componentName, {
87
+ filePath: args.file,
88
+ pretty: args.pretty,
89
+ });
90
+ } catch (error) {
91
+ output.error(`Failed to export config: ${error}`);
92
+ process.exit(1);
93
+ } finally {
94
+ await envService.dispose();
95
+ }
96
+ },
97
+ };