@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,320 @@
1
+ /**
2
+ * @fileoverview Workflow-level validator
3
+ */
4
+
5
+ import { ValidationErrorType, type ValidationResult, type RawWorkflowDefinition, type RawNodeDefinition, type ValidationError } from './types';
6
+ import { getNodeValidator, getSupportedNodeTypes } from './node-validator-factory';
7
+
8
+ /**
9
+ * Workflow validator - validates both structure and nodes
10
+ */
11
+ export class WorkflowValidator {
12
+ /**
13
+ * Validate a raw workflow definition
14
+ */
15
+ validate(workflow: RawWorkflowDefinition): ValidationResult {
16
+ const errors: ValidationError[] = [];
17
+
18
+ // 1. Structure validation
19
+ const structureErrors = this.validateStructure(workflow);
20
+ errors.push(...structureErrors);
21
+
22
+ // If structure is invalid, skip node validation
23
+ if (structureErrors.length > 0) {
24
+ return {
25
+ valid: false,
26
+ errors,
27
+ workflowName: typeof workflow.name === 'string' ? workflow.name : undefined,
28
+ };
29
+ }
30
+
31
+ // 2. Node validation
32
+ const nodes = workflow.nodes || [];
33
+ const nodeIds = new Set<string>();
34
+ const nodeErrors = this.validateNodes(nodes, nodeIds);
35
+ errors.push(...nodeErrors);
36
+
37
+ // 3. Dependency validation
38
+ const depErrors = this.validateDependencies(nodes, nodeIds);
39
+ errors.push(...depErrors);
40
+
41
+ // 4. Template reference validation
42
+ const templateErrors = this.validateTemplateReferences(nodes, nodeIds);
43
+ errors.push(...templateErrors);
44
+
45
+ return {
46
+ valid: errors.length === 0,
47
+ errors,
48
+ workflowName: workflow.name as string,
49
+ nodeCount: nodes.length,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Validate workflow structure
55
+ */
56
+ private validateStructure(workflow: RawWorkflowDefinition): ValidationError[] {
57
+ const errors: ValidationError[] = [];
58
+
59
+ // Check name
60
+ if (!workflow.name || typeof workflow.name !== 'string' || !workflow.name.trim()) {
61
+ errors.push({
62
+ type: ValidationErrorType.MISSING_REQUIRED_FIELD,
63
+ description: 'Workflow name is missing or empty',
64
+ expected: 'name must be a non-empty string',
65
+ actual: String(workflow.name),
66
+ fix: 'Add a "name" field to the workflow, e.g., name: "my-workflow"',
67
+ });
68
+ }
69
+
70
+ // Check nodes
71
+ if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
72
+ errors.push({
73
+ type: ValidationErrorType.EMPTY_NODES,
74
+ description: 'Workflow nodes is missing or not an array',
75
+ expected: 'nodes must be an array of node definitions',
76
+ actual: String(workflow.nodes),
77
+ fix: 'Add a "nodes" array to the workflow',
78
+ });
79
+ return errors;
80
+ }
81
+
82
+ if (workflow.nodes.length === 0) {
83
+ errors.push({
84
+ type: ValidationErrorType.EMPTY_NODES,
85
+ description: 'Workflow nodes array is empty',
86
+ expected: 'nodes must contain at least one node',
87
+ actual: '[]',
88
+ fix: 'Add at least one node to the workflow',
89
+ });
90
+ }
91
+
92
+ return errors;
93
+ }
94
+
95
+ /**
96
+ * Validate all nodes
97
+ */
98
+ private validateNodes(nodes: RawNodeDefinition[], nodeIds: Set<string>): ValidationError[] {
99
+ const errors: ValidationError[] = [];
100
+ const seenIds = new Set<string>();
101
+
102
+ for (const node of nodes) {
103
+ // Check for duplicate IDs
104
+ if (node.id) {
105
+ if (seenIds.has(node.id)) {
106
+ errors.push({
107
+ type: ValidationErrorType.DUPLICATE_NODE_ID,
108
+ description: `Duplicate node ID found: "${node.id}"`,
109
+ expected: 'Each node must have a unique ID',
110
+ actual: `ID "${node.id}" appears multiple times`,
111
+ fix: `Rename one of the nodes with ID "${node.id}" to a unique value`,
112
+ nodeId: node.id,
113
+ });
114
+ }
115
+ seenIds.add(node.id);
116
+ nodeIds.add(node.id);
117
+ }
118
+
119
+ // Validate node type
120
+ if (!node.type) {
121
+ errors.push({
122
+ type: ValidationErrorType.MISSING_REQUIRED_FIELD,
123
+ description: 'Node type is missing',
124
+ expected: `type must be one of: ${getSupportedNodeTypes().join(', ')}`,
125
+ actual: 'undefined',
126
+ fix: `Add a "type" field to the node, e.g., type: "tool"`,
127
+ nodeId: node.id,
128
+ });
129
+ continue;
130
+ }
131
+
132
+ // Get validator for node type
133
+ const validator = getNodeValidator(node.type);
134
+ if (!validator) {
135
+ errors.push({
136
+ type: ValidationErrorType.INVALID_NODE_TYPE,
137
+ description: `Unknown node type: "${node.type}"`,
138
+ expected: `type must be one of: ${getSupportedNodeTypes().join(', ')}`,
139
+ actual: String(node.type),
140
+ fix: `Change type to one of: ${getSupportedNodeTypes().join(', ')}`,
141
+ nodeId: node.id,
142
+ });
143
+ continue;
144
+ }
145
+
146
+ // Validate with type-specific validator
147
+ const nodeErrors = validator.validate(node);
148
+ errors.push(...nodeErrors);
149
+ }
150
+
151
+ return errors;
152
+ }
153
+
154
+ /**
155
+ * Validate node dependencies
156
+ */
157
+ private validateDependencies(nodes: RawNodeDefinition[], nodeIds: Set<string>): ValidationError[] {
158
+ const errors: ValidationError[] = [];
159
+
160
+ for (const node of nodes) {
161
+ if (!node.depends_on || !Array.isArray(node.depends_on)) continue;
162
+
163
+ for (const depId of node.depends_on) {
164
+ if (!nodeIds.has(depId)) {
165
+ errors.push({
166
+ type: ValidationErrorType.INVALID_REFERENCE,
167
+ description: `depends_on references non-existent node`,
168
+ expected: 'All node IDs in depends_on must exist in workflow',
169
+ actual: `depends_on: ["${depId}"], but "${depId}" not found`,
170
+ fix: `Remove "${depId}" from depends_on or create a node with that ID`,
171
+ nodeId: node.id,
172
+ });
173
+ }
174
+ }
175
+ }
176
+
177
+ // Check for circular dependencies
178
+ const circularErrors = this.detectCircularDependencies(nodes);
179
+ errors.push(...circularErrors);
180
+
181
+ return errors;
182
+ }
183
+
184
+ /**
185
+ * Validate template references in node configs
186
+ *
187
+ * Extracts all template references like {{nodes.xxx.output}}, {{xxx.output}}, ${xxx.output}
188
+ * and validates that the referenced node IDs exist in the workflow.
189
+ */
190
+ private validateTemplateReferences(nodes: RawNodeDefinition[], nodeIds: Set<string>): ValidationError[] {
191
+ const errors: ValidationError[] = [];
192
+
193
+ // Template reference patterns:
194
+ // 1. {{nodes.nodeId}} or {{nodes.nodeId.path}} or {{nodes.nodeId.path.more}}
195
+ // 2. {{nodeId}} or {{nodeId.path}} (bare node reference)
196
+ // 3. ${nodes.nodeId} or ${nodes.nodeId.path} (dollar-style)
197
+ // 4. ${nodeId} or ${nodeId.path} (dollar-style bare)
198
+ // 5. {{input.key}} - workflow input, not a node reference
199
+
200
+ const templatePatterns = [
201
+ /\{\{nodes\.([a-zA-Z_][a-zA-Z0-9_-]*)(?:\.[^}]+)?\}\}/g, // {{nodes.xxx}} or {{nodes.xxx.yyy}}
202
+ /\{\{([a-zA-Z_][a-zA-Z0-9_-]*)(?:\.[^}]+)?\}\}/g, // {{xxx}} or {{xxx.yyy}} (not input or nodes)
203
+ /\$\{nodes\.([a-zA-Z_][a-zA-Z0-9_-]*)(?:\.[^}]+)?\}/g, // ${nodes.xxx} or ${nodes.xxx.yyy}
204
+ /\$\{([a-zA-Z_][a-zA-Z0-9_-]*)(?:\.[^}]+)?\}/g, // ${xxx} or ${xxx.yyy}
205
+ ];
206
+
207
+ for (const node of nodes) {
208
+ if (!node.id || !node.config) continue;
209
+
210
+ // Get all config values as strings to search for templates
211
+ const configStr = JSON.stringify(node.config);
212
+
213
+ // Find all template references
214
+ const foundRefs = new Set<string>();
215
+
216
+ for (const pattern of templatePatterns) {
217
+ pattern.lastIndex = 0; // Reset regex state
218
+ let match;
219
+ while ((match = pattern.exec(configStr)) !== null) {
220
+ // Skip 'input' and 'nodes' keywords
221
+ const refId = match[1];
222
+ if (refId !== 'input' && refId !== 'nodes') {
223
+ foundRefs.add(refId);
224
+ }
225
+ }
226
+ }
227
+
228
+ // Check each reference against node IDs
229
+ for (const refId of foundRefs) {
230
+ if (!nodeIds.has(refId)) {
231
+ // Check if there's a similar node ID (hyphen/underscore mismatch)
232
+ const normalizedRef = refId.includes('-')
233
+ ? refId.replace(/-/g, '_')
234
+ : refId.replace(/_/g, '-');
235
+
236
+ let fix: string;
237
+ if (nodeIds.has(normalizedRef)) {
238
+ fix = `Change "${refId}" to "${normalizedRef}" to match the existing node ID`;
239
+ } else {
240
+ // Find all existing node IDs that are similar (for suggestions)
241
+ const similarIds = Array.from(nodeIds).filter(id => {
242
+ const n1 = id.replace(/-/g, '_').replace(/_/g, '-');
243
+ const n2 = refId.replace(/-/g, '_').replace(/_/g, '-');
244
+ return n1 === n2;
245
+ });
246
+
247
+ if (similarIds.length > 0) {
248
+ fix = `Change "${refId}" to one of: ${similarIds.join(', ')}`;
249
+ } else {
250
+ fix = `Remove "${refId}" or create a node with that ID`;
251
+ }
252
+ }
253
+
254
+ errors.push({
255
+ type: ValidationErrorType.INVALID_TEMPLATE_REFERENCE,
256
+ description: `Template references non-existent node: "${refId}"`,
257
+ expected: `Referenced node ID must exist in the workflow`,
258
+ actual: `Template references "${refId}" but this node does not exist`,
259
+ fix,
260
+ nodeId: node.id,
261
+ template: `{{${refId}}}`,
262
+ referencedNodeId: refId,
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ return errors;
269
+ }
270
+
271
+ /**
272
+ * Detect circular dependencies using DFS
273
+ */
274
+ private detectCircularDependencies(nodes: RawNodeDefinition[]): ValidationError[] {
275
+ const errors: ValidationError[] = [];
276
+ const nodeMap = new Map<string, RawNodeDefinition>();
277
+
278
+ for (const node of nodes) {
279
+ if (node.id) nodeMap.set(node.id, node);
280
+ }
281
+
282
+ const visited = new Set<string>();
283
+ const recursionStack = new Set<string>();
284
+
285
+ const dfs = (nodeId: string, path: string[]): boolean => {
286
+ visited.add(nodeId);
287
+ recursionStack.add(nodeId);
288
+
289
+ const node = nodeMap.get(nodeId);
290
+ if (!node) return false;
291
+
292
+ for (const depId of node.depends_on || []) {
293
+ if (!visited.has(depId)) {
294
+ if (dfs(depId, [...path, depId])) return true;
295
+ } else if (recursionStack.has(depId)) {
296
+ errors.push({
297
+ type: ValidationErrorType.CIRCULAR_DEPENDENCY,
298
+ description: 'Circular dependency detected',
299
+ expected: 'Workflow should be a DAG (no cycles)',
300
+ actual: `Cycle: ${[...path, depId].join(' → ')} → ${depId}`,
301
+ fix: 'Remove the circular dependency by changing depends_on references',
302
+ nodeId,
303
+ });
304
+ return true;
305
+ }
306
+ }
307
+
308
+ recursionStack.delete(nodeId);
309
+ return false;
310
+ };
311
+
312
+ for (const node of nodes) {
313
+ if (node.id && !visited.has(node.id)) {
314
+ dfs(node.id, [node.id]);
315
+ }
316
+ }
317
+
318
+ return errors;
319
+ }
320
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @roy-agent/cli - CLI for roy-agent
3
+ */
4
+
5
+ // Re-export for convenience
6
+ export { runCli } from "./cli";
7
+ export { OutputService } from "./services/output.service";
8
+ export { EnvironmentService } from "./services/environment.service";
9
+
10
+ // Re-export all commands
11
+ export { ActCommand } from "./commands/act";
12
+ export { InteractiveCommand } from "./commands/interactive";
13
+ export { SessionsCommand } from "./commands/sessions";
14
+ export { TasksCommand } from "./commands/tasks";
15
+ export { CommandsCommand } from "./commands/commands";
16
+ export { MemoryCommand } from "./commands/memory";
17
+ export { SkillsCommand } from "./commands/skills";
18
+ export { ToolsCommand } from "./commands/tools";
19
+ export { McpCommand } from "./commands/mcp";
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @fileoverview Apply Plugins to Yargs
3
+ *
4
+ * 将插件命令应用到 yargs 实例
5
+ */
6
+
7
+ import { globalPluginRegistry } from "./registry";
8
+
9
+ /**
10
+ * 将插件命令应用到 yargs 实例
11
+ *
12
+ * @param yargsInstance - yargs 实例
13
+ * @returns 应用了插件命令的 yargs 实例
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { applyPluginCommands } from "./plugin";
18
+ *
19
+ * let y = yargs();
20
+ * y = y.command(ActCommand);
21
+ * y = applyPluginCommands(y);
22
+ * y.parse();
23
+ * ```
24
+ */
25
+ export function applyPluginCommands<T = any>(
26
+ yargsInstance: any
27
+ ): any {
28
+ const commands = globalPluginRegistry.getAllCommands();
29
+
30
+ for (const { command } of commands) {
31
+ yargsInstance = yargsInstance.command(command as any);
32
+ }
33
+
34
+ return yargsInstance;
35
+ }
36
+
37
+ /**
38
+ * 创建插件命令包装命令
39
+ *
40
+ * 用于在 yargs 中添加一个空的命令包装器,
41
+ * 使得插件命令可以在 strict 模式下正常工作
42
+ *
43
+ * @returns CommandModule
44
+ *
45
+ * @deprecated 此方法已废弃,仅用于兼容旧版本
46
+ */
47
+ export function createPluginCommandsCommand(): any {
48
+ return {
49
+ command: "$0",
50
+ describe: false,
51
+ handler: () => {
52
+ // 不做任何事,由其他命令处理
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * 获取所有插件命令信息摘要
59
+ *
60
+ * @returns 命令摘要数组
61
+ */
62
+ export function getPluginCommandsSummary(): Array<{
63
+ plugin: string;
64
+ command: string;
65
+ description?: string;
66
+ }> {
67
+ const summary: Array<{
68
+ plugin: string;
69
+ command: string;
70
+ description?: string;
71
+ }> = [];
72
+
73
+ for (const plugin of globalPluginRegistry.getAll()) {
74
+ const commands = plugin.getCommands?.() ?? [];
75
+ for (const { command } of commands) {
76
+ summary.push({
77
+ plugin: plugin.info.name,
78
+ command: command.command as string,
79
+ description: command.describe as string | undefined,
80
+ });
81
+ }
82
+ }
83
+
84
+ return summary;
85
+ }
86
+
87
+ /**
88
+ * 检查是否存在指定名称的命令
89
+ *
90
+ * @param commandName - 命令名称
91
+ * @returns 是否存在
92
+ */
93
+ export function hasPluginCommand(commandName: string): boolean {
94
+ for (const plugin of globalPluginRegistry.getAll()) {
95
+ const commands = plugin.getCommands?.() ?? [];
96
+ for (const { command } of commands) {
97
+ if ((command.command as string).startsWith(commandName)) {
98
+ return true;
99
+ }
100
+ }
101
+ }
102
+ return false;
103
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @fileoverview Plugin Discovery
3
+ *
4
+ * 扫描并发现已安装的插件
5
+ */
6
+
7
+ import type { RoyCliPlugin } from "./types";
8
+ import { globalPluginRegistry } from "./registry";
9
+ import { readFileSync, existsSync } from "fs";
10
+ import { join, resolve } from "path";
11
+
12
+ /**
13
+ * 插件包识别模式
14
+ *
15
+ * 扫描 node_modules 中匹配这些模式的包
16
+ */
17
+ const PLUGIN_PATTERNS = [
18
+ "@ai-setting/agent-*",
19
+ "@roy-agent/plugin-*",
20
+ ];
21
+
22
+ /**
23
+ * 插件入口文件名
24
+ *
25
+ * 插件包根目录下必须包含此文件
26
+ */
27
+ const PLUGIN_ENTRY_FILE = "roy-plugin.js";
28
+
29
+ /**
30
+ * 扫描路径
31
+ *
32
+ * 从这些路径扫描插件,默认包含项目 node_modules 和全局 node_modules
33
+ */
34
+ const DEFAULT_SCAN_PATHS: string[] = [];
35
+
36
+ /**
37
+ * 缓存已发现的插件路径,避免重复加载
38
+ */
39
+ const loadedPaths = new Set<string>();
40
+
41
+ /**
42
+ * 初始化扫描路径
43
+ */
44
+ function initScanPaths(): string[] {
45
+ if (DEFAULT_SCAN_PATHS.length > 0) {
46
+ return DEFAULT_SCAN_PATHS;
47
+ }
48
+
49
+ // 项目 node_modules
50
+ const projectNodeModules = resolve(process.cwd(), "node_modules");
51
+ if (existsSync(projectNodeModules)) {
52
+ DEFAULT_SCAN_PATHS.push(projectNodeModules);
53
+ }
54
+
55
+ // 全局 bun/node_modules
56
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
57
+ if (homeDir) {
58
+ const globalNodeModules = join(homeDir, ".bun", "install", "global", "node_modules");
59
+ if (existsSync(globalNodeModules)) {
60
+ DEFAULT_SCAN_PATHS.push(globalNodeModules);
61
+ }
62
+ }
63
+
64
+ // NODE_PATH 环境变量
65
+ if (process.env.NODE_PATH) {
66
+ const extraPaths = process.env.NODE_PATH.split(":").filter((p) => {
67
+ const fullPath = resolve(p);
68
+ if (!DEFAULT_SCAN_PATHS.includes(fullPath) && existsSync(fullPath)) {
69
+ return true;
70
+ }
71
+ return false;
72
+ });
73
+ DEFAULT_SCAN_PATHS.push(...extraPaths);
74
+ }
75
+
76
+ return DEFAULT_SCAN_PATHS;
77
+ }
78
+
79
+ /**
80
+ * 类型守卫:检查对象是否为有效的 RoyCliPlugin
81
+ */
82
+ function isRoyCliPlugin(obj: unknown): obj is RoyCliPlugin {
83
+ return (
84
+ typeof obj === "object" &&
85
+ obj !== null &&
86
+ "info" in obj &&
87
+ typeof (obj as any).info.name === "string" &&
88
+ typeof (obj as any).info.version === "string"
89
+ );
90
+ }
91
+
92
+ /**
93
+ * 加载单个插件
94
+ *
95
+ * @param pkgPath - 插件包路径
96
+ * @returns 是否成功加载
97
+ */
98
+ async function loadPlugin(pkgPath: string): Promise<boolean> {
99
+ const pluginFile = join(pkgPath, PLUGIN_ENTRY_FILE);
100
+
101
+ if (!existsSync(pluginFile)) {
102
+ return false;
103
+ }
104
+
105
+ if (loadedPaths.has(pluginFile)) {
106
+ return true; // 已加载过
107
+ }
108
+
109
+ try {
110
+ // 使用动态导入加载插件
111
+ const plugin = await import(pluginFile);
112
+
113
+ // 尝试多种方式获取插件实例
114
+ const pluginInstance =
115
+ plugin.default ||
116
+ plugin.bountyPlugin ||
117
+ plugin;
118
+
119
+ if (isRoyCliPlugin(pluginInstance)) {
120
+ // 跳过已注册的插件
121
+ if (globalPluginRegistry.has(pluginInstance.info.name)) {
122
+ console.log(`[roy] Plugin "${pluginInstance.info.name}" is already registered, skipping`);
123
+ return true;
124
+ }
125
+
126
+ globalPluginRegistry.register(pluginInstance);
127
+ console.log(`[roy] Loaded plugin: ${pluginInstance.info.name}@${pluginInstance.info.version}`);
128
+ loadedPaths.add(pluginFile);
129
+ return true;
130
+ } else {
131
+ console.warn(`[roy] Invalid plugin at ${pkgPath}: missing required "info" property`);
132
+ // 调试信息
133
+ console.warn(`[roy] Available exports:`, Object.keys(plugin));
134
+ }
135
+ } catch (err) {
136
+ console.warn(`[roy] Failed to load plugin from ${pkgPath}:`, (err as Error).message);
137
+ }
138
+
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * 扫描单个路径下的插件
144
+ *
145
+ * @param basePath - 基础路径
146
+ */
147
+ async function scanPath(basePath: string): Promise<void> {
148
+ if (!existsSync(basePath)) {
149
+ return;
150
+ }
151
+
152
+ const { glob } = await import("glob");
153
+
154
+ for (const pattern of PLUGIN_PATTERNS) {
155
+ try {
156
+ const matches = await glob(pattern, {
157
+ cwd: basePath,
158
+ onlyDirectories: true,
159
+ absolute: true,
160
+ } as any);
161
+
162
+ for (const match of matches) {
163
+ await loadPlugin(match);
164
+ }
165
+ } catch {
166
+ // 忽略 glob 扫描错误
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 扫描指定路径并加载所有发现的插件
173
+ *
174
+ * @param scanPaths - 要扫描的路径数组,默认扫描标准 node_modules 位置
175
+ * @returns 已加载的插件数量
176
+ */
177
+ export async function discoverPlugins(
178
+ scanPaths: string[] = initScanPaths()
179
+ ): Promise<number> {
180
+ const initialCount = globalPluginRegistry.size;
181
+
182
+ for (const path of scanPaths) {
183
+ await scanPath(path);
184
+ }
185
+
186
+ const loadedCount = globalPluginRegistry.size - initialCount;
187
+ if (loadedCount > 0) {
188
+ console.log(`[roy] Discovered and loaded ${loadedCount} plugin(s)`);
189
+ }
190
+
191
+ return loadedCount;
192
+ }
193
+
194
+ /**
195
+ * 获取已加载的插件数量
196
+ */
197
+ export function getLoadedPluginCount(): number {
198
+ return globalPluginRegistry.size;
199
+ }
200
+
201
+ /**
202
+ * 清除插件加载缓存
203
+ *
204
+ * 主要用于测试
205
+ */
206
+ export function clearPluginCache(): void {
207
+ loadedPaths.clear();
208
+ }
209
+
210
+ /**
211
+ * 重新扫描并加载插件
212
+ *
213
+ * 会先清空已注册的插件,然后重新发现
214
+ */
215
+ export async function reloadPlugins(): Promise<number> {
216
+ globalPluginRegistry.clear();
217
+ loadedPaths.clear();
218
+ return discoverPlugins();
219
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @fileoverview Plugin System
3
+ *
4
+ * Roy CLI 插件系统入口
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { globalPluginRegistry } from "@ai-setting/roy-agent-cli/plugin";
9
+ * import type { RoyCliPlugin } from "@ai-setting/roy-agent-cli/plugin";
10
+ * ```
11
+ */
12
+
13
+ // 类型导出
14
+ export type {
15
+ PluginInfo,
16
+ PluginComponentDefinition,
17
+ PluginCommandDefinition,
18
+ PluginEnvironmentContext,
19
+ RoyCliPlugin,
20
+ ComponentPluginDefinition,
21
+ ComponentPluginInstance,
22
+ } from "./types";
23
+
24
+ // 注册表导出
25
+ export {
26
+ PluginRegistry,
27
+ globalPluginRegistry,
28
+ loadBuiltinComponentPlugin,
29
+ } from "./registry";
30
+
31
+ // 发现机制导出
32
+ export {
33
+ discoverPlugins,
34
+ getLoadedPluginCount,
35
+ clearPluginCache,
36
+ reloadPlugins,
37
+ } from "./discover";
38
+
39
+ // 应用插件导出
40
+ export {
41
+ applyPluginCommands,
42
+ createPluginCommandsCommand,
43
+ getPluginCommandsSummary,
44
+ hasPluginCommand,
45
+ } from "./apply";