flower-trellis 0.2.4 → 0.2.5-beta.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.2.4",
3
+ "version": "0.2.5-beta.2",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,10 +6,10 @@ import { flowerVersion } from "./versions.js";
6
6
 
7
7
  /**
8
8
  * 版本自动检测 —— 在 init / update 启动时尽力而为地比对 npm 上 flower-trellis 自身的
9
- * 最新版本,发现新版时提示用户并(交互场景下)询问是否立即升级。
9
+ * 可用版本,发现新版时提示用户并(交互场景下)询问是否立即升级。
10
10
  *
11
11
  * 设计基调与本项目一致:**绝不阻断主流程**。网络探测带超时,离线/超时/失败一律静默跳过;
12
- * 仅在「全局直跑 + 有新版」时才打扰用户。详见 design.md / research/version-check-conventions.md
12
+ * 仅在「全局直跑 + 有新版」时才打扰用户。稳定版只跟 latest;预发布版同时看 latest / beta
13
13
  */
14
14
 
15
15
  /** npm registry 根地址。 */
@@ -20,23 +20,26 @@ const PKG = "flower-trellis";
20
20
  const TIMEOUT_MS = 2500;
21
21
 
22
22
  /**
23
- * 取 npm 上 flower-trellis 的 latest 版本号;任何失败(离线/超时/非 200/解析异常)一律
23
+ * 取 npm 上 flower-trellis 的 dist-tags;任何失败(离线/超时/非 200/解析异常)一律
24
24
  * 返回 null —— 调用方据此「拿不到就当没这回事」继续主流程。
25
25
  *
26
26
  * 用 AbortController 给内置 fetch 加超时,finally 清除定时器防句柄泄漏。
27
- * @returns {Promise<string|null>} latest 版本号,或失败时 null
27
+ * @returns {Promise<{latest:string|null,beta:string|null}|null>} 可用 dist-tags,或失败时 null
28
28
  */
