@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,358 @@
1
+ /**
2
+ * @fileoverview Workflow CLI Renderers
3
+ *
4
+ * Output formatters for workflow CLI commands
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import type { Workflow, WorkflowRun } from '@ai-setting/roy-agent-core/env/workflow/types';
9
+
10
+ /**
11
+ * 计算字符串的视觉宽度(中文字符占2格)
12
+ */
13
+ function visualWidth(str: string): number {
14
+ let width = 0;
15
+ for (const char of str) {
16
+ if (char.charCodeAt(0) > 255) {
17
+ width += 2;
18
+ } else {
19
+ width += 1;
20
+ }
21
+ }
22
+ return width;
23
+ }
24
+
25
+ /**
26
+ * 按视觉宽度截断字符串
27
+ */
28
+ function truncateVisual(str: string, maxWidth: number): string {
29
+ if (!str) return '-';
30
+ let result = '';
31
+ let width = 0;
32
+ for (const char of str) {
33
+ const charWidth = char.charCodeAt(0) > 255 ? 2 : 1;
34
+ if (width + charWidth > maxWidth) break;
35
+ result += char;
36
+ width += charWidth;
37
+ }
38
+ return result || '-';
39
+ }
40
+
41
+ /**
42
+ * 格式化时间
43
+ */
44
+ function formatDate(date: Date | string | undefined): string {
45
+ if (!date) return '-';
46
+ const d = typeof date === 'string' ? new Date(date) : date;
47
+ return d.toLocaleString('zh-CN');
48
+ }
49
+
50
+ /**
51
+ * 格式化时长
52
+ */
53
+ function formatDuration(ms: number | undefined): string {
54
+ if (ms === undefined || ms === null) return '-';
55
+ if (ms < 1000) return `${ms}ms`;
56
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
57
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
58
+ return `${Math.floor(ms / 3600000)}h ${Math.floor((ms % 3600000) / 60000)}m`;
59
+ }
60
+
61
+ /**
62
+ * 获取状态颜色
63
+ */
64
+ function statusColor(status: string): typeof chalk.green {
65
+ switch (status) {
66
+ case 'completed':
67
+ return chalk.green;
68
+ case 'running':
69
+ case 'idle':
70
+ return chalk.blue;
71
+ case 'paused':
72
+ return chalk.yellow;
73
+ case 'failed':
74
+ return chalk.red;
75
+ case 'stopped':
76
+ return chalk.gray;
77
+ default:
78
+ return chalk.white;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 渲染 Workflow 列表
84
+ */
85
+ export function renderWorkflowList(workflows: Workflow[], options?: { json?: boolean }): string {
86
+ if (options?.json) {
87
+ return JSON.stringify({
88
+ workflows: workflows.map(w => ({
89
+ id: w.id,
90
+ name: w.name,
91
+ version: w.version,
92
+ description: w.description,
93
+ tags: w.tags,
94
+ createdAt: w.createdAt.toISOString(),
95
+ updatedAt: w.updatedAt.toISOString(),
96
+ })),
97
+ total: workflows.length,
98
+ }, null, 2);
99
+ }
100
+
101
+ if (workflows.length === 0) {
102
+ return chalk.yellow('No workflows found');
103
+ }
104
+
105
+ const NAME_WIDTH = 30;
106
+ const VERSION_WIDTH = 10;
107
+ const TAGS_WIDTH = 20;
108
+ const UPDATED_WIDTH = 20;
109
+ const GAP = ' ';
110
+
111
+ const headerLine = [
112
+ chalk.bold('NAME'.padEnd(NAME_WIDTH)),
113
+ chalk.bold('VER'.padEnd(VERSION_WIDTH)),
114
+ chalk.bold('TAGS'.padEnd(TAGS_WIDTH)),
115
+ chalk.bold('UPDATED'),
116
+ ].join(GAP);
117
+
118
+ const sepLine = '─'.repeat(NAME_WIDTH + VERSION_WIDTH + TAGS_WIDTH + UPDATED_WIDTH + GAP.length * 3);
119
+
120
+ const rows = workflows.map(w => {
121
+ const name = truncateVisual(w.name, NAME_WIDTH).padEnd(NAME_WIDTH);
122
+ const version = w.version.padEnd(VERSION_WIDTH);
123
+ const tags = truncateVisual(w.tags.join(', '), TAGS_WIDTH).padEnd(TAGS_WIDTH);
124
+ const updated = formatDate(w.updatedAt);
125
+ return `${name}${GAP}${version}${GAP}${tags}${GAP}${updated}`;
126
+ });
127
+
128
+ return [headerLine, sepLine, ...rows].join('\n');
129
+ }
130
+
131
+ /**
132
+ * 渲染 Workflow 详情
133
+ */
134
+ export function renderWorkflowDetail(workflow: Workflow, options?: { includeRuns?: WorkflowRun[] }): string {
135
+ const lines: string[] = [];
136
+
137
+ lines.push(chalk.bold('\n📋 Workflow Details\n'));
138
+ lines.push(` ${chalk.cyan('ID:')} ${workflow.id}`);
139
+ lines.push(` ${chalk.cyan('Name:')} ${workflow.name}`);
140
+ lines.push(` ${chalk.cyan('Version:')} ${workflow.version}`);
141
+ lines.push(` ${chalk.cyan('Description:')} ${workflow.description || '-'}`);
142
+ lines.push(` ${chalk.cyan('Tags:')} ${workflow.tags.join(', ') || '-'}`);
143
+ lines.push(` ${chalk.cyan('Created:')} ${formatDate(workflow.createdAt)}`);
144
+ lines.push(` ${chalk.cyan('Updated:')} ${formatDate(workflow.updatedAt)}`);
145
+
146
+ // Nodes summary
147
+ const nodeCount = workflow.definition.nodes.length;
148
+ lines.push(chalk.bold('\n📊 Nodes Summary\n'));
149
+ lines.push(` Total: ${nodeCount} nodes`);
150
+
151
+ // Group nodes by type
152
+ const nodesByType: Record<string, number> = {};
153
+ for (const node of workflow.definition.nodes) {
154
+ nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
155
+ }
156
+ for (const [type, count] of Object.entries(nodesByType)) {
157
+ lines.push(` - ${type}: ${count}`);
158
+ }
159
+
160
+ // Config summary
161
+ if (workflow.definition.config) {
162
+ lines.push(chalk.bold('\n⚙️ Configuration\n'));
163
+ if (workflow.definition.config.parallel_limit !== undefined) {
164
+ lines.push(` parallel_limit: ${workflow.definition.config.parallel_limit}`);
165
+ }
166
+ if (workflow.definition.config.timeout !== undefined) {
167
+ lines.push(` timeout: ${workflow.definition.config.timeout}ms`);
168
+ }
169
+ if (workflow.definition.config.debug !== undefined) {
170
+ lines.push(` debug: ${workflow.definition.config.debug}`);
171
+ }
172
+ }
173
+
174
+ // Runs summary (if included)
175
+ if (options?.includeRuns && options.includeRuns.length > 0) {
176
+ lines.push(chalk.bold('\n📜 Recent Runs\n'));
177
+ lines.push(renderRunsList(options.includeRuns.slice(0, 5)));
178
+ }
179
+
180
+ return lines.join('\n');
181
+ }
182
+
183
+ /**
184
+ * 渲染 Runs 列表
185
+ */
186
+ export function renderRunsList(runs: WorkflowRun[], options?: { json?: boolean }): string {
187
+ if (options?.json) {
188
+ return JSON.stringify({
189
+ runs: runs.map(r => ({
190
+ id: r.id,
191
+ workflowId: r.workflowId,
192
+ status: r.status,
193
+ startedAt: r.startedAt?.toISOString(),
194
+ completedAt: r.completedAt?.toISOString(),
195
+ durationMs: r.durationMs,
196
+ error: r.error,
197
+ })),
198
+ total: runs.length,
199
+ }, null, 2);
200
+ }
201
+
202
+ if (runs.length === 0) {
203
+ return chalk.yellow('No runs found');
204
+ }
205
+
206
+ const ID_WIDTH = 20;
207
+ const STATUS_WIDTH = 12;
208
+ const DURATION_WIDTH = 12;
209
+ const UPDATED_WIDTH = 20;
210
+ const GAP = ' ';
211
+
212
+ const headerLine = [
213
+ chalk.bold('RUN ID'.padEnd(ID_WIDTH)),
214
+ chalk.bold('STATUS'.padEnd(STATUS_WIDTH)),
215
+ chalk.bold('DURATION'.padEnd(DURATION_WIDTH)),
216
+ chalk.bold('STARTED'),
217
+ ].join(GAP);
218
+
219
+ const sepLine = '─'.repeat(ID_WIDTH + STATUS_WIDTH + DURATION_WIDTH + UPDATED_WIDTH + GAP.length * 3);
220
+
221
+ const rows = runs.map(r => {
222
+ const id = truncateVisual(r.id, ID_WIDTH).padEnd(ID_WIDTH);
223
+ const status = statusColor(r.status)(r.status.padEnd(STATUS_WIDTH));
224
+ const duration = formatDuration(r.durationMs).padEnd(DURATION_WIDTH);
225
+ const started = formatDate(r.startedAt);
226
+ return `${id}${GAP}${status}${GAP}${duration}${GAP}${started}`;
227
+ });
228
+
229
+ return [headerLine, sepLine, ...rows].join('\n');
230
+ }
231
+
232
+ /**
233
+ * 渲染 Run 详情
234
+ */
235
+ export function renderRunDetail(run: WorkflowRun): string {
236
+ const lines: string[] = [];
237
+
238
+ lines.push(chalk.bold('\n🏃 Run Details\n'));
239
+ lines.push(` ${chalk.cyan('Run ID:')} ${run.id}`);
240
+ lines.push(` ${chalk.cyan('Workflow:')} ${run.workflowId}`);
241
+ lines.push(` ${chalk.cyan('Status:')} ${statusColor(run.status)(run.status)}`);
242
+ lines.push(` ${chalk.cyan('Started:')} ${formatDate(run.startedAt)}`);
243
+
244
+ if (run.pausedAt) {
245
+ lines.push(` ${chalk.cyan('Paused:')} ${formatDate(run.pausedAt)}`);
246
+ }
247
+ if (run.resumedAt) {
248
+ lines.push(` ${chalk.cyan('Resumed:')} ${formatDate(run.resumedAt)}`);
249
+ }
250
+ if (run.stoppedAt) {
251
+ lines.push(` ${chalk.cyan('Stopped:')} ${formatDate(run.stoppedAt)}`);
252
+ }
253
+ if (run.completedAt) {
254
+ lines.push(` ${chalk.cyan('Completed:')} ${formatDate(run.completedAt)}`);
255
+ }
256
+
257
+ lines.push(` ${chalk.cyan('Duration:')} ${formatDuration(run.durationMs)}`);
258
+
259
+ if (run.error) {
260
+ lines.push(chalk.bold('\n❌ Error\n'));
261
+ lines.push(` ${chalk.red(run.error)}`);
262
+ }
263
+
264
+ if (run.output) {
265
+ lines.push(chalk.bold('\n📤 Output\n'));
266
+ lines.push(' ' + JSON.stringify(run.output, null, 2).split('\n').join('\n '));
267
+ }
268
+
269
+ return lines.join('\n');
270
+ }
271
+
272
+ /**
273
+ * 渲染工作流已添加成功
274
+ */
275
+ export function renderWorkflowAdded(workflow: Workflow): string {
276
+ return chalk.green(`\n✅ Workflow '${workflow.name}' added successfully\n`) +
277
+ ` ID: ${workflow.id}\n` +
278
+ ` Version: ${workflow.version}\n`;
279
+ }
280
+
281
+ /**
282
+ * 渲染工作流已更新成功
283
+ */
284
+ export function renderWorkflowUpdated(workflow: Workflow): string {
285
+ return chalk.green(`\n✅ Workflow '${workflow.name}' updated successfully\n`) +
286
+ ` ID: ${workflow.id}\n`;
287
+ }
288
+
289
+ /**
290
+ * 渲染工作流已删除成功
291
+ */
292
+ export function renderWorkflowDeleted(name: string): string {
293
+ return chalk.green(`\n✅ Workflow '${name}' deleted successfully\n`);
294
+ }
295
+
296
+ /**
297
+ * 渲染工作流运行结果
298
+ */
299
+ export function renderRunResult(result: { runId: string; status: string; output?: any; error?: string; durationMs?: number }): string {
300
+ const lines: string[] = [];
301
+
302
+ if (result.status === 'completed') {
303
+ lines.push(chalk.green('\n✅ Workflow completed successfully'));
304
+ } else if (result.status === 'failed') {
305
+ lines.push(chalk.red('\n❌ Workflow failed'));
306
+ } else if (result.status === 'stopped') {
307
+ lines.push(chalk.yellow('\n⚠️ Workflow stopped'));
308
+ } else {
309
+ lines.push(chalk.blue(`\n🔄 Workflow ${result.status}`));
310
+ }
311
+
312
+ lines.push(` Run ID: ${result.runId}`);
313
+
314
+ if (result.durationMs) {
315
+ lines.push(` Duration: ${formatDuration(result.durationMs)}`);
316
+ }
317
+
318
+ if (result.error) {
319
+ lines.push(chalk.red(`\n Error: ${result.error}`));
320
+ }
321
+
322
+ if (result.output) {
323
+ lines.push(chalk.bold('\n📤 Output:'));
324
+ lines.push(JSON.stringify(result.output, null, 2));
325
+ }
326
+
327
+ return lines.join('\n');
328
+ }
329
+
330
+ /**
331
+ * 渲染节点列表
332
+ */
333
+ export function renderNodesList(nodes: Array<{ id?: string; type?: string; name?: string; depends_on?: string[] }>): string {
334
+ const ID_WIDTH = 25;
335
+ const TYPE_WIDTH = 12;
336
+ const NAME_WIDTH = 20;
337
+ const DEPS_WIDTH = 20;
338
+ const GAP = ' ';
339
+
340
+ const headerLine = [
341
+ chalk.bold('NODE ID'.padEnd(ID_WIDTH)),
342
+ chalk.bold('TYPE'.padEnd(TYPE_WIDTH)),
343
+ chalk.bold('NAME'.padEnd(NAME_WIDTH)),
344
+ chalk.bold('DEPENDS ON'),
345
+ ].join(GAP);
346
+
347
+ const sepLine = '─'.repeat(ID_WIDTH + TYPE_WIDTH + NAME_WIDTH + DEPS_WIDTH + GAP.length * 3);
348
+
349
+ const rows = nodes.map(n => {
350
+ const id = truncateVisual(n.id || '-', ID_WIDTH).padEnd(ID_WIDTH);
351
+ const type = (n.type || '-').padEnd(TYPE_WIDTH);
352
+ const name = truncateVisual(n.name || '-', NAME_WIDTH).padEnd(NAME_WIDTH);
353
+ const deps = n.depends_on?.join(', ') || '-';
354
+ return `${id}${GAP}${type}${GAP}${name}${GAP}${deps}`;
355
+ });
356
+
357
+ return [headerLine, sepLine, ...rows].join('\n');
358
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Workflow validators exports
3
+ */
4
+
5
+ export * from './types';
6
+ export * from './node-validator';
7
+ export * from './workflow-validator';
8
+ export * from './node-validator-factory';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview Node validator factory
3
+ */
4
+
5
+ import type { NodeValidator } from './node-validator';
6
+ import { ToolNodeValidator } from './nodes/tool-node-validator';
7
+ import { SkillNodeValidator } from './nodes/skill-node-validator';
8
+ import { AgentNodeValidator } from './nodes/agent-node-validator';
9
+ import { WorkflowNodeValidator } from './nodes/workflow-node-validator';
10
+ import { ConditionNodeValidator } from './nodes/condition-node-validator';
11
+ import { MergeNodeValidator } from './nodes/merge-node-validator';
12
+ import { DecoratorNodeValidator } from './nodes/decorator-node-validator';
13
+
14
+ /**
15
+ * Registry of node validators
16
+ */
17
+ const validators: Map<string, NodeValidator> = new Map();
18
+
19
+ // Register built-in validators
20
+ validators.set('tool', new ToolNodeValidator());
21
+ validators.set('skill', new SkillNodeValidator());
22
+ validators.set('agent', new AgentNodeValidator());
23
+ validators.set('workflow', new WorkflowNodeValidator());
24
+ validators.set('condition', new ConditionNodeValidator());
25
+ validators.set('merge', new MergeNodeValidator());
26
+ validators.set('decorator', new DecoratorNodeValidator());
27
+
28
+ /**
29
+ * Get validator for a node type
30
+ */
31
+ export function getNodeValidator(type: string): NodeValidator | null {
32
+ return validators.get(type) || null;
33
+ }
34
+
35
+ /**
36
+ * Get all supported node types
37
+ */
38
+ export function getSupportedNodeTypes(): string[] {
39
+ return Array.from(validators.keys());
40
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @fileoverview Node validator base class and interface
3
+ */
4
+
5
+ import { ValidationErrorType, type ValidationError, type RawNodeDefinition } from './types';
6
+
7
+ /**
8
+ * Base interface for node validators
9
+ */
10
+ export interface NodeValidator {
11
+ /** Node type this validator handles */
12
+ readonly nodeType: string;
13
+
14
+ /**
15
+ * Validate a node of this type
16
+ * @param node Raw node definition from YAML
17
+ * @returns List of validation errors (empty if valid)
18
+ */
19
+ validate(node: RawNodeDefinition): ValidationError[];
20
+ }
21
+
22
+ /**
23
+ * Base class for node validators with common utilities
24
+ */
25
+ export abstract class BaseNodeValidator implements NodeValidator {
26
+ abstract readonly nodeType: string;
27
+
28
+ /**
29
+ * Get required field names for this node type
30
+ */
31
+ protected abstract getRequiredFields(): string[];
32
+
33
+ validate(node: RawNodeDefinition): ValidationError[] {
34
+ const errors: ValidationError[] = [];
35
+
36
+ // Check node ID
37
+ if (!node.id || typeof node.id !== 'string' || !node.id.trim()) {
38
+ errors.push({
39
+ type: ValidationErrorType.MISSING_REQUIRED_FIELD,
40
+ description: 'Node ID is missing or empty',
41
+ expected: 'id must be a non-empty string',
42
+ actual: String(node.id),
43
+ fix: 'Add an "id" field to the node, e.g., id: "node-1"',
44
+ nodeId: undefined,
45
+ });
46
+ }
47
+
48
+ // Check required fields
49
+ for (const field of this.getRequiredFields()) {
50
+ const value = node.config?.[field];
51
+ if (value === undefined || value === null || value === '') {
52
+ errors.push({
53
+ type: ValidationErrorType.MISSING_REQUIRED_FIELD,
54
+ description: `Required field "${field}" is missing`,
55
+ expected: `${field} must be a non-empty value`,
56
+ actual: String(value),
57
+ fix: `Add "${field}: <value>" to node config`,
58
+ nodeId: node.id,
59
+ });
60
+ }
61
+ }
62
+
63
+ // Add type-specific validation
64
+ errors.push(...this.validateTypeSpecific(node));
65
+
66
+ return errors;
67
+ }
68
+
69
+ /**
70
+ * Override in subclasses for type-specific validation
71
+ */
72
+ protected validateTypeSpecific(_node: RawNodeDefinition): ValidationError[] {
73
+ return [];
74
+ }
75
+
76
+ /**
77
+ * Helper: Check field type
78
+ */
79
+ protected checkFieldType(
80
+ node: RawNodeDefinition,
81
+ field: string,
82
+ expectedType: string,
83
+ expectedFormat?: string
84
+ ): ValidationError | null {
85
+ const value = node.config?.[field];
86
+ if (value === undefined || value === null) return null;
87
+
88
+ let isValid = false;
89
+ switch (expectedType) {
90
+ case 'string':
91
+ isValid = typeof value === 'string';
92
+ break;
93
+ case 'number':
94
+ isValid = typeof value === 'number' && value > 0;
95
+ break;
96
+ case 'boolean':
97
+ isValid = typeof value === 'boolean';
98
+ break;
99
+ case 'object':
100
+ isValid = typeof value === 'object' && value !== null && !Array.isArray(value);
101
+ break;
102
+ case 'array':
103
+ isValid = Array.isArray(value);
104
+ break;
105
+ case 'string|boolean':
106
+ isValid = typeof value === 'string' || typeof value === 'boolean';
107
+ break;
108
+ }
109
+
110
+ if (!isValid) {
111
+ return {
112
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
113
+ description: `Field "${field}" has invalid type`,
114
+ expected: expectedFormat || `${expectedType}`,
115
+ actual: typeof value === 'object' ? JSON.stringify(value) : String(value),
116
+ fix: expectedFormat?.includes('non-empty') && typeof value === 'string' && !value.trim()
117
+ ? `Remove quotes or whitespace from ${field} value`
118
+ : `Check ${field} type: expected ${expectedFormat || expectedType}`,
119
+ nodeId: node.id,
120
+ };
121
+ }
122
+
123
+ return null;
124
+ }
125
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @fileoverview Agent node validator
3
+ */
4
+
5
+ import { BaseNodeValidator } from '../node-validator';
6
+ import { ValidationErrorType } from '../types';
7
+ import type { RawNodeDefinition, ValidationError } from '../types';
8
+
9
+ export class AgentNodeValidator extends BaseNodeValidator {
10
+ readonly nodeType = 'agent';
11
+
12
+ protected getRequiredFields(): string[] {
13
+ return ['agent_type', 'prompt'];
14
+ }
15
+
16
+ protected validateTypeSpecific(node: RawNodeDefinition): ValidationError[] {
17
+ const errors: ValidationError[] = [];
18
+
19
+ const agentType = node.config?.agent_type;
20
+ if (agentType !== undefined && typeof agentType !== 'string') {
21
+ errors.push({
22
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
23
+ description: 'Field "agent_type" has invalid type',
24
+ expected: 'agent_type must be a string (e.g., "general", "research")',
25
+ actual: `typeof agent_type is "${typeof agentType}"`,
26
+ fix: `Change agent_type value to a string: agent_type: "${String(agentType)}"`,
27
+ nodeId: node.id,
28
+ });
29
+ }
30
+
31
+ const prompt = node.config?.prompt;
32
+ if (prompt !== undefined && typeof prompt !== 'string') {
33
+ errors.push({
34
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
35
+ description: 'Field "prompt" has invalid type',
36
+ expected: 'prompt must be a string',
37
+ actual: `typeof prompt is "${typeof prompt}"`,
38
+ fix: `Change prompt value to a string`,
39
+ nodeId: node.id,
40
+ });
41
+ }
42
+
43
+ // Check options if present
44
+ const options = node.config?.options;
45
+ if (options !== undefined && (typeof options !== 'object' || options === null || Array.isArray(options))) {
46
+ errors.push({
47
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
48
+ description: 'Field "options" has invalid type',
49
+ expected: 'options must be an object',
50
+ actual: Array.isArray(options) ? 'array' : typeof options,
51
+ fix: 'Change options to an object: options: { timeout: 60000 }',
52
+ nodeId: node.id,
53
+ });
54
+ }
55
+
56
+ return errors;
57
+ }
58
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Condition node validator
3
+ */
4
+
5
+ import { BaseNodeValidator } from '../node-validator';
6
+ import { ValidationErrorType } from '../types';
7
+ import type { RawNodeDefinition, ValidationError } from '../types';
8
+
9
+ export class ConditionNodeValidator extends BaseNodeValidator {
10
+ readonly nodeType = 'condition';
11
+
12
+ protected getRequiredFields(): string[] {
13
+ // No required fields for condition node
14
+ return [];
15
+ }
16
+
17
+ protected validateTypeSpecific(node: RawNodeDefinition): ValidationError[] {
18
+ const errors: ValidationError[] = [];
19
+
20
+ const condition = node.config?.condition;
21
+ if (condition !== undefined && typeof condition !== 'string' && typeof condition !== 'boolean') {
22
+ errors.push({
23
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
24
+ description: 'Field "condition" has invalid type',
25
+ expected: 'condition must be a string or boolean',
26
+ actual: typeof condition,
27
+ fix: 'Change condition to a string (template) or boolean value',
28
+ nodeId: node.id,
29
+ });
30
+ }
31
+
32
+ return errors;
33
+ }
34
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @fileoverview Decorator node validator
3
+ */
4
+
5
+ import { BaseNodeValidator } from '../node-validator';
6
+ import { ValidationErrorType } from '../types';
7
+ import type { RawNodeDefinition, ValidationError } from '../types';
8
+
9
+ export class DecoratorNodeValidator extends BaseNodeValidator {
10
+ readonly nodeType = 'decorator';
11
+
12
+ protected getRequiredFields(): string[] {
13
+ return ['_methodName', '_instance'];
14
+ }
15
+
16
+ protected validateTypeSpecific(node: RawNodeDefinition): ValidationError[] {
17
+ const errors: ValidationError[] = [];
18
+
19
+ const methodName = node.config?._methodName;
20
+ if (methodName !== undefined && typeof methodName !== 'string') {
21
+ errors.push({
22
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
23
+ description: 'Field "_methodName" has invalid type',
24
+ expected: '_methodName must be a string',
25
+ actual: typeof methodName,
26
+ fix: `Change _methodName value to a string: _methodName: "${String(methodName)}"`,
27
+ nodeId: node.id,
28
+ });
29
+ }
30
+
31
+ const instance = node.config?._instance;
32
+ if (instance !== undefined && typeof instance !== 'string') {
33
+ errors.push({
34
+ type: ValidationErrorType.INVALID_FIELD_TYPE,
35
+ description: 'Field "_instance" has invalid type',
36
+ expected: '_instance must be a string (template reference)',
37
+ actual: typeof instance,
38
+ fix: 'Change _instance to a template string: _instance: "{{input.instance}}"',
39
+ nodeId: node.id,
40
+ });
41
+ }
42
+
43
+ return errors;
44
+ }
45
+ }