@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @xfe-repo/cli
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 新增deploy自动部署支持
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @xfe-repo/cli-presets@2.1.0
13
+ - @xfe-repo/cli-core@2.1.0
14
+
3
15
  ## 2.0.13
4
16
 
5
17
  ### Patch Changes
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), 3000);
41
- ctx.sessions
42
- .killAll()
43
- .then(() => ctx.exec.killAll())
44
- .then(() => runner.triggerCleanup())
45
- .then(() => waitUntilRenderFlush())
46
- .finally(() => {
47
- clearTimeout(forceExitTimer);
48
- exit();
49
- });
50
- }, [runner, exit, waitUntilRenderFlush]);
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
- return (_jsxs(Text, { dimColor: true, children: [projectName, scriptName && _jsxs(Text, { children: [" | ", scriptName] }), statusBadges.map((badge) => {
15
- const value = storeSnapshot ? resolveBadgeProp(badge.value, storeSnapshot) : undefined;
16
- if (!value)
17
- return null;
18
- const resolvedColor = storeSnapshot ? resolveBadgeProp(badge.color, storeSnapshot) : undefined;
19
- const resolvedUrl = storeSnapshot ? resolveBadgeProp(badge.url, storeSnapshot) : undefined;
20
- const label = (_jsxs(Text, { color: resolvedColor, children: [badge.label, ": ", value] }));
21
- return (_jsxs(Text, { children: [' | ', resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: label }) : label] }, badge.name));
22
- }), elapsed !== null && _jsxs(Text, { children: [" | ", formatElapsed(stopped ? endTime - startTime : elapsed)] })] }));
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
- }, TIMER_INTERVAL_MS);
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: !scriptName,
72
+ alternateScreen: false,
73
73
  kittyKeyboard: { mode: !scriptName ? 'enabled' : 'disabled' },
74
74
  });
75
75
  await waitUntilExit();
@@ -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, {}), _jsx(Box, { flexDirection: "column", margin: 1, flexShrink: 0, children: _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u5F53\u524D\u9879\u76EE:", ' '] }), _jsx(Text, { bold: true, children: projectName }), headerBadges.map((badge) => {
72
- const value = resolveBadgeProp(badge.value, storeSnapshot);
73
- if (!value)
74
- return null;
75
- const resolvedColor = resolveBadgeProp(badge.color, storeSnapshot);
76
- const resolvedUrl = resolveBadgeProp(badge.url, storeSnapshot);
77
- const content = (_jsxs(Text, { color: resolvedColor ?? 'gray', bold: true, children: ["[", badge.label, ": ", value, "]"] }));
78
- return _jsxs(Text, { children: [" ", resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: content }) : content] }, badge.name);
79
- })] }) }), 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) => {
80
- const value = resolveBadgeProp(badge.value, storeSnapshot);
81
- if (!value)
82
- return null;
83
- const resolvedColor = resolveBadgeProp(badge.color, storeSnapshot);
84
- const resolvedIcon = resolveBadgeProp(badge.icon, storeSnapshot);
85
- const resolvedUrl = resolveBadgeProp(badge.url, storeSnapshot);
86
- const prefix = resolvedIcon ?? '●';
87
- const content = (_jsxs(Text, { color: resolvedColor ?? 'gray', children: [prefix, " ", value] }));
88
- return (_jsx(Box, { marginRight: 2, children: resolvedUrl ? _jsx(Link, { url: resolvedUrl, children: content }) : content }, badge.name));
89
- }) })), toasts.length > 0 && (_jsx(Box, { flexDirection: "column", alignItems: "flex-end", children: toasts.map((t) => (_jsx(Toast, { message: t.message, level: t.level }, t.id))) }))] })] }));
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.13",
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.8",
21
- "@xfe-repo/cli-presets": "2.0.8"
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",