flower-trellis 0.4.10 → 0.4.11

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 CHANGED
@@ -131,22 +131,33 @@ flower-trellis self-check --json --target .
131
131
  - **本地一致性检查**:每次启动都会比对当前安装的 `flower-trellis` / 捆绑 Trellis 版本与项目 `.trellis/.flower-manifest.json` / `.trellis/.version`;不受 `intervalHours` 限制。
132
132
  - **远程版本探测**:访问 npm registry 读取 `flower-trellis` dist-tags;受 `intervalHours` 节流,带超时,失败静默。
133
133
 
134
- 策略和缓存统一保存在 `.trellis/.flower-manifest.json` 的 `updateCheck`:
134
+ 策略保存在 `.trellis/.flower-manifest.json` 的 `updateCheck`;运行缓存保存在 gitignored 的 `.trellis/.flower-update-check.tmp`,避免 `lastCheckedAt` 等字段反复污染 git。
135
+
136
+ manifest 中只保留用户策略:
135
137
 
136
138
  ```json
137
139
  {
138
140
  "updateCheck": {
139
141
  "enabled": true,
140
142
  "policy": "ask",
141
- "intervalHours": 8,
143
+ "intervalHours": 8
144
+ }
145
+ }
146
+ ```
147
+
148
+ tmp 中保存本地运行缓存:
149
+
150
+ ```json
151
+ {
142
152
  "lastCheckedAt": "2026-07-07T00:00:00.000Z",
143
153
  "lastRemote": { "latest": "0.4.2", "beta": null },
144
154
  "lastStatus": "update_available",
145
155
  "lastErrorCode": null
146
- }
147
156
  }
148
157
  ```
149
158
 
159
+ 旧项目如果已经在 manifest 的 `updateCheck` 里带有缓存字段,新版本会先读取兼容,并在下一次写入时清理这些旧字段。
160
+
150
161
  `policy` 可选:
151
162
 
152
163
  | policy | 行为 |
@@ -1,5 +1,5 @@
1
1
  {
2
- "syncedAt": "2026-07-08T03:53:14.101Z",
2
+ "syncedAt": "2026-07-09T03:31:09.573Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
4
  "sourceCommit": "6ecfb1b46320d82aeb3e83442235f271d92aa398",
5
5
  "common": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,9 +58,9 @@
58
58
  "commit-and-tag-version": "^12.7.3"
59
59
  },
60
60
  "flowerReleaseNotes": {
61
- "version": "0.4.10",
61
+ "version": "0.4.11",
62
62
  "source": "CHANGELOG.md",
63
- "body": "### 🐛 修复 Bug Fixes\n\n* **flower:** 项目侧待更新提示会显示对应版本更新摘要 ([cc98c02](https://github.com/SilentFlower/flower-trellis/commit/cc98c021eb0536f693f50651854093a4795d9e93))",
63
+ "body": "### 新功能 Features\n\n* **update-check:** 迁移运行缓存到 tmp ([986659a](https://github.com/SilentFlower/flower-trellis/commit/986659a7d3678384be055dcae0067c2c31c836fb))",
64
64
  "truncated": false
65
65
  }
66
66
  }
@@ -4,6 +4,7 @@ import {
4
4
  manifestPath,
5
5
  readManifest,
6
6
  readUpdateCheck,
7
+ updateCheckCachePath,
7
8
  writeUpdateCheck,
8
9
  } from "../lib/manifest.js";
9
10
 
@@ -24,7 +25,7 @@ function assertTrellisProject(target) {
24
25
  }
25
26
 
26
27
  /**
27
- * flower-trellis update-check:管理 manifest 内的启动更新检查策略。
28
+ * flower-trellis update-check:管理启动更新检查策略,并展示本地运行缓存。
28
29
  *
29
30
  * @param {object} ctx 见 cli.js 的 parse()
30
31
  */
