@xfe-repo/cli 2.0.12 → 2.0.13

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # @xfe-repo/cli
2
2
 
3
+ ## 2.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 修复窗口高度计算问题
8
+ - Updated dependencies
9
+ - @xfe-repo/cli-presets@2.0.8
10
+ - @xfe-repo/cli-core@2.0.8
11
+
3
12
  ## 2.0.12
4
13
 
5
14
  ### Patch Changes
package/dist/app.js CHANGED
@@ -166,7 +166,7 @@ function AppInner({ runner, toast, scriptName }) {
166
166
  const activeLogger = activeSession?.logger ?? emptyLoggerRef.current;
167
167
  const { entries: logEntries } = useLogs(activeLogger);
168
168
  const { rows: terminalRows } = useWindowSize();
169
- const availableViewRows = resolveAvailableViewRows({ terminalRows, staticRows: logEntries.length });
169
+ const availableViewRows = resolveAvailableViewRows({ terminalRows });
170
170
  // 当 session 被移除但 view 仍为 session 时,自动回退到菜单(直接命令模式不回退,等待 doExit 完成)
171
171
  useEffect(() => {
172
172
  if (scriptName)
@@ -193,7 +193,7 @@ function AppInner({ runner, toast, scriptName }) {
193
193
  return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["ERROR: ", view.message] }) }));
194
194
  }
195
195
  };
196
- return (_jsxs(_Fragment, { children: [_jsx(Static, { items: logEntries, children: (entry, i) => _jsx(LogLine, { entry: entry }, `${entry.timestamp}-${i}`) }), _jsx(Box, { flexDirection: "column", height: availableViewRows, overflow: "hidden", children: renderView(availableViewRows) })] }));
196
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: logEntries, children: (entry, i) => _jsx(LogLine, { entry: entry }, `${entry.timestamp}-${i}`) }), _jsx(Box, { flexDirection: "column", maxHeight: availableViewRows, overflow: "hidden", children: renderView(availableViewRows) })] }));
197
197
  }
198
198
  // ─── Render Helpers ─────────────────────────────────────────
