@xfe-repo/cli 2.0.8 → 2.0.11
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/CHANGELOG.md +31 -0
- package/dist/backend/http-backend.d.ts +47 -0
- package/dist/backend/http-backend.js +84 -0
- package/dist/backend/server-task-commands.d.ts +9 -0
- package/dist/backend/server-task-commands.js +262 -0
- package/dist/components/Prompts/FuzzyPathPrompt.js +1 -1
- package/dist/components/Prompts/OptionList.js +14 -2
- package/dist/components/Prompts/index.d.ts +2 -5
- package/dist/components/Prompts/index.js +13 -2
- package/dist/components/Prompts/use-selectable-list.d.ts +2 -0
- package/dist/components/Prompts/use-selectable-list.js +7 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @xfe-repo/cli
|
|
2
2
|
|
|
3
|
+
## 2.0.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 去除server依赖
|
|
8
|
+
|
|
9
|
+
## 2.0.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 同步插件优化
|
|
14
|
+
|
|
15
|
+
## 2.0.9
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 优化插件
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @xfe-repo/cli-presets@2.0.6
|
|
22
|
+
- @xfe-repo/cli-core@2.0.6
|
|
23
|
+
- @xfe-repo/server@0.1.1
|
|
24
|
+
|
|
25
|
+
## 2.0.9
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 修复ai-rules插件的排序问题
|
|
30
|
+
- Updated dependencies
|
|
31
|
+
- @xfe-repo/cli-presets@2.0.5
|
|
32
|
+
- @xfe-repo/cli-core@2.0.5
|
|
33
|
+
|
|
3
34
|
## 2.0.8
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XFE CLI HTTP Backend Gateway。
|
|
3
|
+
*
|
|
4
|
+
* CLI 通过该适配器连接本地或远端 xfe server。
|
|
5
|
+
*/
|
|
6
|
+
export interface BackendGatewayOptions {
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
readonly tenantId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateRemoteTaskOptions {
|
|
11
|
+
readonly repoUrl: string;
|
|
12
|
+
readonly taskId: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly documentUrl?: string;
|
|
15
|
+
readonly baseBranch?: string;
|
|
16
|
+
readonly branchName?: string;
|
|
17
|
+
readonly workflowId?: string;
|
|
18
|
+
readonly prepareWorkspace?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface StartRemoteTaskOptions {
|
|
21
|
+
readonly taskId: string;
|
|
22
|
+
readonly executor?: string;
|
|
23
|
+
readonly autoApprove?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface RespondClientRequestOptions {
|
|
26
|
+
readonly requestId: string;
|
|
27
|
+
readonly action: 'accept' | 'decline' | 'cancel';
|
|
28
|
+
readonly content?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface ListClientRequestsOptions {
|
|
31
|
+
readonly status?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare class HttpBackendGateway {
|
|
34
|
+
private readonly baseUrl;
|
|
35
|
+
private readonly tenantId?;
|
|
36
|
+
constructor(options: BackendGatewayOptions);
|
|
37
|
+
health(): Promise<unknown>;
|
|
38
|
+
createTask(options: CreateRemoteTaskOptions): Promise<unknown>;
|
|
39
|
+
startTask(options: StartRemoteTaskOptions): Promise<unknown>;
|
|
40
|
+
listClientRequests(options?: ListClientRequestsOptions): Promise<unknown>;
|
|
41
|
+
respondClientRequest(options: RespondClientRequestOptions): Promise<unknown>;
|
|
42
|
+
private get;
|
|
43
|
+
private post;
|
|
44
|
+
private createUrl;
|
|
45
|
+
private createHeaders;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=http-backend.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XFE CLI HTTP Backend Gateway。
|
|
3
|
+
*
|
|
4
|
+
* CLI 通过该适配器连接本地或远端 xfe server。
|
|
5
|
+
*/
|
|
6
|
+
// ─── Gateway ───────────────────────────────────────────────
|
|
7
|
+
export class HttpBackendGateway {
|
|
8
|
+
baseUrl;
|
|
9
|
+
tenantId;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.baseUrl = trimTrailingSlash(options.baseUrl);
|
|
12
|
+
this.tenantId = options.tenantId;
|
|
13
|
+
}
|
|
14
|
+
async health() {
|
|
15
|
+
return this.get('/health');
|
|
16
|
+
}
|
|
17
|
+
async createTask(options) {
|
|
18
|
+
return this.post('/tasks', options);
|
|
19
|
+
}
|
|
20
|
+
async startTask(options) {
|
|
21
|
+
return this.post(`/tasks/${encodeURIComponent(options.taskId)}/start`, {
|
|
22
|
+
executor: options.executor,
|
|
23
|
+
autoApprove: options.autoApprove,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async listClientRequests(options = {}) {
|
|
27
|
+
return this.get('/client-requests', { status: options.status });
|
|
28
|
+
}
|
|
29
|
+
async respondClientRequest(options) {
|
|
30
|
+
return this.post(`/client-requests/${encodeURIComponent(options.requestId)}/respond`, {
|
|
31
|
+
action: options.action,
|
|
32
|
+
content: options.content,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async get(pathname, query) {
|
|
36
|
+
const response = await fetch(this.createUrl(pathname, query), { headers: this.createHeaders() });
|
|
37
|
+
return readResponse(response);
|
|
38
|
+
}
|
|
39
|
+
async post(pathname, body) {
|
|
40
|
+
const response = await fetch(this.createUrl(pathname), {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { ...this.createHeaders(), 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
return readResponse(response);
|
|
46
|
+
}
|
|
47
|
+
createUrl(pathname, query = {}) {
|
|
48
|
+
const url = new URL(`${this.baseUrl}${pathname}`);
|
|
49
|
+
for (const [key, value] of Object.entries(query)) {
|
|
50
|
+
if (value)
|
|
51
|
+
url.searchParams.set(key, value);
|
|
52
|
+
}
|
|
53
|
+
return url.toString();
|
|
54
|
+
}
|
|
55
|
+
createHeaders() {
|
|
56
|
+
if (!this.tenantId)
|
|
57
|
+
return {};
|
|
58
|
+
return { 'x-xfe-tenant-id': this.tenantId };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
62
|
+
async function readResponse(response) {
|
|
63
|
+
const body = await readJson(response);
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const message = typeof body === 'object' && body && 'error' in body ? String(body.error) : response.statusText;
|
|
66
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
67
|
+
}
|
|
68
|
+
return body;
|
|
69
|
+
}
|
|
70
|
+
async function readJson(response) {
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
if (!text)
|
|
73
|
+
return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function trimTrailingSlash(value) {
|
|
82
|
+
return value.replace(/\/+$/, '');
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=http-backend.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XFE CLI Server/Task 静态命令。
|
|
3
|
+
*
|
|
4
|
+
* 负责本地 server 启动、远端任务创建和 client request 交互循环。
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
export declare function registerServerCommands(program: Command): void;
|
|
8
|
+
export declare function registerTaskCommands(program: Command): void;
|
|
9
|
+
//# sourceMappingURL=server-task-commands.d.ts.map
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XFE CLI Server/Task 静态命令。
|
|
3
|
+
*
|
|
4
|
+
* 负责本地 server 启动、远端任务创建和 client request 交互循环。
|
|
5
|
+
*/
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { DEFAULT_SERVER_PORT, startServer } from '@xfe-repo/server';
|
|
8
|
+
import { HttpBackendGateway } from './http-backend.js';
|
|
9
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
10
|
+
export function registerServerCommands(program) {
|
|
11
|
+
const serverCmd = program.command('server').helpCommand(false).description('XFE Server 管理');
|
|
12
|
+
serverCmd
|
|
13
|
+
.command('start')
|
|
14
|
+
.description('启动本地 xfe server')
|
|
15
|
+
.option('-p, --port <port>', `监听端口,默认 ${DEFAULT_SERVER_PORT}`)
|
|
16
|
+
.option('-H, --host <host>', '监听地址')
|
|
17
|
+
.action((options) => {
|
|
18
|
+
const server = startServer({ port: resolvePort(options.port), hostname: options.host });
|
|
19
|
+
printServerStarted(server);
|
|
20
|
+
bindServerSignals(server);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function registerTaskCommands(program) {
|
|
24
|
+
const taskCmd = program.command('task').helpCommand(false).description('远端/本地 XFE Server 任务');
|
|
25
|
+
registerTaskCreateCommand(taskCmd);
|
|
26
|
+
registerTaskStartCommand(taskCmd);
|
|
27
|
+
registerRequestCommands(taskCmd);
|
|
28
|
+
}
|
|
29
|
+
// ─── Task Commands ─────────────────────────────────────────
|
|
30
|
+
function registerTaskCreateCommand(taskCmd) {
|
|
31
|
+
taskCmd
|
|
32
|
+
.command('create')
|
|
33
|
+
.description('通过 xfe server 创建远端 agent 任务')
|
|
34
|
+
.requiredOption('--repo <repoUrl>', 'Git 仓库地址')
|
|
35
|
+
.requiredOption('--task <taskId>', '任务号,例如 3600')
|
|
36
|
+
.requiredOption('--description <description>', '任务需求描述')
|
|
37
|
+
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
38
|
+
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
39
|
+
.option('--document-url <url>', '需求文档链接')
|
|
40
|
+
.option('--base-branch <branch>', '基准分支,默认 main')
|
|
41
|
+
.option('--branch <branchName>', '任务分支,默认 feat-${taskId}')
|
|
42
|
+
.option('--workflow <workflowId>', '任务流程 ID,默认 plan-code-review')
|
|
43
|
+
.option('--prepare-workspace', '创建任务后立即 clone 仓库并创建任务分支')
|
|
44
|
+
.option('--start', '创建任务后启动 agent workflow')
|
|
45
|
+
.option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
|
|
46
|
+
.option('--auto-approve', '启动 agent workflow 后自动通过计划审批')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
const gateway = createBackendGateway(options);
|
|
49
|
+
const result = await gateway.createTask(createRemoteTaskPayload(options));
|
|
50
|
+
printJson(result);
|
|
51
|
+
if (!options.start)
|
|
52
|
+
return;
|
|
53
|
+
printJson(await gateway.startTask({ taskId: options.task, executor: options.executor, autoApprove: options.autoApprove }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function registerTaskStartCommand(taskCmd) {
|
|
57
|
+
taskCmd
|
|
58
|
+
.command('start <taskId>')
|
|
59
|
+
.description('启动已创建的 agent workflow')
|
|
60
|
+
.option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
|
|
61
|
+
.option('--auto-approve', '自动通过计划审批')
|
|
62
|
+
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
63
|
+
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
64
|
+
.action(async (taskId, options) => {
|
|
65
|
+
const gateway = createBackendGateway(options);
|
|
66
|
+
printJson(await gateway.startTask({ taskId, executor: options.executor, autoApprove: options.autoApprove }));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function registerRequestCommands(taskCmd) {
|
|
70
|
+
taskCmd
|
|
71
|
+
.command('requests')
|
|
72
|
+
.description('列出等待用户处理的 ask/approval 请求')
|
|
73
|
+
.option('--status <status>', '请求状态,默认不过滤')
|
|
74
|
+
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
75
|
+
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
printJson(await createBackendGateway(options).listClientRequests({ status: options.status }));
|
|
78
|
+
});
|
|
79
|
+
taskCmd
|
|
80
|
+
.command('watch')
|
|
81
|
+
.description('监听并处理等待用户处理的 ask/approval 请求')
|
|
82
|
+
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
83
|
+
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
84
|
+
.option('--interval <ms>', '轮询间隔,默认 1000')
|
|
85
|
+
.option('--once', '只处理当前 pending 请求后退出')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
await watchClientRequests({ gateway: createBackendGateway(options), intervalMs: resolvePollInterval(options.interval), once: Boolean(options.once) });
|
|
88
|
+
});
|
|
89
|
+
taskCmd
|
|
90
|
+
.command('respond <requestId>')
|
|
91
|
+
.description('响应 ask/approval 请求')
|
|
92
|
+
.requiredOption('--action <action>', 'accept | decline | cancel')
|
|
93
|
+
.option('--content <json>', '响应内容 JSON')
|
|
94
|
+
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
95
|
+
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
96
|
+
.action(async (requestId, options) => {
|
|
97
|
+
const result = await createBackendGateway(options).respondClientRequest({
|
|
98
|
+
requestId,
|
|
99
|
+
action: parseResponseAction(options.action),
|
|
100
|
+
content: parseJsonRecord(options.content),
|
|
101
|
+
});
|
|
102
|
+
printJson(result);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// ─── Watch Flow ────────────────────────────────────────────
|
|
106
|
+
async function watchClientRequests(options) {
|
|
107
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
108
|
+
const handledRequestIds = new Set();
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
await handlePendingRequests(terminal, options.gateway, handledRequestIds);
|
|
112
|
+
if (options.once)
|
|
113
|
+
return;
|
|
114
|
+
await wait(options.intervalMs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
terminal.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function handlePendingRequests(terminal, gateway, handledRequestIds) {
|
|
122
|
+
const requests = await listPendingClientRequests(gateway, handledRequestIds);
|
|
123
|
+
for (const request of requests) {
|
|
124
|
+
handledRequestIds.add(request.id);
|
|
125
|
+
const response = await promptClientRequest(terminal, request);
|
|
126
|
+
await gateway.respondClientRequest({ requestId: request.id, action: response.action, content: response.content });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function listPendingClientRequests(gateway, handledRequestIds) {
|
|
130
|
+
const response = await gateway.listClientRequests({ status: 'pending' });
|
|
131
|
+
return extractClientRequests(response).filter((request) => !handledRequestIds.has(request.id));
|
|
132
|
+
}
|
|
133
|
+
async function promptClientRequest(terminal, request) {
|
|
134
|
+
console.log(`\n[${request.kind}] ${request.id}`);
|
|
135
|
+
console.log(request.message);
|
|
136
|
+
if (isActionOnlyRequest(request.kind))
|
|
137
|
+
return { action: await promptAction(terminal), content: undefined };
|
|
138
|
+
return { action: 'accept', content: await promptSchemaContent(terminal, request.schema) };
|
|
139
|
+
}
|
|
140
|
+
async function promptAction(terminal) {
|
|
141
|
+
const answer = await terminal.question('处理方式 accept/decline/cancel,默认 accept: ');
|
|
142
|
+
const normalizedAnswer = answer.trim().toLowerCase();
|
|
143
|
+
if (!normalizedAnswer)
|
|
144
|
+
return 'accept';
|
|
145
|
+
if (normalizedAnswer === 'a' || normalizedAnswer === 'accept')
|
|
146
|
+
return 'accept';
|
|
147
|
+
if (normalizedAnswer === 'd' || normalizedAnswer === 'decline')
|
|
148
|
+
return 'decline';
|
|
149
|
+
if (normalizedAnswer === 'c' || normalizedAnswer === 'cancel')
|
|
150
|
+
return 'cancel';
|
|
151
|
+
throw new Error('处理方式必须是 accept、decline 或 cancel');
|
|
152
|
+
}
|
|
153
|
+
async function promptSchemaContent(terminal, schema) {
|
|
154
|
+
const fields = extractSchemaFields(schema);
|
|
155
|
+
if (fields.length === 0)
|
|
156
|
+
return undefined;
|
|
157
|
+
const content = {};
|
|
158
|
+
for (const field of fields) {
|
|
159
|
+
content[field.name] = await promptSchemaField(terminal, field);
|
|
160
|
+
}
|
|
161
|
+
return content;
|
|
162
|
+
}
|
|
163
|
+
async function promptSchemaField(terminal, field) {
|
|
164
|
+
const enumText = field.enumValues?.length ? ` (${field.enumValues.join(' | ')})` : '';
|
|
165
|
+
const defaultText = field.defaultValue === undefined ? '' : `,默认 ${String(field.defaultValue)}`;
|
|
166
|
+
const answer = await terminal.question(`${field.title}${enumText}${defaultText}: `);
|
|
167
|
+
const value = answer.trim() || field.defaultValue;
|
|
168
|
+
if (field.type === 'boolean')
|
|
169
|
+
return parseBooleanField(value);
|
|
170
|
+
if (field.type === 'number' || field.type === 'integer')
|
|
171
|
+
return Number(value);
|
|
172
|
+
return String(value ?? '');
|
|
173
|
+
}
|
|
174
|
+
function createRemoteTaskPayload(options) {
|
|
175
|
+
return {
|
|
176
|
+
repoUrl: options.repo,
|
|
177
|
+
taskId: options.task,
|
|
178
|
+
description: options.description,
|
|
179
|
+
documentUrl: options.documentUrl,
|
|
180
|
+
baseBranch: options.baseBranch,
|
|
181
|
+
branchName: options.branch,
|
|
182
|
+
workflowId: options.workflow,
|
|
183
|
+
prepareWorkspace: options.prepareWorkspace,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createBackendGateway(options) {
|
|
187
|
+
return new HttpBackendGateway({ baseUrl: options.server ?? `http://localhost:${DEFAULT_SERVER_PORT}`, tenantId: options.tenant });
|
|
188
|
+
}
|
|
189
|
+
function printServerStarted(server) {
|
|
190
|
+
console.log(`XFE Server listening on ${server.hostname}:${server.port}`);
|
|
191
|
+
console.log(`HTTP health: http://${server.hostname}:${server.port}/health`);
|
|
192
|
+
}
|
|
193
|
+
function bindServerSignals(server) {
|
|
194
|
+
process.once('SIGINT', () => void stopLocalServer(server));
|
|
195
|
+
process.once('SIGTERM', () => void stopLocalServer(server));
|
|
196
|
+
}
|
|
197
|
+
async function stopLocalServer(server) {
|
|
198
|
+
await server.close();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
function resolvePort(rawPort) {
|
|
202
|
+
const port = Number(rawPort);
|
|
203
|
+
return Number.isInteger(port) && port > 0 ? port : DEFAULT_SERVER_PORT;
|
|
204
|
+
}
|
|
205
|
+
function resolvePollInterval(rawInterval) {
|
|
206
|
+
const intervalMs = Number(rawInterval);
|
|
207
|
+
return Number.isInteger(intervalMs) && intervalMs > 0 ? intervalMs : 1000;
|
|
208
|
+
}
|
|
209
|
+
function parseResponseAction(action) {
|
|
210
|
+
if (action === 'accept' || action === 'decline' || action === 'cancel')
|
|
211
|
+
return action;
|
|
212
|
+
throw new Error('action 必须是 accept、decline 或 cancel');
|
|
213
|
+
}
|
|
214
|
+
function parseJsonRecord(content) {
|
|
215
|
+
if (!content)
|
|
216
|
+
return undefined;
|
|
217
|
+
const parsed = JSON.parse(content);
|
|
218
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
219
|
+
throw new Error('content 必须是 JSON object');
|
|
220
|
+
return parsed;
|
|
221
|
+
}
|
|
222
|
+
function extractClientRequests(response) {
|
|
223
|
+
if (!isRecord(response) || !Array.isArray(response.requests))
|
|
224
|
+
return [];
|
|
225
|
+
return response.requests.filter(isClientRequestView);
|
|
226
|
+
}
|
|
227
|
+
function isClientRequestView(value) {
|
|
228
|
+
if (!isRecord(value))
|
|
229
|
+
return false;
|
|
230
|
+
return typeof value.id === 'string' && typeof value.kind === 'string' && typeof value.status === 'string' && typeof value.message === 'string';
|
|
231
|
+
}
|
|
232
|
+
function extractSchemaFields(schema) {
|
|
233
|
+
if (!schema || !isRecord(schema.properties))
|
|
234
|
+
return [];
|
|
235
|
+
return Object.entries(schema.properties).map(([name, value]) => createSchemaField(name, value));
|
|
236
|
+
}
|
|
237
|
+
function createSchemaField(name, value) {
|
|
238
|
+
const property = isRecord(value) ? value : {};
|
|
239
|
+
const enumValues = Array.isArray(property.enum) ? property.enum.filter((item) => typeof item === 'string') : undefined;
|
|
240
|
+
const title = typeof property.title === 'string' ? property.title : name;
|
|
241
|
+
const type = typeof property.type === 'string' ? property.type : undefined;
|
|
242
|
+
return { name, title, type, enumValues, defaultValue: property.default };
|
|
243
|
+
}
|
|
244
|
+
function isActionOnlyRequest(kind) {
|
|
245
|
+
return kind === 'approval' || kind === 'permission' || kind === 'confirm';
|
|
246
|
+
}
|
|
247
|
+
function parseBooleanField(value) {
|
|
248
|
+
if (typeof value === 'boolean')
|
|
249
|
+
return value;
|
|
250
|
+
const normalizedValue = String(value).trim().toLowerCase();
|
|
251
|
+
return normalizedValue === 'true' || normalizedValue === 'yes' || normalizedValue === 'y' || normalizedValue === '1';
|
|
252
|
+
}
|
|
253
|
+
function isRecord(value) {
|
|
254
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
255
|
+
}
|
|
256
|
+
function wait(intervalMs) {
|
|
257
|
+
return new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
258
|
+
}
|
|
259
|
+
function printJson(value) {
|
|
260
|
+
console.log(JSON.stringify(value, null, 2));
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=server-task-commands.js.map
|
|
@@ -10,7 +10,7 @@ import { useKeymap, KeymapLayer } from '../../hooks/use-keymap.js';
|
|
|
10
10
|
import { useSelectableList } from './use-selectable-list.js';
|
|
11
11
|
import { OptionList, SearchBar } from './OptionList.js';
|
|
12
12
|
export const FuzzyPathPrompt = memo(function FuzzyPathPrompt({ question, onSubmit }) {
|
|
13
|
-
const options = useMemo(() => collectPaths(question.rootPath || '.', question.itemType || 'any', question.excludePath).map((p) => ({ label: p, value: p })), [question.rootPath, question.itemType, question.excludePath]);
|
|
13
|
+
const options = useMemo(() => collectPaths(question.rootPath || '.', question.itemType || 'any', question.excludePath).map((p) => ({ key: p, label: p, value: p })), [question.rootPath, question.itemType, question.excludePath]);
|
|
14
14
|
const { searchText, filtered, selectedIndex, selectedItem } = useSelectableList({
|
|
15
15
|
items: options,
|
|
16
16
|
searchable: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* @xfe-repo/cli - 可滚动选项列表展示组件
|
|
4
4
|
*
|
|
@@ -15,13 +15,25 @@ export const OptionList = memo(function OptionList({ options, selectedIndex, pag
|
|
|
15
15
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [startIndex > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 \u8FD8\u6709 ", startIndex, " \u9879"] }), visibleItems.map((opt, i) => {
|
|
16
16
|
const actualIndex = startIndex + i;
|
|
17
17
|
const isFocused = actualIndex === selectedIndex;
|
|
18
|
-
return (_jsxs(Box, {
|
|
18
|
+
return (_jsxs(Box, { alignItems: "flex-start", children: [_jsx(OptionPrefix, { mode: mode, checked: opt.checked, isFocused: isFocused }), _jsx(OptionContent, { option: opt, isFocused: isFocused })] }, opt.key));
|
|
19
19
|
}), startIndex + windowSize < totalItems && _jsxs(Text, { dimColor: true, children: [" \u2193 \u8FD8\u6709 ", totalItems - startIndex - windowSize, " \u9879"] })] }));
|
|
20
20
|
});
|
|
21
21
|
export const SearchBar = memo(function SearchBar({ text }) {
|
|
22
22
|
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u641C\u7D22: " }), _jsxs(Text, { children: [text, _jsx(Text, { color: "gray", children: "\u258C" })] })] }));
|
|
23
23
|
});
|
|
24
24
|
// ─── Render Helpers ─────────────────────────────────────────
|
|
25
|
+
const OPTION_PREFIX_WIDTH = 2;
|
|
26
|
+
const CHECKBOX_OPTION_PREFIX_WIDTH = 4;
|
|
27
|
+
const FOCUSED_MARK = '❯';
|
|
28
|
+
const EMPTY_MARK = ' ';
|
|
29
|
+
const DESCRIPTION_COLOR = 'gray';
|
|
30
|
+
const OptionPrefix = memo(function OptionPrefix({ mode, checked, isFocused, }) {
|
|
31
|
+
const prefixWidth = mode === 'checkbox' ? CHECKBOX_OPTION_PREFIX_WIDTH : OPTION_PREFIX_WIDTH;
|
|
32
|
+
return (_jsxs(Box, { width: prefixWidth, flexShrink: 0, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? FOCUSED_MARK : EMPTY_MARK }), mode === 'checkbox' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(CheckboxIcon, { checked: checked })] })), _jsx(Text, { children: " " })] }));
|
|
33
|
+
});
|
|
34
|
+
const OptionContent = memo(function OptionContent({ option, isFocused }) {
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, bold: isFocused, children: option.label }), option.description && (_jsx(Text, { color: DESCRIPTION_COLOR, dimColor: true, children: option.description }))] }));
|
|
36
|
+
});
|
|
25
37
|
const CheckboxIcon = memo(function CheckboxIcon({ checked }) {
|
|
26
38
|
return _jsx(Text, { color: checked ? 'green' : 'gray', children: checked ? '◉' : '○' });
|
|
27
39
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PromptQuestion } from '@xfe-repo/cli-core';
|
|
1
|
+
import type { PromptChoice, PromptQuestion } from '@xfe-repo/cli-core';
|
|
2
2
|
import type { SelectOption } from './use-selectable-list.js';
|
|
3
3
|
export interface PromptViewProps {
|
|
4
4
|
readonly question: PromptQuestion;
|
|
@@ -11,8 +11,5 @@ export { SearchListPrompt } from './SearchListPrompt.js';
|
|
|
11
11
|
export { SearchCheckboxPrompt } from './SearchCheckboxPrompt.js';
|
|
12
12
|
export { FuzzyPathPrompt } from './FuzzyPathPrompt.js';
|
|
13
13
|
export declare const Prompt: import("react").NamedExoticComponent<PromptViewProps>;
|
|
14
|
-
export declare function normalizeChoices(choices?:
|
|
15
|
-
name: string;
|
|
16
|
-
value: any;
|
|
17
|
-
}[] | string[]): SelectOption[];
|
|
14
|
+
export declare function normalizeChoices(choices?: PromptChoice[] | string[]): SelectOption[];
|
|
18
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -40,8 +40,19 @@ export function normalizeChoices(choices) {
|
|
|
40
40
|
return [];
|
|
41
41
|
return choices.map((choice) => {
|
|
42
42
|
if (typeof choice === 'string')
|
|
43
|
-
return { label: choice, value: choice };
|
|
44
|
-
return {
|
|
43
|
+
return { key: choice, label: choice, value: choice };
|
|
44
|
+
return {
|
|
45
|
+
key: choice.key ?? getChoiceFallbackKey(choice),
|
|
46
|
+
label: choice.name,
|
|
47
|
+
value: choice.value,
|
|
48
|
+
description: choice.description,
|
|
49
|
+
};
|
|
45
50
|
});
|
|
46
51
|
}
|
|
52
|
+
function getChoiceFallbackKey(choice) {
|
|
53
|
+
if (typeof choice.value === 'string' || typeof choice.value === 'number' || typeof choice.value === 'boolean') {
|
|
54
|
+
return String(choice.value);
|
|
55
|
+
}
|
|
56
|
+
return choice.short ?? choice.name;
|
|
57
|
+
}
|
|
47
58
|
//# sourceMappingURL=index.js.map
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* 供 ListPrompt / SearchListPrompt / FuzzyPathPrompt / SearchCheckboxPrompt 复用
|
|
6
6
|
*/
|
|
7
7
|
export interface SelectOption {
|
|
8
|
+
readonly key: string;
|
|
8
9
|
readonly label: string;
|
|
9
10
|
readonly value: any;
|
|
11
|
+
readonly description?: string;
|
|
10
12
|
}
|
|
11
13
|
interface UseSelectableListOptions<T extends SelectOption> {
|
|
12
14
|
readonly items: T[];
|
|
@@ -14,8 +14,8 @@ export function useSelectableList({ items, defaultIndex = 0, searchable = false,
|
|
|
14
14
|
const filtered = useMemo(() => {
|
|
15
15
|
if (!searchable || !searchText)
|
|
16
16
|
return items;
|
|
17
|
-
const
|
|
18
|
-
return items.filter((
|
|
17
|
+
const searchKeyword = searchText.toLowerCase();
|
|
18
|
+
return items.filter((option) => getOptionSearchText(option).includes(searchKeyword));
|
|
19
19
|
}, [items, searchText, searchable]);
|
|
20
20
|
// 过滤结果变化时重置选中(跳过首次渲染,避免覆盖 defaultIndex)
|
|
21
21
|
useUpdateEffect(() => {
|
|
@@ -54,4 +54,9 @@ export function useSelectableList({ items, defaultIndex = 0, searchable = false,
|
|
|
54
54
|
selectedItem: filtered[selectedIndex],
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
58
|
+
function getOptionSearchText(option) {
|
|
59
|
+
const searchableText = [option.label, option.description ?? ''].join(' ');
|
|
60
|
+
return searchableText.toLowerCase();
|
|
61
|
+
}
|
|
57
62
|
//# sourceMappingURL=use-selectable-list.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xfe-repo/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "XFE CLI - Ink-based terminal UI for project scaffolding",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"ink-link": "^5.0.0",
|
|
18
18
|
"react": "^19.1.0",
|
|
19
19
|
"zod": "^4.3.6",
|
|
20
|
-
"@xfe-repo/cli-
|
|
21
|
-
"@xfe-repo/cli-
|
|
20
|
+
"@xfe-repo/cli-presets": "2.0.6",
|
|
21
|
+
"@xfe-repo/cli-core": "2.0.6"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^24.3.0",
|