@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.
- package/README.md +126 -0
- package/dist/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
- package/package.json +91 -0
- package/src/bin/roy.ts +12 -0
- package/src/cli.ts +101 -0
- package/src/commands/act.ts +480 -0
- package/src/commands/commands-add.ts +110 -0
- package/src/commands/commands-dirs.ts +70 -0
- package/src/commands/commands-info.ts +90 -0
- package/src/commands/commands-list.ts +161 -0
- package/src/commands/commands-remove.ts +147 -0
- package/src/commands/commands.ts +55 -0
- package/src/commands/config/config-service.test.ts +449 -0
- package/src/commands/config/config-service.ts +312 -0
- package/src/commands/config/deep-merge.test.ts +168 -0
- package/src/commands/config/deep-merge.ts +63 -0
- package/src/commands/config/export.ts +97 -0
- package/src/commands/config/filter-history-e2e.test.ts +141 -0
- package/src/commands/config/import-preserve-refs.test.ts +212 -0
- package/src/commands/config/import.ts +119 -0
- package/src/commands/config/index.ts +35 -0
- package/src/commands/config/list.ts +281 -0
- package/src/commands/config/roy-config-e2e.test.ts +297 -0
- package/src/commands/config/types.ts +54 -0
- package/src/commands/debug/index.ts +38 -0
- package/src/commands/debug/log.test.ts +233 -0
- package/src/commands/debug/log.ts +123 -0
- package/src/commands/debug/span.test.ts +297 -0
- package/src/commands/debug/span.ts +211 -0
- package/src/commands/debug/trace.test.ts +254 -0
- package/src/commands/debug/trace.ts +140 -0
- package/src/commands/eventsource/add.ts +133 -0
- package/src/commands/eventsource/index.ts +48 -0
- package/src/commands/eventsource/list.ts +194 -0
- package/src/commands/eventsource/remove.ts +95 -0
- package/src/commands/eventsource/start.ts +103 -0
- package/src/commands/eventsource/status.ts +185 -0
- package/src/commands/eventsource/stop.ts +89 -0
- package/src/commands/index.ts +22 -0
- package/src/commands/input-handler.test.ts +76 -0
- package/src/commands/input-handler.ts +43 -0
- package/src/commands/interactive-esc.test.ts +254 -0
- package/src/commands/interactive.shutdown.test.ts +122 -0
- package/src/commands/interactive.test.ts +221 -0
- package/src/commands/interactive.ts +1015 -0
- package/src/commands/lsp/check.ts +92 -0
- package/src/commands/lsp/index.ts +32 -0
- package/src/commands/lsp/install.ts +126 -0
- package/src/commands/lsp/list.ts +64 -0
- package/src/commands/mcp/index.ts +27 -0
- package/src/commands/mcp/list.ts +116 -0
- package/src/commands/mcp/reload.ts +70 -0
- package/src/commands/mcp/tools.ts +121 -0
- package/src/commands/memory/extract-e2e.test.ts +388 -0
- package/src/commands/memory/index.ts +11 -0
- package/src/commands/memory/memory-simplified.test.ts +58 -0
- package/src/commands/memory/memory.ts +25 -0
- package/src/commands/memory/organize.ts +300 -0
- package/src/commands/memory/recall.test.ts +120 -0
- package/src/commands/memory/recall.ts +88 -0
- package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
- package/src/commands/memory/record-prompt-component.test.ts +343 -0
- package/src/commands/memory/record.test.ts +92 -0
- package/src/commands/memory/record.ts +332 -0
- package/src/commands/plugin.test.ts +292 -0
- package/src/commands/plugin.ts +267 -0
- package/src/commands/sessions/active.ts +96 -0
- package/src/commands/sessions/add-message.ts +96 -0
- package/src/commands/sessions/checkpoints.ts +154 -0
- package/src/commands/sessions/compact.test.ts +215 -0
- package/src/commands/sessions/compact.ts +269 -0
- package/src/commands/sessions/delete.ts +236 -0
- package/src/commands/sessions/get.ts +165 -0
- package/src/commands/sessions/grep.ts +233 -0
- package/src/commands/sessions/index.ts +95 -0
- package/src/commands/sessions/list.ts +210 -0
- package/src/commands/sessions/messages.test.ts +333 -0
- package/src/commands/sessions/messages.ts +248 -0
- package/src/commands/sessions/mock.ts +194 -0
- package/src/commands/sessions/new.ts +82 -0
- package/src/commands/sessions/rename.ts +98 -0
- package/src/commands/shared/event-handler.ts +213 -0
- package/src/commands/shared/event-message-formatter.ts +295 -0
- package/src/commands/shared/index.ts +11 -0
- package/src/commands/shared/query-executor.test.ts +434 -0
- package/src/commands/shared/query-executor.ts +324 -0
- package/src/commands/shared/repl-engine.test.ts +354 -0
- package/src/commands/shared/session-manager.test.ts +212 -0
- package/src/commands/shared/session-manager.ts +114 -0
- package/src/commands/skills/get.ts +90 -0
- package/src/commands/skills/index.ts +39 -0
- package/src/commands/skills/list.ts +129 -0
- package/src/commands/skills/reload.ts +59 -0
- package/src/commands/skills/search.ts +132 -0
- package/src/commands/skills/show-config.ts +93 -0
- package/src/commands/tasks/complete.ts +92 -0
- package/src/commands/tasks/create.ts +118 -0
- package/src/commands/tasks/delete.ts +86 -0
- package/src/commands/tasks/get.ts +116 -0
- package/src/commands/tasks/index.ts +53 -0
- package/src/commands/tasks/list.ts +140 -0
- package/src/commands/tasks/operations.ts +120 -0
- package/src/commands/tasks/update.ts +122 -0
- package/src/commands/tools/exec-tool.ts +128 -0
- package/src/commands/tools/get.ts +114 -0
- package/src/commands/tools/index.ts +35 -0
- package/src/commands/tools/list.ts +107 -0
- package/src/commands/tools/shared/index.ts +7 -0
- package/src/commands/tools/shared/schema-helper.ts +111 -0
- package/src/commands/workflow/commands/add.ts +315 -0
- package/src/commands/workflow/commands/get.ts +193 -0
- package/src/commands/workflow/commands/list.ts +137 -0
- package/src/commands/workflow/commands/nodes.ts +528 -0
- package/src/commands/workflow/commands/remove.ts +94 -0
- package/src/commands/workflow/commands/run.ts +398 -0
- package/src/commands/workflow/commands/status.ts +147 -0
- package/src/commands/workflow/commands/stop.ts +91 -0
- package/src/commands/workflow/commands/update.ts +130 -0
- package/src/commands/workflow/commands/validate.ts +139 -0
- package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
- package/src/commands/workflow/index.ts +65 -0
- package/src/commands/workflow/renderers.ts +358 -0
- package/src/commands/workflow/validators/index.ts +8 -0
- package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
- package/src/commands/workflow/validators/node-validator.ts +125 -0
- package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
- package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
- package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
- package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
- package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
- package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
- package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
- package/src/commands/workflow/validators/types.ts +78 -0
- package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
- package/src/commands/workflow/validators/workflow-validator.ts +320 -0
- package/src/index.ts +19 -0
- package/src/plugin/apply.ts +103 -0
- package/src/plugin/discover.ts +219 -0
- package/src/plugin/index.ts +45 -0
- package/src/plugin/registry.ts +272 -0
- package/src/plugin/types.ts +165 -0
- package/src/services/context-handler.service.test.ts +501 -0
- package/src/services/context-handler.service.ts +372 -0
- package/src/services/environment.service.commands-prompt.test.ts +167 -0
- package/src/services/environment.service.ts +656 -0
- package/src/services/output.service.test.ts +92 -0
- package/src/services/output.service.ts +122 -0
- package/src/services/quiet-mode.service.test.ts +114 -0
- package/src/services/quiet-mode.service.ts +81 -0
- package/src/services/stream-output.service.test.ts +214 -0
- package/src/services/stream-output.service.ts +323 -0
- package/src/util/which.test.ts +101 -0
- package/src/util/which.ts +55 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EventSource Status Command
|
|
3
|
+
*
|
|
4
|
+
* 查看事件源状态
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CommandModule } from "yargs";
|
|
8
|
+
import { EnvironmentService } from "../../services/environment.service";
|
|
9
|
+
import { OutputService } from "../../services/output.service";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import type { EventSourceComponentInterface, EventSourceStatus } from "@ai-setting/roy-agent-core";
|
|
12
|
+
|
|
13
|
+
export interface EventSourceStatusOptions {
|
|
14
|
+
id?: string;
|
|
15
|
+
config?: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATUS_COLORS: Record<EventSourceStatus, (text: string) => string> = {
|
|
20
|
+
created: chalk.gray,
|
|
21
|
+
starting: chalk.yellow,
|
|
22
|
+
running: chalk.green,
|
|
23
|
+
stopping: chalk.yellow,
|
|
24
|
+
stopped: chalk.gray,
|
|
25
|
+
error: chalk.red,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STATUS_ICONS: Record<EventSourceStatus, string> = {
|
|
29
|
+
created: "○",
|
|
30
|
+
starting: "◐",
|
|
31
|
+
running: "●",
|
|
32
|
+
stopping: "◑",
|
|
33
|
+
stopped: "○",
|
|
34
|
+
error: "✗",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const STATUS_LABELS: Record<EventSourceStatus, string> = {
|
|
38
|
+
created: "已创建",
|
|
39
|
+
starting: "启动中",
|
|
40
|
+
running: "运行中",
|
|
41
|
+
stopping: "停止中",
|
|
42
|
+
stopped: "已停止",
|
|
43
|
+
error: "错误",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const EventSourceStatusCommand: CommandModule<object, EventSourceStatusOptions> = {
|
|
47
|
+
command: "status [id]",
|
|
48
|
+
aliases: ["stat"],
|
|
49
|
+
describe: "查看事件源状态",
|
|
50
|
+
|
|
51
|
+
builder: (yargs) =>
|
|
52
|
+
yargs
|
|
53
|
+
.positional("id", {
|
|
54
|
+
describe: "事件源 ID(可选,查看所有或单个)",
|
|
55
|
+
type: "string",
|
|
56
|
+
})
|
|
57
|
+
.option("json", {
|
|
58
|
+
alias: "j",
|
|
59
|
+
describe: "JSON 格式输出",
|
|
60
|
+
type: "boolean",
|
|
61
|
+
default: false,
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
async handler(args) {
|
|
65
|
+
const output = new OutputService();
|
|
66
|
+
const envService = new EnvironmentService(output);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await envService.create({ configPath: args.config });
|
|
70
|
+
const env = envService.getEnvironment();
|
|
71
|
+
if (!env) {
|
|
72
|
+
output.error("Failed to create environment");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const esComponent = env.getComponent("event-source") as unknown as EventSourceComponentInterface | undefined;
|
|
76
|
+
|
|
77
|
+
if (!esComponent) {
|
|
78
|
+
output.error("EventSourceComponent not available");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 如果指定了 ID,查看单个
|
|
83
|
+
if (args.id) {
|
|
84
|
+
const sources = esComponent.list();
|
|
85
|
+
const matchedSource = sources.find(
|
|
86
|
+
(s) => s.id === args.id || s.id.startsWith(args.id!)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!matchedSource) {
|
|
90
|
+
output.error(`事件源不存在: ${args.id}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const status = esComponent.getStatus(matchedSource.id) || "unknown";
|
|
95
|
+
const statusColor = STATUS_COLORS[status as EventSourceStatus] || chalk.gray;
|
|
96
|
+
|
|
97
|
+
if (args.json) {
|
|
98
|
+
output.json({
|
|
99
|
+
id: matchedSource.id,
|
|
100
|
+
name: matchedSource.name,
|
|
101
|
+
type: matchedSource.type,
|
|
102
|
+
status,
|
|
103
|
+
enabled: matchedSource.enabled,
|
|
104
|
+
eventTypes: matchedSource.eventTypes,
|
|
105
|
+
config: {
|
|
106
|
+
command: matchedSource.command,
|
|
107
|
+
interval: matchedSource.interval,
|
|
108
|
+
url: matchedSource.url,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
output.log(chalk.bold("事件源详情"));
|
|
115
|
+
output.log("─".repeat(50));
|
|
116
|
+
output.log(` ID: ${chalk.gray(matchedSource.id)}`);
|
|
117
|
+
output.log(` Name: ${chalk.cyan(matchedSource.name)}`);
|
|
118
|
+
output.log(` Type: ${chalk.cyan(matchedSource.type)}`);
|
|
119
|
+
output.log(` Status: ${statusColor(`${STATUS_ICONS[status as EventSourceStatus]} ${STATUS_LABELS[status as EventSourceStatus]}`)}`);
|
|
120
|
+
output.log(` Enabled: ${matchedSource.enabled ? chalk.green("是") : chalk.gray("否")}`);
|
|
121
|
+
|
|
122
|
+
if (matchedSource.eventTypes?.length) {
|
|
123
|
+
output.log(` Events: ${chalk.gray(matchedSource.eventTypes.join(", "))}`);
|
|
124
|
+
}
|
|
125
|
+
if (matchedSource.command) {
|
|
126
|
+
output.log(` Command: ${chalk.gray(matchedSource.command)}`);
|
|
127
|
+
}
|
|
128
|
+
if (matchedSource.interval) {
|
|
129
|
+
output.log(` Interval: ${chalk.gray(`${matchedSource.interval}ms`)}`);
|
|
130
|
+
}
|
|
131
|
+
if (matchedSource.url) {
|
|
132
|
+
output.log(` URL: ${chalk.gray(matchedSource.url)}`);
|
|
133
|
+
}
|
|
134
|
+
output.log("");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 查看所有状态
|
|
139
|
+
const sources = esComponent.list();
|
|
140
|
+
|
|
141
|
+
if (sources.length === 0) {
|
|
142
|
+
output.log(chalk.yellow("没有配置的事件源"));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (args.json) {
|
|
147
|
+
const sourcesWithStatus = sources.map((s) => ({
|
|
148
|
+
id: s.id,
|
|
149
|
+
name: s.name,
|
|
150
|
+
type: s.type,
|
|
151
|
+
status: esComponent.getStatus(s.id) || "unknown",
|
|
152
|
+
enabled: s.enabled,
|
|
153
|
+
}));
|
|
154
|
+
output.json({ sources: sourcesWithStatus, count: sources.length });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
output.log(chalk.bold("事件源状态概览"));
|
|
159
|
+
output.log("─".repeat(60));
|
|
160
|
+
|
|
161
|
+
for (const source of sources) {
|
|
162
|
+
const status = esComponent.getStatus(source.id) || "unknown";
|
|
163
|
+
const statusColor = STATUS_COLORS[status as EventSourceStatus] || chalk.gray;
|
|
164
|
+
const icon = STATUS_ICONS[status as EventSourceStatus] || "?";
|
|
165
|
+
const label = STATUS_LABELS[status as EventSourceStatus] || status;
|
|
166
|
+
|
|
167
|
+
output.log("");
|
|
168
|
+
output.log(` ${chalk.cyan(source.name)} ${chalk.gray(`(${source.type})`)}`);
|
|
169
|
+
output.log(` └─ Status: ${statusColor(`${icon} ${label}`)}`);
|
|
170
|
+
output.log(` ID: ${chalk.gray(source.id.substring(0, 8))}...`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
output.log("");
|
|
174
|
+
const runningCount = sources.filter(
|
|
175
|
+
(s) => esComponent.getStatus(s.id) === "running"
|
|
176
|
+
).length;
|
|
177
|
+
output.log(
|
|
178
|
+
chalk.green(`✅ ${runningCount}/${sources.length} 运行中`)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
} finally {
|
|
182
|
+
await envService.dispose();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EventSource Stop Command
|
|
3
|
+
*
|
|
4
|
+
* 停止事件源
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CommandModule } from "yargs";
|
|
8
|
+
import { EnvironmentService } from "../../services/environment.service";
|
|
9
|
+
import { OutputService } from "../../services/output.service";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import type { EventSourceComponentInterface } from "@ai-setting/roy-agent-core";
|
|
12
|
+
|
|
13
|
+
export interface EventSourceStopOptions {
|
|
14
|
+
id: string;
|
|
15
|
+
config?: string;
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const EventSourceStopCommand: CommandModule<object, EventSourceStopOptions> = {
|
|
20
|
+
command: "stop <id>",
|
|
21
|
+
aliases: ["kill"],
|
|
22
|
+
describe: "停止指定的事件源",
|
|
23
|
+
|
|
24
|
+
builder: (yargs) =>
|
|
25
|
+
yargs
|
|
26
|
+
.positional("id", {
|
|
27
|
+
describe: "事件源 ID(支持前缀匹配)",
|
|
28
|
+
type: "string",
|
|
29
|
+
demandOption: true,
|
|
30
|
+
})
|
|
31
|
+
.option("force", {
|
|
32
|
+
alias: "f",
|
|
33
|
+
describe: "强制停止",
|
|
34
|
+
type: "boolean",
|
|
35
|
+
default: false,
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
async handler(args) {
|
|
39
|
+
const output = new OutputService();
|
|
40
|
+
const envService = new EnvironmentService(output);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await envService.create({ configPath: args.config });
|
|
44
|
+
const env = envService.getEnvironment();
|
|
45
|
+
if (!env) {
|
|
46
|
+
output.error("Failed to create environment");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const esComponent = env.getComponent("event-source") as unknown as EventSourceComponentInterface | undefined;
|
|
50
|
+
|
|
51
|
+
if (!esComponent) {
|
|
52
|
+
output.error("EventSourceComponent not available");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 查找事件源(支持前缀匹配)
|
|
57
|
+
const sources = esComponent.list();
|
|
58
|
+
const matchedSource = sources.find(
|
|
59
|
+
(s) => s.id === args.id || s.id.startsWith(args.id)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!matchedSource) {
|
|
63
|
+
output.error(`事件源不存在: ${args.id}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 检查状态
|
|
68
|
+
const status = esComponent.getStatus(matchedSource.id);
|
|
69
|
+
if (status !== "running") {
|
|
70
|
+
output.log(chalk.yellow(`事件源未运行: ${matchedSource.name} (${status || "unknown"})`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 停止
|
|
75
|
+
output.info(`正在停止事件源 '${matchedSource.name}'...`);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await esComponent.stopSource(matchedSource.id);
|
|
79
|
+
output.success(chalk.green(`事件源已停止: ${matchedSource.name}`));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
output.error(`停止失败: ${error}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
} finally {
|
|
86
|
+
await envService.dispose();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CLI Commands Index
|
|
3
|
+
*
|
|
4
|
+
* 导出所有内置命令,供插件复用
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { ActCommand } from "./act";
|
|
8
|
+
export type { ActOptions } from "./act";
|
|
9
|
+
|
|
10
|
+
export { InteractiveCommand } from "./interactive";
|
|
11
|
+
|
|
12
|
+
export { SessionsCommand } from "./sessions";
|
|
13
|
+
|
|
14
|
+
export { TasksCommand } from "./tasks";
|
|
15
|
+
|
|
16
|
+
export { CommandsCommand } from "./commands";
|
|
17
|
+
|
|
18
|
+
export { ToolsCommand } from "./tools";
|
|
19
|
+
|
|
20
|
+
export { EventSourceCommand } from "./eventsource";
|
|
21
|
+
|
|
22
|
+
export { WorkflowCommand } from "./workflow";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview InputHandler - TDD Tests
|
|
3
|
+
*
|
|
4
|
+
* 核心设计:
|
|
5
|
+
* - 纯粹 buffer 管理
|
|
6
|
+
* - 只负责 pushLine, getBuffer, reset, getPrompt
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from "bun:test";
|
|
10
|
+
import { InputHandler } from "./input-handler";
|
|
11
|
+
|
|
12
|
+
describe("InputHandler", () => {
|
|
13
|
+
|
|
14
|
+
describe("pushLine - 添加输入", () => {
|
|
15
|
+
|
|
16
|
+
test("添加单行内容", () => {
|
|
17
|
+
const handler = new InputHandler();
|
|
18
|
+
handler.pushLine("hello");
|
|
19
|
+
expect(handler.getBuffer()).toBe("hello");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("添加多行内容", () => {
|
|
23
|
+
const handler = new InputHandler();
|
|
24
|
+
handler.pushLine("line1");
|
|
25
|
+
handler.pushLine("line2");
|
|
26
|
+
expect(handler.getBuffer()).toBe("line1\nline2");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("添加空行", () => {
|
|
30
|
+
const handler = new InputHandler();
|
|
31
|
+
handler.pushLine("hello");
|
|
32
|
+
handler.pushLine("");
|
|
33
|
+
expect(handler.getBuffer()).toBe("hello\n");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("支持空白行", () => {
|
|
37
|
+
const handler = new InputHandler();
|
|
38
|
+
handler.pushLine("line1");
|
|
39
|
+
handler.pushLine("");
|
|
40
|
+
handler.pushLine("");
|
|
41
|
+
handler.pushLine("line2");
|
|
42
|
+
expect(handler.getBuffer()).toBe("line1\n\n\nline2");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("reset - 重置", () => {
|
|
47
|
+
|
|
48
|
+
test("重置后 buffer 为空", () => {
|
|
49
|
+
const handler = new InputHandler();
|
|
50
|
+
handler.pushLine("hello");
|
|
51
|
+
handler.reset();
|
|
52
|
+
expect(handler.getBuffer()).toBe("");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("getPrompt - 获取提示符", () => {
|
|
57
|
+
|
|
58
|
+
test("初始状态返回用户提示符", () => {
|
|
59
|
+
const handler = new InputHandler();
|
|
60
|
+
expect(handler.getPrompt()).toBe("❯ ");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("有多行输入时返回继续提示符", () => {
|
|
64
|
+
const handler = new InputHandler();
|
|
65
|
+
handler.pushLine("hello");
|
|
66
|
+
expect(handler.getPrompt()).toBe("... ");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("reset 后恢复用户提示符", () => {
|
|
70
|
+
const handler = new InputHandler();
|
|
71
|
+
handler.pushLine("hello");
|
|
72
|
+
handler.reset();
|
|
73
|
+
expect(handler.getPrompt()).toBe("❯ ");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview InputHandler - 纯粹 buffer 管理
|
|
3
|
+
*
|
|
4
|
+
* 只负责累积输入,不处理结束检测
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class InputHandler {
|
|
8
|
+
private buffer: string[] = [];
|
|
9
|
+
|
|
10
|
+
// 提示符
|
|
11
|
+
private static readonly USER_PROMPT = "❯ ";
|
|
12
|
+
private static readonly CONTINUATION_PROMPT = "... ";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 添加一行输入
|
|
16
|
+
*/
|
|
17
|
+
pushLine(line: string): void {
|
|
18
|
+
this.buffer.push(line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 获取当前 buffer 内容
|
|
23
|
+
*/
|
|
24
|
+
getBuffer(): string {
|
|
25
|
+
return this.buffer.join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 重置 buffer
|
|
30
|
+
*/
|
|
31
|
+
reset(): void {
|
|
32
|
+
this.buffer = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 获取提示符
|
|
37
|
+
*/
|
|
38
|
+
getPrompt(): string {
|
|
39
|
+
return this.buffer.length > 0
|
|
40
|
+
? InputHandler.CONTINUATION_PROMPT
|
|
41
|
+
: InputHandler.USER_PROMPT;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Interactive Esc Key Handler Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD: 测试 Interactive 模式的 Esc 按键中断功能
|
|
5
|
+
*
|
|
6
|
+
* 场景 1: 正在执行时按 Esc - 中断 LLM 调用 + 停止流式输出 + 恢复 prompt
|
|
7
|
+
* 场景 2: 空闲时按 Esc - 不做任何操作
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect, vi, beforeEach } from "bun:test";
|
|
11
|
+
import { REPL } from "./interactive";
|
|
12
|
+
import { COLORS, abortStream, resetStreamAbort, streamAbortSignal } from "../services/stream-output.service";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Test Suite
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
describe("REPL.handleEscKey - Esc 按键中断流式输出", () => {
|
|
19
|
+
let mockExecute: ReturnType<typeof vi.fn>;
|
|
20
|
+
let mockStatus: ReturnType<typeof vi.fn>;
|
|
21
|
+
let mockShutdown: ReturnType<typeof vi.fn>;
|
|
22
|
+
let mockAbort: ReturnType<typeof vi.fn>;
|
|
23
|
+
let mockEnv: any;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// 重置 abort 信号
|
|
27
|
+
resetStreamAbort();
|
|
28
|
+
|
|
29
|
+
// 创建 mock 函数
|
|
30
|
+
mockExecute = vi.fn().mockResolvedValue("done");
|
|
31
|
+
mockStatus = vi.fn().mockReturnValue({
|
|
32
|
+
sessionId: "test",
|
|
33
|
+
sessionTitle: "Test",
|
|
34
|
+
messageCount: 0,
|
|
35
|
+
tokenCount: 0,
|
|
36
|
+
});
|
|
37
|
+
mockShutdown = vi.fn().mockResolvedValue(undefined);
|
|
38
|
+
mockAbort = vi.fn();
|
|
39
|
+
|
|
40
|
+
// Mock environment with agent component
|
|
41
|
+
mockEnv = {
|
|
42
|
+
getComponent: vi.fn().mockImplementation((name: string) => {
|
|
43
|
+
if (name === "agent") {
|
|
44
|
+
return { abort: mockAbort };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handleEscKey() 方法应该存在", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const repl = new REPL(
|
|
54
|
+
{
|
|
55
|
+
sessionId: "test",
|
|
56
|
+
sessionTitle: "Test",
|
|
57
|
+
onExecute: mockExecute,
|
|
58
|
+
onStatus: mockStatus,
|
|
59
|
+
onSwitchSession: vi.fn(),
|
|
60
|
+
onCompact: vi.fn(),
|
|
61
|
+
onShutdown: mockShutdown,
|
|
62
|
+
},
|
|
63
|
+
mockEnv
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Assert - handleEscKey 应该是 REPL 的一个方法
|
|
67
|
+
expect(typeof repl.handleEscKey).toBe("function");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("正在执行时按 Esc - 应该调用 AgentComponent.abort", () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
const repl = new REPL(
|
|
73
|
+
{
|
|
74
|
+
sessionId: "test",
|
|
75
|
+
sessionTitle: "Test",
|
|
76
|
+
onExecute: mockExecute,
|
|
77
|
+
onStatus: mockStatus,
|
|
78
|
+
onSwitchSession: vi.fn(),
|
|
79
|
+
onCompact: vi.fn(),
|
|
80
|
+
onShutdown: mockShutdown,
|
|
81
|
+
},
|
|
82
|
+
mockEnv
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 模拟正在执行状态
|
|
86
|
+
// 通过私有属性访问(测试用途)
|
|
87
|
+
(repl as any).isExecuting = true;
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
repl.handleEscKey();
|
|
91
|
+
|
|
92
|
+
// Assert - AgentComponent.abort 应该被调用
|
|
93
|
+
expect(mockAbort).toHaveBeenCalledWith("default");
|
|
94
|
+
expect(mockAbort).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("正在执行时按 Esc - 应该调用 abortStream", () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const repl = new REPL(
|
|
100
|
+
{
|
|
101
|
+
sessionId: "test",
|
|
102
|
+
sessionTitle: "Test",
|
|
103
|
+
onExecute: mockExecute,
|
|
104
|
+
onStatus: mockStatus,
|
|
105
|
+
onSwitchSession: vi.fn(),
|
|
106
|
+
onCompact: vi.fn(),
|
|
107
|
+
onShutdown: mockShutdown,
|
|
108
|
+
},
|
|
109
|
+
mockEnv
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// 模拟正在执行状态
|
|
113
|
+
(repl as any).isExecuting = true;
|
|
114
|
+
|
|
115
|
+
// 重置确认
|
|
116
|
+
resetStreamAbort();
|
|
117
|
+
expect(streamAbortSignal.aborted).toBe(false);
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
repl.handleEscKey();
|
|
121
|
+
|
|
122
|
+
// Assert - abortStream 应该被调用,streamAbortSignal.aborted 应该为 true
|
|
123
|
+
expect(streamAbortSignal.aborted).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("正在执行时按 Esc - 应该重置 isExecuting 状态为 false", () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const repl = new REPL(
|
|
129
|
+
{
|
|
130
|
+
sessionId: "test",
|
|
131
|
+
sessionTitle: "Test",
|
|
132
|
+
onExecute: mockExecute,
|
|
133
|
+
onStatus: mockStatus,
|
|
134
|
+
onSwitchSession: vi.fn(),
|
|
135
|
+
onCompact: vi.fn(),
|
|
136
|
+
onShutdown: mockShutdown,
|
|
137
|
+
},
|
|
138
|
+
mockEnv
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 模拟正在执行状态
|
|
142
|
+
(repl as any).isExecuting = true;
|
|
143
|
+
expect((repl as any).isExecuting).toBe(true);
|
|
144
|
+
|
|
145
|
+
// Act
|
|
146
|
+
repl.handleEscKey();
|
|
147
|
+
|
|
148
|
+
// Assert - isExecuting 应该被重置为 false
|
|
149
|
+
expect((repl as any).isExecuting).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("空闲时按 Esc - 不应该调用 AgentComponent.abort", () => {
|
|
153
|
+
// Arrange
|
|
154
|
+
const repl = new REPL(
|
|
155
|
+
{
|
|
156
|
+
sessionId: "test",
|
|
157
|
+
sessionTitle: "Test",
|
|
158
|
+
onExecute: mockExecute,
|
|
159
|
+
onStatus: mockStatus,
|
|
160
|
+
onSwitchSession: vi.fn(),
|
|
161
|
+
onCompact: vi.fn(),
|
|
162
|
+
onShutdown: mockShutdown,
|
|
163
|
+
},
|
|
164
|
+
mockEnv
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// 确保处于空闲状态
|
|
168
|
+
(repl as any).isExecuting = false;
|
|
169
|
+
|
|
170
|
+
// Act
|
|
171
|
+
repl.handleEscKey();
|
|
172
|
+
|
|
173
|
+
// Assert - AgentComponent.abort 不应该被调用
|
|
174
|
+
expect(mockAbort).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("空闲时按 Esc - 不应该调用 abortStream", () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
const repl = new REPL(
|
|
180
|
+
{
|
|
181
|
+
sessionId: "test",
|
|
182
|
+
sessionTitle: "Test",
|
|
183
|
+
onExecute: mockExecute,
|
|
184
|
+
onStatus: mockStatus,
|
|
185
|
+
onSwitchSession: vi.fn(),
|
|
186
|
+
onCompact: vi.fn(),
|
|
187
|
+
onShutdown: mockShutdown,
|
|
188
|
+
},
|
|
189
|
+
mockEnv
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// 确保处于空闲状态
|
|
193
|
+
(repl as any).isExecuting = false;
|
|
194
|
+
|
|
195
|
+
// 重置确认
|
|
196
|
+
resetStreamAbort();
|
|
197
|
+
|
|
198
|
+
// Act
|
|
199
|
+
repl.handleEscKey();
|
|
200
|
+
|
|
201
|
+
// Assert - abortStream 不应该被调用
|
|
202
|
+
expect(streamAbortSignal.aborted).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("没有 env 时按 Esc - 应该仍然调用 abortStream", () => {
|
|
206
|
+
// Arrange - 不传入 env
|
|
207
|
+
const repl = new REPL(
|
|
208
|
+
{
|
|
209
|
+
sessionId: "test",
|
|
210
|
+
sessionTitle: "Test",
|
|
211
|
+
onExecute: mockExecute,
|
|
212
|
+
onStatus: mockStatus,
|
|
213
|
+
onSwitchSession: vi.fn(),
|
|
214
|
+
onCompact: vi.fn(),
|
|
215
|
+
onShutdown: mockShutdown,
|
|
216
|
+
},
|
|
217
|
+
undefined // 不传入 env
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// 模拟正在执行状态
|
|
221
|
+
(repl as any).isExecuting = true;
|
|
222
|
+
resetStreamAbort();
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
repl.handleEscKey();
|
|
226
|
+
|
|
227
|
+
// Assert - 即使没有 env,abortStream 也应该被调用
|
|
228
|
+
expect(streamAbortSignal.aborted).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("Esc 与 Ctrl+C 的区别", () => {
|
|
233
|
+
test("Esc 和 Ctrl+C 都会中断流式输出,但行为不同", () => {
|
|
234
|
+
// 这个测试文档化两种中断方式的区别
|
|
235
|
+
|
|
236
|
+
// Ctrl+C 第一次:中断执行 + 显示 "按 Ctrl+C 再次中断或退出程序"
|
|
237
|
+
// Ctrl+C 第二次(在空闲时):退出程序
|
|
238
|
+
|
|
239
|
+
// Esc:仅在执行时中断 + 显示 "[已中断,可以继续输入]"
|
|
240
|
+
// Esc 在空闲时:不做任何操作
|
|
241
|
+
|
|
242
|
+
// 两种方式都调用:
|
|
243
|
+
// 1. AgentComponent.abort("default") - 中断底层 LLM 调用
|
|
244
|
+
// 2. abortStream() - 停止流式输出显示
|
|
245
|
+
// 3. isExecuting = false - 重置状态
|
|
246
|
+
// 4. rl.prompt(true) - 恢复 prompt
|
|
247
|
+
|
|
248
|
+
// 不同点:
|
|
249
|
+
// - Esc 在空闲时不做任何操作
|
|
250
|
+
// - Ctrl+C 在空闲时会退出程序
|
|
251
|
+
|
|
252
|
+
expect(true).toBe(true); // 文档化测试
|
|
253
|
+
});
|
|
254
|
+
});
|