@xfe-repo/cli 2.0.7 → 2.0.8

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.
@@ -0,0 +1,201 @@
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
@@ -5,9 +5,15 @@
5
5
  * 通过 Commander.js 适配器集成,支持 zsh / bash / fish
6
6
  * install / uninstall 需手动检测 shell 类型并读写配置文件
7
7
  */
8
+ import tab from '@bomb.sh/tab/commander';
8
9
  import type { Command } from 'commander';
9
- /** 将 @bomb.sh/tab 补全能力注册到 Commander program(原地修改,包装 program.parse)*/
10
- export declare function initTabCompletion(program: Command): void;
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>;
11
17
  /** 安装 shell 自动补全到当前 shell 配置文件 */
12
18
  export declare function installShellCompletion(): void;
13
19
  /** 卸载 shell 自动补全(从配置文件中删除相关行) */
@@ -44,9 +44,14 @@ function getShellConfig(shell) {
44
44
  // ============================================================
45
45
  // 公开 API
46
46
  // ============================================================
47
- /** 将 @bomb.sh/tab 补全能力注册到 Commander program(原地修改,包装 program.parse)*/
47
+ /**
48
+ * 将 @bomb.sh/tab 补全能力注册到 Commander program(原地修改,包装 program.parse)
49
+ *
50
+ * 返回 RootCommand,可通过 `result.commands.get(name)?.options.get(key)?.handler = ...`
51
+ * 注册 option 值补全。
52
+ */
48
53
  export function initTabCompletion(program) {
49
- tab(program);
54
+ return tab(program);
50
55
  }
51
56
  /** 安装 shell 自动补全到当前 shell 配置文件 */
52
57
  export function installShellCompletion() {
@@ -5,29 +5,24 @@
5
5
  * 编辑器使用自定义 Editor 支持多行编辑和完整终端快捷键
6
6
  * 通过 useKeymap 注册建议导航和 Tab 补全,避免快捷键冲突
7
7
  *
8
- * 支持二级菜单:
9
- * - 带 `:` 的命令自动折叠为 group 项(如 `aiRules ▸`)
10
- * - 输入 `/groupName` 精确匹配后展开二级命令列表
11
- * - 输入 `/groupName action` 过滤二级命令
8
+ * 补全核心逻辑(query 解析、extras 模式、命令寻址、prefilled 提取)
9
+ * 已下沉至 completion/core.ts,本文件只保留状态/快捷键/渲染
12
10
  */
13
- export interface CommandItem {
14
- readonly name: string;
15
- readonly description: string;
16
- readonly disabled?: boolean;
17
- readonly type?: 'command' | 'group' | 'back';
18
- readonly children?: CommandItem[];
19
- }
11
+ import type { CommandOptionMeta } from '../completion/command.js';
12
+ import type { CommandCatalogItem } from '../completion/core.js';
13
+ export type { CommandOptionMeta };
14
+ /** 兼容别名:原 CommandItem 现统一为 completion/core 中的 CommandCatalogItem */
15
+ export type CommandItem = CommandCatalogItem;
20
16
  interface CommandPaletteProps {
21
17
  readonly commandOptions: CommandItem[];
22
18
  readonly isActive: boolean;
23
19
  /** 建议列表最大显示行数(根据终端高度动态计算) */
24
20
  readonly maxSuggestions?: number;
25
- readonly onSelect: (name: string) => void;
21
+ readonly onSelect: (name: string, prefilled?: Record<string, string>) => void;
26
22
  readonly onUserInput: (input: string) => void;
27
23
  readonly onExit: () => void;
28
24
  /** 当 Tab 无法补全时(无建议),通知父组件切换面板 */
29
25
  readonly onTabFallthrough?: () => void;
30
26
  }
31
27
  export declare const Commands: import("react").NamedExoticComponent<CommandPaletteProps>;
32
- export {};
33
28
  //# sourceMappingURL=Commands.d.ts.map
@@ -6,19 +6,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * 编辑器使用自定义 Editor 支持多行编辑和完整终端快捷键
7
7
  * 通过 useKeymap 注册建议导航和 Tab 补全,避免快捷键冲突
8
8
  *
9
- * 支持二级菜单:
10
- * - 带 `:` 的命令自动折叠为 group 项(如 `aiRules ▸`)
11
- * - 输入 `/groupName` 精确匹配后展开二级命令列表
12
- * - 输入 `/groupName action` 过滤二级命令
9
+ * 补全核心逻辑(query 解析、extras 模式、命令寻址、prefilled 提取)
10
+ * 已下沉至 completion/core.ts,本文件只保留状态/快捷键/渲染
13
11
  */
14
12
  import { useState, useMemo, useEffect, useCallback, useRef, memo } from 'react';
15
13
  import { Box, Text } from 'ink';
14
+ import { splitCommandName } from '../completion/command.js';
15
+ import { applyExtrasCompletion, buildCompletionState, buildSuggestions, extractCommandAndPrefilled, hasFlagToken, } from '../completion/core.js';
16
16
  import { useKeymap, KeymapLayer } from '../hooks/use-keymap.js';
17
17
  import { Editor } from './Editor.js';
18
18
  import { computeWindow } from './Prompts/OptionList.js';
19
19
  const SUGGESTION_PAGE_SIZE = 10;
20
20
  const EXIT_COMMAND = 'exit';
21
- const BACK_COMMAND = '__back__';
22
21
  // ─── Component ──────────────────────────────────────────────
23
22
  export const Commands = memo(function Commands({ commandOptions, isActive, maxSuggestions, onSelect, onUserInput, onExit, onTabFallthrough, }) {
24
23
  const pageSize = maxSuggestions ? Math.min(SUGGESTION_PAGE_SIZE, maxSuggestions) : SUGGESTION_PAGE_SIZE;
@@ -26,34 +25,21 @@ export const Commands = memo(function Commands({ commandOptions, isActive, maxSu
26
25
  const [suggestIndex, setSuggestIndex] = useState(0);
27
26
  const [resetKey, setResetKey] = useState(0);
28
27
  const showSuggestions = commandInput.startsWith('/');
29
- const query = showSuggestions ? commandInput.slice(1).toLowerCase() : '';
30
- // 解析 query 用于二级菜单:`groupName subQuery` `topLevelQuery`
31
- const { activeGroup, subQuery, topQuery } = useMemo(() => parseQuery(query, commandOptions), [query, commandOptions]);
32
- const suggestions = useMemo(() => {
33
- if (!showSuggestions)
34
- return [];
35
- // 二级模式:展示 group 的子命令
36
- if (activeGroup) {
37
- const children = activeGroup.children ?? [];
38
- const backItem = { name: BACK_COMMAND, description: '返回上级', type: 'back' };
39
- if (!subQuery)
40
- return [backItem, ...children];
41
- const filtered = filterAndSort(children, subQuery);
42
- return [backItem, ...filtered];
43
- }
44
- if (!topQuery)
45
- return commandOptions;
46
- return filterTopLevel(commandOptions, topQuery);
47
- }, [showSuggestions, activeGroup, subQuery, topQuery, commandOptions]);
28
+ const rawQuery = showSuggestions ? commandInput.slice(1) : '';
29
+ const completionState = useMemo(() => buildCompletionState(rawQuery, commandOptions), [rawQuery, commandOptions]);
30
+ const { activeGroup, extrasMode } = completionState;
31
+ const suggestions = useMemo(() => (showSuggestions ? buildSuggestions(completionState, commandOptions) : []), [showSuggestions, completionState, commandOptions]);
48
32
  // 查询变化时重置选中索引
49
33
  useEffect(() => {
50
- // 二级模式下默认选中第二项(跳过「返回」)
34
+ if (extrasMode) {
35
+ setSuggestIndex(0);
36
+ return;
37
+ }
51
38
  setSuggestIndex(activeGroup && suggestions.length > 1 ? 1 : 0);
52
- }, [showSuggestions, query]);
39
+ }, [showSuggestions, rawQuery, completionState.mode]);
53
40
  // ref 供 keymap handler 使用,避免陈旧闭包
54
- const stateRef = useRef({ suggestions, suggestIndex, showSuggestions, activeGroup });
55
- stateRef.current = { suggestions, suggestIndex, showSuggestions, activeGroup };
56
- // ── 重置输入的辅助函数 ──
41
+ const stateRef = useRef({ suggestions, suggestIndex, showSuggestions, completionState, commandInput, commandOptions });
42
+ stateRef.current = { suggestions, suggestIndex, showSuggestions, completionState, commandInput, commandOptions };
57
43
  const resetInput = useCallback((value) => {
58
44
  setCommandInput(value);
59
45
  setResetKey((p) => p + 1);
@@ -63,7 +49,14 @@ export const Commands = memo(function Commands({ commandOptions, isActive, maxSu
63
49
  setCommandInput(value);
64
50
  }, []);
65
51
  const handleSubmit = useCallback((_value) => {
66
- const { suggestions: sug, suggestIndex: idx, showSuggestions: show, activeGroup: group } = stateRef.current;
52
+ const { suggestions: sug, suggestIndex: idx, showSuggestions: show, commandInput: input, commandOptions: opts } = stateRef.current;
53
+ // 包含 flag 的输入:只要能解析出命令就直接执行,携带 prefilled
54
+ const { command, prefilled } = extractCommandAndPrefilled(input, opts);
55
+ if (command) {
56
+ resetInput('');
57
+ onSelect(command.name, prefilled);
58
+ return;
59
+ }
67
60
  if (show && sug.length > 0) {
68
61
  const selected = sug[idx];
69
62
  if (!selected)
@@ -119,11 +112,19 @@ export const Commands = memo(function Commands({ commandOptions, isActive, maxSu
119
112
  useKeymap({
120
113
  key: 'tab',
121
114
  handler: () => {
122
- const { suggestions: sug, suggestIndex: idx, showSuggestions: show, activeGroup: group } = stateRef.current;
115
+ const { suggestions: sug, suggestIndex: idx, showSuggestions: show, completionState: state, commandInput: input } = stateRef.current;
116
+ const { activeGroup: group, extrasMode: extras } = state;
123
117
  if (show && sug.length > 0) {
124
118
  const selected = sug[idx];
125
119
  if (!selected)
126
120
  return;
121
+ if (extras) {
122
+ resetInput(applyExtrasCompletion(input, extras, selected.name));
123
+ return;
124
+ }
125
+ // 已带 flag 的输入回退到「展示匹配命令」状态:Tab 不应清空已输入参数
126
+ if (hasFlagToken(input))
127
+ return;
127
128
  if (selected.type === 'back') {
128
129
  resetInput('/');
129
130
  return;
@@ -133,7 +134,7 @@ export const Commands = memo(function Commands({ commandOptions, isActive, maxSu
133
134
  return;
134
135
  }
135
136
  if (group) {
136
- const action = selected.name.includes(':') ? selected.name.split(':').pop() : selected.name;
137
+ const action = selected.name.includes(':') ? splitCommandName(selected.name).action : selected.name;
137
138
  resetInput(`/${group.name} ${action}`);
138
139
  return;
139
140
  }
@@ -171,7 +172,7 @@ function SuggestionList({ suggestions, selectedIndex, pageSize }) {
171
172
  return _jsx(BackItem, { isFocused: isFocused }, "__back__");
172
173
  if (item.type === 'group')
173
174
  return _jsx(GroupItem, { item: item, isFocused: isFocused }, item.name);
174
- return _jsx(SuggestionItem, { item: item, isFocused: isFocused }, item.name);
175
+ return _jsx(SuggestionItem, { item: item, isFocused: isFocused, displayName: formatSuggestionName(item) }, item.name);
175
176
  }), startIndex + windowSize < totalItems && _jsxs(Text, { dimColor: true, children: [" \u2193 \u8FD8\u6709 ", totalItems - startIndex - windowSize, " \u9879"] })] }));
176
177
  }
177
178
  function BackItem({ isFocused }) {
@@ -180,8 +181,12 @@ function BackItem({ isFocused }) {
180
181
  function GroupItem({ item, isFocused }) {
181
182
  return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? '❯' : ' ' }), _jsxs(Text, { color: isFocused ? 'cyan' : undefined, bold: isFocused, children: [item.name, " \u25B8"] }), _jsx(Text, { dimColor: true, children: item.description })] }));
182
183
  }
183
- function SuggestionItem({ item, isFocused }) {
184
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? '❯' : ' ' }), _jsx(Text, { color: item.disabled ? undefined : isFocused ? 'cyan' : undefined, bold: isFocused && !item.disabled, dimColor: item.disabled, children: item.name }), _jsx(Text, { dimColor: true, children: item.description })] }));
184
+ function SuggestionItem({ item, isFocused, displayName }) {
185
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? '❯' : ' ' }), _jsx(Text, { color: item.disabled ? undefined : isFocused ? 'cyan' : undefined, bold: isFocused && !item.disabled, dimColor: item.disabled, children: displayName }), _jsx(Text, { dimColor: true, children: item.description })] }));
186
+ }
187
+ /** option 候选项加 `--` 前缀,与子命令区分 */
188
+ function formatSuggestionName(item) {
189
+ return item.type === 'option' ? `--${item.name}` : item.name;
185
190
  }
