@xfe-repo/cli 2.0.7 → 2.0.9
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 +18 -0
- package/dist/app.js +2 -2
- package/dist/backend/http-backend.d.ts +45 -0
- package/dist/backend/http-backend.js +88 -0
- package/dist/backend/server-task-commands.d.ts +9 -0
- package/dist/backend/server-task-commands.js +254 -0
- package/dist/bin.js +63 -109
- package/dist/command-catalog.d.ts +19 -0
- package/dist/command-catalog.js +67 -0
- package/dist/command.d.ts +45 -0
- package/dist/command.js +88 -0
- package/dist/completion/catalog.d.ts +11 -0
- package/dist/completion/catalog.js +60 -0
- package/dist/completion/command.d.ts +45 -0
- package/dist/completion/command.js +88 -0
- package/dist/completion/core.d.ts +63 -0
- package/dist/completion/core.js +237 -0
- package/dist/completion/index.d.ts +9 -0
- package/dist/completion/index.js +5 -0
- package/dist/completion/shell.d.ts +24 -0
- package/dist/completion/shell.js +160 -0
- package/dist/completion-core.d.ts +64 -0
- package/dist/completion-core.js +201 -0
- package/dist/completion.d.ts +8 -2
- package/dist/completion.js +7 -2
- package/dist/components/Commands.d.ts +8 -13
- package/dist/components/Commands.js +40 -89
- package/dist/hooks/use-session-manager.d.ts +1 -1
- package/dist/hooks/use-session-manager.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/views/MenuView.d.ts +1 -1
- package/dist/views/MenuView.js +2 -56
- package/package.json +3 -3
- package/dist/components/LogPanel.d.ts +0 -19
- package/dist/components/LogPanel.js +0 -50
- package/dist/components/TerminalLink.d.ts +0 -13
- package/dist/components/TerminalLink.js +0 -16
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - 补全核心(与 UI / shell 协议解耦)
|
|
3
|
+
*
|
|
4
|
+
* 集中维护:命令目录条目类型、查询/输入解析、option/value 建议计算、
|
|
5
|
+
* 已用 flag 过滤、命令寻址、prefilled 提取、模糊匹配。
|
|
6
|
+
*/
|
|
7
|
+
import type { CommandOptionMeta } from './command.js';
|
|
8
|
+
export type CommandCatalogItemType = 'command' | 'group' | 'back' | 'option' | 'value';
|
|
9
|
+
export interface CommandCatalogItem {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly description: string;
|
|
12
|
+
readonly disabled?: boolean;
|
|
13
|
+
readonly type?: CommandCatalogItemType;
|
|
14
|
+
readonly children?: CommandCatalogItem[];
|
|
15
|
+
readonly options?: readonly CommandOptionMeta[];
|
|
16
|
+
}
|
|
17
|
+
export interface ParsedQuery {
|
|
18
|
+
readonly activeGroup: CommandCatalogItem | null;
|
|
19
|
+
readonly subQuery: string;
|
|
20
|
+
readonly topQuery: string;
|
|
21
|
+
}
|
|
22
|
+
export type ExtrasMode = {
|
|
23
|
+
readonly kind: 'value';
|
|
24
|
+
readonly command: CommandCatalogItem;
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly partial: string;
|
|
27
|
+
readonly values: readonly string[];
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'name';
|
|
30
|
+
readonly command: CommandCatalogItem;
|
|
31
|
+
readonly partial: string;
|
|
32
|
+
readonly usedNames: ReadonlySet<string>;
|
|
33
|
+
};
|
|
34
|
+
export declare function parseQuery(query: string, options: readonly CommandCatalogItem[]): ParsedQuery;
|
|
35
|
+
export declare function filterTopLevel(options: readonly CommandCatalogItem[], query: string): CommandCatalogItem[];
|
|
36
|
+
export declare function filterSubCommands(options: readonly CommandCatalogItem[], query: string): CommandCatalogItem[];
|
|
37
|
+
export declare function parseExtrasMode(rawQuery: string, options: readonly CommandCatalogItem[]): ExtrasMode | null;
|
|
38
|
+
export declare function buildExtrasSuggestions(mode: ExtrasMode): CommandCatalogItem[];
|
|
39
|
+
export declare function applyExtrasCompletion(input: string, mode: ExtrasMode, selected: string): string;
|
|
40
|
+
export declare function resolveMatchedCommandWithFlags(rawQuery: string, options: readonly CommandCatalogItem[]): CommandCatalogItem | null;
|
|
41
|
+
export declare function extractCommandAndPrefilled(input: string, options: readonly CommandCatalogItem[]): {
|
|
42
|
+
command: CommandCatalogItem | null;
|
|
43
|
+
prefilled: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
export declare function hasFlagToken(input: string): boolean;
|
|
46
|
+
export type CompletionMode = 'extras' | 'matched' | 'group' | 'search';
|
|
47
|
+
export interface CompletionState {
|
|
48
|
+
readonly mode: CompletionMode;
|
|
49
|
+
readonly extrasMode: ExtrasMode | null;
|
|
50
|
+
readonly activeGroup: CommandCatalogItem | null;
|
|
51
|
+
readonly subQuery: string;
|
|
52
|
+
readonly topQuery: string;
|
|
53
|
+
readonly matchedCommand: CommandCatalogItem | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 从原始查询字符串派生完整补全状态
|
|
57
|
+
*
|
|
58
|
+
* 优先级:extras flag 补全 → 命令+flag 已匹配 → 分组导航 → 顶层搜索
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildCompletionState(rawQuery: string, options: readonly CommandCatalogItem[]): CompletionState;
|
|
61
|
+
/** 根据 CompletionState 计算建议列表;group 模式下自动在首项插入「返回上级」条目 */
|
|
62
|
+
export declare function buildSuggestions(state: CompletionState, options: readonly CommandCatalogItem[]): CommandCatalogItem[];
|
|
63
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - 补全核心(与 UI / shell 协议解耦)
|
|
3
|
+
*
|
|
4
|
+
* 集中维护:命令目录条目类型、查询/输入解析、option/value 建议计算、
|
|
5
|
+
* 已用 flag 过滤、命令寻址、prefilled 提取、模糊匹配。
|
|
6
|
+
*/
|
|
7
|
+
import { parseCommandFlagTokens, splitCommandName, splitFlagTokens } from './command.js';
|
|
8
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
9
|
+
export function parseQuery(query, options) {
|
|
10
|
+
if (!query)
|
|
11
|
+
return { activeGroup: null, subQuery: '', topQuery: '' };
|
|
12
|
+
const spaceIndex = query.indexOf(' ');
|
|
13
|
+
if (spaceIndex > 0) {
|
|
14
|
+
const groupPart = query.slice(0, spaceIndex);
|
|
15
|
+
const subQuery = query.slice(spaceIndex + 1).trim();
|
|
16
|
+
const group = options.find((item) => item.type === 'group' && item.name.toLowerCase() === groupPart);
|
|
17
|
+
if (group)
|
|
18
|
+
return { activeGroup: group, subQuery, topQuery: '' };
|
|
19
|
+
}
|
|
20
|
+
const exactGroup = options.find((item) => item.type === 'group' && item.name.toLowerCase() === query);
|
|
21
|
+
if (exactGroup)
|
|
22
|
+
return { activeGroup: exactGroup, subQuery: '', topQuery: '' };
|
|
23
|
+
return { activeGroup: null, subQuery: '', topQuery: query };
|
|
24
|
+
}
|
|
25
|
+
export function filterTopLevel(options, query) {
|
|
26
|
+
return options.filter((item) => fuzzyMatch(item.name, query)).sort((a, b) => matchScore(a.name, query) - matchScore(b.name, query));
|
|
27
|
+
}
|
|
28
|
+
export function filterSubCommands(options, query) {
|
|
29
|
+
return options
|
|
30
|
+
.filter((item) => {
|
|
31
|
+
const action = splitCommandName(item.name).action;
|
|
32
|
+
return fuzzyMatch(action, query) || fuzzyMatch(item.name, query);
|
|
33
|
+
})
|
|
34
|
+
.sort((a, b) => {
|
|
35
|
+
const aAction = splitCommandName(a.name).action;
|
|
36
|
+
const bAction = splitCommandName(b.name).action;
|
|
37
|
+
return matchScore(aAction, query) - matchScore(bAction, query);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function parseExtrasMode(rawQuery, options) {
|
|
41
|
+
const split = splitCommandAndFlags(rawQuery, options);
|
|
42
|
+
if (!split || !split.command.options?.length)
|
|
43
|
+
return null;
|
|
44
|
+
const { command, flagTokens, endsWithSpace } = split;
|
|
45
|
+
if (flagTokens.length === 0 && !endsWithSpace)
|
|
46
|
+
return null;
|
|
47
|
+
const newFlag = endsWithSpace || flagTokens.length === 0;
|
|
48
|
+
const usedNames = collectUsedNames(newFlag ? flagTokens : flagTokens.slice(0, -1));
|
|
49
|
+
if (newFlag)
|
|
50
|
+
return { kind: 'name', command, partial: '', usedNames };
|
|
51
|
+
const last = flagTokens[flagTokens.length - 1];
|
|
52
|
+
if (!last.startsWith('--'))
|
|
53
|
+
return null;
|
|
54
|
+
const eqIdx = last.indexOf('=');
|
|
55
|
+
if (eqIdx >= 0) {
|
|
56
|
+
const rawKey = last.slice(2, eqIdx);
|
|
57
|
+
const partial = last.slice(eqIdx + 1);
|
|
58
|
+
const option = command.options?.find((item) => item.name.toLowerCase() === rawKey.toLowerCase());
|
|
59
|
+
if (!option?.enumValues?.length)
|
|
60
|
+
return null;
|
|
61
|
+
return { kind: 'value', command, key: option.name, partial, values: option.enumValues };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'name', command, partial: last.slice(2), usedNames };
|
|
64
|
+
}
|
|
65
|
+
export function buildExtrasSuggestions(mode) {
|
|
66
|
+
if (mode.kind === 'value') {
|
|
67
|
+
const partial = mode.partial.toLowerCase();
|
|
68
|
+
return mode.values
|
|
69
|
+
.filter((value) => !partial || value.toLowerCase().startsWith(partial))
|
|
70
|
+
.map((value) => ({ name: value, description: '', type: 'value' }));
|
|
71
|
+
}
|
|
72
|
+
const partial = mode.partial.toLowerCase();
|
|
73
|
+
return (mode.command.options ?? [])
|
|
74
|
+
.filter((option) => !mode.usedNames.has(option.name.toLowerCase()))
|
|
75
|
+
.filter((option) => !partial || option.name.toLowerCase().startsWith(partial))
|
|
76
|
+
.map((option) => ({ name: option.name, description: option.description, type: 'option' }));
|
|
77
|
+
}
|
|
78
|
+
export function applyExtrasCompletion(input, mode, selected) {
|
|
79
|
+
const lastSpace = input.lastIndexOf(' ');
|
|
80
|
+
const prefix = input.slice(0, lastSpace + 1);
|
|
81
|
+
if (mode.kind === 'value')
|
|
82
|
+
return `${prefix}--${mode.key}=${selected} `;
|
|
83
|
+
const isBoolean = mode.command.options?.find((option) => option.name === selected)?.isBoolean === true;
|
|
84
|
+
return `${prefix}${isBoolean ? `--${selected} ` : `--${selected}=`}`;
|
|
85
|
+
}
|
|
86
|
+
export function resolveMatchedCommandWithFlags(rawQuery, options) {
|
|
87
|
+
const split = splitFlagTokens(rawQuery);
|
|
88
|
+
if (!split || split.flagTokens.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
if (split.cmdTokens.length === 0 || split.cmdTokens.length > 2)
|
|
91
|
+
return null;
|
|
92
|
+
return resolveCommand(split.cmdTokens.join(' '), options);
|
|
93
|
+
}
|
|
94
|
+
export function extractCommandAndPrefilled(input, options) {
|
|
95
|
+
const raw = input.startsWith('/') ? input.slice(1) : '';
|
|
96
|
+
const split = splitCommandAndFlags(raw, options);
|
|
97
|
+
if (!split || split.flagTokens.length === 0)
|
|
98
|
+
return { command: null, prefilled: {} };
|
|
99
|
+
return {
|
|
100
|
+
command: split.command,
|
|
101
|
+
prefilled: parseCommandFlagTokens(split.flagTokens, split.command.options),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function hasFlagToken(input) {
|
|
105
|
+
const raw = input.startsWith('/') ? input.slice(1) : input;
|
|
106
|
+
const split = splitFlagTokens(raw);
|
|
107
|
+
return !!split && split.flagTokens.length > 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 从原始查询字符串派生完整补全状态
|
|
111
|
+
*
|
|
112
|
+
* 优先级:extras flag 补全 → 命令+flag 已匹配 → 分组导航 → 顶层搜索
|
|
113
|
+
*/
|
|
114
|
+
export function buildCompletionState(rawQuery, options) {
|
|
115
|
+
const extrasMode = parseExtrasMode(rawQuery, options);
|
|
116
|
+
if (extrasMode) {
|
|
117
|
+
return { mode: 'extras', extrasMode, activeGroup: null, subQuery: '', topQuery: '', matchedCommand: null };
|
|
118
|
+
}
|
|
119
|
+
const matchedCommand = resolveMatchedCommandWithFlags(rawQuery, options);
|
|
120
|
+
if (matchedCommand) {
|
|
121
|
+
return { mode: 'matched', extrasMode: null, activeGroup: null, subQuery: '', topQuery: '', matchedCommand };
|
|
122
|
+
}
|
|
123
|
+
const { activeGroup, subQuery, topQuery } = parseQuery(rawQuery.toLowerCase(), options);
|
|
124
|
+
if (activeGroup) {
|
|
125
|
+
return { mode: 'group', extrasMode: null, activeGroup, subQuery, topQuery: '', matchedCommand: null };
|
|
126
|
+
}
|
|
127
|
+
return { mode: 'search', extrasMode: null, activeGroup: null, subQuery: '', topQuery, matchedCommand: null };
|
|
128
|
+
}
|
|
129
|
+
/** 根据 CompletionState 计算建议列表;group 模式下自动在首项插入「返回上级」条目 */
|
|
130
|
+
export function buildSuggestions(state, options) {
|
|
131
|
+
switch (state.mode) {
|
|
132
|
+
case 'extras':
|
|
133
|
+
return buildExtrasSuggestions(state.extrasMode);
|
|
134
|
+
case 'matched':
|
|
135
|
+
return [state.matchedCommand];
|
|
136
|
+
case 'group': {
|
|
137
|
+
const children = state.activeGroup.children ?? [];
|
|
138
|
+
const backItem = { name: '__back__', description: '返回上级', type: 'back' };
|
|
139
|
+
if (!state.subQuery)
|
|
140
|
+
return [backItem, ...children];
|
|
141
|
+
return [backItem, ...filterSubCommands(children, state.subQuery)];
|
|
142
|
+
}
|
|
143
|
+
case 'search':
|
|
144
|
+
if (!state.topQuery)
|
|
145
|
+
return options;
|
|
146
|
+
return filterTopLevel(options, state.topQuery);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
150
|
+
function splitCommandAndFlags(rawQuery, options) {
|
|
151
|
+
const split = splitFlagTokens(rawQuery);
|
|
152
|
+
if (!split || split.cmdTokens.length === 0 || split.cmdTokens.length > 2)
|
|
153
|
+
return null;
|
|
154
|
+
const command = resolveCommand(split.cmdTokens.join(' '), options);
|
|
155
|
+
if (!command)
|
|
156
|
+
return null;
|
|
157
|
+
return { command, flagTokens: split.flagTokens, endsWithSpace: split.endsWithSpace };
|
|
158
|
+
}
|
|
159
|
+
function resolveCommand(cmdPart, options) {
|
|
160
|
+
const parts = cmdPart.trim().split(/\s+/);
|
|
161
|
+
if (parts.length === 1)
|
|
162
|
+
return findCommandByName(options, parts[0]);
|
|
163
|
+
if (parts.length !== 2)
|
|
164
|
+
return null;
|
|
165
|
+
const [groupName, action] = parts;
|
|
166
|
+
const group = options.find((item) => item.type === 'group' && item.name.toLowerCase() === groupName.toLowerCase());
|
|
167
|
+
if (!group?.children)
|
|
168
|
+
return null;
|
|
169
|
+
return group.children.find((child) => splitCommandName(child.name).action.toLowerCase() === action.toLowerCase()) ?? null;
|
|
170
|
+
}
|
|
171
|
+
function findCommandByName(options, name) {
|
|
172
|
+
const target = name.toLowerCase();
|
|
173
|
+
const directMatch = findDirectCommand(options, target);
|
|
174
|
+
if (directMatch)
|
|
175
|
+
return directMatch;
|
|
176
|
+
return findUniqueActionMatch(options, target);
|
|
177
|
+
}
|
|
178
|
+
function findDirectCommand(options, target) {
|
|
179
|
+
for (const item of options) {
|
|
180
|
+
if (item.type === 'group') {
|
|
181
|
+
const fullNameMatch = item.children?.find((child) => child.name.toLowerCase() === target);
|
|
182
|
+
if (fullNameMatch)
|
|
183
|
+
return fullNameMatch;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (item.name.toLowerCase() === target)
|
|
187
|
+
return item;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function findUniqueActionMatch(options, target) {
|
|
192
|
+
let matched = null;
|
|
193
|
+
for (const item of options) {
|
|
194
|
+
if (item.type !== 'group' || !item.children)
|
|
195
|
+
continue;
|
|
196
|
+
for (const child of item.children) {
|
|
197
|
+
if (splitCommandName(child.name).action.toLowerCase() !== target)
|
|
198
|
+
continue;
|
|
199
|
+
if (matched)
|
|
200
|
+
return null;
|
|
201
|
+
matched = child;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return matched;
|
|
205
|
+
}
|
|
206
|
+
function collectUsedNames(flagTokens) {
|
|
207
|
+
const used = new Set();
|
|
208
|
+
for (const token of flagTokens) {
|
|
209
|
+
if (!token.startsWith('--'))
|
|
210
|
+
continue;
|
|
211
|
+
const eq = token.indexOf('=');
|
|
212
|
+
const name = (eq >= 0 ? token.slice(2, eq) : token.slice(2)).toLowerCase();
|
|
213
|
+
if (name)
|
|
214
|
+
used.add(name);
|
|
215
|
+
}
|
|
216
|
+
return used;
|
|
217
|
+
}
|
|
218
|
+
function matchScore(text, query) {
|
|
219
|
+
const lower = text.toLowerCase();
|
|
220
|
+
if (lower.startsWith(query))
|
|
221
|
+
return 0;
|
|
222
|
+
if (lower.includes(query))
|
|
223
|
+
return 1;
|
|
224
|
+
return 2;
|
|
225
|
+
}
|
|
226
|
+
function fuzzyMatch(text, query) {
|
|
227
|
+
const lower = text.toLowerCase();
|
|
228
|
+
if (lower.includes(query))
|
|
229
|
+
return true;
|
|
230
|
+
let queryIndex = 0;
|
|
231
|
+
for (let i = 0; i < lower.length && queryIndex < query.length; i++) {
|
|
232
|
+
if (lower[i] === query[queryIndex])
|
|
233
|
+
queryIndex++;
|
|
234
|
+
}
|
|
235
|
+
return queryIndex === query.length;
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { getCommandOptionsMeta, parseCommandFlagTokens, splitCommandName, splitFlagTokens } from './command';
|
|
2
|
+
export type { CommandOptionMeta, SplitCommandName } from './command';
|
|
3
|
+
export { buildCommandCatalog } from './catalog';
|
|
4
|
+
export type { BuildCatalogOptions } from './catalog';
|
|
5
|
+
export { applyExtrasCompletion, buildCompletionState, buildExtrasSuggestions, buildSuggestions, extractCommandAndPrefilled, filterSubCommands, filterTopLevel, hasFlagToken, parseExtrasMode, parseQuery, resolveMatchedCommandWithFlags, } from './core';
|
|
6
|
+
export type { CommandCatalogItem, CommandCatalogItemType, CompletionMode, CompletionState, ExtrasMode, ParsedQuery } from './core';
|
|
7
|
+
export { applyCommandOptionMetas, attachEnumOptionValueCompletions, createDynamicCommandBinding, initTabCompletion, installShellCompletion, parsePrefilledFromArgv, uninstallShellCompletion, } from './shell';
|
|
8
|
+
export type { DynamicCommandBinding, TabRoot } from './shell';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { getCommandOptionsMeta, parseCommandFlagTokens, splitCommandName, splitFlagTokens } from './command';
|
|
2
|
+
export { buildCommandCatalog } from './catalog';
|
|
3
|
+
export { applyExtrasCompletion, buildCompletionState, buildExtrasSuggestions, buildSuggestions, extractCommandAndPrefilled, filterSubCommands, filterTopLevel, hasFlagToken, parseExtrasMode, parseQuery, resolveMatchedCommandWithFlags, } from './core';
|
|
4
|
+
export { applyCommandOptionMetas, attachEnumOptionValueCompletions, createDynamicCommandBinding, initTabCompletion, installShellCompletion, parsePrefilledFromArgv, uninstallShellCompletion, } from './shell';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - Shell 补全 / Commander 适配
|
|
3
|
+
*/
|
|
4
|
+
import type { RootCommand } from '@bomb.sh/tab';
|
|
5
|
+
import tab from '@bomb.sh/tab/commander';
|
|
6
|
+
import type { CommandDefinition } from '@xfe-repo/cli-core';
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
import type { CommandOptionMeta } from './command.js';
|
|
9
|
+
export type TabRoot = RootCommand;
|
|
10
|
+
export interface DynamicCommandBinding {
|
|
11
|
+
readonly scriptName: string;
|
|
12
|
+
readonly namespace: string | null;
|
|
13
|
+
readonly action: string;
|
|
14
|
+
readonly tabName: string;
|
|
15
|
+
readonly optionMetas?: readonly CommandOptionMeta[];
|
|
16
|
+
}
|
|
17
|
+
export declare function initTabCompletion(program: Command): ReturnType<typeof tab>;
|
|
18
|
+
export declare function createDynamicCommandBinding(definition: Pick<CommandDefinition, 'name' | 'parameters'>): DynamicCommandBinding;
|
|
19
|
+
export declare function applyCommandOptionMetas(cmd: Command, optionMetas?: readonly CommandOptionMeta[]): void;
|
|
20
|
+
export declare function attachEnumOptionValueCompletions(root: TabRoot, bindings: readonly DynamicCommandBinding[]): void;
|
|
21
|
+
export declare function parsePrefilledFromArgv(argv: readonly string[], commandSegments: readonly string[], optionMetas?: readonly CommandOptionMeta[]): Record<string, string>;
|
|
22
|
+
export declare function installShellCompletion(): void;
|
|
23
|
+
export declare function uninstallShellCompletion(): void;
|
|
24
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - Shell 补全 / Commander 适配
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import tab from '@bomb.sh/tab/commander';
|
|
9
|
+
import { getCommandOptionsMeta, parseCommandFlagTokens, splitCommandName } from './command.js';
|
|
10
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
11
|
+
export function initTabCompletion(program) {
|
|
12
|
+
return tab(program);
|
|
13
|
+
}
|
|
14
|
+
export function createDynamicCommandBinding(definition) {
|
|
15
|
+
const { namespace, action } = splitCommandName(definition.name);
|
|
16
|
+
return {
|
|
17
|
+
scriptName: definition.name,
|
|
18
|
+
namespace,
|
|
19
|
+
action,
|
|
20
|
+
tabName: namespace ? `${namespace} ${action}` : action,
|
|
21
|
+
optionMetas: getCommandOptionsMeta(definition.parameters),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function applyCommandOptionMetas(cmd, optionMetas) {
|
|
25
|
+
if (!optionMetas)
|
|
26
|
+
return;
|
|
27
|
+
for (const meta of optionMetas) {
|
|
28
|
+
cmd.option(buildOptionFlags(meta), meta.description || meta.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function attachEnumOptionValueCompletions(root, bindings) {
|
|
32
|
+
for (const binding of bindings) {
|
|
33
|
+
const tabCommand = root.commands.get(binding.tabName);
|
|
34
|
+
if (!tabCommand || !binding.optionMetas)
|
|
35
|
+
continue;
|
|
36
|
+
for (const meta of binding.optionMetas) {
|
|
37
|
+
if (!meta.enumValues)
|
|
38
|
+
continue;
|
|
39
|
+
const option = tabCommand.options.get(meta.name);
|
|
40
|
+
if (!option)
|
|
41
|
+
continue;
|
|
42
|
+
option.handler = (complete) => {
|
|
43
|
+
for (const value of meta.enumValues)
|
|
44
|
+
complete(value, meta.description);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function parsePrefilledFromArgv(argv, commandSegments, optionMetas) {
|
|
50
|
+
const startIndex = locateSegmentsTail(argv, commandSegments);
|
|
51
|
+
return parseCommandFlagTokens(argv.slice(startIndex), optionMetas);
|
|
52
|
+
}
|
|
53
|
+
export function installShellCompletion() {
|
|
54
|
+
const shell = detectShell();
|
|
55
|
+
const config = getShellConfig(shell);
|
|
56
|
+
if (shell === 'fish') {
|
|
57
|
+
installFishCompletion(config.rcFile);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const { rcFile, sourceLine } = config;
|
|
61
|
+
if (fs.existsSync(rcFile)) {
|
|
62
|
+
const content = fs.readFileSync(rcFile, 'utf8');
|
|
63
|
+
if (content.includes(sourceLine)) {
|
|
64
|
+
console.log(`✅ 补全已存在于 ${rcFile},无需重复安装`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
fs.appendFileSync(rcFile, `\n# xfe shell completion\n${sourceLine}\n`);
|
|
69
|
+
console.log(`✅ 补全已安装到 ${rcFile}`);
|
|
70
|
+
console.log(` 请运行 \`source ${rcFile}\` 或重新打开终端以生效`);
|
|
71
|
+
}
|
|
72
|
+
export function uninstallShellCompletion() {
|
|
73
|
+
const shell = detectShell();
|
|
74
|
+
const config = getShellConfig(shell);
|
|
75
|
+
if (shell === 'fish') {
|
|
76
|
+
uninstallFishCompletion(config.rcFile);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const { rcFile, sourceLine } = config;
|
|
80
|
+
if (!fs.existsSync(rcFile)) {
|
|
81
|
+
console.log(`⚠️ 配置文件 ${rcFile} 不存在,无需卸载`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const original = fs.readFileSync(rcFile, 'utf8');
|
|
85
|
+
const cleaned = original
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((line) => line !== sourceLine && line !== '# xfe shell completion')
|
|
88
|
+
.join('\n')
|
|
89
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
90
|
+
if (original === cleaned) {
|
|
91
|
+
console.log(`⚠️ 未在 ${rcFile} 中找到 xfe 补全配置,无需卸载`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
fs.writeFileSync(rcFile, cleaned, 'utf8');
|
|
95
|
+
console.log(`✅ 补全已从 ${rcFile} 中移除`);
|
|
96
|
+
}
|
|
97
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
98
|
+
function buildOptionFlags(meta) {
|
|
99
|
+
return meta.isBoolean ? `--${meta.name}` : `--${meta.name} <value>`;
|
|
100
|
+
}
|
|
101
|
+
function detectShell() {
|
|
102
|
+
const shell = process.env.SHELL ?? '';
|
|
103
|
+
if (shell.endsWith('zsh'))
|
|
104
|
+
return 'zsh';
|
|
105
|
+
if (shell.endsWith('bash'))
|
|
106
|
+
return 'bash';
|
|
107
|
+
if (shell.endsWith('fish'))
|
|
108
|
+
return 'fish';
|
|
109
|
+
return 'zsh';
|
|
110
|
+
}
|
|
111
|
+
function getShellConfig(shell) {
|
|
112
|
+
const home = os.homedir();
|
|
113
|
+
switch (shell) {
|
|
114
|
+
case 'zsh':
|
|
115
|
+
return {
|
|
116
|
+
rcFile: path.join(home, '.zshrc'),
|
|
117
|
+
sourceLine: 'source <(xfe complete zsh)',
|
|
118
|
+
};
|
|
119
|
+
case 'bash':
|
|
120
|
+
return {
|
|
121
|
+
rcFile: path.join(home, '.bashrc'),
|
|
122
|
+
sourceLine: 'source <(xfe complete bash)',
|
|
123
|
+
};
|
|
124
|
+
case 'fish':
|
|
125
|
+
return {
|
|
126
|
+
rcFile: path.join(home, '.config', 'fish', 'completions', 'xfe.fish'),
|
|
127
|
+
sourceLine: '',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function locateSegmentsTail(argv, segments) {
|
|
132
|
+
for (let index = 0; index <= argv.length - segments.length; index++) {
|
|
133
|
+
const matched = segments.every((segment, offset) => argv[index + offset] === segment);
|
|
134
|
+
if (matched)
|
|
135
|
+
return index + segments.length;
|
|
136
|
+
}
|
|
137
|
+
return argv.length;
|
|
138
|
+
}
|
|
139
|
+
function installFishCompletion(completionFile) {
|
|
140
|
+
const dir = path.dirname(completionFile);
|
|
141
|
+
if (!fs.existsSync(dir))
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
try {
|
|
144
|
+
const script = execSync('xfe complete fish', { encoding: 'utf8' });
|
|
145
|
+
fs.writeFileSync(completionFile, script, 'utf8');
|
|
146
|
+
console.log(`✅ Fish 补全已安装到 ${completionFile}`);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
console.error('❌ 生成 fish 补全脚本失败,请确认 xfe 已正确安装');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function uninstallFishCompletion(completionFile) {
|
|
153
|
+
if (!fs.existsSync(completionFile)) {
|
|
154
|
+
console.log(`⚠️ Fish 补全文件 ${completionFile} 不存在,无需卸载`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
fs.unlinkSync(completionFile);
|
|
158
|
+
console.log(`✅ Fish 补全文件 ${completionFile} 已删除`);
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|