@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.
- package/CHANGELOG.md +9 -0
- package/dist/app.js +2 -2
- 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
package/CHANGELOG.md
CHANGED
package/dist/app.js
CHANGED
|
@@ -134,8 +134,8 @@ function AppInner({ runner, toast, scriptName }) {
|
|
|
134
134
|
layer: KeymapLayer.Global,
|
|
135
135
|
});
|
|
136
136
|
// ── 菜单事件 ──
|
|
137
|
-
const handleSelect = useCallback((name) => {
|
|
138
|
-
sessionManager.startSession(name);
|
|
137
|
+
const handleSelect = useCallback((name, prefilled) => {
|
|
138
|
+
sessionManager.startSession(name, prefilled);
|
|
139
139
|
}, [sessionManager]);
|
|
140
140
|
/** 从命令面板提交非命令文本 → 检查是否有插件处理,未注册时 toast 提示 */
|
|
141
141
|
const handleUserInput = useCallback((input) => {
|
package/dist/bin.js
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { Command } from 'commander';
|
|
10
|
-
import { z } from 'zod';
|
|
11
10
|
import { globalRegistry } from '@xfe-repo/cli-core';
|
|
12
11
|
import { registerAllPresets } from '@xfe-repo/cli-presets';
|
|
13
12
|
import { launch, quickInitRunner } from './launcher.js';
|
|
14
|
-
import { initTabCompletion, installShellCompletion, uninstallShellCompletion } from './completion.js';
|
|
13
|
+
import { applyCommandOptionMetas, attachEnumOptionValueCompletions, createDynamicCommandBinding, initTabCompletion, installShellCompletion, parsePrefilledFromArgv, uninstallShellCompletion, } from './completion/shell.js';
|
|
15
14
|
const require = createRequire(import.meta.url);
|
|
16
15
|
const { version: CLI_VERSION } = require('../package.json');
|
|
17
16
|
// ============================================================
|
|
@@ -22,17 +21,21 @@ registerAllPresets((def) => globalRegistry.register(def));
|
|
|
22
21
|
// 主流程
|
|
23
22
|
// ============================================================
|
|
24
23
|
async function main() {
|
|
25
|
-
const
|
|
24
|
+
const dynamicBindingsByScriptName = new Map();
|
|
25
|
+
const program = buildProgram(dynamicBindingsByScriptName);
|
|
26
26
|
// ── 动态注册已初始化项目的命令为子命令 ──
|
|
27
|
-
await registerDynamicCommands(program);
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const dynamicCommands = await registerDynamicCommands(program);
|
|
28
|
+
syncDynamicBindings(dynamicBindingsByScriptName, dynamicCommands);
|
|
29
|
+
// ── 自动补全(必须在动态命令注册后)──
|
|
30
|
+
const completion = initTabCompletion(program);
|
|
31
|
+
attachEnumOptionValueCompletions(completion, dynamicCommands);
|
|
30
32
|
// ── 默认行为(无子命令匹配时)→ 交互式菜单或快捷执行 ──
|
|
31
33
|
program
|
|
32
34
|
.argument('[scriptName]', '直接执行指定脚本')
|
|
33
35
|
.allowUnknownOption()
|
|
34
36
|
.action(async (scriptName) => {
|
|
35
|
-
const
|
|
37
|
+
const optionMetas = scriptName ? dynamicBindingsByScriptName.get(scriptName)?.optionMetas : undefined;
|
|
38
|
+
const prefilled = scriptName ? parsePrefilledFromArgv(process.argv, [scriptName], optionMetas) : {};
|
|
36
39
|
await launch({
|
|
37
40
|
projectRoot: process.cwd(),
|
|
38
41
|
cliRoot: getCLIRoot(),
|
|
@@ -49,7 +52,7 @@ main().catch((err) => {
|
|
|
49
52
|
// ============================================================
|
|
50
53
|
// 程序构建
|
|
51
54
|
// ============================================================
|
|
52
|
-
function buildProgram() {
|
|
55
|
+
function buildProgram(dynamicBindingsByScriptName) {
|
|
53
56
|
const program = new Command('xfe');
|
|
54
57
|
program
|
|
55
58
|
.description(`xfe 项目脚手架 v${CLI_VERSION}`)
|
|
@@ -58,7 +61,7 @@ function buildProgram() {
|
|
|
58
61
|
.helpCommand(false)
|
|
59
62
|
.showHelpAfterError(true);
|
|
60
63
|
// ── 静态子命令 ──
|
|
61
|
-
registerCommandSubcommands(program);
|
|
64
|
+
registerCommandSubcommands(program, dynamicBindingsByScriptName);
|
|
62
65
|
registerProjectCommands(program);
|
|
63
66
|
registerCompleteCommands(program);
|
|
64
67
|
return program;
|
|
@@ -66,7 +69,7 @@ function buildProgram() {
|
|
|
66
69
|
// ============================================================
|
|
67
70
|
// 子命令注册
|
|
68
71
|
// ============================================================
|
|
69
|
-
function registerCommandSubcommands(program) {
|
|
72
|
+
function registerCommandSubcommands(program, dynamicBindingsByScriptName) {
|
|
70
73
|
const cmdGroup = program.command('command').helpCommand(false).description('命令管理');
|
|
71
74
|
cmdGroup
|
|
72
75
|
.command('list')
|
|
@@ -83,7 +86,8 @@ function registerCommandSubcommands(program) {
|
|
|
83
86
|
.description('运行指定命令')
|
|
84
87
|
.allowUnknownOption()
|
|
85
88
|
.action(async (name) => {
|
|
86
|
-
const
|
|
89
|
+
const optionMetas = dynamicBindingsByScriptName.get(name)?.optionMetas;
|
|
90
|
+
const prefilled = parsePrefilledFromArgv(process.argv, ['command', 'run', name], optionMetas);
|
|
87
91
|
await launch({
|
|
88
92
|
projectRoot: process.cwd(),
|
|
89
93
|
cliRoot: getCLIRoot(),
|
|
@@ -140,97 +144,61 @@ function registerCompleteCommands(program) {
|
|
|
140
144
|
// 动态脚本命令
|
|
141
145
|
// ============================================================
|
|
142
146
|
/**
|
|
143
|
-
*
|
|
147
|
+
* 将当前项目可用命令注册为 Commander 子命令,并返回补全绑定列表
|
|
144
148
|
*
|
|
145
|
-
* - 扁平命令(无 `:`)→
|
|
146
|
-
* - 命名空间命令(有 `:`)→
|
|
149
|
+
* - 扁平命令(无 `:`)→ 顶级子命令(xfe build)
|
|
150
|
+
* - 命名空间命令(有 `:`)→ 二级子命令(xfe rules sync)
|
|
147
151
|
*/
|
|
148
152
|
async function registerDynamicCommands(program) {
|
|
153
|
+
const result = [];
|
|
154
|
+
const runner = await safeQuickInit();
|
|
155
|
+
if (!runner)
|
|
156
|
+
return result;
|
|
157
|
+
const namespaceCache = new Map();
|
|
158
|
+
for (const def of runner.getCommands()) {
|
|
159
|
+
const binding = createDynamicCommandBinding(def);
|
|
160
|
+
const parent = binding.namespace ? getOrCreateNamespaceGroup(program, binding.namespace, namespaceCache) : program;
|
|
161
|
+
const exists = parent.commands.find((c) => c.name() === binding.action);
|
|
162
|
+
if (exists)
|
|
163
|
+
continue;
|
|
164
|
+
const cmd = parent.command(binding.action).summary(def.simpleDescription).description(def.description).allowUnknownOption();
|
|
165
|
+
applyCommandOptionMetas(cmd, binding.optionMetas);
|
|
166
|
+
cmd.action(() => runDynamicScript(binding));
|
|
167
|
+
result.push(binding);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/** 安全初始化 runner:项目未配置或加载失败时返回 null,不影响静态命令 */
|
|
172
|
+
async function safeQuickInit() {
|
|
149
173
|
try {
|
|
150
|
-
|
|
151
|
-
if (!runner)
|
|
152
|
-
return;
|
|
153
|
-
const commands = runner.getCommands();
|
|
154
|
-
// 按 namespace 分组
|
|
155
|
-
const flat = [];
|
|
156
|
-
const namespaced = new Map();
|
|
157
|
-
for (const command of commands) {
|
|
158
|
-
const colonIndex = command.name.indexOf(':');
|
|
159
|
-
if (colonIndex > 0) {
|
|
160
|
-
const ns = command.name.slice(0, colonIndex);
|
|
161
|
-
const group = namespaced.get(ns) ?? [];
|
|
162
|
-
group.push(command);
|
|
163
|
-
namespaced.set(ns, group);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
flat.push(command);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const command of flat) {
|
|
170
|
-
registerSingleCommand(program, command);
|
|
171
|
-
}
|
|
172
|
-
for (const [ns, cmds] of namespaced) {
|
|
173
|
-
registerNamespaceCommands(program, ns, cmds);
|
|
174
|
-
}
|
|
174
|
+
return await quickInitRunner(process.cwd(), getCLIRoot());
|
|
175
175
|
}
|
|
176
176
|
catch {
|
|
177
|
-
|
|
177
|
+
return null;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
/**
|
|
181
|
-
function
|
|
182
|
-
|
|
180
|
+
/** 复用或新建 namespace 命令组(如 `xfe rules`) */
|
|
181
|
+
function getOrCreateNamespaceGroup(program, namespace, cache) {
|
|
182
|
+
const cached = cache.get(namespace);
|
|
183
|
+
if (cached)
|
|
184
|
+
return cached;
|
|
183
185
|
const existing = program.commands.find((cmd) => cmd.name() === namespace);
|
|
184
|
-
if (existing)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for (const command of commands) {
|
|
188
|
-
const action = command.name.slice(namespace.length + 1);
|
|
189
|
-
const sub = group.command(action).summary(command.simpleDescription).description(command.description).allowUnknownOption();
|
|
190
|
-
if (command.parameters && command.parameters instanceof z.ZodObject) {
|
|
191
|
-
const shape = command.parameters.shape;
|
|
192
|
-
for (const key of Object.keys(shape)) {
|
|
193
|
-
const description = shape[key].description ?? key;
|
|
194
|
-
sub.option(`--${key} <value>`, description);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
sub.action(async () => {
|
|
198
|
-
const nsIndex = process.argv.indexOf(namespace);
|
|
199
|
-
const prefilled = parseExtraArgs(process.argv.slice(nsIndex + 2));
|
|
200
|
-
await launch({
|
|
201
|
-
projectRoot: process.cwd(),
|
|
202
|
-
cliRoot: getCLIRoot(),
|
|
203
|
-
scriptName: command.name,
|
|
204
|
-
prefilled,
|
|
205
|
-
});
|
|
206
|
-
});
|
|
186
|
+
if (existing) {
|
|
187
|
+
cache.set(namespace, existing);
|
|
188
|
+
return existing;
|
|
207
189
|
}
|
|
190
|
+
const group = program.command(namespace).helpCommand(false).description(`${namespace} 命令组`);
|
|
191
|
+
cache.set(namespace, group);
|
|
192
|
+
return group;
|
|
208
193
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const zodObj = command.parameters;
|
|
218
|
-
const shape = zodObj.shape;
|
|
219
|
-
for (const key of Object.keys(shape)) {
|
|
220
|
-
const zodField = shape[key];
|
|
221
|
-
const description = zodField.description ?? key;
|
|
222
|
-
cmd.option(`--${key} <value>`, description);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
cmd.action(async () => {
|
|
226
|
-
const nameIndex = process.argv.indexOf(command.name);
|
|
227
|
-
const prefilled = parseExtraArgs(process.argv.slice(nameIndex + 1));
|
|
228
|
-
await launch({
|
|
229
|
-
projectRoot: process.cwd(),
|
|
230
|
-
cliRoot: getCLIRoot(),
|
|
231
|
-
scriptName: command.name,
|
|
232
|
-
prefilled,
|
|
233
|
-
});
|
|
194
|
+
/** 执行动态命令:用 fullName 段序列定位 argv 切片起点,避免 indexOf 误匹配 */
|
|
195
|
+
async function runDynamicScript(binding) {
|
|
196
|
+
const prefilled = parsePrefilledFromArgv(process.argv, binding.tabName.split(' '), binding.optionMetas);
|
|
197
|
+
await launch({
|
|
198
|
+
projectRoot: process.cwd(),
|
|
199
|
+
cliRoot: getCLIRoot(),
|
|
200
|
+
scriptName: binding.scriptName,
|
|
201
|
+
prefilled,
|
|
234
202
|
});
|
|
235
203
|
}
|
|
236
204
|
// ============================================================
|
|
@@ -240,24 +208,10 @@ function registerSingleCommand(program, command) {
|
|
|
240
208
|
function getCLIRoot() {
|
|
241
209
|
return new URL('..', import.meta.url).pathname;
|
|
242
210
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
* 输出:{ env: 'test', test: '11' }
|
|
248
|
-
*/
|
|
249
|
-
function parseExtraArgs(args) {
|
|
250
|
-
const result = {};
|
|
251
|
-
for (let i = 0; i < args.length; i++) {
|
|
252
|
-
const [curr, next] = [args[i], args[i + 1]];
|
|
253
|
-
// 只处理以 '--' 开头的参数
|
|
254
|
-
if (!curr.startsWith('--'))
|
|
255
|
-
continue;
|
|
256
|
-
const key = curr.slice(2);
|
|
257
|
-
// 下一个参数存在且不是 flag,那就是当前 key 的值
|
|
258
|
-
const hasValue = next && !next.startsWith('--');
|
|
259
|
-
result[key] = hasValue ? (i++, next) : 'true';
|
|
211
|
+
function syncDynamicBindings(target, bindings) {
|
|
212
|
+
target.clear();
|
|
213
|
+
for (const binding of bindings) {
|
|
214
|
+
target.set(binding.scriptName, binding);
|
|
260
215
|
}
|
|
261
|
-
return result;
|
|
262
216
|
}
|
|
263
217
|
//# sourceMappingURL=bin.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
|
@@ -0,0 +1,45 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - 命令目录构造(TUI / shell 共用的命令视图聚合)
|
|
3
|
+
*/
|
|
4
|
+
import type { CommandWithSource } from '@xfe-repo/cli-core';
|
|
5
|
+
import type { CommandCatalogItem } from './core.js';
|
|
6
|
+
export interface BuildCatalogOptions {
|
|
7
|
+
readonly runningCommandNames?: ReadonlySet<string>;
|
|
8
|
+
readonly includeExit?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildCommandCatalog(commandsWithSource: readonly CommandWithSource[], options?: BuildCatalogOptions): CommandCatalogItem[];
|
|
11
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xfe-repo/cli - 命令目录构造(TUI / shell 共用的命令视图聚合)
|
|
3
|
+
*/
|
|
4
|
+
import { getCommandOptionsMeta, splitCommandName } from './command.js';
|
|
5
|
+
export function buildCommandCatalog(commandsWithSource, options) {
|
|
6
|
+
const running = options?.runningCommandNames ?? new Set();
|
|
7
|
+
const includeExit = options?.includeExit ?? true;
|
|
8
|
+
const sourceGroups = groupBySource(commandsWithSource);
|
|
9
|
+
const items = [];
|
|
10
|
+
for (const entries of sourceGroups.values()) {
|
|
11
|
+
const namespacePrefix = detectSharedNamespace(entries);
|
|
12
|
+
if (namespacePrefix) {
|
|
13
|
+
const children = entries.map((entry) => toCatalogItem(entry, running));
|
|
14
|
+
items.push({
|
|
15
|
+
name: namespacePrefix,
|
|
16
|
+
description: `${children.length} 个命令`,
|
|
17
|
+
type: 'group',
|
|
18
|
+
children,
|
|
19
|
+
});
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries)
|
|
23
|
+
items.push(toCatalogItem(entry, running));
|
|
24
|
+
}
|
|
25
|
+
if (includeExit)
|
|
26
|
+
items.push({ name: 'exit', description: '退出 CLI' });
|
|
27
|
+
return items;
|
|
28
|
+
}
|
|
29
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
30
|
+
function groupBySource(commandsWithSource) {
|
|
31
|
+
const groups = new Map();
|
|
32
|
+
for (const entry of commandsWithSource) {
|
|
33
|
+
const list = groups.get(entry.source) ?? [];
|
|
34
|
+
list.push(entry);
|
|
35
|
+
groups.set(entry.source, list);
|
|
36
|
+
}
|
|
37
|
+
return groups;
|
|
38
|
+
}
|
|
39
|
+
function detectSharedNamespace(entries) {
|
|
40
|
+
if (entries.length < 2)
|
|
41
|
+
return null;
|
|
42
|
+
const prefixes = entries.map((entry) => splitCommandName(entry.command.name).namespace);
|
|
43
|
+
const first = prefixes[0];
|
|
44
|
+
if (!first)
|
|
45
|
+
return null;
|
|
46
|
+
if (prefixes.every((prefix) => prefix === first))
|
|
47
|
+
return first;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function toCatalogItem(entry, running) {
|
|
51
|
+
const { name, simpleDescription } = entry.command;
|
|
52
|
+
const isRunning = running.has(name);
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
description: isRunning ? `(运行中) ${simpleDescription}` : simpleDescription,
|
|
56
|
+
disabled: isRunning,
|
|
57
|
+
options: getCommandOptionsMeta(entry.command.parameters),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|