186
191
  function HintBar({ showSuggestions, isSubMenu }) {
187
192
  const hint = !showSuggestions
@@ -191,58 +196,4 @@ function HintBar({ showSuggestions, isSubMenu }) {
191
196
  : '↑↓ 导航 Tab 补全 Enter 执行';
192
197
  return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: hint }) }));
193
198
  }
194
- // ─── Helpers ────────────────────────────────────────────────
195
- /** 解析 query,判断是否进入二级菜单模式 */
196
- function parseQuery(query, options) {
197
- if (!query)
198
- return { activeGroup: null, subQuery: '', topQuery: '' };
199
- const spaceIndex = query.indexOf(' ');
200
- if (spaceIndex > 0) {
201
- const groupPart = query.slice(0, spaceIndex);
202
- const subQuery = query.slice(spaceIndex + 1).trim();
203
- const group = options.find((o) => o.type === 'group' && o.name.toLowerCase() === groupPart);
204
- if (group)
205
- return { activeGroup: group, subQuery, topQuery: '' };
206
- }
207
- // 精确匹配 group(无空格时也检查)
208
- const exactGroup = options.find((o) => o.type === 'group' && o.name.toLowerCase() === query);
209
- if (exactGroup)
210
- return { activeGroup: exactGroup, subQuery: '', topQuery: '' };
211
- return { activeGroup: null, subQuery: '', topQuery: query };
212
- }
213
- function filterTopLevel(options, query) {
214
- return options.filter((item) => fuzzyMatch(item.name, query)).sort((a, b) => matchScore(a.name, query) - matchScore(b.name, query));
215
- }
216
- function filterAndSort(options, query) {
217
- return options
218
- .filter((opt) => {
219
- // 匹配子命令时,同时尝试匹配完整名和去掉前缀的 action 名
220
- const action = opt.name.includes(':') ? opt.name.split(':').pop() : opt.name;
221
- return fuzzyMatch(action, query) || fuzzyMatch(opt.name, query);
222
- })
223
- .sort((a, b) => {
224
- const aAction = a.name.includes(':') ? a.name.split(':').pop() : a.name;
225
- const bAction = b.name.includes(':') ? b.name.split(':').pop() : b.name;
226
- return matchScore(aAction, query) - matchScore(bAction, query);
227
- });
228
- }
229
- function matchScore(text, query) {
230
- const lower = text.toLowerCase();
231
- if (lower.startsWith(query))
232
- return 0;
233
- if (lower.includes(query))
234
- return 1;
235
- return 2;
236
- }
237
- function fuzzyMatch(text, query) {
238
- const lower = text.toLowerCase();
239
- if (lower.includes(query))
240
- return true;
241
- let qi = 0;
242
- for (let i = 0; i < lower.length && qi < query.length; i++) {
243
- if (lower[i] === query[qi])
244
- qi++;
245
- }
246
- return qi === query.length;
247
- }
248
199
  //# sourceMappingURL=Commands.js.map
