flower-trellis 0.6.8 → 0.6.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/README.md +24 -1
- package/enhancements/0.6/.agents/skills/trellis-route/scripts/route_state.py +2 -1
- package/enhancements/0.6/.claude/skills/trellis-route/scripts/route_state.py +2 -1
- package/enhancements/0.6/overrides/bundles/shared-hook-runtime.json +1 -1
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/common-content.py +10 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/common-selector.py +14 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/hook-content.py +7 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/hook-selector.py +6 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/patch.json +186 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/prefix-content.py +1 -0
- package/enhancements/0.6/overrides/patches/scripts/windows-streams/prefix-selector.py +1 -0
- package/enhancements/0.6/scripts/auto_loop.py +21 -10
- package/enhancements/0.6/scripts/decision_log.py +7 -5
- package/enhancements/0.6/scripts/git_evidence.py +2 -1
- package/enhancements/0.6/scripts/pre_check_state.py +1 -1
- package/enhancements/0.6/scripts/task_intent.py +4 -0
- package/enhancements/0.6/scripts/worktree_setup.py +8 -3
- package/enhancements/MANIFEST.json +9 -2
- package/package.json +4 -4
- package/src/assets/flower_session_start.py +40 -15
- package/src/assets/flower_telemetry_hook.py +18 -3
- package/src/cli.js +7 -1
- package/src/commands/self-check.js +3 -2
- package/src/commands/self-update.js +10 -6
- package/src/commands/update.js +33 -19
- package/src/lib/global-trellis-sync.js +60 -8
- package/src/lib/operation-timing.js +50 -0
- package/src/lib/self-check.js +14 -54
- package/src/lib/terminal-state.js +5 -5
- package/src/lib/trellis-runner.js +34 -10
- package/src/lib/trellis-update-entry.js +10 -0
- package/src/lib/trellis-update-fetch.js +18 -0
- package/src/lib/update-check-cache.js +47 -0
- package/src/lib/update-check.js +125 -56
|
@@ -41,13 +41,13 @@ export function installWindowsTerminalInputRecovery(options = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* 在 Windows
|
|
44
|
+
* 在 Windows 命令完成或交互完成页选择“退出”后恢复终端,并显式结束 CLI 进程。
|
|
45
45
|
*
|
|
46
46
|
* node-pty 的 Windows ConPTY worker 在子进程自然退出后仍可能持有 MessagePort / Socket,
|
|
47
|
-
* 因此不能只依赖 Node
|
|
48
|
-
*
|
|
47
|
+
* 因此不能只依赖 Node 事件循环自然清空。调用者必须确认顶层命令已结束或用户已经选择退出,
|
|
48
|
+
* 并把实际退出延后到下一轮事件循环,让 Inquirer 完成当前输出清理。
|
|
49
49
|
*
|
|
50
|
-
* @param {{platform?:string,input?:{isTTY?:boolean,isRaw?:boolean,setRawMode?:(value:boolean)=>unknown,pause?:()=>unknown},output?:{isTTY?:boolean,write:(value:string)=>unknown},schedule?:(callback:()=>void)=>unknown,exitProcess?:(code:number)=>unknown}} [options]
|
|
50
|
+
* @param {{platform?:string,input?:{isTTY?:boolean,isRaw?:boolean,setRawMode?:(value:boolean)=>unknown,pause?:()=>unknown},output?:{isTTY?:boolean,write:(value:string)=>unknown},schedule?:(callback:()=>void)=>unknown,exitProcess?:(code:number)=>unknown,exitCode?:number}} [options] 终端、调度器、退出码与退出函数测试注入
|
|
51
51
|
* @returns {boolean} 是否安排了 Windows CLI 显式退出
|
|
52
52
|
*/
|
|
53
53
|
export function scheduleWindowsTerminalExit(options = {}) {
|
|
@@ -73,6 +73,6 @@ export function scheduleWindowsTerminalExit(options = {}) {
|
|
|
73
73
|
|
|
74
74
|
const schedule = options.schedule ?? ((callback) => setTimeout(callback, 0));
|
|
75
75
|
const exitProcess = options.exitProcess ?? ((code) => process.exit(code));
|
|
76
|
-
schedule(() => exitProcess(0));
|
|
76
|
+
schedule(() => exitProcess(options.exitCode ?? 0));
|
|
77
77
|
return true;
|
|
78
78
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
@@ -72,6 +73,9 @@ const UPSTREAM_UPGRADE_NOTICE = /Your CLI \(([^)]+)\) is behind npm\b/;
|
|
|
72
73
|
*/
|
|
73
74
|
const UPSTREAM_UPGRADE_ACTION = /^Run:\s*trellis upgrade$/;
|
|
74
75
|
|
|
76
|
+
/** 受管更新由宿主说明版本查询策略,避免依赖上游终端输出格式。 */
|
|
77
|
+
const MANAGED_UPDATE_NOTICE = " · Trellis 版本由 Flower 固定,已跳过独立版本查询";
|
|
78
|
+
|
|
75
79
|
/**
|
|
76
80
|
* 正文开始后继续按行过滤的行数上限。
|
|
77
81
|
*
|
|
@@ -89,16 +93,33 @@ const UPSTREAM_NOTICE_LINE_BUDGET = 40;
|
|
|
89
93
|
* 让用户仍然知道版本检查发生过、以及为什么不需要照做。
|
|
90
94
|
*
|
|
91
95
|
* @param {string} line 未去色的原始输出行
|
|
96
|
+
* @param {{managedUpdate?:boolean}} [options] 是否使用 Flower 管理的更新入口
|
|
92
97
|
* @returns {string|null|undefined} 替换文案;null 表示丢弃该行;undefined 表示不匹配
|
|
93
98
|
*/
|
|
94
|
-
export function rewriteUpstreamUpgradeNotice(line) {
|
|
99
|
+
export function rewriteUpstreamUpgradeNotice(line, options = {}) {
|
|
95
100
|
const t = stripAnsi(line).trim();
|
|
101
|
+
if (options.managedUpdate && /^Latest on npm:\s+\(unable to fetch\)$/.test(t)) {
|
|
102
|
+
return MANAGED_UPDATE_NOTICE;
|
|
103
|
+
}
|
|
96
104
|
const notice = UPSTREAM_UPGRADE_NOTICE.exec(t);
|
|
97
105
|
if (notice) return ` · Trellis 版本由 Flower 固定(${notice[1]}),已忽略上游 npm 升级提示`;
|
|
98
106
|
if (UPSTREAM_UPGRADE_ACTION.test(t)) return null;
|
|
99
107
|
return undefined;
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 构造捆绑 Trellis 启动参数,仅显式管理的 update 经过隔离引导。
|
|
112
|
+
* @param {string[]} args 原始上游参数
|
|
113
|
+
* @param {{managedUpdate?:boolean}} [options] 内部启动选项
|
|
114
|
+
* @returns {string[]} 传给 Node 的文件与参数
|
|
115
|
+
*/
|
|
116
|
+
export function trellisLaunchArgs(args, options = {}) {
|
|
117
|
+
const bin = resolveTrellisBin();
|
|
118
|
+
return options.managedUpdate && args[0] === "update"
|
|
119
|
+
? [fileURLToPath(new URL("./trellis-update-entry.js", import.meta.url)), bin, ...args]
|
|
120
|
+
: [bin, ...args];
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
/**
|
|
103
124
|
* 用当前 node 执行 trellis bin(普通 spawn),透传子命令与参数。
|
|
104
125
|
*
|
|
@@ -106,14 +127,14 @@ export function rewriteUpstreamUpgradeNotice(line) {
|
|
|
106
127
|
*
|
|
107
128
|
* @param {string[]} args
|
|
108
129
|
* @param {string} cwd
|
|
109
|
-
* @param {object} [opts] { stripBanner }
|
|
130
|
+
* @param {object} [opts] { stripBanner, managedUpdate }
|
|
110
131
|
* @returns {Promise<number>} 退出码(信号终止返回 128)
|
|
111
132
|
*/
|
|
112
133
|
export function runTrellis(args, cwd, opts = {}) {
|
|
113
|
-
const
|
|
134
|
+
const launchArgs = trellisLaunchArgs(args, opts);
|
|
114
135
|
const strip = opts.stripBanner;
|
|
115
136
|
return new Promise((resolve, reject) => {
|
|
116
|
-
const child = spawn(process.execPath,
|
|
137
|
+
const child = spawn(process.execPath, launchArgs, {
|
|
117
138
|
cwd,
|
|
118
139
|
stdio: strip ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
119
140
|
env: strip ? { ...process.env, FORCE_COLOR: "1" } : process.env,
|
|
@@ -122,7 +143,7 @@ export function runTrellis(args, cwd, opts = {}) {
|
|
|
122
143
|
const banner = trellisBannerLines();
|
|
123
144
|
const rl = createInterface({ input: child.stdout });
|
|
124
145
|
rl.on("line", (line) => {
|
|
125
|
-
const notice = rewriteUpstreamUpgradeNotice(line);
|
|
146
|
+
const notice = rewriteUpstreamUpgradeNotice(line, { managedUpdate: opts.managedUpdate && args[0] === "update" });
|
|
126
147
|
if (notice !== undefined) {
|
|
127
148
|
if (notice !== null) process.stdout.write(notice + "\n");
|
|
128
149
|
return;
|
|
@@ -156,11 +177,12 @@ export function runTrellis(args, cwd, opts = {}) {
|
|
|
156
177
|
*
|
|
157
178
|
* @param {string[]} args
|
|
158
179
|
* @param {string} cwd
|
|
159
|
-
* @param {object} [opts] { stripBanner, ptySpawn, stdin, stdout, platform }
|
|
180
|
+
* @param {object} [opts] { stripBanner, managedUpdate, ptySpawn, stdin, stdout, platform }
|
|
160
181
|
* @returns {Promise<number>} 退出码(信号终止返回 128)
|
|
161
182
|
*/
|
|
162
183
|
export function runTrellisPty(args, cwd, opts = {}) {
|
|
163
|
-
const
|
|
184
|
+
const launchArgs = trellisLaunchArgs(args, opts);
|
|
185
|
+
const managedUpdate = opts.managedUpdate && args[0] === "update";
|
|
164
186
|
const stdin = opts.stdin || process.stdin;
|
|
165
187
|
const stdout = opts.stdout || process.stdout;
|
|
166
188
|
const platform = opts.platform ?? process.platform;
|
|
@@ -168,7 +190,7 @@ export function runTrellisPty(args, cwd, opts = {}) {
|
|
|
168
190
|
return new Promise((resolve, reject) => {
|
|
169
191
|
let child;
|
|
170
192
|
try {
|
|
171
|
-
child = ptySpawn(process.execPath,
|
|
193
|
+
child = ptySpawn(process.execPath, launchArgs, {
|
|
172
194
|
name: "xterm-256color",
|
|
173
195
|
cols: stdout.columns || 80,
|
|
174
196
|
rows: stdout.rows || 30,
|
|
@@ -181,6 +203,8 @@ export function runTrellisPty(args, cwd, opts = {}) {
|
|
|
181
203
|
return;
|
|
182
204
|
}
|
|
183
205
|
|
|
206
|
+
// ConPTY 可能从首帧就进入原样透传;在转发前说明策略,不改写交互控制序列。
|
|
207
|
+
if (managedUpdate) stdout.write(MANAGED_UPDATE_NOTICE + "\r\n");
|
|
184
208
|
const bannerSet = trellisBannerLines();
|
|
185
209
|
let phase = opts.stripBanner ? "header" : "raw";
|
|
186
210
|
let noticeBudget = UPSTREAM_NOTICE_LINE_BUDGET;
|
|
@@ -211,9 +235,9 @@ export function runTrellisPty(args, cwd, opts = {}) {
|
|
|
211
235
|
stdout.write(line + "\r\n");
|
|
212
236
|
continue;
|
|
213
237
|
}
|
|
214
|
-
const notice = rewriteUpstreamUpgradeNotice(line);
|
|
238
|
+
const notice = rewriteUpstreamUpgradeNotice(line, { managedUpdate });
|
|
215
239
|
if (notice !== undefined) {
|
|
216
|
-
if (notice !== null) stdout.write(notice + "\r\n");
|
|
240
|
+
if (notice !== null && !(managedUpdate && notice === MANAGED_UPDATE_NOTICE)) stdout.write(notice + "\r\n");
|
|
217
241
|
continue;
|
|
218
242
|
}
|
|
219
243
|
if (phase === "header") {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Flower 管理的 update 子进程入口;只影响本进程,不改安装目录或全局环境。 */
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { withoutTrellisUpdateNotice } from "./trellis-update-fetch.js";
|
|
4
|
+
|
|
5
|
+
const [bin, ...args] = process.argv.slice(2);
|
|
6
|
+
if (!bin || args[0] !== "update") throw new Error("Flower 上游更新入口只接受 update");
|
|
7
|
+
// Commander 需要原始 CLI argv,不能使用 eval 入口或让额外的引导文件成为子命令。
|
|
8
|
+
process.argv = [process.execPath, bin, ...args];
|
|
9
|
+
globalThis.fetch = withoutTrellisUpdateNotice(globalThis.fetch);
|
|
10
|
+
await import(pathToFileURL(bin).href);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为 Flower 管理的更新隔离上游独立版本提示,保留所有实际资源请求。
|
|
3
|
+
*
|
|
4
|
+
* 上游 0.6.14 的 getLatestNpmVersion 只把该响应用于提示,且已有失败降级。
|
|
5
|
+
* 返回失败而非虚构版本,避免让远程事实与捆绑版本混淆。
|
|
6
|
+
* @param {Function} originalFetch 原 fetch
|
|
7
|
+
* @returns {Function} 只跳过精确 GET 提示请求的 fetch
|
|
8
|
+
*/
|
|
9
|
+
export function withoutTrellisUpdateNotice(originalFetch) {
|
|
10
|
+
return function fetchWithoutNotice(input, options) {
|
|
11
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input?.url;
|
|
12
|
+
const method = String(options?.method || input?.method || "GET").toUpperCase();
|
|
13
|
+
if (method === "GET" && url === "https://registry.npmjs.org/@mindfoldhq/trellis/latest") {
|
|
14
|
+
return Promise.reject(new Error("Trellis 版本由 Flower 管理,跳过独立版本查询"));
|
|
15
|
+
}
|
|
16
|
+
return originalFetch(input, options);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断远程探测缓存是否仍在配置的 interval 内。
|
|
3
|
+
* @param {object} updateCheck 归一化缓存
|
|
4
|
+
* @param {Date} [now] 当前时间
|
|
5
|
+
* @returns {boolean} 是否可复用远端版本证据
|
|
6
|
+
*/
|
|
7
|
+
export function isRemoteCacheFresh(updateCheck, now = new Date()) {
|
|
8
|
+
if (updateCheck.lastStatus === "offline" || updateCheck.lastErrorCode) return false;
|
|
9
|
+
if (!updateCheck.lastRemote) return false;
|
|
10
|
+
if (!updateCheck.lastCheckedAt) return false;
|
|
11
|
+
const checkedAt = new Date(updateCheck.lastCheckedAt).getTime();
|
|
12
|
+
if (!Number.isFinite(checkedAt)) return false;
|
|
13
|
+
return now.getTime() - checkedAt < updateCheck.intervalHours * 60 * 60 * 1000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判断缓存的 release notes 是否匹配本次检查范围。
|
|
18
|
+
*
|
|
19
|
+
* @param {object} updateCheck 归一化 updateCheck 配置
|
|
20
|
+
* @param {{from:string|null,to:string|null,channel:string,reason:string}|null} range 本次期望范围
|
|
21
|
+
* @returns {object|null} 可复用的缓存摘要
|
|
22
|
+
*/
|
|
23
|
+
export function cachedReleaseNotes(updateCheck, range) {
|
|
24
|
+
const cached = updateCheck.lastReleaseNotes;
|
|
25
|
+
if (!cached || !range || cached.unavailable) return null;
|
|
26
|
+
const cachedRange = cached.range || {};
|
|
27
|
+
if (
|
|
28
|
+
cachedRange.from !== range.from ||
|
|
29
|
+
cachedRange.to !== range.to ||
|
|
30
|
+
cachedRange.channel !== range.channel
|
|
31
|
+
) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(cached.versions) || !cached.versions.length) return null;
|
|
35
|
+
// 同一版本范围的内容相同;reason 只是触发路径,不能让项目追平场景丢失摘要。
|
|
36
|
+
return {
|
|
37
|
+
...cached,
|
|
38
|
+
range: {
|
|
39
|
+
...cachedRange,
|
|
40
|
+
from: range.from,
|
|
41
|
+
to: range.to,
|
|
42
|
+
channel: range.channel,
|
|
43
|
+
reason: range.reason,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
package/src/lib/update-check.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { beginOperationTiming, timeOperation } from "./operation-timing.js";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { isRemoteCacheFresh, cachedReleaseNotes } from "./update-check-cache.js";
|
|
1
4
|
import { spawnSync } from "node:child_process";
|
|
2
5
|
import { confirm } from "@inquirer/prompts";
|
|
3
6
|
import chalk from "chalk";
|
|
@@ -84,52 +87,92 @@ function parseReleaseNotesByVersion(json) {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* @returns {Promise<{tags:{latest:string|null,beta:string|null},releaseNotesByVersion:Record<string,{version:string,body:string,truncated:boolean,source:string}>}|null>} 更新 metadata
|
|
90
|
+
* 在同一截止时间内读取 registry JSON,预算覆盖响应头及完整响应体。
|
|
91
|
+
* @param {string} endpoint registry 相对路径
|
|
92
|
+
* @param {{deadline?:number,timeoutMs?:number,fetchImpl?:Function,abortController?:AbortController}} [options] 请求预算与测试替身
|
|
93
|
+
* @returns {Promise<object|null>} JSON 或降级空值
|
|
93
94
|
*/
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
95
|
+
async function fetchRegistryJson(endpoint, options = {}) {
|
|
96
|
+
const remaining = (options.deadline ?? performance.now() + (options.timeoutMs ?? TIMEOUT_MS)) - performance.now();
|
|
97
|
+
const ac = options.abortController ?? new AbortController();
|
|
98
|
+
if (remaining <= 0 || ac.signal.aborted) return null;
|
|
99
|
+
// 共享取消状态持久记录超时,避免计时器与单调时钟边界不同步时再次发请求。
|
|
100
|
+
const timer = setTimeout(() => ac.abort(), Math.ceil(remaining));
|
|
97
101
|
try {
|
|
98
|
-
const res = await fetch(`${REGISTRY}/${
|
|
102
|
+
const res = await (options.fetchImpl || fetch)(`${REGISTRY}/${endpoint}`, {
|
|
99
103
|
signal: ac.signal,
|
|
100
104
|
headers: { Accept: "application/json" },
|
|
101
105
|
});
|
|
102
|
-
if (!res.ok)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
tags,
|
|
108
|
-
releaseNotesByVersion: parseReleaseNotesByVersion(json),
|
|
109
|
-
};
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
await res.body?.cancel();
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return await res.json();
|
|
110
111
|
} catch {
|
|
111
|
-
return null;
|
|
112
|
+
return null;
|
|
112
113
|
} finally {
|
|
113
114
|
clearTimeout(timer);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
* 读取完整版本 metadata;仅在需要发布说明时调用。
|
|
120
|
+
* @param {{deadline?:number,timeoutMs?:number,fetchImpl?:Function,abortController?:AbortController}} [options] 共享网络预算
|
|
121
|
+
* @returns {Promise<{tags:object,releaseNotesByVersion:object}|null>} 版本与发布说明
|
|
122
|
+
*/
|
|
123
|
+
export async function fetchPackageUpdateMetadata(options = {}) {
|
|
124
|
+
const json = await fetchRegistryJson(PKG, options);
|
|
125
|
+
const tags = parseDistTags(json);
|
|
126
|
+
return tags ? { tags, releaseNotesByVersion: parseReleaseNotesByVersion(json) } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 仅查询 npm dist-tags,避免无更新时下载全部历史版本。
|
|
131
|
+
* @param {{deadline?:number,timeoutMs?:number,fetchImpl?:Function,abortController?:AbortController}} [options] 共享网络预算
|
|
132
|
+
* @returns {Promise<{latest:string|null,beta:string|null}|null>} 版本标签或降级空值
|
|
123
133
|
*/
|
|
124
|
-
export async function fetchPackageDistTags() {
|
|
125
|
-
const
|
|
126
|
-
return
|
|
134
|
+
export async function fetchPackageDistTags(options = {}) {
|
|
135
|
+
const json = await fetchRegistryJson(`-/package/${PKG}/dist-tags`, options);
|
|
136
|
+
return parseDistTags({ "dist-tags": json });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 为一次检查创建按需读取器;标签与摘要共享预算,完整 metadata 至多读取一次。
|
|
141
|
+
* @param {{fetchMetadata?:Function,fetchTags?:Function,timeoutMs?:number,fetchImpl?:Function}} [options] 请求替身与预算
|
|
142
|
+
* @returns {{readTags:Function,readMetadata:Function}} 延迟执行的读取接口
|
|
143
|
+
*/
|
|
144
|
+
export function createUpdateMetadataReader(options = {}) {
|
|
145
|
+
let deadline;
|
|
146
|
+
let metadata;
|
|
147
|
+
const abortController = new AbortController();
|
|
148
|
+
const requestOptions = () => {
|
|
149
|
+
deadline ??= performance.now() + (options.timeoutMs ?? TIMEOUT_MS);
|
|
150
|
+
return { deadline, fetchImpl: options.fetchImpl, abortController };
|
|
151
|
+
};
|
|
152
|
+
const readMetadata = () => {
|
|
153
|
+
metadata ??= timeOperation("版本检查", "读取发布说明", () => Promise.resolve().then(() => (
|
|
154
|
+
(options.fetchMetadata || fetchPackageUpdateMetadata)(requestOptions())
|
|
155
|
+
)).catch(() => null));
|
|
156
|
+
return metadata;
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
readMetadata,
|
|
160
|
+
async readTags() {
|
|
161
|
+
// 保留已有完整 metadata 注入点;生产默认始终使用独立的轻量 endpoint。
|
|
162
|
+
if (options.fetchMetadata && !options.fetchTags) return (await readMetadata())?.tags ?? null;
|
|
163
|
+
try {
|
|
164
|
+
return await timeOperation("版本检查", "读取版本标签", () => (options.fetchTags || fetchPackageDistTags)(requestOptions()));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
/**
|
|
130
173
|
* 取 npm 上 flower-trellis 的 latest 版本号。
|
|
131
174
|
*
|
|
132
|
-
*
|
|
175
|
+
* 保留这个导出是为了兼容已有调用方;新逻辑使用按需读取器共享标签与摘要的网络预算。
|
|
133
176
|
* @returns {Promise<string|null>} latest 版本号,或失败时 null
|
|
134
177
|
*/
|
|
135
178
|
export async function fetchLatestVersion() {
|
|
@@ -422,7 +465,6 @@ function rememberRemoteTags(target, tags, status, releaseNotes = null) {
|
|
|
422
465
|
lastRemote: tags,
|
|
423
466
|
lastStatus: status,
|
|
424
467
|
lastErrorCode: null,
|
|
425
|
-
lastReleaseNotes: null,
|
|
426
468
|
};
|
|
427
469
|
if (releaseNotes && !releaseNotes.unavailable) {
|
|
428
470
|
patch.lastReleaseNotes = releaseNotes;
|
|
@@ -450,10 +492,30 @@ function rememberRemoteTags(target, tags, status, releaseNotes = null) {
|
|
|
450
492
|
*
|
|
451
493
|
* @param {object} ctx cli-args.js parseCliArgs() 产出的上下文(用到 updateCheck / passthrough)
|
|
452
494
|
* @param {string} commandLabel 当前命令名,用于「请重新运行 ft <command>」文案(如 "init"/"update")
|
|
453
|
-
* @param {{fetchMetadata?:
|
|
495
|
+
* @param {{fetchMetadata?:Function,fetchTags?:Function,fetchImpl?:Function,timeoutMs?:number,report?:Function,confirm?:Function,install?:Function}} [options] 测试替换项
|
|
454
496
|
* @returns {Promise<void>} 注意:用户确认升级且成功时本函数会直接退出进程,不返回
|
|
455
497
|
*/
|
|
456
498
|
export async function checkForUpdate(ctx, commandLabel, options = {}) {
|
|
499
|
+
const finish = beginOperationTiming(commandLabel, "版本检查(含确认等待)");
|
|
500
|
+
try {
|
|
501
|
+
return await executeCheckForUpdate(ctx, commandLabel, options, finish);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
finish(false);
|
|
504
|
+
throw error;
|
|
505
|
+
} finally {
|
|
506
|
+
finish();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 执行版本提示与可选升级;直接退出前必须先冻结诊断阶段。
|
|
512
|
+
* @param {object} ctx 命令上下文
|
|
513
|
+
* @param {string} commandLabel 命令名
|
|
514
|
+
* @param {object} options 测试替身
|
|
515
|
+
* @param {Function} finish 版本检查阶段的幂等结束函数
|
|
516
|
+
* @returns {Promise<void>} 未升级退出时返回
|
|
517
|
+
*/
|
|
518
|
+
async function executeCheckForUpdate(ctx, commandLabel, options, finish) {
|
|
457
519
|
// 1. 关闭开关:显式 flag 或环境变量
|
|
458
520
|
if (ctx.updateCheck === false || process.env.FLOWER_NO_UPDATE_CHECK) return;
|
|
459
521
|
const updateCheck = readUpdateCheck(ctx.target);
|
|
@@ -461,35 +523,37 @@ export async function checkForUpdate(ctx, commandLabel, options = {}) {
|
|
|
461
523
|
// 2. npx 本就是最新版,跳过(连通知都不打,避免误导)
|
|
462
524
|
if (isRunningViaNpx(import.meta.url)) return;
|
|
463
525
|
|
|
464
|
-
|
|
465
|
-
const
|
|
526
|
+
const reader = createUpdateMetadataReader(options);
|
|
527
|
+
const fromCache = isRemoteCacheFresh(updateCheck);
|
|
466
528
|
const report = options.report || reportTelemetry;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
.catch(() => null),
|
|
529
|
+
if (!fromCache) console.log(" · 正在检查 Flower 版本");
|
|
530
|
+
const [tags] = fromCache ? [updateCheck.lastRemote] : await Promise.all([
|
|
531
|
+
reader.readTags(),
|
|
532
|
+
Promise.resolve().then(() => report(ctx.target, "version_check")).catch(() => null),
|
|
472
533
|
]);
|
|
473
|
-
if (!
|
|
474
|
-
const tags = metadata.tags;
|
|
534
|
+
if (!tags) return;
|
|
475
535
|
|
|
476
|
-
// 4. 根据本地版本通道生成推荐;无推荐时不打扰
|
|
477
536
|
const current = flowerVersion();
|
|
478
537
|
const recommendation = getUpdateRecommendation(current, tags);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
tags,
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
538
|
+
const range = recommendation ? {
|
|
539
|
+
from: current,
|
|
540
|
+
to: recommendation.version,
|
|
541
|
+
channel: recommendation.tag,
|
|
542
|
+
reason: "update_available",
|
|
543
|
+
} : null;
|
|
544
|
+
const releaseNotes = cachedReleaseNotes(updateCheck, range) || (range
|
|
545
|
+
? buildReleaseNotesSummary((await reader.readMetadata())?.releaseNotesByVersion, range)
|
|
546
|
+
: null);
|
|
547
|
+
if (!fromCache) {
|
|
548
|
+
rememberRemoteTags(ctx.target, tags, recommendation ? "update_available" : "up_to_date", releaseNotes);
|
|
549
|
+
} else if (releaseNotes && !releaseNotes.unavailable) {
|
|
550
|
+
// 摘要补拉不能把旧标签伪装成刚确认的远端版本。
|
|
551
|
+
try {
|
|
552
|
+
if (readManifest(ctx.target) || new ProjectStore(ctx.target).readLock()) {
|
|
553
|
+
writeUpdateCheck(ctx.target, { lastReleaseNotes: releaseNotes });
|
|
554
|
+
}
|
|
555
|
+
} catch { /* 缓存失败不阻断更新。 */ }
|
|
556
|
+
}
|
|
493
557
|
if (!recommendation) return;
|
|
494
558
|
|
|
495
559
|
// 5. 打印发现新版本通知(粉色品牌色,与 banner 一致)
|
|
@@ -511,7 +575,7 @@ export async function checkForUpdate(ctx, commandLabel, options = {}) {
|
|
|
511
575
|
}
|
|
512
576
|
|
|
513
577
|
// 7. 交互:询问是否升级(@inquirer/confirm,返回 boolean)
|
|
514
|
-
const doUpgrade = await confirm({
|
|
578
|
+
const doUpgrade = await (options.confirm || confirm)({
|
|
515
579
|
message: `是否现在升级到 ${recommendation.version}(${recommendation.tag})?(升级后需重新运行命令)`,
|
|
516
580
|
default: true,
|
|
517
581
|
});
|
|
@@ -521,11 +585,16 @@ export async function checkForUpdate(ctx, commandLabel, options = {}) {
|
|
|
521
585
|
}
|
|
522
586
|
|
|
523
587
|
// 8. 执行全局升级。失败(含 EACCES 权限问题、npm 不存在)不自行提权,降级为打印手动命令
|
|
524
|
-
const res =
|
|
588
|
+
const res = timeOperation(commandLabel, "全局安装(含依赖与postinstall)", () => (
|
|
589
|
+
(options.install || installFlowerVersion)(recommendation.version)
|
|
590
|
+
));
|
|
525
591
|
if (res.status === 0) {
|
|
526
592
|
console.log(`\n ✓ 已升级到 ${recommendation.version}(${recommendation.tag})`);
|
|
527
593
|
console.log(` · 请重新运行 ft ${commandLabel} 以使用新版本`);
|
|
528
594
|
console.log(" · 强化包随版本更新,升级后可 ft update 重新叠加到现有项目");
|
|
595
|
+
// process.exit 不会展开调用者的 finally;成功升级的旧进程退出前先结束阶段与父总计。
|
|
596
|
+
finish();
|
|
597
|
+
ctx.finishUpdateTiming?.();
|
|
529
598
|
// 当前进程内存里仍是旧代码,必须退出由用户重跑新版本(不做 re-exec,规避权限/平台/进程态坑)
|
|
530
599
|
process.exit(0);
|
|
531
600
|
} else {
|