199
199
  function LogLine({ entry }) {
@@ -208,7 +208,7 @@ function LogLine({ entry }) {
208
208
  return _jsx(Text, { color: color, children: entry.message });
209
209
  return _jsx(Text, { children: entry.message });
210
210
  }
211
- function resolveAvailableViewRows({ terminalRows, staticRows }) {
212
- return Math.max(MIN_VIEW_ROWS, terminalRows - staticRows - BOTTOM_RESERVED_ROWS);
211
+ function resolveAvailableViewRows({ terminalRows }) {
212
+ return Math.max(MIN_VIEW_ROWS, terminalRows - BOTTOM_RESERVED_ROWS);
213
213
  }
214
214
  //# sourceMappingURL=app.js.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * @xfe-repo/cli - 运行视图
4
4
  */
@@ -26,7 +26,7 @@ export const RunnerView = memo(function RunnerView({ availableRows, projectName,
26
26
  hasSpinnerStatus: isSpinnerStatusVisible(spinnerState),
27
27
  hasStatusBar: !isSingleCommand,
28
28
  });
29
- return (_jsxs(_Fragment, { children: [layout.showLogo && _jsx(Logo, {}), _jsxs(Box, { flexDirection: "column", gap: 1, overflowX: "hidden", children: [_jsx(SpinnerStatus, { state: spinnerState }), hasPrompt && _jsx(PromptView, { question: request.question, maxRows: layout.promptRows, onSubmit: complete }, request.id)] }), !isSingleCommand && (_jsxs(Box, { gap: 1, borderStyle: "round", borderColor: isReadOnly ? 'blue' : 'cyan', paddingX: 1, overflowX: "hidden", children: [_jsx(StatusBar, { projectName: projectName, scriptName: scriptName, startTime: startTime, endTime: endTime, badges: badges, storeSnapshot: storeSnapshot }), _jsx(StatusHint, { isReadOnly: isReadOnly, hasError: hasError, isCompleted: isCompleted })] }))] }));
29
+ return (_jsxs(Box, { flexDirection: "column", children: [layout.showLogo && _jsx(Logo, {}), _jsxs(Box, { flexDirection: "column", gap: 1, overflowX: "hidden", children: [_jsx(SpinnerStatus, { state: spinnerState }), hasPrompt && _jsx(PromptView, { question: request.question, maxRows: layout.promptRows, onSubmit: complete }, request.id)] }), !isSingleCommand && (_jsxs(Box, { gap: 1, borderStyle: "round", borderColor: isReadOnly ? 'blue' : 'cyan', paddingX: 1, overflowX: "hidden", children: [_jsx(StatusBar, { projectName: projectName, scriptName: scriptName, startTime: startTime, endTime: endTime, badges: badges, storeSnapshot: storeSnapshot }), _jsx(StatusHint, { isReadOnly: isReadOnly, hasError: hasError, isCompleted: isCompleted })] }))] }));
30
30
  });
31
31
  // ─── Layout Helpers ────────────────────────────────────────
32
32
  function resolveRunnerLayout({ availableRows, hasPrompt, hasSpinnerStatus, hasStatusBar }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfe-repo/cli",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
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-core": "2.0.7",
21
- "@xfe-repo/cli-presets": "2.0.7"
20
+ "@xfe-repo/cli-core": "2.0.8",
21
+ "@xfe-repo/cli-presets": "2.0.8"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^24.3.0",
@@ -1,47 +0,0 @@
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
@@ -1,84 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -1,262 +0,0 @@
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
@@ -1,19 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 命令目录构造(TUI / shell 共用的命令视图聚合)
3
- *
4
- * 把 core 的 CommandWithSource 列表转换为 completion-core 的 CommandCatalogItem 树
5
- * - 同一 source 且共享 namespace 前缀的命令折叠为 group 项
6
- * - 命令支持的 options 由 Zod parameters 派生(getCommandOptionsMeta)
7
- * - 运行中命令标记为 disabled,描述加 `(运行中)` 前缀
8
- */
9
- import type { CommandWithSource } from '@xfe-repo/cli-core';
10
- import type { CommandCatalogItem } from './completion-core.js';
11
- export interface BuildCatalogOptions {
12
- /** 当前正在运行的命令名集合,用于禁用对应条目 */
13
- readonly runningCommandNames?: ReadonlySet<string>;
14
- /** 是否在末尾追加 exit 项(默认 true,TUI 使用) */
15
- readonly includeExit?: boolean;
16
- }
17
- /** 构造命令目录:按 source 分组,namespace 命令折叠为 group */
18
- export declare function buildCommandCatalog(commandsWithSource: readonly CommandWithSource[], options?: BuildCatalogOptions): CommandCatalogItem[];
19
- //# sourceMappingURL=command-catalog.d.ts.map
@@ -1,67 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 命令目录构造(TUI / shell 共用的命令视图聚合)
3
- *
4
- * 把 core 的 CommandWithSource 列表转换为 completion-core 的 CommandCatalogItem 树
5
- * - 同一 source 且共享 namespace 前缀的命令折叠为 group 项
6
- * - 命令支持的 options 由 Zod parameters 派生(getCommandOptionsMeta)
7
- * - 运行中命令标记为 disabled,描述加 `(运行中)` 前缀
8
- */
9
- import { getCommandOptionsMeta, splitCommandName } from './command.js';
10
- /** 构造命令目录:按 source 分组,namespace 命令折叠为 group */
11
- export function buildCommandCatalog(commandsWithSource, options) {
12
- const running = options?.runningCommandNames ?? new Set();
13
- const includeExit = options?.includeExit ?? true;
14
- const sourceGroups = groupBySource(commandsWithSource);
15
- const items = [];
16
- for (const entries of sourceGroups.values()) {
17
- const namespacePrefix = detectSharedNamespace(entries);
18
- if (namespacePrefix) {
19
- const children = entries.map((e) => toCatalogItem(e, running));
20
- items.push({
21
- name: namespacePrefix,
22
- description: `${children.length} 个命令`,
23
- type: 'group',
24
- children,
25
- });
26
- continue;
27
- }
28
- for (const entry of entries)
29
- items.push(toCatalogItem(entry, running));
30
- }
31
- if (includeExit)
32
- items.push({ name: 'exit', description: '退出 CLI' });
33
- return items;
34
- }
35
- // ─── Helpers ────────────────────────────────────────────────
36
- function groupBySource(commandsWithSource) {
37
- const groups = new Map();
38
- for (const entry of commandsWithSource) {
39
- const list = groups.get(entry.source) ?? [];
40
- list.push(entry);
41
- groups.set(entry.source, list);
42
- }
43
- return groups;
44
- }
45
- /** 检测一组命令是否共享同一个 namespace 前缀 */
46
- function detectSharedNamespace(entries) {
47
- if (entries.length < 2)
48
- return null;
49
- const prefixes = entries.map((e) => splitCommandName(e.command.name).namespace);
50
- const first = prefixes[0];
51
- if (!first)
52
- return null;
53
- if (prefixes.every((p) => p === first))
54
- return first;
55
- return null;
56
- }
57
- function toCatalogItem(entry, running) {
58
- const { name, simpleDescription } = entry.command;
59
- const isRunning = running.has(name);
60
- return {
61
- name,
62
- description: isRunning ? `(运行中) ${simpleDescription}` : simpleDescription,
63
- disabled: isRunning,
64
- options: getCommandOptionsMeta(entry.command.parameters),
65
- };
66
- }
67
- //# sourceMappingURL=command-catalog.js.map
package/dist/command.d.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 命令元数据 / 参数解析公用工具
3
- *
4
- * 集中三类职责:
5
- * - 从命令 Zod parameters 提取 option 元数据(供 commander、TUI 补全、shell 补全共用)
6
- * - 解析 `--key value` / `--key=value` / `--flag` 形式的命令 token 为 prefilled 对象
7
- * - 拆分 `namespace:action` 形式的命令名
8
- */
9
- export interface CommandOptionMeta {
10
- readonly name: string;
11
- readonly description: string;
12
- readonly enumValues?: readonly string[];
13
- /** 是否为开关型 option(boolean,无值) */
14
- readonly isBoolean?: boolean;
15
- }
16
- export interface SplitCommandName {
17
- readonly namespace: string | null;
18
- readonly action: string;
19
- readonly full: string;
20
- }
21
- /** 从命令的 Zod parameters 提取 option 元数据;非 ZodObject 或无字段时返回 undefined */
22
- export declare function getCommandOptionsMeta(parameters: unknown): CommandOptionMeta[] | undefined;
23
- /**
24
- * 解析 `--key value` / `--key=value` / 裸 `--flag` 形式的命令 token 列表
25
- *
26
- * - 非 `--` 前缀的 token 直接跳过
27
- * - 裸 flag 视为 `'true'`
28
- * - 提供 metas 时,boolean 字段视为开关,下一 token 不会被消费
29
- */
30
- export declare function parseCommandFlagTokens(tokens: readonly string[], metas?: readonly CommandOptionMeta[]): Record<string, string>;
31
- /** 按首个 `:` 拆分命令名;无冒号时 namespace 为 null */
32
- export declare function splitCommandName(name: string): SplitCommandName;
33
- /**
34
- * 把 `<cmd...> [--flag ...]` 形式的查询拆分为命令 tokens 与 flag tokens
35
- *
36
- * - 以首个 `--token` 为分界,前面是命令部分(可能是 1~N 个 token)
37
- * - `endsWithSpace` 用于补全场景区分「正在敲」与「敲完了」
38
- * - 纯字符串拆分,不感知具体命令注册表
39
- */
40
- export declare function splitFlagTokens(rawQuery: string): {
41
- cmdTokens: string[];
42
- flagTokens: string[];
43
- endsWithSpace: boolean;
44
- } | null;
45
- //# sourceMappingURL=command.d.ts.map
package/dist/command.js DELETED
@@ -1,88 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 命令元数据 / 参数解析公用工具
3
- *
4
- * 集中三类职责:
5
- * - 从命令 Zod parameters 提取 option 元数据(供 commander、TUI 补全、shell 补全共用)
6
- * - 解析 `--key value` / `--key=value` / `--flag` 形式的命令 token 为 prefilled 对象
7
- * - 拆分 `namespace:action` 形式的命令名
8
- */
9
- import { z } from 'zod';
10
- // ─── Public API ─────────────────────────────────────────────
11
- /** 从命令的 Zod parameters 提取 option 元数据;非 ZodObject 或无字段时返回 undefined */
12
- export function getCommandOptionsMeta(parameters) {
13
- if (!(parameters instanceof z.ZodObject))
14
- return undefined;
15
- const shape = parameters.shape;
16
- const result = [];
17
- for (const [name, field] of Object.entries(shape)) {
18
- const inner = unwrapOptional(field);
19
- result.push({
20
- name,
21
- description: field.description ?? '',
22
- enumValues: inner instanceof z.ZodEnum ? inner.options : undefined,
23
- isBoolean: inner instanceof z.ZodBoolean ? true : undefined,
24
- });
25
- }
26
- return result.length > 0 ? result : undefined;
27
- }
28
- /**
29
- * 解析 `--key value` / `--key=value` / 裸 `--flag` 形式的命令 token 列表
30
- *
31
- * - 非 `--` 前缀的 token 直接跳过
32
- * - 裸 flag 视为 `'true'`
33
- * - 提供 metas 时,boolean 字段视为开关,下一 token 不会被消费
34
- */
35
- export function parseCommandFlagTokens(tokens, metas) {
36
- const result = {};
37
- const byName = new Map();
38
- for (const m of metas ?? [])
39
- byName.set(m.name, m);
40
- for (let i = 0; i < tokens.length; i++) {
41
- const curr = tokens[i];
42
- if (!curr.startsWith('--'))
43
- continue;
44
- const eq = curr.indexOf('=');
45
- if (eq >= 0) {
46
- result[curr.slice(2, eq)] = curr.slice(eq + 1);
47
- continue;
48
- }
49
- const key = curr.slice(2);
50
- const isBoolean = byName.get(key)?.isBoolean === true;
51
- const next = tokens[i + 1];
52
- const hasValue = !isBoolean && next && !next.startsWith('--');
53
- result[key] = hasValue ? (i++, next) : 'true';
54
- }
55
- return result;
56
- }
57
- /** 按首个 `:` 拆分命令名;无冒号时 namespace 为 null */
58
- export function splitCommandName(name) {
59
- const idx = name.indexOf(':');
60
- if (idx <= 0)
61
- return { namespace: null, action: name, full: name };
62
- return { namespace: name.slice(0, idx), action: name.slice(idx + 1), full: name };
63
- }
64
- /**
65
- * 把 `<cmd...> [--flag ...]` 形式的查询拆分为命令 tokens 与 flag tokens
66
- *
67
- * - 以首个 `--token` 为分界,前面是命令部分(可能是 1~N 个 token)
68
- * - `endsWithSpace` 用于补全场景区分「正在敲」与「敲完了」
69
- * - 纯字符串拆分,不感知具体命令注册表
70
- */
71
- export function splitFlagTokens(rawQuery) {
72
- const trimmedRight = rawQuery.replace(/\s+$/, '');
73
- if (!trimmedRight)
74
- return null;
75
- const endsWithSpace = trimmedRight.length < rawQuery.length;
76
- const tokens = trimmedRight.split(/\s+/);
77
- const firstFlagIdx = tokens.findIndex((t) => t.startsWith('--'));
78
- return {
79
- cmdTokens: firstFlagIdx === -1 ? tokens : tokens.slice(0, firstFlagIdx),
80
- flagTokens: firstFlagIdx === -1 ? [] : tokens.slice(firstFlagIdx),
81
- endsWithSpace,
82
- };
83
- }
84
- // ─── Helpers ────────────────────────────────────────────────
85
- function unwrapOptional(field) {
86
- return field instanceof z.ZodOptional ? field.unwrap() : field;
87
- }
88
- //# sourceMappingURL=command.js.map
@@ -1,64 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 补全核心(与 UI / shell 协议解耦)
3
- *
4
- * 集中维护:命令目录条目类型、查询/输入解析、option/value 建议计算、
5
- * 已用 flag 过滤、命令寻址、prefilled 提取、模糊匹配。
6
- *
7
- * 调用方:
8
- * - TUI Commands 组件复用全部能力(含模糊匹配 / 二级菜单 / 已用 flag 过滤)
9
- * - shell / 其他入口可按需复用 catalog 与 option 建议部分,不依赖 UI
10
- */
11
- import type { CommandOptionMeta } from './command.js';
12
- export type CommandCatalogItemType = 'command' | 'group' | 'back' | 'option' | 'value';
13
- export interface CommandCatalogItem {
14
- readonly name: string;
15
- readonly description: string;
16
- readonly disabled?: boolean;
17
- readonly type?: CommandCatalogItemType;
18
- readonly children?: CommandCatalogItem[];
19
- /** 命令支持的 options(仅 type=command 时有意义,用于 --key= 补全) */
20
- readonly options?: readonly CommandOptionMeta[];
21
- }
22
- export interface ParsedQuery {
23
- readonly activeGroup: CommandCatalogItem | null;
24
- readonly subQuery: string;
25
- readonly topQuery: string;
26
- }
27
- export type ExtrasMode = {
28
- readonly kind: 'value';
29
- readonly command: CommandCatalogItem;
30
- readonly key: string;
31
- readonly partial: string;
32
- readonly values: readonly string[];
33
- } | {
34
- readonly kind: 'name';
35
- readonly command: CommandCatalogItem;
36
- readonly partial: string;
37
- readonly usedNames: ReadonlySet<string>;
38
- };
39
- /** 解析 query 是否进入二级菜单模式 */
40
- export declare function parseQuery(query: string, options: readonly CommandCatalogItem[]): ParsedQuery;
41
- /** 顶级查询过滤 + 评分排序 */
42
- export declare function filterTopLevel(options: readonly CommandCatalogItem[], query: string): CommandCatalogItem[];
43
- /** 二级子命令过滤:同时尝试匹配完整名和 action 名 */
44
- export declare function filterSubCommands(options: readonly CommandCatalogItem[], query: string): CommandCatalogItem[];
45
- /**
46
- * 识别 `<cmd> [--k=v ...] [--partial]` 形式的输入,返回 extras 模式
47
- *
48
- * 末尾 `--key=` 进入 value 模式;`--key` / `--` / 空 进入 name 模式
49
- */
50
- export declare function parseExtrasMode(rawQuery: string, options: readonly CommandCatalogItem[]): ExtrasMode | null;
51
- /** extras 模式建议项(option 名或枚举值) */
52
- export declare function buildExtrasSuggestions(mode: ExtrasMode): CommandCatalogItem[];
53
- /** extras 模式 Tab 补全:替换末尾未完成 token,或在末尾空格后追加新 flag */
54
- export declare function applyExtrasCompletion(input: string, mode: ExtrasMode, selected: string): string;
55
- /** 输入包含 flag 时尝试解析对应命令;不依赖命令是否声明 options,仅用于建议回显 */
56
- export declare function resolveMatchedCommandWithFlags(rawQuery: string, options: readonly CommandCatalogItem[]): CommandCatalogItem | null;
57
- /** Enter 提交时把 `/cmd --k1=v1 --k2 v2` 解析为命令 + prefilled */
58
- export declare function extractCommandAndPrefilled(input: string, options: readonly CommandCatalogItem[]): {
59
- command: CommandCatalogItem | null;
60
- prefilled: Record<string, string>;
61
- };
62
- /** 判断输入是否已经包含 `--flag` token */
63
- export declare function hasFlagToken(input: string): boolean;
64
- //# sourceMappingURL=completion-core.d.ts.map
@@ -1,201 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 补全核心(与 UI / shell 协议解耦)
3
- *
4
- * 集中维护:命令目录条目类型、查询/输入解析、option/value 建议计算、
5
- * 已用 flag 过滤、命令寻址、prefilled 提取、模糊匹配。
6
- *
7
- * 调用方:
8
- * - TUI Commands 组件复用全部能力(含模糊匹配 / 二级菜单 / 已用 flag 过滤)
9
- * - shell / 其他入口可按需复用 catalog 与 option 建议部分,不依赖 UI
10
- */
11
- import { parseCommandFlagTokens, splitCommandName, splitFlagTokens } from './command.js';
12
- // ─── Public API ─────────────────────────────────────────────
13
- /** 解析 query 是否进入二级菜单模式 */
14
- export function parseQuery(query, options) {
15
- if (!query)
16
- return { activeGroup: null, subQuery: '', topQuery: '' };
17
- const spaceIndex = query.indexOf(' ');
18
- if (spaceIndex > 0) {
19
- const groupPart = query.slice(0, spaceIndex);
20
- const subQuery = query.slice(spaceIndex + 1).trim();
21
- const group = options.find((o) => o.type === 'group' && o.name.toLowerCase() === groupPart);
22
- if (group)
23
- return { activeGroup: group, subQuery, topQuery: '' };
24
- }
25
- const exactGroup = options.find((o) => o.type === 'group' && o.name.toLowerCase() === query);
26
- if (exactGroup)
27
- return { activeGroup: exactGroup, subQuery: '', topQuery: '' };
28
- return { activeGroup: null, subQuery: '', topQuery: query };
29
- }
30
- /** 顶级查询过滤 + 评分排序 */
31
- export function filterTopLevel(options, query) {
32
- return options
33
- .filter((item) => fuzzyMatch(item.name, query))
34
- .sort((a, b) => matchScore(a.name, query) - matchScore(b.name, query));
35
- }
36
- /** 二级子命令过滤:同时尝试匹配完整名和 action 名 */
37
- export function filterSubCommands(options, query) {
38
- return options
39
- .filter((opt) => {
40
- const action = splitCommandName(opt.name).action;
41
- return fuzzyMatch(action, query) || fuzzyMatch(opt.name, query);
42
- })
43
- .sort((a, b) => {
44
- const aAction = splitCommandName(a.name).action;
45
- const bAction = splitCommandName(b.name).action;
46
- return matchScore(aAction, query) - matchScore(bAction, query);
47
- });
48
- }
49
- /**
50
- * 识别 `<cmd> [--k=v ...] [--partial]` 形式的输入,返回 extras 模式
51
- *
52
- * 末尾 `--key=` 进入 value 模式;`--key` / `--` / 空 进入 name 模式
53
- */
54
- export function parseExtrasMode(rawQuery, options) {
55
- const split = splitCommandAndFlags(rawQuery, options);
56
- if (!split || !split.command.options?.length)
57
- return null;
58
- const { command, flagTokens, endsWithSpace } = split;
59
- if (flagTokens.length === 0 && !endsWithSpace)
60
- return null;
61
- const newFlag = endsWithSpace || flagTokens.length === 0;
62
- const usedNames = collectUsedNames(newFlag ? flagTokens : flagTokens.slice(0, -1));
63
- if (newFlag)
64
- return { kind: 'name', command, partial: '', usedNames };
65
- const last = flagTokens[flagTokens.length - 1];
66
- if (!last.startsWith('--'))
67
- return null;
68
- const eqIdx = last.indexOf('=');
69
- if (eqIdx >= 0) {
70
- const rawKey = last.slice(2, eqIdx);
71
- const partial = last.slice(eqIdx + 1);
72
- const opt = command.options?.find((o) => o.name.toLowerCase() === rawKey.toLowerCase());
73
- if (!opt?.enumValues?.length)
74
- return null;
75
- return { kind: 'value', command, key: opt.name, partial, values: opt.enumValues };
76
- }
77
- return { kind: 'name', command, partial: last.slice(2), usedNames };
78
- }
79
- /** extras 模式建议项(option 名或枚举值) */
80
- export function buildExtrasSuggestions(mode) {
81
- if (mode.kind === 'value') {
82
- const partial = mode.partial.toLowerCase();
83
- return mode.values
84
- .filter((v) => !partial || v.toLowerCase().startsWith(partial))
85
- .map((v) => ({ name: v, description: '', type: 'value' }));
86
- }
87
- const partial = mode.partial.toLowerCase();
88
- return (mode.command.options ?? [])
89
- .filter((o) => !mode.usedNames.has(o.name.toLowerCase()))
90
- .filter((o) => !partial || o.name.toLowerCase().startsWith(partial))
91
- .map((o) => ({ name: o.name, description: o.description, type: 'option' }));
92
- }
93
- /** extras 模式 Tab 补全:替换末尾未完成 token,或在末尾空格后追加新 flag */
94
- export function applyExtrasCompletion(input, mode, selected) {
95
- const lastSpace = input.lastIndexOf(' ');
96
- const prefix = input.slice(0, lastSpace + 1);
97
- if (mode.kind === 'value')
98
- return `${prefix}--${mode.key}=${selected} `;
99
- const isBoolean = mode.command.options?.find((o) => o.name === selected)?.isBoolean === true;
100
- return `${prefix}${isBoolean ? `--${selected} ` : `--${selected}=`}`;
101
- }
102
- /** 输入包含 flag 时尝试解析对应命令;不依赖命令是否声明 options,仅用于建议回显 */
103
- export function resolveMatchedCommandWithFlags(rawQuery, options) {
104
- const split = splitFlagTokens(rawQuery);
105
- if (!split || split.flagTokens.length === 0)
106
- return null;
107
- if (split.cmdTokens.length === 0 || split.cmdTokens.length > 2)
108
- return null;
109
- return resolveCommand(split.cmdTokens.join(' '), options);
110
- }
111
- /** Enter 提交时把 `/cmd --k1=v1 --k2 v2` 解析为命令 + prefilled */
112
- export function extractCommandAndPrefilled(input, options) {
113
- const raw = input.startsWith('/') ? input.slice(1) : '';
114
- const split = splitCommandAndFlags(raw, options);
115
- if (!split || split.flagTokens.length === 0)
116
- return { command: null, prefilled: {} };
117
- return {
118
- command: split.command,
119
- prefilled: parseCommandFlagTokens(split.flagTokens, split.command.options),
120
- };
121
- }
122
- /** 判断输入是否已经包含 `--flag` token */
123
- export function hasFlagToken(input) {
124
- const raw = input.startsWith('/') ? input.slice(1) : input;
125
- const split = splitFlagTokens(raw);
126
- return !!split && split.flagTokens.length > 0;
127
- }
128
- // ─── Helpers ────────────────────────────────────────────────
129
- /**
130
- * 把 `<cmd...> [--flag ...]` 查询拆分为命令 + flag tokens
131
- *
132
- * 在 splitFlagTokens 基础上叠加目录解析,限制命令 tokens ≤ 2(顶级命令 或 group+action)
133
- */
134
- function splitCommandAndFlags(rawQuery, options) {
135
- const split = splitFlagTokens(rawQuery);
136
- if (!split || split.cmdTokens.length === 0 || split.cmdTokens.length > 2)
137
- return null;
138
- const command = resolveCommand(split.cmdTokens.join(' '), options);
139
- if (!command)
140
- return null;
141
- return { command, flagTokens: split.flagTokens, endsWithSpace: split.endsWithSpace };
142
- }
143
- /** 通过 `<name>` 或 `<group> <action>` 解析命令 */
144
- function resolveCommand(cmdPart, options) {
145
- const parts = cmdPart.trim().split(/\s+/);
146
- if (parts.length === 1)
147
- return findCommandByName(options, parts[0]);
148
- if (parts.length !== 2)
149
- return null;
150
- const [groupName, action] = parts;
151
- const group = options.find((o) => o.type === 'group' && o.name.toLowerCase() === groupName.toLowerCase());
152
- if (!group?.children)
153
- return null;
154
- return group.children.find((c) => splitCommandName(c.name).action.toLowerCase() === action.toLowerCase()) ?? null;
155
- }
156
- function findCommandByName(options, name) {
157
- const target = name.toLowerCase();
158
- for (const opt of options) {
159
- if (opt.type === 'group') {
160
- const found = opt.children?.find((c) => c.name.toLowerCase() === target || splitCommandName(c.name).action.toLowerCase() === target);
161
- if (found)
162
- return found;
163
- continue;
164
- }
165
- if (opt.name.toLowerCase() === target)
166
- return opt;
167
- }
168
- return null;
169
- }
170
- function collectUsedNames(flagTokens) {
171
- const used = new Set();
172
- for (const t of flagTokens) {
173
- if (!t.startsWith('--'))
174
- continue;
175
- const eq = t.indexOf('=');
176
- const name = (eq >= 0 ? t.slice(2, eq) : t.slice(2)).toLowerCase();
177
- if (name)
178
- used.add(name);
179
- }
180
- return used;
181
- }
182
- function matchScore(text, query) {
183
- const lower = text.toLowerCase();
184
- if (lower.startsWith(query))
185
- return 0;
186
- if (lower.includes(query))
187
- return 1;
188
- return 2;
189
- }
190
- function fuzzyMatch(text, query) {
191
- const lower = text.toLowerCase();
192
- if (lower.includes(query))
193
- return true;
194
- let qi = 0;
195
- for (let i = 0; i < lower.length && qi < query.length; i++) {
196
- if (lower[i] === query[qi])
197
- qi++;
198
- }
199
- return qi === query.length;
200
- }
201
- //# sourceMappingURL=completion-core.js.map
@@ -1,21 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - Shell 补全管理
3
- *
4
- * 使用 @bomb.sh/tab 实现 shell 自动补全
5
- * 通过 Commander.js 适配器集成,支持 zsh / bash / fish
6
- * install / uninstall 需手动检测 shell 类型并读写配置文件
7
- */
8
- import tab from '@bomb.sh/tab/commander';
9
- import type { Command } from 'commander';
10
- /**
11
- * 将 @bomb.sh/tab 补全能力注册到 Commander program(原地修改,包装 program.parse)
12
- *
13
- * 返回 RootCommand,可通过 `result.commands.get(name)?.options.get(key)?.handler = ...`
14
- * 注册 option 值补全。
15
- */
16
- export declare function initTabCompletion(program: Command): ReturnType<typeof tab>;
17
- /** 安装 shell 自动补全到当前 shell 配置文件 */
18
- export declare function installShellCompletion(): void;
19
- /** 卸载 shell 自动补全(从配置文件中删除相关行) */
20
- export declare function uninstallShellCompletion(): void;
21
- //# sourceMappingURL=completion.d.ts.map
@@ -1,127 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - Shell 补全管理
3
- *
4
- * 使用 @bomb.sh/tab 实现 shell 自动补全
5
- * 通过 Commander.js 适配器集成,支持 zsh / bash / fish
6
- * install / uninstall 需手动检测 shell 类型并读写配置文件
7
- */
8
- import fs from 'node:fs';
9
- import os from 'node:os';
10
- import path from 'node:path';
11
- import { execSync } from 'node:child_process';
12
- import tab from '@bomb.sh/tab/commander';
13
- /** 根据 $SHELL 环境变量检测当前 shell 类型 */
14
- function detectShell() {
15
- const shell = process.env.SHELL ?? '';
16
- if (shell.endsWith('zsh'))
17
- return 'zsh';
18
- if (shell.endsWith('bash'))
19
- return 'bash';
20
- if (shell.endsWith('fish'))
21
- return 'fish';
22
- return 'zsh';
23
- }
24
- function getShellConfig(shell) {
25
- const home = os.homedir();
26
- switch (shell) {
27
- case 'zsh':
28
- return {
29
- rcFile: path.join(home, '.zshrc'),
30
- sourceLine: 'source <(xfe complete zsh)',
31
- };
32
- case 'bash':
33
- return {
34
- rcFile: path.join(home, '.bashrc'),
35
- sourceLine: 'source <(xfe complete bash)',
36
- };
37
- case 'fish':
38
- return {
39
- rcFile: path.join(home, '.config', 'fish', 'completions', 'xfe.fish'),
40
- sourceLine: '', // fish 采用直接写入策略
41
- };
42
- }
43
- }
44
- // ============================================================
45
- // 公开 API
46
- // ============================================================
47
- /**
48
- * 将 @bomb.sh/tab 补全能力注册到 Commander program(原地修改,包装 program.parse)
49
- *
50
- * 返回 RootCommand,可通过 `result.commands.get(name)?.options.get(key)?.handler = ...`
51
- * 注册 option 值补全。
52
- */
53
- export function initTabCompletion(program) {
54
- return tab(program);
55
- }
56
- /** 安装 shell 自动补全到当前 shell 配置文件 */
57
- export function installShellCompletion() {
58
- const shell = detectShell();
59
- const config = getShellConfig(shell);
60
- if (shell === 'fish') {
61
- installFishCompletion(config.rcFile);
62
- return;
63
- }
64
- const { rcFile, sourceLine } = config;
65
- // 如果配置文件中已存在补全行则幂等跳过
66
- if (fs.existsSync(rcFile)) {
67
- const content = fs.readFileSync(rcFile, 'utf8');
68
- if (content.includes(sourceLine)) {
69
- console.log(`✅ 补全已存在于 ${rcFile},无需重复安装`);
70
- return;
71
- }
72
- }
73
- fs.appendFileSync(rcFile, `\n# xfe shell completion\n${sourceLine}\n`);
74
- console.log(`✅ 补全已安装到 ${rcFile}`);
75
- console.log(` 请运行 \`source ${rcFile}\` 或重新打开终端以生效`);
76
- }
77
- /** 卸载 shell 自动补全(从配置文件中删除相关行) */
78
- export function uninstallShellCompletion() {
79
- const shell = detectShell();
80
- const config = getShellConfig(shell);
81
- if (shell === 'fish') {
82
- uninstallFishCompletion(config.rcFile);
83
- return;
84
- }
85
- const { rcFile, sourceLine } = config;
86
- if (!fs.existsSync(rcFile)) {
87
- console.log(`⚠️ 配置文件 ${rcFile} 不存在,无需卸载`);
88
- return;
89
- }
90
- const original = fs.readFileSync(rcFile, 'utf8');
91
- const cleaned = original
92
- .split('\n')
93
- .filter((line) => line !== sourceLine && line !== '# xfe shell completion')
94
- .join('\n')
95
- .replace(/\n{3,}/g, '\n\n'); // 合并多余空行
96
- if (original === cleaned) {
97
- console.log(`⚠️ 未在 ${rcFile} 中找到 xfe 补全配置,无需卸载`);
98
- return;
99
- }
100
- fs.writeFileSync(rcFile, cleaned, 'utf8');
101
- console.log(`✅ 补全已从 ${rcFile} 中移除`);
102
- }
103
- // ============================================================
104
- // Fish 专用处理(直接写入补全文件)
105
- // ============================================================
106
- function installFishCompletion(completionFile) {
107
- const dir = path.dirname(completionFile);
108
- if (!fs.existsSync(dir))
109
- fs.mkdirSync(dir, { recursive: true });
110
- try {
111
- const script = execSync('xfe complete fish', { encoding: 'utf8' });
112
- fs.writeFileSync(completionFile, script, 'utf8');
113
- console.log(`✅ Fish 补全已安装到 ${completionFile}`);
114
- }
115
- catch {
116
- console.error('❌ 生成 fish 补全脚本失败,请确认 xfe 已正确安装');
117
- }
118
- }
119
- function uninstallFishCompletion(completionFile) {
120
- if (!fs.existsSync(completionFile)) {
121
- console.log(`⚠️ Fish 补全文件 ${completionFile} 不存在,无需卸载`);
122
- return;
123
- }
124
- fs.unlinkSync(completionFile);
125
- console.log(`✅ Fish 补全文件 ${completionFile} 已删除`);
126
- }
127
- //# sourceMappingURL=completion.js.map