flower-trellis 0.4.3 → 0.4.5-beta.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "syncedAt": "2026-07-07T06:55:11.344Z",
2
+ "syncedAt": "2026-07-07T08:40:30.218Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
4
  "sourceCommit": "5fdebe60e923c91ba912461b99413b5c8e11a38c",
5
5
  "common": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.3",
3
+ "version": "0.4.5-beta.0",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -81,6 +81,7 @@ def _format_context(data: dict) -> str:
81
81
  remote = data.get("remote") or {}
82
82
  ai = data.get("ai") or {}
83
83
  safety = data.get("safety") or {}
84
+ out_of_sync_reasons = project.get("outOfSyncReasons") or []
84
85
  lines = [
85
86
  "<flower-update>",
86
87
  f"status: {data.get('status')}",
@@ -90,13 +91,23 @@ def _format_context(data: dict) -> str:
90
91
  f"bundled_trellis: {current.get('bundledTrellisVersion')}",
91
92
  f"project_trellis: {project.get('trellisVersion')}",
92
93
  ]
94
+ if "outOfSync" in project:
95
+ lines.append(f"project_out_of_sync: {project.get('outOfSync')}")
96
+ if out_of_sync_reasons:
97
+ lines.append(f"project_out_of_sync_reasons: {', '.join(out_of_sync_reasons)}")
93
98
  if remote.get("tags"):
94
99
  lines.append(f"remote: {json.dumps(remote.get('tags'), ensure_ascii=False)}")
100
+ if remote.get("errorCode"):
101
+ lines.append(f"remote_error_code: {remote.get('errorCode')}")
95
102
  command = (data.get("commands") or {}).get("recommended") or ai.get("command")
96
103
  if command:
97
104
  lines.append(f"recommended_command: {command}")
98
105
  if safety.get("reasons"):
99
106
  lines.append(f"safety_reasons: {', '.join(safety.get('reasons') or [])}")
107
+ if ai.get("mode"):
108
+ lines.append(f"ai_mode: {ai.get('mode')}")
109
+ if ai.get("mode") == "ask":
110
+ lines.append("ai_required_action: 必须先向用户提出明确确认问题;用户确认前禁止执行 recommended_command。")
100
111
  if ai.get("instruction"):
101
112
  lines.append(f"ai_instruction: {ai.get('instruction')}")
102
113
  lines.append("</flower-update>")
@@ -104,7 +115,7 @@ def _format_context(data: dict) -> str:
104
115
 
105
116
 
106
117
  def _emit_context(context: str) -> None:
107
- """同时兼容 Codex / Claude Code / Cursor 风格的 hook JSON 输出。"""
118
+ """输出 Codex / Claude Code 都接受的 SessionStart hook JSON"""
108
119
  result = {
109
120
  "suppressOutput": True,
110
121
  "systemMessage": "flower-trellis update context injected",
@@ -112,7 +123,6 @@ def _emit_context(context: str) -> None:
112
123
  "hookEventName": "SessionStart",
113
124
  "additionalContext": context,
114
125
  },
115
- "additional_context": context,
116
126
  }
117
127
  print(json.dumps(result, ensure_ascii=False), flush=True)
118
128
 
@@ -15,6 +15,9 @@ import { FLOWER_UPDATE_HOOK_REL } from "./flower-assets.js";
15
15
  const WORKFLOW_HOOK_SCRIPT = ".codex/hooks/inject-workflow-state.py";
16
16
  const SESSION_START_SCRIPT = ".codex/hooks/session-start.py";
17
17
  const CODEX_DISPATCH_MODE = "sub-agent";
18
+ const SESSION_START_MATCHER = "startup|resume|clear|compact";
19
+ const FLOWER_UPDATE_MATCHER = "startup";
20
+ const SESSION_START_TIMEOUT = 30;
18
21
 
19
22
  /** 返回一行开头的空白缩进。 */
20
23
  function leadingWhitespace(line) {
@@ -258,20 +261,48 @@ function hasCommand(hooks, commandNeedle) {
258
261
  );
259
262
  }
