@xfe-repo/cli 2.0.13 → 2.1.0
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 +12 -0
- package/dist/app.js +24 -11
- package/dist/components/StatusBar.js +33 -10
- package/dist/hooks/use-elapsed-time.d.ts +1 -1
- package/dist/hooks/use-elapsed-time.js +3 -3
- package/dist/launcher.js +1 -1
- package/dist/views/MenuView.js +51 -19
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
package/dist/app.js
CHANGED
|
@@ -20,6 +20,7 @@ import { MenuView } from './views/MenuView.js';
|
|
|
20
20
|
import { RunnerView } from './views/RunnerView.js';
|
|
21
21
|
const BOTTOM_RESERVED_ROWS = 1;
|
|
22
22
|
const MIN_VIEW_ROWS = 1;
|
|
23
|
+
const FORCE_EXIT_TIMEOUT_MS = 3000;
|
|
23
24
|
// ============================================================
|
|
24
25
|
// App
|
|
25
26
|
// ============================================================
|
|
@@ -33,21 +34,31 @@ function AppInner({ runner, toast, scriptName }) {
|
|
|
33
34
|
const store = runner.getStore();
|
|
34
35
|
const storeSnapshot = useSyncExternalStore(store.subscribe, store.getState);
|
|
35
36
|
const viewRef = useRef(view);
|
|
37
|
+
const isExitingRef = useRef(false);
|
|
36
38
|
viewRef.current = view;
|
|
37
39
|
const doExit = useCallback(() => {
|
|
40
|
+
if (isExitingRef.current)
|
|
41
|
+
return;
|
|
42
|
+
isExitingRef.current = true;
|
|
43
|
+
if (!scriptName)
|
|
44
|
+
setView({ type: 'exiting' });
|
|
38
45
|
const ctx = runner.getContext();
|
|
39
46
|
// 兜底:若异步清理被未完成的 git 等操作阻塞,强制退出
|
|
40
|
-
const forceExitTimer = setTimeout(() => process.exit(0),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const forceExitTimer = setTimeout(() => process.exit(0), FORCE_EXIT_TIMEOUT_MS);
|
|
48
|
+
const cleanupAndExit = async () => {
|
|
49
|
+
try {
|
|
50
|
+
await ctx.sessions.killAll();
|
|
51
|
+
await ctx.exec.killAll();
|
|
52
|
+
await runner.triggerCleanup();
|
|
53
|
+
await waitUntilRenderFlush();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(forceExitTimer);
|
|
57
|
+
exit();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
void cleanupAndExit();
|
|
61
|
+
}, [runner, scriptName, exit, waitUntilRenderFlush]);
|
|
51
62
|
const sessionManager = useSessionManager({
|
|
52
63
|
runner,
|
|
53
64
|
directScriptName: scriptName,
|
|
@@ -189,6 +200,8 @@ function AppInner({ runner, toast, scriptName }) {
|
|
|
189
200
|
}
|
|
190
201
|
case 'exit-confirm':
|
|
191
202
|
return (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u5F53\u524D\u6709 ", sessionManager.runningSessions.length, " \u4E2A\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C"] }), _jsx(Text, { children: "\u9000\u51FA\u5C06\u7EC8\u6B62\u6240\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u786E\u8BA4\u9000\u51FA\uFF1F(\u56DE\u8F66\u76F4\u63A5\u9000\u51FA)" }), _jsx(ConfirmInput, { onConfirm: doExit, onCancel: () => setView({ type: 'menu' }) })] }));
|
|
203
|
+
case 'exiting':
|
|
204
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Spinner, { label: "\u6B63\u5728\u9000\u51FA..." }), _jsx(Text, { color: "gray", children: "\u6B63\u5728\u7EC8\u6B62\u4EFB\u52A1\u5E76\u91CA\u653E\u8D44\u6E90" })] }));
|
|
192
205
|
case 'error':
|
|
193
206
|
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["ERROR: ", view.message] }) }));
|
|
194
207
|
}
|
|
@@ -7,23 +7,46 @@ import { Text } from 'ink';
|
|
|
7
7
|
import Link from 'ink-link';
|
|
8
8
|
import { resolveBadgeProp } from '@xfe-repo/cli-core';
|
|
9
9
|
import { useElapsedTime, formatElapsed } from '../hooks/use-elapsed-time.js';
|
|
10
|
+
const BADGE_REFRESH_START_TIME = Date.now();
|
|
10
11
|
export const StatusBar = memo(function StatusBar({ projectName, scriptName, startTime, endTime, badges, storeSnapshot }) {
|
|
11
12
|
const stopped = endTime !== undefined;
|
|
12
|
-
const elapsed = useElapsedTime(startTime, stopped);
|
|
13
13
|
const statusBadges = useMemo(() => (badges ?? []).filter((b) => matchSlot(b, 'status')), [badges]);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
const elapsed = useElapsedTime(startTime, stopped);
|
|
15
|
+
return (_jsxs(Text, { dimColor: true, children: [projectName, scriptName && _jsxs(Text, { children: [" | ", scriptName] }), statusBadges.map((badge) => (_jsx(StatusBadgeItem, { badge: badge, storeSnapshot: storeSnapshot }, badge.name))), elapsed !== null && _jsxs(Text, { children: [" | ", formatElapsed(stopped ? endTime - startTime : elapsed)] })] }));
|
|
16
|
+
});
|
|
17
|
+
const StatusBadgeItem = memo(function StatusBadgeItem({ badge, storeSnapshot }) {
|
|
18
|
+
useBadgeRefresh(badge);
|
|
19
|
+
const value = storeSnapshot ? resolveBadgeProp(badge.value, storeSnapshot) : undefined;
|
|
20
|
+
if (!value)
|
|
21
|
+
return null;
|
|
22
|
+
const resolvedColor = storeSnapshot ? resolveBadgeProp(badge.color, storeSnapshot) : undefined;
|
|
23
|
+
const resolvedIcon = storeSnapshot ? resolveBadgeProp(badge.icon, storeSnapshot) : undefined;
|
|
24
|
+
const resolvedUrl = storeSnapshot ? resolveBadgeProp(badge.url, storeSnapshot) : undefined;
|
|
25
|
+
const label = (_jsxs(Text, { color: resolvedColor, children: [badge.label, ": ", value, resolvedIcon ? ` ${resolvedIcon}` : ''] }));
|
|
26
|
+
return (_jsxs(Text, { children: [' | ', resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: label }) : label] }));
|
|
23
27
|
});
|
|
24
28
|
// ─── Helpers ────────────────────────────────────────────────
|
|
25
29
|
/** 判断 badge 是否匹配目标 slot(兼容单个或数组) */
|
|
26
30
|
function matchSlot(badge, target) {
|
|
27
31
|
return Array.isArray(badge.slot) ? badge.slot.includes(target) : badge.slot === target;
|
|
28
32
|
}
|
|
33
|
+
function useBadgeRefresh(badge) {
|
|
34
|
+
const interval = resolveBadgeRefreshInterval(badge);
|
|
35
|
+
useElapsedTime(interval ? BADGE_REFRESH_START_TIME : null, false, interval);
|
|
36
|
+
}
|
|
37
|
+
function resolveBadgeRefreshInterval(badge) {
|
|
38
|
+
if (!badge.interval)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!Number.isFinite(badge.interval) || badge.interval <= 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (!hasDynamicBadgeProp(badge))
|
|
43
|
+
return undefined;
|
|
44
|
+
return badge.interval;
|
|
45
|
+
}
|
|
46
|
+
function hasDynamicBadgeProp(badge) {
|
|
47
|
+
return (typeof badge.value === 'function' ||
|
|
48
|
+
typeof badge.color === 'function' ||
|
|
49
|
+
typeof badge.icon === 'function' ||
|
|
50
|
+
typeof badge.url === 'function');
|
|
51
|
+
}
|
|
29
52
|
//# sourceMappingURL=StatusBar.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 根据 startTime 计算并实时更新已用时间
|
|
5
5
|
*/
|
|
6
6
|
/** 根据 startTime 实时计算经过的毫秒数,stopped 为 true 时停止计时并保留最终值 */
|
|
7
|
-
export declare function useElapsedTime(startTime?: number | null, stopped?: boolean): number | null;
|
|
7
|
+
export declare function useElapsedTime(startTime?: number | null, stopped?: boolean, intervalMs?: number): number | null;
|
|
8
8
|
/** 将毫秒格式化为 MM:SS */
|
|
9
9
|
export declare function formatElapsed(ms: number): string;
|
|
10
10
|
//# sourceMappingURL=use-elapsed-time.d.ts.map
|
|
@@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react';
|
|
|
7
7
|
const TIMER_INTERVAL_MS = 1000;
|
|
8
8
|
const SECONDS_PER_MINUTE = 60;
|
|
9
9
|
/** 根据 startTime 实时计算经过的毫秒数,stopped 为 true 时停止计时并保留最终值 */
|
|
10
|
-
export function useElapsedTime(startTime, stopped = false) {
|
|
10
|
+
export function useElapsedTime(startTime, stopped = false, intervalMs = TIMER_INTERVAL_MS) {
|
|
11
11
|
const [elapsed, setElapsed] = useState(null);
|
|
12
12
|
const intervalRef = useRef(null);
|
|
13
13
|
useEffect(() => {
|
|
@@ -24,9 +24,9 @@ export function useElapsedTime(startTime, stopped = false) {
|
|
|
24
24
|
return cleanup;
|
|
25
25
|
intervalRef.current = setInterval(() => {
|
|
26
26
|
setElapsed(Date.now() - startTime);
|
|
27
|
-
},
|
|
27
|
+
}, intervalMs);
|
|
28
28
|
return cleanup;
|
|
29
|
-
}, [startTime, stopped]);
|
|
29
|
+
}, [startTime, stopped, intervalMs]);
|
|
30
30
|
return elapsed;
|
|
31
31
|
}
|
|
32
32
|
/** 将毫秒格式化为 MM:SS */
|
package/dist/launcher.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function launch(options) {
|
|
|
69
69
|
exitOnCtrlC: false,
|
|
70
70
|
incrementalRendering: true,
|
|
71
71
|
concurrent: true,
|
|
72
|
-
alternateScreen:
|
|
72
|
+
alternateScreen: false,
|
|
73
73
|
kittyKeyboard: { mode: !scriptName ? 'enabled' : 'disabled' },
|
|
74
74
|
});
|
|
75
75
|
await waitUntilExit();
|
package/dist/views/MenuView.js
CHANGED
|
@@ -19,6 +19,8 @@ import { Toast } from '../components/Toast.js';
|
|
|
19
19
|
import { Commands } from '../components/Commands.js';
|
|
20
20
|
// ─── Constants ──────────────────────────────────────────────
|
|
21
21
|
const LOGO_HEIGHT = 7;
|
|
22
|
+
const BADGE_REFRESH_START_TIME = Date.now();
|
|
23
|
+
const HEADER_BADGE_SEPARATOR = '·';
|
|
22
24
|
// 非建议区域的固定行数(header + commands chrome + footer 等)
|
|
23
25
|
const BASE_CHROME_HEIGHT = 12;
|
|
24
26
|
const TASK_GRID_CHROME_HEIGHT = 6;
|
|
@@ -68,29 +70,59 @@ export const MenuView = memo(function MenuView({ availableRows, projectName, com
|
|
|
68
70
|
layer: KeymapLayer.Panel,
|
|
69
71
|
isActive: effectiveFocus === 'tasks',
|
|
70
72
|
});
|
|
71
|
-
return (_jsxs(Box, { flexDirection: "column", children: [showLogo && _jsx(Logo, {}),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showLogo && _jsx(Logo, {}), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", margin: 1, flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u5F53\u524D\u9879\u76EE:", ' '] }), _jsx(Text, { bold: true, children: projectName })] }), headerBadges.map((badge) => (_jsx(HeaderBadge, { badge: badge, storeSnapshot: storeSnapshot }, badge.name)))] }), hasActiveSessions && (_jsx(Box, { flexShrink: 0, children: _jsx(TaskGrid, { activeSessions: activeSessions, isFocused: effectiveFocus === 'tasks', onFocusSession: onFocusSession }) })), _jsx(Box, { flexShrink: 1, flexDirection: "column", overflow: "hidden", children: _jsx(Commands, { commandOptions: commandOptions, isActive: effectiveFocus === 'commands', maxSuggestions: maxSuggestions, onSelect: onSelect, onUserInput: onUserInput, onExit: onExit, onTabFallthrough: handleTabFallthrough }) }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", justifyContent: "space-between", marginLeft: 1, flexShrink: 0, children: [footerBadges.length > 0 && (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: footerBadges.map((badge) => (_jsx(FooterBadge, { badge: badge, storeSnapshot: storeSnapshot }, badge.name))) })), toasts.length > 0 && (_jsx(Box, { flexDirection: "column", alignItems: "flex-end", children: toasts.map((t) => (_jsx(Toast, { message: t.message, level: t.level }, t.id))) }))] })] }));
|
|
74
|
+
});
|
|
75
|
+
const HeaderBadge = memo(function HeaderBadge({ badge, storeSnapshot }) {
|
|
76
|
+
useBadgeRefresh(badge);
|
|
77
|
+
const value = resolveBadgeProp(badge.value, storeSnapshot);
|
|
78
|
+
if (!value)
|
|
79
|
+
return null;
|
|
80
|
+
const resolvedColor = resolveBadgeProp(badge.color, storeSnapshot);
|
|
81
|
+
const resolvedIcon = resolveBadgeProp(badge.icon, storeSnapshot);
|
|
82
|
+
const resolvedUrl = resolveBadgeProp(badge.url, storeSnapshot);
|
|
83
|
+
const badgeValue = formatHeaderBadgeValue(value, resolvedIcon);
|
|
84
|
+
const content = (_jsxs(Text, { color: resolvedColor ?? 'gray', children: [badge.label, " ", badgeValue] }));
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "row", marginLeft: 1, children: [_jsx(Text, { dimColor: true, children: HEADER_BADGE_SEPARATOR }), _jsx(Text, { children: " " }), resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: content }) : content] }));
|
|
86
|
+
});
|
|
87
|
+
const FooterBadge = memo(function FooterBadge({ badge, storeSnapshot }) {
|
|
88
|
+
useBadgeRefresh(badge);
|
|
89
|
+
const value = resolveBadgeProp(badge.value, storeSnapshot);
|
|
90
|
+
if (!value)
|
|
91
|
+
return null;
|
|
92
|
+
const resolvedColor = resolveBadgeProp(badge.color, storeSnapshot);
|
|
93
|
+
const resolvedIcon = resolveBadgeProp(badge.icon, storeSnapshot);
|
|
94
|
+
const resolvedUrl = resolveBadgeProp(badge.url, storeSnapshot);
|
|
95
|
+
const prefix = resolvedIcon ?? '●';
|
|
96
|
+
const content = (_jsxs(Text, { color: resolvedColor ?? 'gray', children: [prefix, " ", value] }));
|
|
97
|
+
return _jsx(Box, { marginRight: 2, children: resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: content }) : content });
|
|
90
98
|
});
|
|
91
99
|
// ─── Helpers ────────────────────────────────────────────────
|
|
92
100
|
/** 判断 badge 是否匹配目标 slot(兼容单个或数组) */
|
|
93
101
|
function matchSlot(badge, target) {
|
|
94
102
|
return Array.isArray(badge.slot) ? badge.slot.includes(target) : badge.slot === target;
|
|
95
103
|
}
|
|
104
|
+
function formatHeaderBadgeValue(value, icon) {
|
|
105
|
+
if (!icon)
|
|
106
|
+
return value;
|
|
107
|
+
return `${value} ${icon}`;
|
|
108
|
+
}
|
|
109
|
+
function useBadgeRefresh(badge) {
|
|
110
|
+
const interval = resolveBadgeRefreshInterval(badge);
|
|
111
|
+
useElapsedTime(interval ? BADGE_REFRESH_START_TIME : null, false, interval);
|
|
112
|
+
}
|
|
113
|
+
function resolveBadgeRefreshInterval(badge) {
|
|
114
|
+
if (!badge.interval)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (!Number.isFinite(badge.interval) || badge.interval <= 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
if (!hasDynamicBadgeProp(badge))
|
|
119
|
+
return undefined;
|
|
120
|
+
return badge.interval;
|
|
121
|
+
}
|
|
122
|
+
function hasDynamicBadgeProp(badge) {
|
|
123
|
+
return (typeof badge.value === 'function' ||
|
|
124
|
+
typeof badge.color === 'function' ||
|
|
125
|
+
typeof badge.icon === 'function' ||
|
|
126
|
+
typeof badge.url === 'function');
|
|
127
|
+
}
|
|
96
128
|
//# sourceMappingURL=MenuView.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xfe-repo/cli",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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
|
|
21
|
-
"@xfe-repo/cli-presets": "2.0
|
|
20
|
+
"@xfe-repo/cli-core": "2.1.0",
|
|
21
|
+
"@xfe-repo/cli-presets": "2.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^24.3.0",
|