@@ -35,9 +36,14 @@ export async function updateCheck(ctx) {
35
36
 
36
37
  if (action === "get") {
37
38
  const manifest = readManifest(ctx.target);
39
+ const cachePath = updateCheckCachePath(ctx.target);
38
40
  console.log(`manifest: ${manifestPath(ctx.target)}`);
41
+ console.log(`cache: ${cachePath}`);
39
42
  console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
40
43
  if (!manifest) console.log(" · manifest 不存在,当前显示默认策略");
44
+ if (!fs.existsSync(cachePath)) {
45
+ console.log(" · cache 不存在,当前显示默认或旧 manifest 兼容缓存");
46
+ }
41
47
  return;
42
48
  }
43
49
 
@@ -11,17 +11,56 @@ import path from "node:path";
11
11
  * 放在 .trellis/ 下,随项目的 Trellis 生命周期存在(uninstall 删 .trellis 时一并消失)。
12
12
  */
13
13
  const MANIFEST_REL = path.join(".trellis", ".flower-manifest.json");
14
+ const UPDATE_CHECK_CACHE_REL = path.join(".trellis", ".flower-update-check.tmp");
14
15
  const UPDATE_POLICIES = new Set(["off", "notify", "ask", "auto"]);
15
- const DEFAULT_UPDATE_CHECK = {
16
+ const UPDATE_CHECK_POLICY_KEYS = new Set(["enabled", "policy", "intervalHours"]);
17
+ const UPDATE_CHECK_CACHE_KEYS = new Set([
18
+ "lastCheckedAt",
19
+ "lastRemote",
20
+ "lastReleaseNotes",
21
+ "lastStatus",
22
+ "lastErrorCode",
23
+ ]);
24
+ const DEFAULT_UPDATE_CHECK_POLICY = {
16
25
  enabled: true,
17
26
  policy: "ask",
18
27
  intervalHours: 8,
28
+ };
29
+ const DEFAULT_UPDATE_CHECK_CACHE = {
19
30
  lastCheckedAt: null,
20
31
  lastRemote: null,
21
32
  lastReleaseNotes: null,
22
33
  lastStatus: null,
23
34
  lastErrorCode: null,
24
35
  };
36
+ const DEFAULT_UPDATE_CHECK = {
37
+ ...DEFAULT_UPDATE_CHECK_POLICY,
38
+ ...DEFAULT_UPDATE_CHECK_CACHE,
39
+ };
40
+
41
+ /** 判断值是否为普通对象。 */
42
+ function isPlainObject(value) {
43
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
44
+ }
45
+
46
+ /** 判断对象是否显式包含给定字段。 */
47
+ function hasAnyOwn(value, keys) {
48
+ if (!isPlainObject(value)) return false;
49
+ for (const key of keys) {
50
+ if (Object.prototype.hasOwnProperty.call(value, key)) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /** 从对象里取出指定字段。 */
56
+ function pickOwn(value, keys) {
57
+ const raw = isPlainObject(value) ? value : {};
58
+ const picked = {};
59
+ for (const key of keys) {
60
+ if (Object.prototype.hasOwnProperty.call(raw, key)) picked[key] = raw[key];
61
+ }
62
+ return picked;
63
+ }
25
64
 
26
65
  /**
27
66
  * 归一化 release notes 范围字段。
@@ -73,12 +112,126 @@ function normalizeLastReleaseNotes(value) {
73
112
  };
74
113
  }
75
114
 
76
- /** manifest 文件的绝对路径。 */
115
+ /**
116
+ * 归一化远端 dist-tags 缓存。
117
+ *
118
+ * @param {object|null|undefined} value 原始 lastRemote 字段
119
+ * @returns {{latest:string|null,beta:string|null}|null} 归一化后的 dist-tags
120
+ */
121
+ function normalizeLastRemote(value) {
122
+ return value && typeof value === "object"
123
+ ? {
124
+ latest: typeof value.latest === "string" ? value.latest : null,
125
+ beta: typeof value.beta === "string" ? value.beta : null,
126
+ }
127
+ : null;
128
+ }
129
+
130
+ /**
131
+ * 归一化启动更新检查策略字段。
132
+ *
133
+ * @param {object|null|undefined} value 原始 updateCheck 字段
134
+ * @returns {{enabled:boolean,policy:string,intervalHours:number}} 归一化后的策略
135
+ */
136
+ function normalizeUpdateCheckPolicy(value) {
137
+ const raw = value && typeof value === "object" ? value : {};
138
+ const policy = UPDATE_POLICIES.has(raw.policy) ? raw.policy : DEFAULT_UPDATE_CHECK.policy;
139
+ const interval = Number(raw.intervalHours);
140
+ const intervalHours = Number.isFinite(interval) && interval >= 0
141
+ ? interval
142
+ : DEFAULT_UPDATE_CHECK.intervalHours;
143
+ return {
144
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_UPDATE_CHECK.enabled,
145
+ policy,
146
+ intervalHours,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * 归一化启动更新检查运行缓存字段。
152
+ *
153
+ * @param {object|null|undefined} value 原始缓存字段
154
+ * @returns {{lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的缓存
155
+ */
156
+ function normalizeUpdateCheckCache(value) {
157
+ const raw = value && typeof value === "object" ? value : {};
158
+ return {
159
+ lastCheckedAt: typeof raw.lastCheckedAt === "string" ? raw.lastCheckedAt : null,
160
+ lastRemote: normalizeLastRemote(raw.lastRemote),
161
+ lastReleaseNotes: normalizeLastReleaseNotes(raw.lastReleaseNotes),
162
+ lastStatus: typeof raw.lastStatus === "string" ? raw.lastStatus : null,
163
+ lastErrorCode: typeof raw.lastErrorCode === "string" ? raw.lastErrorCode : null,
164
+ };
165
+ }
166
+
167
+ /** 读取 tmp 内的 updateCheck 运行缓存;不存在或损坏时返回 null。 */
168
+ function readUpdateCheckCacheFile(target) {
169
+ try {
170
+ return normalizeUpdateCheckCache(
171
+ JSON.parse(fs.readFileSync(updateCheckCachePath(target), "utf8")),
172
+ );
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ /** 判断 updateCheck 是否仍含旧版 manifest 缓存字段。 */
179
+ function hasLegacyUpdateCheckCache(updateCheck) {
180
+ return hasAnyOwn(updateCheck, UPDATE_CHECK_CACHE_KEYS);
181
+ }
182
+
183
+ /**
184
+ * 把旧 manifest 内的缓存字段迁移到 tmp。
185
+ *
186
+ * 只在 tmp 尚不存在时迁移,避免旧 manifest 里的陈旧缓存覆盖新版本地缓存。
187
+ */
188
+ function migrateLegacyUpdateCheckCache(target, updateCheck) {
189
+ if (!hasLegacyUpdateCheckCache(updateCheck)) return;
190
+ if (fs.existsSync(updateCheckCachePath(target))) return;
191
+ fs.writeFileSync(
192
+ updateCheckCachePath(target),
193
+ JSON.stringify(normalizeUpdateCheckCache(updateCheck), null, 2) + "\n",
194
+ );
195
+ }
196
+
197
+ /** 写入 tmp 内的 updateCheck 运行缓存。 */
198
+ function writeUpdateCheckCache(target, patch, legacyUpdateCheck) {
199
+ const current = readUpdateCheckCacheFile(target) ||
200
+ normalizeUpdateCheckCache(legacyUpdateCheck);
201
+ const cache = normalizeUpdateCheckCache({
202
+ ...current,
203
+ ...patch,
204
+ });
205
+ fs.writeFileSync(updateCheckCachePath(target), JSON.stringify(cache, null, 2) + "\n");
206
+ return cache;
207
+ }
208
+
209
+ /**
210
+ * manifest 文件的绝对路径。
211
+ *
212
+ * @param {string} target 目标项目根
213
+ * @returns {string} manifest 文件绝对路径
214
+ */
77
215
  export function manifestPath(target) {
78
216
  return path.join(target, MANIFEST_REL);
79
217
  }
80
218
 
81
- /** 读取 manifest;不存在或损坏时返回 null。 */
219
+ /**
220
+ * updateCheck 运行缓存 tmp 文件的绝对路径。
221
+ *
222
+ * @param {string} target 目标项目根
223
+ * @returns {string} updateCheck 运行缓存文件绝对路径
224
+ */
225
+ export function updateCheckCachePath(target) {
226
+ return path.join(target, UPDATE_CHECK_CACHE_REL);
227
+ }
228
+
229
+ /**
230
+ * 读取 manifest;不存在或损坏时返回 null。
231
+ *
232
+ * @param {string} target 目标项目根
233
+ * @returns {object|null} manifest 内容
234
+ */
82
235
  export function readManifest(target) {
83
236
  try {
84
237
  return JSON.parse(fs.readFileSync(manifestPath(target), "utf8"));
@@ -96,55 +249,65 @@ export function readManifest(target) {
96
249
  * @returns {{enabled:boolean,policy:string,intervalHours:number,lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的 updateCheck
97
250
  */
98
251
  export function normalizeUpdateCheck(value) {
99
- const raw = value && typeof value === "object" ? value : {};
100
- const policy = UPDATE_POLICIES.has(raw.policy) ? raw.policy : DEFAULT_UPDATE_CHECK.policy;
101
- const interval = Number(raw.intervalHours);
102
- const intervalHours = Number.isFinite(interval) && interval >= 0
103
- ? interval
104
- : DEFAULT_UPDATE_CHECK.intervalHours;
105
- const lastRemote = raw.lastRemote && typeof raw.lastRemote === "object"
106
- ? {
107
- latest: typeof raw.lastRemote.latest === "string" ? raw.lastRemote.latest : null,
108
- beta: typeof raw.lastRemote.beta === "string" ? raw.lastRemote.beta : null,
109
- }
110
- : null;
111
-
112
252
  return {
113
- enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_UPDATE_CHECK.enabled,
114
- policy,
115
- intervalHours,
116
- lastCheckedAt: typeof raw.lastCheckedAt === "string" ? raw.lastCheckedAt : null,
117
- lastRemote,
118
- lastReleaseNotes: normalizeLastReleaseNotes(raw.lastReleaseNotes),
119
- lastStatus: typeof raw.lastStatus === "string" ? raw.lastStatus : null,
120
- lastErrorCode: typeof raw.lastErrorCode === "string" ? raw.lastErrorCode : null,
253
+ ...normalizeUpdateCheckPolicy(value),
254
+ ...normalizeUpdateCheckCache(value),
121
255
  };
122
256
  }
123
257
 
124
258
  /**
125
- * 读取 manifest 里的启动更新检查配置。
259
+ * 读取启动更新检查配置。
260
+ *
261
+ * 返回 manifest 策略与 tmp 运行缓存的合并视图;tmp 不存在时兼容读取旧 manifest 缓存字段。
126
262
  *
127
263
  * @param {string} target 目标项目根
128
264
  * @returns {ReturnType<typeof normalizeUpdateCheck>} 归一化后的配置
129
265
  */
130
266
  export function readUpdateCheck(target) {
131
- return normalizeUpdateCheck(readManifest(target)?.updateCheck);
267
+ const manifestUpdateCheck = readManifest(target)?.updateCheck;
268
+ return {
269
+ ...normalizeUpdateCheckPolicy(manifestUpdateCheck),
270
+ ...(
271
+ readUpdateCheckCacheFile(target) ||
272
+ // 兼容旧版本:首次写入前仍可复用 manifest 里的运行缓存。
273
+ normalizeUpdateCheckCache(manifestUpdateCheck)
274
+ ),
275
+ };
132
276
  }
133
277
 
134
278
  /**
135
- * 写入启动更新检查配置,保留 manifest 其它安装清单字段。
279
+ * 写入启动更新检查配置。
280
+ *
281
+ * 策略字段写入 `.flower-manifest.json`;运行缓存字段写入 `.flower-update-check.tmp`。
282
+ * 若旧 manifest 仍包含缓存字段,本函数会在写入时顺带清理,避免运行态继续进入 git。
136
283
  *
137
284
  * @param {string} target 目标项目根
138
285
  * @param {object} patch 要合并进 updateCheck 的字段
139
- * @returns {object} 写入后的 manifest
286
+ * @returns {object|null} 写入后的 manifest;没有 manifest 且仅写缓存时返回 null
140
287
  */
141
288
  export function writeUpdateCheck(target, patch) {
142
- const current = readManifest(target) || {};
143
- const updateCheck = normalizeUpdateCheck({
144
- ...normalizeUpdateCheck(current.updateCheck),
145
- ...(patch || {}),
146
- });
147
- const next = { ...current, updateCheck };
289
+ const current = readManifest(target);
290
+ const policyPatch = pickOwn(patch, UPDATE_CHECK_POLICY_KEYS);
291
+ const cachePatch = pickOwn(patch, UPDATE_CHECK_CACHE_KEYS);
292
+ const hasPolicyPatch = Object.keys(policyPatch).length > 0;
293
+ const hasCachePatch = Object.keys(cachePatch).length > 0;
294
+ const hasLegacyCache = hasLegacyUpdateCheckCache(current?.updateCheck);
295
+
296
+ if (hasCachePatch && current) {
297
+ writeUpdateCheckCache(target, cachePatch, current.updateCheck);
298
+ } else if (hasLegacyCache) {
299
+ migrateLegacyUpdateCheckCache(target, current.updateCheck);
300
+ }
301
+
302
+ if (!hasPolicyPatch && !hasLegacyCache) return current;
303
+
304
+ const next = {
305
+ ...(current || {}),
306
+ updateCheck: normalizeUpdateCheckPolicy({
307
+ ...normalizeUpdateCheckPolicy(current?.updateCheck),
308
+ ...policyPatch,
309
+ }),
310
+ };
148
311
  fs.writeFileSync(manifestPath(target), JSON.stringify(next, null, 2) + "\n");
149
312
  return next;
150
313
  }
@@ -152,7 +315,8 @@ export function writeUpdateCheck(target, patch) {
152
315
  /**
153
316
  * 写入 manifest。
154
317
  *
155
- * 若调用方没有显式传入 `updateCheck`,保留目标项目已有策略与缓存,避免全装重写时覆盖用户选择。
318
+ * 若调用方没有显式传入 `updateCheck`,保留目标项目已有策略,避免全装重写时覆盖用户选择。
319
+ * 运行缓存会迁移到 `.flower-update-check.tmp`,不得继续写入 manifest。
156
320
  *
157
321
  * @param {string} target 目标项目根
158
322
  * @param {object} data manifest 新内容
@@ -160,10 +324,14 @@ export function writeUpdateCheck(target, patch) {
160
324
  export function writeManifest(target, data) {
161
325
  const current = readManifest(target);
162
326
  const next = { ...data };
327
+ if (hasLegacyUpdateCheckCache(current?.updateCheck)) {
328
+ migrateLegacyUpdateCheckCache(target, current.updateCheck);
329
+ }
163
330
  if (Object.prototype.hasOwnProperty.call(data, "updateCheck")) {
164
- next.updateCheck = normalizeUpdateCheck(data.updateCheck);
331
+ migrateLegacyUpdateCheckCache(target, data.updateCheck);
332
+ next.updateCheck = normalizeUpdateCheckPolicy(data.updateCheck);
165
333
  } else {
166
- next.updateCheck = normalizeUpdateCheck(current?.updateCheck);
334
+ next.updateCheck = normalizeUpdateCheckPolicy(current?.updateCheck);
167
335
  }
168
336
  fs.writeFileSync(manifestPath(target), JSON.stringify(next, null, 2) + "\n");
169
337
  }
@@ -348,7 +348,7 @@ export function getUpdateRecommendation(current, tags) {
348
348
  return null;
349
349
  }
350
350
 
351
- /** 尽力而为刷新目标项目 manifest 里的远端探测缓存。 */
351
+ /** 尽力而为刷新目标项目的远端探测缓存。 */
352
352
  function rememberRemoteTags(target, tags, status, releaseNotes = null) {
353
353
  try {
354
354
  if (!readManifest(target)) return;