flower-trellis 0.4.4 → 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.
- package/enhancements/MANIFEST.json +1 -1
- package/package.json +1 -1
- package/src/assets/flower_update_hook.py +11 -0
- package/src/lib/codex-tweaks.js +58 -15
- package/src/lib/self-check.js +91 -52
package/package.json
CHANGED
|
@@ -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>")
|
package/src/lib/codex-tweaks.js
CHANGED
|
@@ -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
|
-
/**
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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(
|
|
294
|
-
|
|
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";
|
package/src/lib/self-check.js
CHANGED
|
@@ -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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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,
|
|
348
|
+
remote: { ...base.remote, ...remotePatch },
|
|
314
349
|
};
|
|
315
350
|
}
|
|
316
351
|
|
|
317
352
|
const recommendation = getUpdateRecommendation(currentFlower, tags);
|
|
318
|
-
const
|
|
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:
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
result.safety = safety;
|
|
350
|
-
result.ai = actionForPolicy(updateCheck.policy, command, safety);
|
|
351
|
-
return result;
|
|
389
|
+
command,
|
|
390
|
+
);
|
|
352
391
|
}
|