@xfe-repo/cli 2.0.9 → 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 +22 -0
- package/dist/backend/http-backend.d.ts +4 -2
- package/dist/backend/http-backend.js +21 -25
- package/dist/backend/server-task-commands.js +57 -49
- 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,27 @@
|
|
|
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
|
+
|
|
3
25
|
## 2.0.9
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -13,6 +13,8 @@ export interface CreateRemoteTaskOptions {
|
|
|
13
13
|
readonly description: string;
|
|
14
14
|
readonly documentUrl?: string;
|
|
15
15
|
readonly baseBranch?: string;
|
|
16
|
+
readonly branchName?: string;
|
|
17
|
+
readonly workflowId?: string;
|
|
16
18
|
readonly prepareWorkspace?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface StartRemoteTaskOptions {
|
|
@@ -29,8 +31,8 @@ export interface ListClientRequestsOptions {
|
|
|
29
31
|
readonly status?: string;
|
|
30
32
|
}
|
|
31
33
|
export declare class HttpBackendGateway {
|
|
32
|
-
private baseUrl;
|
|
33
|
-
private tenantId?;
|
|
34
|
+
private readonly baseUrl;
|
|
35
|
+
private readonly tenantId?;
|
|
34
36
|
constructor(options: BackendGatewayOptions);
|
|
35
37
|
health(): Promise<unknown>;
|
|
36
38
|
createTask(options: CreateRemoteTaskOptions): Promise<unknown>;
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* CLI 通过该适配器连接本地或远端 xfe server。
|
|
5
5
|
*/
|
|
6
|
-
// ───
|
|
6
|
+
// ─── Gateway ───────────────────────────────────────────────
|
|
7
7
|
export class HttpBackendGateway {
|
|
8
8
|
baseUrl;
|
|
9
9
|
tenantId;
|
|
10
10
|
constructor(options) {
|
|
11
|
-
this.baseUrl =
|
|
11
|
+
this.baseUrl = trimTrailingSlash(options.baseUrl);
|
|
12
12
|
this.tenantId = options.tenantId;
|
|
13
13
|
}
|
|
14
14
|
async health() {
|
|
@@ -24,7 +24,7 @@ export class HttpBackendGateway {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
async listClientRequests(options = {}) {
|
|
27
|
-
return this.get(
|
|
27
|
+
return this.get('/client-requests', { status: options.status });
|
|
28
28
|
}
|
|
29
29
|
async respondClientRequest(options) {
|
|
30
30
|
return this.post(`/client-requests/${encodeURIComponent(options.requestId)}/respond`, {
|
|
@@ -32,20 +32,25 @@ export class HttpBackendGateway {
|
|
|
32
32
|
content: options.content,
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
async get(pathname) {
|
|
36
|
-
const response = await fetch(this.createUrl(pathname), { headers: this.createHeaders() });
|
|
35
|
+
async get(pathname, query) {
|
|
36
|
+
const response = await fetch(this.createUrl(pathname, query), { headers: this.createHeaders() });
|
|
37
37
|
return readResponse(response);
|
|
38
38
|
}
|
|
39
39
|
async post(pathname, body) {
|
|
40
40
|
const response = await fetch(this.createUrl(pathname), {
|
|
41
41
|
method: 'POST',
|
|
42
|
-
headers: { ...this.createHeaders(), '
|
|
42
|
+
headers: { ...this.createHeaders(), 'Content-Type': 'application/json' },
|
|
43
43
|
body: JSON.stringify(body),
|
|
44
44
|
});
|
|
45
45
|
return readResponse(response);
|
|
46
46
|
}
|
|
47
|
-
createUrl(pathname) {
|
|
48
|
-
|
|
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();
|
|
49
54
|
}
|
|
50
55
|
createHeaders() {
|
|
51
56
|
if (!this.tenantId)
|
|
@@ -53,17 +58,17 @@ export class HttpBackendGateway {
|
|
|
53
58
|
return { 'x-xfe-tenant-id': this.tenantId };
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
|
-
// ─── Helpers
|
|
61
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
57
62
|
async function readResponse(response) {
|
|
58
|
-
const
|
|
59
|
-
const body = parseJson(text);
|
|
63
|
+
const body = await readJson(response);
|
|
60
64
|
if (!response.ok) {
|
|
61
|
-
const message = typeof body === 'object' && body && 'error' in body ? String(body.error) :
|
|
62
|
-
throw new Error(
|
|
65
|
+
const message = typeof body === 'object' && body && 'error' in body ? String(body.error) : response.statusText;
|
|
66
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
63
67
|
}
|
|
64
68
|
return body;
|
|
65
69
|
}
|
|
66
|
-
function
|
|
70
|
+
async function readJson(response) {
|
|
71
|
+
const text = await response.text();
|
|
67
72
|
if (!text)
|
|
68
73
|
return undefined;
|
|
69
74
|
try {
|
|
@@ -73,16 +78,7 @@ function parseJson(text) {
|
|
|
73
78
|
return text;
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
|
-
function
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
function createPathWithQuery(pathname, query) {
|
|
80
|
-
const searchParams = new URLSearchParams();
|
|
81
|
-
for (const [key, value] of Object.entries(query)) {
|
|
82
|
-
if (value)
|
|
83
|
-
searchParams.set(key, value);
|
|
84
|
-
}
|
|
85
|
-
const queryString = searchParams.toString();
|
|
86
|
-
return queryString ? `${pathname}?${queryString}` : pathname;
|
|
81
|
+
function trimTrailingSlash(value) {
|
|
82
|
+
return value.replace(/\/+$/, '');
|
|
87
83
|
}
|
|
88
84
|
//# sourceMappingURL=http-backend.js.map
|
|
@@ -15,17 +15,19 @@ export function registerServerCommands(program) {
|
|
|
15
15
|
.option('-p, --port <port>', `监听端口,默认 ${DEFAULT_SERVER_PORT}`)
|
|
16
16
|
.option('-H, --host <host>', '监听地址')
|
|
17
17
|
.action((options) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(`HTTP health: http://${options.host ?? 'localhost'}:${server.port}/health`);
|
|
22
|
-
console.log(`MCP endpoint: http://${options.host ?? 'localhost'}:${server.port}/mcp`);
|
|
23
|
-
process.once('SIGINT', () => void stopLocalServer(server.close));
|
|
24
|
-
process.once('SIGTERM', () => void stopLocalServer(server.close));
|
|
18
|
+
const server = startServer({ port: resolvePort(options.port), hostname: options.host });
|
|
19
|
+
printServerStarted(server);
|
|
20
|
+
bindServerSignals(server);
|
|
25
21
|
});
|
|
26
22
|
}
|
|
27
23
|
export function registerTaskCommands(program) {
|
|
28
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) {
|
|
29
31
|
taskCmd
|
|
30
32
|
.command('create')
|
|
31
33
|
.description('通过 xfe server 创建远端 agent 任务')
|
|
@@ -36,41 +38,34 @@ export function registerTaskCommands(program) {
|
|
|
36
38
|
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
37
39
|
.option('--document-url <url>', '需求文档链接')
|
|
38
40
|
.option('--base-branch <branch>', '基准分支,默认 main')
|
|
41
|
+
.option('--branch <branchName>', '任务分支,默认 feat-${taskId}')
|
|
42
|
+
.option('--workflow <workflowId>', '任务流程 ID,默认 plan-code-review')
|
|
39
43
|
.option('--prepare-workspace', '创建任务后立即 clone 仓库并创建任务分支')
|
|
40
44
|
.option('--start', '创建任务后启动 agent workflow')
|
|
41
|
-
.option('--executor <executor>', 'agent executor,mock | codex | copilot')
|
|
45
|
+
.option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
|
|
42
46
|
.option('--auto-approve', '启动 agent workflow 后自动通过计划审批')
|
|
43
47
|
.action(async (options) => {
|
|
44
48
|
const gateway = createBackendGateway(options);
|
|
45
|
-
const result = await gateway.createTask(
|
|
46
|
-
repoUrl: options.repo,
|
|
47
|
-
taskId: options.task,
|
|
48
|
-
description: options.description,
|
|
49
|
-
documentUrl: options.documentUrl,
|
|
50
|
-
baseBranch: options.baseBranch,
|
|
51
|
-
prepareWorkspace: options.prepareWorkspace,
|
|
52
|
-
});
|
|
49
|
+
const result = await gateway.createTask(createRemoteTaskPayload(options));
|
|
53
50
|
printJson(result);
|
|
54
51
|
if (!options.start)
|
|
55
52
|
return;
|
|
56
|
-
|
|
57
|
-
printJson(started);
|
|
53
|
+
printJson(await gateway.startTask({ taskId: options.task, executor: options.executor, autoApprove: options.autoApprove }));
|
|
58
54
|
});
|
|
55
|
+
}
|
|
56
|
+
function registerTaskStartCommand(taskCmd) {
|
|
59
57
|
taskCmd
|
|
60
58
|
.command('start <taskId>')
|
|
61
59
|
.description('启动已创建的 agent workflow')
|
|
62
|
-
.option('--executor <executor>', 'agent executor,mock | codex | copilot')
|
|
60
|
+
.option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
|
|
63
61
|
.option('--auto-approve', '自动通过计划审批')
|
|
64
62
|
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
65
63
|
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
66
64
|
.action(async (taskId, options) => {
|
|
67
65
|
const gateway = createBackendGateway(options);
|
|
68
|
-
|
|
69
|
-
printJson(started);
|
|
66
|
+
printJson(await gateway.startTask({ taskId, executor: options.executor, autoApprove: options.autoApprove }));
|
|
70
67
|
});
|
|
71
|
-
registerRequestCommands(taskCmd);
|
|
72
68
|
}
|
|
73
|
-
// ─── Command Registration ───────────────────────────────────
|
|
74
69
|
function registerRequestCommands(taskCmd) {
|
|
75
70
|
taskCmd
|
|
76
71
|
.command('requests')
|
|
@@ -79,9 +74,7 @@ function registerRequestCommands(taskCmd) {
|
|
|
79
74
|
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
80
75
|
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
81
76
|
.action(async (options) => {
|
|
82
|
-
|
|
83
|
-
const result = await gateway.listClientRequests({ status: options.status });
|
|
84
|
-
printJson(result);
|
|
77
|
+
printJson(await createBackendGateway(options).listClientRequests({ status: options.status }));
|
|
85
78
|
});
|
|
86
79
|
taskCmd
|
|
87
80
|
.command('watch')
|
|
@@ -91,11 +84,7 @@ function registerRequestCommands(taskCmd) {
|
|
|
91
84
|
.option('--interval <ms>', '轮询间隔,默认 1000')
|
|
92
85
|
.option('--once', '只处理当前 pending 请求后退出')
|
|
93
86
|
.action(async (options) => {
|
|
94
|
-
await watchClientRequests({
|
|
95
|
-
gateway: createBackendGateway(options),
|
|
96
|
-
intervalMs: resolvePollInterval(options.interval),
|
|
97
|
-
once: Boolean(options.once),
|
|
98
|
-
});
|
|
87
|
+
await watchClientRequests({ gateway: createBackendGateway(options), intervalMs: resolvePollInterval(options.interval), once: Boolean(options.once) });
|
|
99
88
|
});
|
|
100
89
|
taskCmd
|
|
101
90
|
.command('respond <requestId>')
|
|
@@ -105,8 +94,7 @@ function registerRequestCommands(taskCmd) {
|
|
|
105
94
|
.option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
|
|
106
95
|
.option('--tenant <tenantId>', '租户 ID,本地默认 local')
|
|
107
96
|
.action(async (requestId, options) => {
|
|
108
|
-
const
|
|
109
|
-
const result = await gateway.respondClientRequest({
|
|
97
|
+
const result = await createBackendGateway(options).respondClientRequest({
|
|
110
98
|
requestId,
|
|
111
99
|
action: parseResponseAction(options.action),
|
|
112
100
|
content: parseJsonRecord(options.content),
|
|
@@ -114,18 +102,13 @@ function registerRequestCommands(taskCmd) {
|
|
|
114
102
|
printJson(result);
|
|
115
103
|
});
|
|
116
104
|
}
|
|
117
|
-
// ─── Watch Flow
|
|
105
|
+
// ─── Watch Flow ────────────────────────────────────────────
|
|
118
106
|
async function watchClientRequests(options) {
|
|
119
107
|
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
120
108
|
const handledRequestIds = new Set();
|
|
121
109
|
try {
|
|
122
110
|
while (true) {
|
|
123
|
-
|
|
124
|
-
for (const request of requests) {
|
|
125
|
-
handledRequestIds.add(request.id);
|
|
126
|
-
const response = await promptClientRequest(terminal, request);
|
|
127
|
-
await options.gateway.respondClientRequest({ requestId: request.id, action: response.action, content: response.content });
|
|
128
|
-
}
|
|
111
|
+
await handlePendingRequests(terminal, options.gateway, handledRequestIds);
|
|
129
112
|
if (options.once)
|
|
130
113
|
return;
|
|
131
114
|
await wait(options.intervalMs);
|
|
@@ -135,6 +118,14 @@ async function watchClientRequests(options) {
|
|
|
135
118
|
terminal.close();
|
|
136
119
|
}
|
|
137
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
|
+
}
|
|
138
129
|
async function listPendingClientRequests(gateway, handledRequestIds) {
|
|
139
130
|
const response = await gateway.listClientRequests({ status: 'pending' });
|
|
140
131
|
return extractClientRequests(response).filter((request) => !handledRequestIds.has(request.id));
|
|
@@ -144,8 +135,7 @@ async function promptClientRequest(terminal, request) {
|
|
|
144
135
|
console.log(request.message);
|
|
145
136
|
if (isActionOnlyRequest(request.kind))
|
|
146
137
|
return { action: await promptAction(terminal), content: undefined };
|
|
147
|
-
|
|
148
|
-
return { action: 'accept', content };
|
|
138
|
+
return { action: 'accept', content: await promptSchemaContent(terminal, request.schema) };
|
|
149
139
|
}
|
|
150
140
|
async function promptAction(terminal) {
|
|
151
141
|
const answer = await terminal.question('处理方式 accept/decline/cancel,默认 accept: ');
|
|
@@ -181,10 +171,33 @@ async function promptSchemaField(terminal, field) {
|
|
|
181
171
|
return Number(value);
|
|
182
172
|
return String(value ?? '');
|
|
183
173
|
}
|
|
184
|
-
|
|
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
|
+
}
|
|
185
186
|
function createBackendGateway(options) {
|
|
186
187
|
return new HttpBackendGateway({ baseUrl: options.server ?? `http://localhost:${DEFAULT_SERVER_PORT}`, tenantId: options.tenant });
|
|
187
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
|
+
}
|
|
188
201
|
function resolvePort(rawPort) {
|
|
189
202
|
const port = Number(rawPort);
|
|
190
203
|
return Number.isInteger(port) && port > 0 ? port : DEFAULT_SERVER_PORT;
|
|
@@ -193,10 +206,6 @@ function resolvePollInterval(rawInterval) {
|
|
|
193
206
|
const intervalMs = Number(rawInterval);
|
|
194
207
|
return Number.isInteger(intervalMs) && intervalMs > 0 ? intervalMs : 1000;
|
|
195
208
|
}
|
|
196
|
-
async function stopLocalServer(close) {
|
|
197
|
-
await close();
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
200
209
|
function parseResponseAction(action) {
|
|
201
210
|
if (action === 'accept' || action === 'decline' || action === 'cancel')
|
|
202
211
|
return action;
|
|
@@ -206,9 +215,8 @@ function parseJsonRecord(content) {
|
|
|
206
215
|
if (!content)
|
|
207
216
|
return undefined;
|
|
208
217
|
const parsed = JSON.parse(content);
|
|
209
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
218
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
210
219
|
throw new Error('content 必须是 JSON object');
|
|
211
|
-
}
|
|
212
220
|
return parsed;
|
|
213
221
|
}
|
|
214
222
|
function extractClientRequests(response) {
|
|
@@ -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",
|