@@ -33,7 +33,7 @@ export interface SessionManager {
33
33
  readonly activeSessions: [string, RunSession][];
34
34
  readonly runningSessions: [string, RunSession][];
35
35
  readonly runningScriptNames: Set<string>;
36
- startSession(name: string): string;
36
+ startSession(name: string, prefilled?: Record<string, string>): string;
37
37
  startInputSession(input: string): string;
38
38
  suspendSession(sessionId: string): void;
39
39
  resumeSession(sessionId: string): void;
@@ -44,13 +44,14 @@ export function useSessionManager({ runner, directScriptName, onExit, onLeaveSes
44
44
  }
45
45
  }, [snapshot, directScriptName, onExit, coreManager]);
46
46
  // ── 操作方法 ──
47
- const startSession = useCallback((name) => {
47
+ const startSession = useCallback((name, prefilled) => {
48
48
  const sessionId = coreManager.createSession({
49
49
  commandName: name,
50
50
  source: 'cli',
51
51
  createLogger: () => new InkLogger(),
52
52
  createPrompt: () => new InkPrompt(),
53
53
  createSpinner: () => new InkSpinner(),
54
+ prefilled,
54
55
  });
55
56
  focusedRef.current = { id: sessionId };
56
57
  onFocusSession(sessionId);
package/dist/index.d.ts CHANGED
@@ -19,4 +19,5 @@ export { PromptView } from './views/PromptView.js';
19
19
  export { App } from './app.js';
20
20
  export { launch } from './launcher.js';
21
21
  export type { LaunchOptions } from './launcher.js';
22
+ export * from './completion';
22
23
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -20,4 +20,7 @@ export { PromptView } from './views/PromptView.js';
20
20
  // 应用
21
21
  export { App } from './app.js';
22
22
  export { launch } from './launcher.js';
23
+ // 命令工具
24
+ // 补全域
25
+ export * from './completion';
23
26
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ interface MenuViewProps {
15
15
  readonly badges: StatusBadge[];
16
16
  readonly storeSnapshot: PluginStoreSnapshot;
17
17
  readonly toast: InkToast;
18
- readonly onSelect: (commandName: string) => void;
18
+ readonly onSelect: (commandName: string, prefilled?: Record<string, string>) => void;
19
19
  readonly onFocusSession: (sessionId: string) => void;
20
20
  readonly onExit: () => void;
21
21
  readonly onUserInput: (input: string) => void;
@@ -9,6 +9,7 @@ import { useState, useMemo, useEffect, useCallback, memo } from 'react';
9
9
  import { Box, Text, useWindowSize } from 'ink';
10
10
  import Link from 'ink-link';
11
11
  import { resolveBadgeProp } from '@xfe-repo/cli-core';
12
+ import { buildCommandCatalog } from '../completion/catalog.js';
12
13
  import { useKeymap, KeymapLayer } from '../hooks/use-keymap.js';
13
14
  import { useElapsedTime } from '../hooks/use-elapsed-time.js';
14
15
  import { useToast } from '../hooks/use-adapters.js';
@@ -41,7 +42,7 @@ export const MenuView = memo(function MenuView({ projectName, commandsWithSource
41
42
  if (!hasActiveSessions)
42
43
  setFocusArea('commands');
43
44
  }, [hasActiveSessions]);
44
- const commandOptions = useMemo(() => buildCommandOptions(commandsWithSource, runningCommandNames), [commandsWithSource, runningCommandNames]);
45
+ const commandOptions = useMemo(() => buildCommandCatalog(commandsWithSource, { runningCommandNames }), [commandsWithSource, runningCommandNames]);
45
46
  const headerBadges = useMemo(() => badges.filter((b) => matchSlot(b, 'header')), [badges]);
46
47
  const footerBadges = useMemo(() => badges.filter((b) => matchSlot(b, 'footer')), [badges]);
47
48
  const toasts = useToast(toast);
@@ -91,61 +92,6 @@ export const MenuView = memo(function MenuView({ projectName, commandsWithSource
91
92
  }) })), toasts.length > 0 && (_jsx(Box, { flexDirection: "column", alignItems: "flex-end", children: toasts.map((t) => (_jsx(Toast, { message: t.message, level: t.level }, t.id))) }))] })] }));