29
- export async function fetchLatestVersion() {
29
+ export async function fetchPackageDistTags() {
30
30
  const ac = new AbortController();
31
31
  const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
32
32
  try {
33
- const res = await fetch(`${REGISTRY}/${PKG}/latest`, {
33
+ const res = await fetch(`${REGISTRY}/${PKG}`, {
34
34
  signal: ac.signal,
35
35
  headers: { Accept: "application/json" },
36
36
  });
37
37
  if (!res.ok) return null; // 非 200(404/5xx 等)→ 静默跳过
38
38
  const json = await res.json();
39
- return typeof json.version === "string" ? json.version : null;
39
+ const tags = json && typeof json === "object" ? json["dist-tags"] : null;
40
+ const latest = typeof tags?.latest === "string" ? tags.latest : null;
41
+ const beta = typeof tags?.beta === "string" ? tags.beta : null;
42
+ return latest || beta ? { latest, beta } : null;
40
43
  } catch {
41
44
  return null; // AbortError(超时)/ fetch failed(离线)/ JSON 解析失败 → 静默
42
45
  } finally {
@@ -44,28 +47,130 @@ export async function fetchLatestVersion() {
44
47
  }
45
48
  }
46
49
 
50
+ /**
51
+ * 取 npm 上 flower-trellis 的 latest 版本号。
52
+ *
53
+ * 保留这个导出是为了兼容已有调用方;新逻辑应优先使用 `fetchPackageDistTags()`。
54
+ * @returns {Promise<string|null>} latest 版本号,或失败时 null
55
+ */
56
+ export async function fetchLatestVersion() {
57
+ const tags = await fetchPackageDistTags();
58
+ return tags?.latest ?? null;
59
+ }
60
+
61
+ /**
62
+ * 解析项目需要的轻量 semver 版本号。
63
+ *
64
+ * @param {string} version 版本号
65
+ * @returns {{major:number,minor:number,patch:number,prerelease:string[]}|null} 解析结果
66
+ */
67
+ function parseVersion(version) {
68
+ const match = String(version || "")
69
+ .trim()
70
+ .replace(/^v/, "")
71
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
72
+ if (!match) return null;
73
+ return {
74
+ major: Number(match[1]),
75
+ minor: Number(match[2]),
76
+ patch: Number(match[3]),
77
+ prerelease: match[4] ? match[4].split(".") : [],
78
+ };
79
+ }
80
+
81
+ /**
82
+ * 判断版本号是否为 prerelease。
83
+ *
84
+ * @param {string} version 版本号
85
+ * @returns {boolean}
86
+ */
87
+ export function isPrerelease(version) {
88
+ const parsed = parseVersion(version);
89
+ return Boolean(parsed && parsed.prerelease.length);
90
+ }
91
+
92
+ /**
93
+ * 比较 prerelease 标识符数组。
94
+ *
95
+ * 同一 base 下,稳定版高于 prerelease;相同 label(如 beta.1 → beta.2)按数值递增比较。
96
+ * 不同 label 之间不做主观排序,返回 0 以避免把 alpha/rc/beta 的跨线比较误判成升级。
97
+ *
98
+ * @param {string[]} aParts A prerelease 标识符
99
+ * @param {string[]} bParts B prerelease 标识符
100
+ * @returns {-1|0|1}
101
+ */
102
+ function comparePrerelease(aParts, bParts) {
103
+ if (!aParts.length && !bParts.length) return 0;
104
+ if (!aParts.length) return 1;
105
+ if (!bParts.length) return -1;
106
+ if (aParts[0] !== bParts[0]) return 0;
107
+
108
+ const len = Math.max(aParts.length, bParts.length);
109
+ for (let i = 0; i < len; i++) {
110
+ const a = aParts[i];
111
+ const b = bParts[i];
112
+ if (a === undefined) return -1;
113
+ if (b === undefined) return 1;
114
+ if (a === b) continue;
115
+
116
+ const aNum = /^\d+$/.test(a);
117
+ const bNum = /^\d+$/.test(b);
118
+ if (aNum && bNum) {
119
+ const delta = Number(a) - Number(b);
120
+ if (delta !== 0) return delta > 0 ? 1 : -1;
121
+ continue;
122
+ }
123
+ if (aNum !== bNum) return aNum ? -1 : 1;
124
+ return a > b ? 1 : -1;
125
+ }
126
+ return 0;
127
+ }
128
+
47
129
  /**
48
130
  * 比较两个版本号,返回 1 / 0 / -1(a 比 b 新 / 相同 / 旧)。
49
131
  *
50
- * 只比较 major.minor.patch 三段数值;预发布后缀(-beta.x 等)在拆分前剥除。
51
- * 之所以不引 semver 库:比较对象是 npm `latest` dist-tag(按语义永远指向稳定发布),
52
- * 天然不含预发布优先级排序问题。与 src/lib/variant.js「剥 -beta.x 再比数值」的先例一致。
132
+ * 支持 `major.minor.patch` 与项目 beta 通道使用的 `major.minor.patch-beta.n`。
133
+ * 不引 `semver` 依赖,因为这里仅服务启动时的轻量升级提示,失败/不认识时宁可不提示。
53
134
  * @param {string} a 版本号 A
54
135
  * @param {string} b 版本号 B
55
136
  * @returns {-1|0|1}
56
137
  */
57
138
  export function compareVersions(a, b) {
58
- const norm = (v) =>
59
- String(v)
60
- .split("-")[0]
61
- .split(".")
62
- .map((n) => parseInt(n, 10) || 0);
63
- const [a1, a2, a3] = norm(a);
64
- const [b1, b2, b3] = norm(b);
65
- if (a1 !== b1) return a1 > b1 ? 1 : -1;
66
- if (a2 !== b2) return a2 > b2 ? 1 : -1;
67
- if (a3 !== b3) return a3 > b3 ? 1 : -1;
68
- return 0;
139
+ const av = parseVersion(a);
140
+ const bv = parseVersion(b);
141
+ if (!av || !bv) return 0;
142
+
143
+ for (const key of ["major", "minor", "patch"]) {
144
+ if (av[key] !== bv[key]) return av[key] > bv[key] ? 1 : -1;
145
+ }
146
+ return comparePrerelease(av.prerelease, bv.prerelease);
147
+ }
148
+
149
+ /**
150
+ * 根据当前版本和 npm dist-tags 生成升级推荐。
151
+ *
152
+ * 稳定版只跟随稳定形态的 latest,避免把稳定用户引导到预发布通道;预发布版同时看
153
+ * 稳定形态的 latest / beta,且 latest 高于当前版本时优先推荐 latest,用于 beta 线被
154
+ * 稳定版追上后的回归稳定。
155
+ *
156
+ * @param {string} current 当前安装的 flower-trellis 版本
157
+ * @param {{latest?:string|null,beta?:string|null}|null} tags npm dist-tags
158
+ * @returns {{version:string,tag:"latest"|"beta",command:string}|null} 升级推荐
159
+ */
160
+ export function getUpdateRecommendation(current, tags) {
161
+ if (!current || !tags) return null;
162
+ const currentIsPrerelease = isPrerelease(current);
163
+ const latest = typeof tags.latest === "string" ? tags.latest : null;
164
+ const beta = typeof tags.beta === "string" ? tags.beta : null;
165
+ const latestIsStable = latest && !isPrerelease(latest);
166
+
167
+ if (latestIsStable && compareVersions(latest, current) === 1) {
168
+ return { version: latest, tag: "latest", command: `npm i -g ${PKG}@latest` };
169
+ }
170
+ if (currentIsPrerelease && beta && compareVersions(beta, current) === 1) {
171
+ return { version: beta, tag: "beta", command: `npm i -g ${PKG}@beta` };
172
+ }
173
+ return null;
69
174
  }
70
175
 
71
176
  /**
@@ -92,10 +197,10 @@ export function isRunningViaNpx() {
92
197
  *
93
198
  * 短路条件(任一命中即跳过,什么都不打印):关闭开关(`--no-update-check` 使
94
199
  * `ctx.updateCheck===false`,或环境变量 `FLOWER_NO_UPDATE_CHECK` 非空)、经 npx 运行、
95
- * 网络探测失败、已是最新或本地更高。
200
+ * 网络探测失败、无升级推荐。
96
201
  *
97
202
  * 发现新版时的行为:
98
- * - 交互 TTY:打印通知 → confirm 询问是否升级 → 同意则执行 `npm i -g flower-trellis@latest`;
203
+ * - 交互 TTY:打印通知 → confirm 询问是否升级 → 同意则执行推荐的 npm install 命令;
99
204
  * 成功后打印「请重新运行」并 **process.exit(0)**(不做 re-exec 自动重跑),失败则降级为
100
205
  * 打印手动升级命令并继续主流程;拒绝则继续主流程。
101
206
  * - 非交互(`-y`/`--yes` 或非 TTY):仅打印通知 + 升级命令,不弹确认、不阻塞。
@@ -110,19 +215,20 @@ export async function checkForUpdate(ctx, commandLabel) {
110
215
  // 2. npx 本就是最新版,跳过(连通知都不打,避免误导)
111
216
  if (isRunningViaNpx()) return;
112
217
 
113
- // 3. 尽力而为取 latest;拿不到就静默退出
114
- const latest = await fetchLatestVersion();
115
- if (!latest) return;
218
+ // 3. 尽力而为取 dist-tags;拿不到就静默退出
219
+ const tags = await fetchPackageDistTags();
220
+ if (!tags) return;
116
221
 
117
- // 4. 与本地版本比较;仅当 latest 严格更新时才打扰
222
+ // 4. 根据本地版本通道生成推荐;无推荐时不打扰
118
223
  const current = flowerVersion();
119
- if (compareVersions(latest, current) !== 1) return;
224
+ const recommendation = getUpdateRecommendation(current, tags);
225
+ if (!recommendation) return;
120
226
 
121
227
  // 5. 打印发现新版本通知(粉色品牌色,与 banner 一致)
122
228
  console.log(
123
229
  "\n🌸 " +
124
- chalk.hex("#ff6fb5")(`发现 flower-trellis 新版本 ${chalk.bold(latest)}`) +
125
- chalk.gray(`(当前 ${current})`),
230
+ chalk.hex("#ff6fb5")(`发现 flower-trellis 新版本 ${chalk.bold(recommendation.version)}`) +
231
+ chalk.gray(`(当前 ${current}, 通道 ${recommendation.tag})`),
126
232
  );
127
233
 
128
234
  // 6. 非交互(-y/--yes 或非 TTY):仅打印升级命令,不弹确认、不阻塞
@@ -131,14 +237,14 @@ export async function checkForUpdate(ctx, commandLabel) {
131
237
  ctx.passthrough.includes("--yes") ||
132
238
  !process.stdin.isTTY;
133
239
  if (nonInteractive) {
134
- console.log(` · 升级:npm i -g ${PKG}@latest`);
135
- console.log(" · 升级后请重跑 ft update,让新版强化包重新叠加到现有项目");
240
+ console.log(` · 升级:${recommendation.command}`);
241
+ console.log(` · 升级后请重跑 ft ${commandLabel},让新版强化包重新叠加到现有项目`);
136
242
  return;
137
243
  }
138
244
 
139
245
  // 7. 交互:询问是否升级(@inquirer/confirm,返回 boolean)
140
246
  const doUpgrade = await confirm({
141
- message: `是否现在升级到 ${latest}?(升级后需重新运行命令)`,
247
+ message: `是否现在升级到 ${recommendation.version}(${recommendation.tag})?(升级后需重新运行命令)`,
142
248
  default: true,
143
249
  });
144
250
  if (!doUpgrade) {
@@ -147,18 +253,18 @@ export async function checkForUpdate(ctx, commandLabel) {
147
253
  }
148
254
 
149
255
  // 8. 执行全局升级。失败(含 EACCES 权限问题、npm 不存在)不自行提权,降级为打印手动命令
150
- const res = spawnSync("npm", ["i", "-g", `${PKG}@latest`], {
256
+ const res = spawnSync("npm", ["i", "-g", `${PKG}@${recommendation.tag}`], {
151
257
  stdio: "inherit",
152
258
  shell: process.platform === "win32", // Windows 上 npm 实为 npm.cmd
153
259
  });
154
260
  if (res.status === 0) {
155
- console.log(`\n ✓ 已升级到 ${latest}`);
261
+ console.log(`\n ✓ 已升级到 ${recommendation.version}(${recommendation.tag})`);
156
262
  console.log(` · 请重新运行 ft ${commandLabel} 以使用新版本`);
157
263
  console.log(" · 强化包随版本更新,升级后可 ft update 重新叠加到现有项目");
158
264
  // 当前进程内存里仍是旧代码,必须退出由用户重跑新版本(不做 re-exec,规避权限/平台/进程态坑)
159
265
  process.exit(0);
160
266
  } else {
161
- console.log(` · 自动升级失败,请手动运行:npm i -g ${PKG}@latest`);
267
+ console.log(` · 自动升级失败,请手动运行:${recommendation.command}`);
162
268
  // 升级未成功:以当前版本继续 init/update,不阻断
163
269
  }
164
270
  }