260
263
 
261
- /** SessionStart 合并单个 command hook,保留既有自定义 hook。 */
262
- function ensureSessionStartCommand(groups, command, timeout, needle) {
263
- for (const group of groups) {
264
- const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
265
- if (hasCommand(hooks, needle)) return false;
264
+ /** 从所有 SessionStart group 移除目标命令,避免 matcher 迁移后重复触发。 */
265
+ function removeSessionStartCommand(groups, needle) {
266
+ let changed = false;
267
+ for (let i = groups.length - 1; i >= 0; i -= 1) {
268
+ const group = groups[i];
269
+ if (!Array.isArray(group?.hooks)) continue;
270
+ const before = group.hooks.length;
271
+ group.hooks = group.hooks.filter(
272
+ (hook) => !(hook?.type === "command" &&
273
+ typeof hook.command === "string" &&
274
+ hook.command.includes(needle)),
275
+ );
276
+ if (group.hooks.length !== before) {
277
+ changed = true;
278
+ if (group.hooks.length === 0 && !group.matcher) {
279
+ groups.splice(i, 1);
280
+ }
281
+ }
282
+ }
283
+ return changed;
284
+ }
285
+
286
+ /** 查找或创建指定 matcher 的 SessionStart group。 */
287
+ function ensureSessionStartGroup(groups, matcher) {
288
+ let group = groups.find((item) => item && item.matcher === matcher);
289
+ if (!group) {
290
+ group = { matcher, hooks: [] };
291
+ groups.push(group);
266
292
  }
267
- groups.push({
268
- hooks: [
269
- {
270
- type: "command",
271
- command,
272
- timeout,
273
- },
274
- ],
293
+ if (!Array.isArray(group.hooks)) group.hooks = [];
294
+ return group;
295
+ }
296
+
297
+ /** 向指定 matcher 的 SessionStart group 归位单个 command hook。 */
298
+ function ensureSessionStartCommand(groups, command, timeout, needle, matcher) {
299
+ const removed = removeSessionStartCommand(groups, needle);
300
+ const group = ensureSessionStartGroup(groups, matcher);
301
+ if (hasCommand(group.hooks, needle)) return removed;
302
+ group.hooks.push({
303
+ type: "command",
304
+ command,
305
+ timeout,
275
306
  });
276
307
  return true;
277
308
  }
@@ -290,8 +321,20 @@ function mergeHooks(hooksPath) {
290
321
  const groups = Array.isArray(config.hooks.SessionStart)
291
322
  ? config.hooks.SessionStart
292
323
  : [];
293
- ensureSessionStartCommand(groups, sessionStartCommand(config), 30, SESSION_START_SCRIPT);
294
- ensureSessionStartCommand(groups, flowerUpdateCommand(config), 8, FLOWER_UPDATE_HOOK_REL);
324
+ ensureSessionStartCommand(
325
+ groups,
326
+ sessionStartCommand(config),
327
+ SESSION_START_TIMEOUT,
328
+ SESSION_START_SCRIPT,
329
+ SESSION_START_MATCHER,
330
+ );
331
+ ensureSessionStartCommand(
332
+ groups,
333
+ flowerUpdateCommand(config),
334
+ SESSION_START_TIMEOUT,
335
+ FLOWER_UPDATE_HOOK_REL,
336
+ FLOWER_UPDATE_MATCHER,
337
+ );
295
338
  config.hooks.SessionStart = groups;
296
339
 
297
340
  const desired = JSON.stringify(config, null, 2) + "\n";
@@ -164,13 +164,43 @@ function actionForPolicy(policy, command, safety) {
164
164
  }
165
165
  return {
166
166
  mode: "ask",
167
- instruction: "先询问用户是否执行更新;用户确认后再运行推荐命令。",
167
+ instruction: "必须先询问用户是否执行推荐命令;用户明确确认前禁止运行推荐命令。",
168
168
  command,
169
169
  downgradedFromAuto: policy === "auto",
170
170
  downgradeReasons: policy === "auto" ? safety.reasons : [],
171
171
  };
172
172
  }
173
173
 
174
+ /** 给可执行状态补齐安全检查和 AI 动作建议。 */
175
+ function withAction(target, policy, result, command) {
176
+ const safety = safetyState(target, result.status, command);
177
+ return {
178
+ ...result,
179
+ safety,
180
+ ai: actionForPolicy(policy, command, safety),
181
+ };
182
+ }
183
+
184
+ /** 生成项目重叠加建议结果。 */
185
+ function projectOutOfSyncResult(base, target, remotePatch = {}) {
186
+ const command = selfUpdateCommand(target, { projectOnly: true });
187
+ return withAction(
188
+ target,
189
+ base.policy,
190
+ {
191
+ ...base,
192
+ status: "project_out_of_sync",
193
+ reason: "local_version_mismatch",
194
+ remote: { ...base.remote, ...remotePatch },
195
+ commands: {
196
+ recommended: command,
197
+ projectUpdate: projectUpdateCommand(target),
198
+ },
199
+ },
200
+ command,
201
+ );
202
+ }
203
+
174
204
  /** 计算 auto 策略安全门槛。 */
175
205
  export function safetyState(target, status, command) {
176
206
  const git = gitSafety(target);
@@ -211,6 +241,14 @@ export async function buildSelfCheck(target, options = {}) {
211
241
  const currentTrellis = trellisVersion();
212
242
  const projectTrellis = readProjectTrellisVersion(absoluteTarget);
213
243
  const projectFlower = typeof manifest?.flowerVersion === "string" ? manifest.flowerVersion : null;
244
+ const projectOutOfSyncReasons = [];
245
+ if (projectFlower && projectFlower !== currentFlower) {
246
+ projectOutOfSyncReasons.push("flower_version_mismatch");
247
+ }
248
+ if (projectTrellis && projectTrellis !== currentTrellis) {
249
+ projectOutOfSyncReasons.push("trellis_version_mismatch");
250
+ }
251
+ const projectOutOfSync = projectOutOfSyncReasons.length > 0;
214
252
 
215
253
  const base = {
216
254
  status: "up_to_date",
@@ -226,6 +264,8 @@ export async function buildSelfCheck(target, options = {}) {
226
264
  flowerVersion: projectFlower,
227
265
  trellisVersion: projectTrellis,
228
266
  manifestPresent: Boolean(manifest),
267
+ outOfSync: projectOutOfSync,
268
+ outOfSyncReasons: projectOutOfSyncReasons,
229
269
  },
230
270
  remote: {
231
271
  tags: updateCheck.lastRemote,
@@ -250,44 +290,35 @@ export async function buildSelfCheck(target, options = {}) {
250
290
  return { ...base, status: "skipped", reason: "npx_runtime" };
251
291
  }
252
292
 
253
- const projectOutOfSync =
254
- (projectFlower && projectFlower !== currentFlower) ||
255
- (projectTrellis && projectTrellis !== currentTrellis);
256
-
257
- if (projectOutOfSync) {
258
- const command = selfUpdateCommand(absoluteTarget, { projectOnly: true });
259
- const result = {
260
- ...base,
261
- status: "project_out_of_sync",
262
- reason: "local_version_mismatch",
263
- commands: {
264
- recommended: command,
265
- projectUpdate: projectUpdateCommand(absoluteTarget),
266
- },
267
- };
268
- const safety = safetyState(absoluteTarget, result.status, command);
269
- result.safety = safety;
270
- result.ai = actionForPolicy(updateCheck.policy, command, safety);
271
- return result;
272
- }
273
-
274
293
  let tags = updateCheck.lastRemote;
275
294
  if (!forceRemote && isRemoteCacheFresh(updateCheck, now)) {
276
295
  const recommendation = getUpdateRecommendation(currentFlower, tags);
277
296
  if (recommendation) {
278
297
  const command = selfUpdateCommand(absoluteTarget);
279
- const result = {
280
- ...base,
281
- status: "update_available",
282
- reason: "cached_remote_update",
283
- remote: { ...base.remote, tags, fromCache: true, skipped: true },
284
- recommendation,
285
- commands: { recommended: command, npm: recommendation.command },
286
- };
287
- const safety = safetyState(absoluteTarget, result.status, command);
288
- result.safety = safety;
289
- result.ai = actionForPolicy(updateCheck.policy, command, safety);
290
- return result;
298
+ return withAction(
299
+ absoluteTarget,
300
+ updateCheck.policy,
301
+ {
302
+ ...base,
303
+ status: "update_available",
304
+ reason: "cached_remote_update",
305
+ remote: { ...base.remote, tags, fromCache: true, skipped: true },
306
+ recommendation,
307
+ commands: {
308
+ recommended: command,
309
+ npm: recommendation.command,
310
+ projectUpdate: projectUpdateCommand(absoluteTarget),
311
+ },
312
+ },
313
+ command,
314
+ );
315
+ }
316
+ if (projectOutOfSync) {
317
+ return projectOutOfSyncResult(base, absoluteTarget, {
318
+ tags,
319
+ fromCache: true,
320
+ skipped: true,
321
+ });
291
322
  }
292
323
  return {
293
324
  ...base,
@@ -306,47 +337,55 @@ export async function buildSelfCheck(target, options = {}) {
306
337
  lastErrorCode: "fetch_failed",
307
338
  });
308
339
  }
340
+ const remotePatch = { tags: updateCheck.lastRemote, errorCode: "fetch_failed" };
341
+ if (projectOutOfSync) {
342
+ return projectOutOfSyncResult(base, absoluteTarget, remotePatch);
343
+ }
309
344
  return {
310
345
  ...base,
311
346
  status: "offline",
312
347
  reason: "fetch_failed",
313
- remote: { ...base.remote, tags: updateCheck.lastRemote, errorCode: "fetch_failed" },
348
+ remote: { ...base.remote, ...remotePatch },
314
349
  };
315
350
  }
316
351
 
317
352
  const recommendation = getUpdateRecommendation(currentFlower, tags);
318
- const status = recommendation ? "update_available" : "up_to_date";
353
+ const remoteStatus = recommendation ? "update_available" : "up_to_date";
319
354
  if (writeCache && manifest) {
320
355
  writeUpdateCheck(absoluteTarget, {
321
356
  lastCheckedAt: now.toISOString(),
322
357
  lastRemote: tags,
323
- lastStatus: status,
358
+ lastStatus: remoteStatus,
324
359
  lastErrorCode: null,
325
360
  });
326
361
  }
327
362
 
328
363
  if (!recommendation) {
364
+ if (projectOutOfSync) {
365
+ return projectOutOfSyncResult(base, absoluteTarget, { tags });
366
+ }
329
367
  return {
330
368
  ...base,
331
- status,
369
+ status: remoteStatus,
332
370
  remote: { ...base.remote, tags },
333
371
  };
334
372
  }
335
373
 
336
374
  const command = selfUpdateCommand(absoluteTarget);
337
- const result = {
338
- ...base,
339
- status,
340
- remote: { ...base.remote, tags },
341
- recommendation,
342
- commands: {
343
- recommended: command,
344
- npm: recommendation.command,
345
- projectUpdate: projectUpdateCommand(absoluteTarget),
375
+ return withAction(
376
+ absoluteTarget,
377
+ updateCheck.policy,
378
+ {
379
+ ...base,
380
+ status: remoteStatus,
381
+ remote: { ...base.remote, tags },
382
+ recommendation,
383
+ commands: {
384
+ recommended: command,
385
+ npm: recommendation.command,
386
+ projectUpdate: projectUpdateCommand(absoluteTarget),
387
+ },
346
388
  },
347
- };
348
- const safety = safetyState(absoluteTarget, result.status, command);
349
- result.safety = safety;
350
- result.ai = actionForPolicy(updateCheck.policy, command, safety);
351
- return result;
389
+ command,
390
+ );
352
391
  }