92
93
  });
93
94
  // ─── Helpers ────────────────────────────────────────────────
94
- function buildCommandOptions(commandsWithSource, runningCommandNames) {
95
- // 按插件来源分组
96
- const sourceGroups = new Map();
97
- for (const entry of commandsWithSource) {
98
- const group = sourceGroups.get(entry.source) ?? [];
99
- group.push(entry);
100
- sourceGroups.set(entry.source, group);
101
- }
102
- const options = [];
103
- // 保持插件注册顺序(Map 插入序 = commandEntries 中首次出现的 source 顺序)
104
- for (const [source] of sourceGroups) {
105
- const entries = sourceGroups.get(source);
106
- // 检查是否所有命令都是同一 namespace:action 格式
107
- const namespacePrefix = detectNamespace(entries);
108
- if (namespacePrefix) {
109
- // 折叠为 group 项
110
- const children = entries.map((e) => toCommandItem(e, runningCommandNames));
111
- options.push({
112
- name: namespacePrefix,
113
- description: `${children.length} 个命令`,
114
- type: 'group',
115
- children,
116
- });
117
- }
118
- else {
119
- entries.forEach((entry) => options.push(toCommandItem(entry, runningCommandNames)));
120
- }
121
- }
122
- options.push({ name: 'exit', description: '退出 CLI' });
123
- return options;
124
- }
125
- /** 检测一组命令是否共享同一个 namespace 前缀 */
126
- function detectNamespace(entries) {
127
- if (entries.length < 2)
128
- return null;
129
- const prefixes = entries.map((e) => {
130
- const idx = e.command.name.indexOf(':');
131
- return idx > 0 ? e.command.name.slice(0, idx) : null;
132
- });
133
- const first = prefixes[0];
134
- if (!first)
135
- return null;
136
- if (prefixes.every((p) => p === first))
137
- return first;
138
- return null;
139
- }
140
- function toCommandItem(entry, runningCommandNames) {
141
- const { name, simpleDescription } = entry.command;
142
- const isRunning = runningCommandNames.has(name);
143
- return {
144
- name,
145
- description: isRunning ? `(运行中) ${simpleDescription}` : simpleDescription,
146
- disabled: isRunning,
147
- };
148
- }
149
95
  /** 判断 badge 是否匹配目标 slot(兼容单个或数组) */
150
96
  function matchSlot(badge, target) {
151
97
  return Array.isArray(badge.slot) ? badge.slot.includes(target) : badge.slot === target;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfe-repo/cli",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
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.3",
21
- "@xfe-repo/cli-presets": "2.0.3"
20
+ "@xfe-repo/cli-core": "2.0.4",
21
+ "@xfe-repo/cli-presets": "2.0.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^24.3.0",
@@ -1,19 +0,0 @@
1
- /**
2
- * @xfe-repo/cli - 日志面板组件
3
- *
4
- * 滚动日志面板,展示带颜色的日志条目
5
- */
6
- import type { LogEntry } from '../adapters/ink-adapter.js';
7
- interface LogPanelProps {
8
- entries: LogEntry[];
9
- /** 最大显示行数 */
10
- maxLines?: number;
11
- }
12
- /**
13
- * LogPanel - 滚动日志面板
14
- *
15
- * 显示最近的日志条目,自动滚动到底部
16
- */
17
- export declare const LogPanel: import("react").NamedExoticComponent<LogPanelProps>;
18
- export {};
19
- //# sourceMappingURL=LogPanel.d.ts.map