flower-trellis 0.6.10-beta.8 → 0.6.10
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 +15 -3
- package/enhancements/0.6/.agents/skills/trellis-flower-update/SKILL.md +14 -8
- package/enhancements/0.6/.claude/skills/trellis-flower-update/SKILL.md +14 -8
- package/enhancements/MANIFEST.json +2 -2
- package/package.json +3 -3
- package/src/assets/flower_session_start.py +15 -0
- package/src/cli.js +6 -0
- package/src/commands/update-all.js +291 -0
- package/src/constants.js +1 -0
- package/src/lib/cli-args.js +21 -1
- package/src/lib/self-check.js +13 -4
- package/src/lib/update-all-projects.js +160 -0
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ flower-trellis -v
|
|
|
100
100
|
| `trellis` | 项目级关闭、恢复或检查全部平台中的 Trellis 集成 |
|
|
101
101
|
| `self-check` | 输出启动更新检查 JSON,供 Codex / Claude Code hook 和 AI 自动化读取 |
|
|
102
102
|
| `self-update` | 受控升级 flower-trellis 并对目标项目执行完整 `flower-trellis update` 重叠加 |
|
|
103
|
+
| `update-all` | 扫描指定父目录并组合显式项目,批量追平 Flower/Trellis 版本 |
|
|
103
104
|
| `update-check` | 管理 `.flower/settings.json` 内的启动更新策略和提示节流 |
|
|
104
105
|
| `telemetry` | 查询、启用或停用用户级匿名安装遥测 |
|
|
105
106
|
| `plugin` | 管理 Flower Plugin、Marketplace 来源、GitLab 授权和作者校验 |
|
|
@@ -116,6 +117,7 @@ flower-trellis -v
|
|
|
116
117
|
| `--skills <a,b,...>` | 只安装指定技能(可省略 `trellis-` 前缀) |
|
|
117
118
|
| `--variant <old\|0.5\|0.6>` | 强制指定强化包变体(默认按 `.trellis/.version` 自动选) |
|
|
118
119
|
| `--target <dir>` | 目标目录(默认当前目录) |
|
|
120
|
+
| `--root <dir>` | `update-all` 扫描的父目录,可重复传入 |
|
|
119
121
|
| `--no-update-check` | 本次跳过 flower-trellis 新版本检测(等价环境变量 `FLOWER_NO_UPDATE_CHECK=1`) |
|
|
120
122
|
| `--backup-retention <n>` | `update` 成功后保留最近 n 份 `.trellis/.backup-<timestamp>` 快照(默认 1,`0` 表示本次不清理) |
|
|
121
123
|
|
|
@@ -145,18 +147,19 @@ flower-trellis enable --target .
|
|
|
145
147
|
|
|
146
148
|
项目处于 disabled 时,`flower-trellis update`、`self-update` 的项目更新链,以及 `plugin add/update/remove/replay` 会临时恢复必要入口,完成写操作后再次 detach,并校验最终仍为 disabled;外部 Plugin 内容不会被当作 Trellis 入口删除。直接运行上游 `trellis update` 不经过这一控制面,可能重新生成入口,此时 `flower-trellis status` 会报告 `drifted`。`disable` 也不等同于 `uninstall`:历史数据和恢复能力会继续保留。
|
|
147
149
|
|
|
148
|
-
## Astra 与 Sol 工作流提示
|
|
150
|
+
## Astra、Sol 与 Sol 6.1 工作流提示
|
|
149
151
|
|
|
150
|
-
Codex 的 SessionStart 在 `startup`、`clear`、`compact` 时,事件输入的当前模型精确为 `gpt-6-astra` 或 `gpt-6-sol`,才会在 state
|
|
152
|
+
Codex 的 SessionStart 在 `startup`、`clear`、`compact` 时,事件输入的当前模型精确为 `gpt-6-astra`、`gpt-6-sol` 或 `gpt-6.1-sol`,才会在 state 分段追加对应模型的一份英文工作流提示。三个模型使用相同的工作流正文和各自的模型标识;其他模型(包括 5.5)、缺失模型和未知别名不会追加;普通用户轮次不重复注入。
|
|
151
153
|
|
|
152
154
|
提示用于辅助遵循技能、工作流、模板和证据陈述,效果仍需行为对照验证。它不能删除宿主或历史指令,也不保证覆盖更高优先级要求。会话中切换模型后,要等下次 SessionStart 才重新判断是否追加。
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
三个模型默认分别开启。要单独关闭某个模型的提示,在项目 `.trellis/config.yaml` 已有的 `codex` 配置下合并对应字段,保留原有字段:
|
|
155
157
|
|
|
156
158
|
```yaml
|
|
157
159
|
codex:
|
|
158
160
|
astra_workflow_hint: false
|
|
159
161
|
sol_workflow_hint: false
|
|
162
|
+
sol_6_1_workflow_hint: false
|
|
160
163
|
```
|
|
161
164
|
|
|
162
165
|
任一字段设置 `true` 或删除可恢复该模型的默认值。开关只影响后续新增提示;做无历史干扰的对照时应使用新会话。原工作流上下文继续注入,Trellis 全局禁用、resume 和非交互跳过规则保持原有行为。`no-trellis` 仍只跳过本轮 UserPromptSubmit。
|
|
@@ -363,6 +366,15 @@ flower-trellis self-update --target . --dry-run
|
|
|
363
366
|
flower-trellis self-update --target . --yes
|
|
364
367
|
```
|
|
365
368
|
|
|
369
|
+
批量更新集中目录和零散项目时,可重复组合 `--root` 与 `--target`,真实路径会去重:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
flower-trellis update-all --root ~/projects --target /work/other-project --dry-run
|
|
373
|
+
flower-trellis update-all --root ~/projects --target /work/other-project --yes
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`--dry-run` 只读列出各项目预检结果,不安装 CLI,也不写项目或更新缓存;真实执行必须加 `--yes`。命令先检查每个项目的 Git clean 状态、活动任务和版本证据;脏工作区、活动任务及无法安全比较版本的项目会跳过并标明原因。远端版本无法确认时只追平到本机已安装版本。全局 Flower CLI 最多安装一次,随后按项目顺序执行完整 `update`;某项目失败仍继续后续项目。末尾汇总逐项结果,有跳过或失败时退出码为 `1`。命令不修改应用版本,也不自动提交或推送;更新后需按各 Git 仓库分别确认变更。
|
|
377
|
+
|
|
366
378
|
项目重叠加阶段默认等价于 Trellis 交互里的 **Apply Overwrite to all**:
|
|
367
379
|
|
|
368
380
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trellis-flower-update
|
|
3
|
-
description: "
|
|
3
|
+
description: "手动检查和执行单项目或父目录下多个已安装 Flower/Trellis 强化包升级。用于用户明确要求更新或升级 flower-trellis、Flower、Trellis 强化包、skill-garden 快照、项目 flower 版本追平,或自动更新提示被稍后/跳过后仍想在对话里升级时。不要用于用户说想发版、发布版本、release、打 tag、npm publish 或修改 package 版本号;这些属于项目发版流程。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Trellis Flower Update
|
|
@@ -11,9 +11,9 @@ description: "手动检查和执行已安装 Flower/Trellis 强化包升级。
|
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. 确认范围。单个 Flower 项目使用该项目路径;用户给出父目录并要求批量升级时,先用 `flower-trellis update-all --root <dir> --dry-run` 清点已安装项目,再逐项目核对。批量命令目前会整项目跳过脏 Git、活动任务和非 Git 目录;这些 `skipped` 只是待复核目标,不能直接作为无法升级的结论。
|
|
15
15
|
2. 检查 `flower-trellis` 是否可执行。缺失或入口异常且目标项目存在 `.trellis/scripts/flower_update_hook.py` 时,先运行目标项目的检测脚本:`python3 <target>/.trellis/scripts/flower_update_hook.py --bootstrap-only --target <target>`(路径按当前 shell 引用,Python 命令沿用项目配置)。遵循返回的 `<flower-cli-bootstrap>`:先征得当前成员确认,再安装锁定版本并验证;当前对话已授权这次安装时不重复确认。脚本不存在时说明项目缺少安装引导入口,不猜版本安装。未安装成功则停止依赖 CLI 的步骤,不循环追问或安装。
|
|
16
|
-
3. CLI
|
|
16
|
+
3. CLI 可用后,对每个目标运行人工检查,默认强制刷新远端版本证据:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
flower-trellis self-check --json --manual --force-remote --target <target>
|
|
@@ -26,10 +26,16 @@ flower-trellis self-check --json --manual --force-remote --target <target>
|
|
|
26
26
|
- `project_unknown`:说明 CLI 未发现新版,但项目 Flower 版本无法确认。linked worktree 进入 `trellis-worktree` 的 Flower Preparation,通过 `prepare --inherit-flower --source <source>` 补齐可验证记录后重新检查;来源缺失或冲突时如实报告,不推定需要重装。
|
|
27
27
|
- `disabled` / `offline` / `skipped`:说明原因;不要靠重置缓存伪造可执行状态。
|
|
28
28
|
5. 写入前遵守确认和安全门槛:
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- `
|
|
32
|
-
|
|
29
|
+
- 用户已明确要求执行升级时,该授权覆盖所指定范围内的项目,不因每个项目的 `safety.reasons` 重复询问;用户只是询问、查看或比较版本时,只展示结果并等待确认。
|
|
30
|
+
- `safety.reasons` 是自动更新的保守门槛,不等于人工升级的最终结论。`dirty_worktree`、`active_task` 和 `not_git_repo` 均需核对实际影响,不单独阻断升级。批量命令跳过的项目也按此规则逐项处理。
|
|
31
|
+
- 对脏 Git 项目读取精确 dirty 路径,对活动任务定位其任务文件;预演 Trellis 与 Plugin 更新,必要时用 Plugin JSON dry-run 获取目标路径。比较计划写入、删除的文件及目录范围与现有改动的交集。`self-update` 默认向 Trellis 传 `--force`,精确提交和失败备份都不能挽回已被覆盖的本地内容。确认无交集后执行推荐命令,保留原有业务和任务文件;活动任务的存在本身不要求另一次确认。
|
|
32
|
+
- 非 Git 项目若属于用户指定范围,预演并核对升级器备份与恢复能力后可升级;不能用 Git 精确提交验证其结果。有交集、预演失败或内容归属不明时,只停受影响项目,查明具体路径及能否无损保留;无法证明安全则报告真实冲突,不以 `--force` 绕过。
|
|
33
|
+
- CLI 或推荐命令缺失、版本证据不足、Git 状态读取失败等原因仍需按实际原因解决。不要把 `update-all --yes` 的整项目跳过当成全部已处理。
|
|
34
|
+
6. 升级后逐项目核对版本与原有改动,分别报告完成项和实际冲突,再处理本地提交:
|
|
35
|
+
- 单项目 `self-update` 返回 `post_action: run_trellis_push_confirmation` 时,按原单项目流程进入 `trellis-push`,展示精确文件与 message 并等待确认。
|
|
36
|
+
- 用户明确要求执行父目录或多个目标的批量升级且未排除提交时,同次授权覆盖升级产物的本地精确提交;`update-all` 的 `post_action: run_flower_batch_commit` 及逐项目补齐的成功结果均由 Flower 批量流程处理,不进入 `trellis-push`,不逐仓重复询问。只检查版本或 dry-run 时不提交。
|
|
37
|
+
- 按所属 Git 仓库收集升级器实际写入的精确路径,结合安装记录、预演结果与升级前状态核对归属;仅提交已证明属于本次升级的路径。逐仓重新核对分支、HEAD、冲突、staged 与文件内容,使用精确路径提交并核对提交文件集;保留其余 staged、未暂存和未跟踪变更,不借升级完成活动任务。无变更、非 Git、detached HEAD、真实路径交集或归属不明的仓库单独待处理,其它独立仓库继续。
|
|
38
|
+
- 批量结果只汇总成功提交数、文件数与待处理原因;用户要求时再展开路径。提交失败不改变升级状态。除非用户另有明确授权,不推送。
|
|
33
39
|
|
|
34
40
|
## Rules
|
|
35
41
|
|
|
@@ -38,4 +44,4 @@ flower-trellis self-check --json --manual --force-remote --target <target>
|
|
|
38
44
|
- 不使用 `update-check reset`、`snooze` 或 `skip` 作为升级绕过手段。
|
|
39
45
|
- 不运行 `npm run release`、不打 tag、不 publish,也不修改 `package.json` 版本号。
|
|
40
46
|
- 不把 `self-check --manual` 用在 SessionStart 自动 hook;自动路径必须继续尊重提示节流。
|
|
41
|
-
-
|
|
47
|
+
- 只在将覆盖现有内容、实际授权不足或证据无法核实时停下受影响项目;不得因为整个工作区脏或存在活动任务而跳过其它可安全升级的项目。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trellis-flower-update
|
|
3
|
-
description: "
|
|
3
|
+
description: "手动检查和执行单项目或父目录下多个已安装 Flower/Trellis 强化包升级。用于用户明确要求更新或升级 flower-trellis、Flower、Trellis 强化包、skill-garden 快照、项目 flower 版本追平,或自动更新提示被稍后/跳过后仍想在对话里升级时。不要用于用户说想发版、发布版本、release、打 tag、npm publish 或修改 package 版本号;这些属于项目发版流程。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Trellis Flower Update
|
|
@@ -11,9 +11,9 @@ description: "手动检查和执行已安装 Flower/Trellis 强化包升级。
|
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. 确认范围。单个 Flower 项目使用该项目路径;用户给出父目录并要求批量升级时,先用 `flower-trellis update-all --root <dir> --dry-run` 清点已安装项目,再逐项目核对。批量命令目前会整项目跳过脏 Git、活动任务和非 Git 目录;这些 `skipped` 只是待复核目标,不能直接作为无法升级的结论。
|
|
15
15
|
2. 检查 `flower-trellis` 是否可执行。缺失或入口异常且目标项目存在 `.trellis/scripts/flower_update_hook.py` 时,先运行目标项目的检测脚本:`python3 <target>/.trellis/scripts/flower_update_hook.py --bootstrap-only --target <target>`(路径按当前 shell 引用,Python 命令沿用项目配置)。遵循返回的 `<flower-cli-bootstrap>`:先征得当前成员确认,再安装锁定版本并验证;当前对话已授权这次安装时不重复确认。脚本不存在时说明项目缺少安装引导入口,不猜版本安装。未安装成功则停止依赖 CLI 的步骤,不循环追问或安装。
|
|
16
|
-
3. CLI
|
|
16
|
+
3. CLI 可用后,对每个目标运行人工检查,默认强制刷新远端版本证据:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
flower-trellis self-check --json --manual --force-remote --target <target>
|
|
@@ -26,10 +26,16 @@ flower-trellis self-check --json --manual --force-remote --target <target>
|
|
|
26
26
|
- `project_unknown`:说明 CLI 未发现新版,但项目 Flower 版本无法确认。linked worktree 进入 `trellis-worktree` 的 Flower Preparation,通过 `prepare --inherit-flower --source <source>` 补齐可验证记录后重新检查;来源缺失或冲突时如实报告,不推定需要重装。
|
|
27
27
|
- `disabled` / `offline` / `skipped`:说明原因;不要靠重置缓存伪造可执行状态。
|
|
28
28
|
5. 写入前遵守确认和安全门槛:
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- `
|
|
32
|
-
|
|
29
|
+
- 用户已明确要求执行升级时,该授权覆盖所指定范围内的项目,不因每个项目的 `safety.reasons` 重复询问;用户只是询问、查看或比较版本时,只展示结果并等待确认。
|
|
30
|
+
- `safety.reasons` 是自动更新的保守门槛,不等于人工升级的最终结论。`dirty_worktree`、`active_task` 和 `not_git_repo` 均需核对实际影响,不单独阻断升级。批量命令跳过的项目也按此规则逐项处理。
|
|
31
|
+
- 对脏 Git 项目读取精确 dirty 路径,对活动任务定位其任务文件;预演 Trellis 与 Plugin 更新,必要时用 Plugin JSON dry-run 获取目标路径。比较计划写入、删除的文件及目录范围与现有改动的交集。`self-update` 默认向 Trellis 传 `--force`,精确提交和失败备份都不能挽回已被覆盖的本地内容。确认无交集后执行推荐命令,保留原有业务和任务文件;活动任务的存在本身不要求另一次确认。
|
|
32
|
+
- 非 Git 项目若属于用户指定范围,预演并核对升级器备份与恢复能力后可升级;不能用 Git 精确提交验证其结果。有交集、预演失败或内容归属不明时,只停受影响项目,查明具体路径及能否无损保留;无法证明安全则报告真实冲突,不以 `--force` 绕过。
|
|
33
|
+
- CLI 或推荐命令缺失、版本证据不足、Git 状态读取失败等原因仍需按实际原因解决。不要把 `update-all --yes` 的整项目跳过当成全部已处理。
|
|
34
|
+
6. 升级后逐项目核对版本与原有改动,分别报告完成项和实际冲突,再处理本地提交:
|
|
35
|
+
- 单项目 `self-update` 返回 `post_action: run_trellis_push_confirmation` 时,按原单项目流程进入 `trellis-push`,展示精确文件与 message 并等待确认。
|
|
36
|
+
- 用户明确要求执行父目录或多个目标的批量升级且未排除提交时,同次授权覆盖升级产物的本地精确提交;`update-all` 的 `post_action: run_flower_batch_commit` 及逐项目补齐的成功结果均由 Flower 批量流程处理,不进入 `trellis-push`,不逐仓重复询问。只检查版本或 dry-run 时不提交。
|
|
37
|
+
- 按所属 Git 仓库收集升级器实际写入的精确路径,结合安装记录、预演结果与升级前状态核对归属;仅提交已证明属于本次升级的路径。逐仓重新核对分支、HEAD、冲突、staged 与文件内容,使用精确路径提交并核对提交文件集;保留其余 staged、未暂存和未跟踪变更,不借升级完成活动任务。无变更、非 Git、detached HEAD、真实路径交集或归属不明的仓库单独待处理,其它独立仓库继续。
|
|
38
|
+
- 批量结果只汇总成功提交数、文件数与待处理原因;用户要求时再展开路径。提交失败不改变升级状态。除非用户另有明确授权,不推送。
|
|
33
39
|
|
|
34
40
|
## Rules
|
|
35
41
|
|
|
@@ -38,4 +44,4 @@ flower-trellis self-check --json --manual --force-remote --target <target>
|
|
|
38
44
|
- 不使用 `update-check reset`、`snooze` 或 `skip` 作为升级绕过手段。
|
|
39
45
|
- 不运行 `npm run release`、不打 tag、不 publish,也不修改 `package.json` 版本号。
|
|
40
46
|
- 不把 `self-check --manual` 用在 SessionStart 自动 hook;自动路径必须继续尊重提示节流。
|
|
41
|
-
-
|
|
47
|
+
- 只在将覆盖现有内容、实际授权不足或证据无法核实时停下受影响项目;不得因为整个工作区脏或存在活动任务而跳过其它可安全升级的项目。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-09-
|
|
2
|
+
"syncedAt": "2026-09-30T15:12:01.883Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "7d02e3d812cd817db285fc51a4656c55b497452b",
|
|
5
5
|
"common": {
|
|
6
6
|
"codexSkills": [
|
|
7
7
|
"aliyun-ops",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.6.10
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"commit-and-tag-version": "^12.7.3"
|
|
71
71
|
},
|
|
72
72
|
"flowerReleaseNotes": {
|
|
73
|
-
"version": "0.6.10
|
|
73
|
+
"version": "0.6.10",
|
|
74
74
|
"source": "CHANGELOG.md",
|
|
75
|
-
"body": "### ✨ 新功能 Features\n\n* **flower:** 同步新版任务 Brief 模板 ([d6702ec](https://github.com/SilentFlower/flower-trellis/commit/d6702ece1f58cb22290bce8fe02de0ea28d852e3))",
|
|
75
|
+
"body": "### ✨ 新功能 Features\n\n* **aliyun-ops:** 新增 ACK 受控切换镜像与环境变量 ([70527ca](https://github.com/SilentFlower/flower-trellis/commit/70527ca2372c22d124a6a38ab3331be5c2119bc0))\n* **aliyun-ops:** 集成 ACK Workbench 只读查询 ([224ef02](https://github.com/SilentFlower/flower-trellis/commit/224ef029beb82e032bb4e336bdc7febe29a96982))\n* **codex:** 为 gpt-6.1-sol 添加专属工作流提示 ([5420292](https://github.com/SilentFlower/flower-trellis/commit/54202928116136d7bdc7e62e8c3e0abc109c2215))\n* **flower:** 同步新版任务 Brief 模板 ([d6702ec](https://github.com/SilentFlower/flower-trellis/commit/d6702ece1f58cb22290bce8fe02de0ea28d852e3))\n* **flower:** 批量更新后由 Flower 统一处理项目本地提交 ([110766d](https://github.com/SilentFlower/flower-trellis/commit/110766d15513de1bea41e54e9fcdfbabb25395a5))\n* **flower:** 支持批量更新多个项目 ([ef32c2e](https://github.com/SilentFlower/flower-trellis/commit/ef32c2e47dcf326c83aceb67549e3374fc68fa01))\n* **flower:** 支持自动 GC 提交默认放行 ([79ef652](https://github.com/SilentFlower/flower-trellis/commit/79ef652abf793c4b84e8b5150b4808be02ad3eec))\n* **flower:** 默认只保留一份升级备份并支持 Sol 提示 ([7704dc9](https://github.com/SilentFlower/flower-trellis/commit/7704dc9e190abc5d6e57de517e2dfc0ec31cbbae))\n* **hooks:** 优化 workflow-state 条件注入 ([ecd9555](https://github.com/SilentFlower/flower-trellis/commit/ecd9555f9c8959a3f3c862f9cda8bf8e7153fbd5))\n* **session:** 精简 SessionStart 开场注入 ([2b68c13](https://github.com/SilentFlower/flower-trellis/commit/2b68c1367199cdb3254d0adc639db20cc0b2b648))\n* **task:** 支持精确收敛未跟踪旧任务 ([4b6871e](https://github.com/SilentFlower/flower-trellis/commit/4b6871e22abc3566aca9373d480ab9bf3d516bd3))\n* **task:** 重构 Close 与物理 GC 生命周期 ([20c14f0](https://github.com/SilentFlower/flower-trellis/commit/20c14f032825eac226887c4334ef6e6e69ffc3dd))\n\n\n### 🐛 修复 Bug Fixes\n\n* **aliyun-ops:** 完善 SLS 引导与跨平台回归 ([e146855](https://github.com/SilentFlower/flower-trellis/commit/e146855122f91ef48c1155baf3aaf63681a0557b))\n* **release-ci:** 修复无依赖发布快照同步 ([a5c954a](https://github.com/SilentFlower/flower-trellis/commit/a5c954aee20d85d455f74583a0597e6b859223c3))\n* **workflow:** 阻止新会话继承旧任务 ([156e63f](https://github.com/SilentFlower/flower-trellis/commit/156e63f668d94d0a35f5f15c9b98998f4cddd411))",
|
|
76
76
|
"truncated": false
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
@@ -20,6 +20,7 @@ MAX_PART_CHARS = 8000
|
|
|
20
20
|
WORKFLOW_BLOCK = re.compile(r"<trellis-workflow>\n(.*?)\n</trellis-workflow>\n*", re.DOTALL)
|
|
21
21
|
ASTRA_MODEL = "gpt-6-astra"
|
|
22
22
|
SOL_MODEL = "gpt-6-sol"
|
|
23
|
+
SOL_6_1_MODEL = "gpt-6.1-sol"
|
|
23
24
|
MODEL_HINT_MAX_BYTES = 2048
|
|
24
25
|
WORKFLOW_STATE_REFRESH_ARG = "--trellis-session-start-refresh"
|
|
25
26
|
MODEL_WORKFLOW_HINT_BODY = """Applies only while the active model is {model}; it does not apply after switching models. Perform checks internally, without a routine checklist report. Keep ordinary answers brief.
|
|
@@ -37,6 +38,9 @@ ASTRA_WORKFLOW_HINT = (f'<trellis-astra-workflow-hint model="{ASTRA_MODEL}" vers
|
|
|
37
38
|
SOL_WORKFLOW_HINT = (f'<trellis-sol-workflow-hint model="{SOL_MODEL}" version="1">\n'
|
|
38
39
|
f'{MODEL_WORKFLOW_HINT_BODY.format(model=SOL_MODEL)}\n'
|
|
39
40
|
'</trellis-sol-workflow-hint>')
|
|
41
|
+
SOL_6_1_WORKFLOW_HINT = (f'<trellis-sol-6-1-workflow-hint model="{SOL_6_1_MODEL}" version="1">\n'
|
|
42
|
+
f'{MODEL_WORKFLOW_HINT_BODY.format(model=SOL_6_1_MODEL)}\n'
|
|
43
|
+
'</trellis-sol-6-1-workflow-hint>')
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
def _model_workflow_hint(root: Path, config_key: str, hint: str, label: str) -> str:
|
|
@@ -88,6 +92,15 @@ def _sol_workflow_hint(root: Path) -> str:
|
|
|
88
92
|
return _model_workflow_hint(root, "sol_workflow_hint", SOL_WORKFLOW_HINT, "Sol")
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
def _sol_6_1_workflow_hint(root: Path) -> str:
|
|
96
|
+
"""读取 Sol 6.1 独立开关并返回提示。
|
|
97
|
+
|
|
98
|
+
@param root: 当前部署项目根目录。
|
|
99
|
+
@return: Sol 6.1 提示块;关闭时为空串。
|
|
100
|
+
"""
|
|
101
|
+
return _model_workflow_hint(root, "sol_6_1_workflow_hint", SOL_6_1_WORKFLOW_HINT, "Sol 6.1")
|
|
102
|
+
|
|
103
|
+
|
|
91
104
|
def split_workflow(summary: str) -> dict[str, str]:
|
|
92
105
|
"""按完整章节拆分工作流,保持原文和顺序。
|
|
93
106
|
|
|
@@ -319,6 +332,8 @@ def render_part(root: Path, hook: str, part: str, hook_input: dict) -> dict | No
|
|
|
319
332
|
hint_builder, label = _astra_workflow_hint, "Astra"
|
|
320
333
|
elif model == SOL_MODEL:
|
|
321
334
|
hint_builder, label = _sol_workflow_hint, "Sol"
|
|
335
|
+
elif model == SOL_6_1_MODEL:
|
|
336
|
+
hint_builder, label = _sol_6_1_workflow_hint, "Sol 6.1"
|
|
322
337
|
if hint_builder is not None:
|
|
323
338
|
try:
|
|
324
339
|
hint = hint_builder(root)
|
package/src/cli.js
CHANGED
|
@@ -78,6 +78,7 @@ function printHelp() {
|
|
|
78
78
|
flower-trellis self-check --json [--manual] [--target <dir>]
|
|
79
79
|
输出启动更新检查 JSON
|
|
80
80
|
flower-trellis self-update --target <dir> --yes 自更新 + 项目重叠加
|
|
81
|
+
flower-trellis update-all --root <dir> --target <dir> 批量发现并更新多个项目
|
|
81
82
|
flower-trellis update-check <get|set|disable|enable|snooze|skip|reset>
|
|
82
83
|
管理启动更新策略
|
|
83
84
|
flower-trellis telemetry <status|enable|disable> 管理匿名安装遥测
|
|
@@ -98,6 +99,7 @@ flower 自有 flag:
|
|
|
98
99
|
--skills <a,b,...> 只装指定技能(支持去 trellis- 前缀匹配)
|
|
99
100
|
--variant <old|0.5|0.6> 强制强化包变体(默认按 .trellis/.version 自动选)
|
|
100
101
|
--target <dir> 目标目录(默认当前目录)
|
|
102
|
+
--root <dir> update-all 扫描目录(可重复)
|
|
101
103
|
--no-update-check 本次跳过 flower-trellis 新版本检测(等价 FLOWER_NO_UPDATE_CHECK=1)
|
|
102
104
|
--backup-retention <n> update 成功后保留最近 n 份升级备份(默认 ${DEFAULT_UPDATE_BACKUP_RETENTION},0=不清理)
|
|
103
105
|
|
|
@@ -172,6 +174,10 @@ async function main() {
|
|
|
172
174
|
} else if (cmd === "self-update") {
|
|
173
175
|
const { selfUpdate } = await import("./commands/self-update.js");
|
|
174
176
|
await selfUpdate(ctx);
|
|
177
|
+
} else if (cmd === "update-all") {
|
|
178
|
+
const { updateAll } = await import("./commands/update-all.js");
|
|
179
|
+
const code = await updateAll(ctx);
|
|
180
|
+
if (code !== 0) process.exitCode = code;
|
|
175
181
|
} else if (cmd === "update-check") {
|
|
176
182
|
const { updateCheck } = await import("./commands/update-check.js");
|
|
177
183
|
await updateCheck(ctx);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { hasHelpFlag } from "../lib/cli-args.js";
|
|
5
|
+
import { gitSafety } from "../lib/self-check.js";
|
|
6
|
+
import { readProjectVersions } from "../lib/telemetry-context.js";
|
|
7
|
+
import {
|
|
8
|
+
compareVersions,
|
|
9
|
+
fetchPackageDistTags,
|
|
10
|
+
getUpdateRecommendation,
|
|
11
|
+
installFlowerVersion,
|
|
12
|
+
} from "../lib/update-check.js";
|
|
13
|
+
import { ProjectStore } from "../plugin/state/project-store.js";
|
|
14
|
+
import { discoverFlowerProjects, gitRepositoryRoot, projectTaskSafety } from "../lib/update-all-projects.js";
|
|
15
|
+
import { flowerVersion, trellisVersion } from "../lib/versions.js";
|
|
16
|
+
|
|
17
|
+
/** 打印批量更新命令帮助。 */
|
|
18
|
+
function printUpdateAllHelp() {
|
|
19
|
+
console.log(`flower-trellis update-all — 批量更新多个 Flower 项目
|
|
20
|
+
|
|
21
|
+
用法:
|
|
22
|
+
flower-trellis update-all --root <dir> [--root <dir>...] --dry-run
|
|
23
|
+
flower-trellis update-all --target <dir> [--target <dir>...] --yes
|
|
24
|
+
flower-trellis update-all --root <dir> --target <dir> --yes
|
|
25
|
+
|
|
26
|
+
选项:
|
|
27
|
+
--root <dir> 扫描指定父目录,可重复
|
|
28
|
+
--target <dir> 指定已安装 Flower 的项目,可重复
|
|
29
|
+
--dry-run 只读预览,不安装 CLI 或修改项目与缓存
|
|
30
|
+
-y, --yes 确认真实更新
|
|
31
|
+
--no-update-check 不查询远端,仅追平已安装的 Flower/Trellis 版本
|
|
32
|
+
|
|
33
|
+
脏 Git 工作区、活动任务和版本证据缺失的项目会跳过;有跳过或失败时退出码为 1。
|
|
34
|
+
远端无法确认时仅使用本机版本。项目更新后需核对 Git 变更,本命令不提交或推送。`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 定位 npm 全局安装的入口,保证后续子进程确实使用安装后的包。 */
|
|
38
|
+
function installedCliScript() {
|
|
39
|
+
const result = spawnSync("npm", ["root", "-g"], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
shell: process.platform === "win32",
|
|
43
|
+
});
|
|
44
|
+
if (result.error || result.status !== 0) {
|
|
45
|
+
throw new Error(`无法定位 npm 全局目录(${result.error?.message || result.stderr?.trim() || `退出码 ${result.status ?? 1}`})`);
|
|
46
|
+
}
|
|
47
|
+
const script = path.join(String(result.stdout).trim(), "flower-trellis", "bin", "flower-trellis.js");
|
|
48
|
+
if (!fs.existsSync(script)) throw new Error(`全局 Flower CLI 不存在: ${script}`);
|
|
49
|
+
return script;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 通过 Node 参数数组启动已安装 CLI,路径不交给 shell 拼接。 */
|
|
53
|
+
function runInstalledCli(script, args, options = {}) {
|
|
54
|
+
return spawnSync(process.execPath, [script, ...args], {
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
env: { ...process.env, FLOWER_NO_UPDATE_CHECK: "1", ...options.env },
|
|
57
|
+
encoding: options.capture ? "utf8" : undefined,
|
|
58
|
+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 从新进程读取实际 CLI 版本,避免全局安装后继续信任旧模块缓存。 */
|
|
63
|
+
function inspectInstalledCli() {
|
|
64
|
+
const script = installedCliScript();
|
|
65
|
+
const packageRoot = path.dirname(path.dirname(script));
|
|
66
|
+
const result = runInstalledCli(script, ["self-check", "--json", "--target", packageRoot, "--no-update-check"], {
|
|
67
|
+
cwd: packageRoot,
|
|
68
|
+
capture: true,
|
|
69
|
+
env: { FLOWER_NO_TELEMETRY: "1" },
|
|
70
|
+
});
|
|
71
|
+
if (result.error || result.status !== 0) {
|
|
72
|
+
throw new Error(`已安装 CLI 版本核对失败(${result.error?.message || result.stderr?.trim() || `退出码 ${result.status ?? 1}`})`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const current = JSON.parse(result.stdout).current;
|
|
76
|
+
if (!current?.flowerVersion || !current?.bundledTrellisVersion) throw new Error("缺少版本字段");
|
|
77
|
+
return { flower: current.flowerVersion, trellis: current.bundledTrellisVersion, script };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`已安装 CLI 版本结果无效(${error.message})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 使用已安装 CLI 完整更新一个项目。 */
|
|
84
|
+
function runProjectUpdate(target, installed) {
|
|
85
|
+
return runInstalledCli(installed.script, ["update", "--target", target, "--no-update-check", "--force", "-y"], { cwd: target });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 将子进程失败转换为可读原因。 */
|
|
89
|
+
function processFailure(result) {
|
|
90
|
+
return result?.error?.message || `退出码 ${result?.status ?? 1}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 防止把已安装版本较新的项目降级,或把无法比较的版本误判为相同。 */
|
|
94
|
+
function versionBlockReason(project, desired) {
|
|
95
|
+
for (const key of ["flower", "trellis"]) {
|
|
96
|
+
const relation = compareVersions(project[key], desired[key]);
|
|
97
|
+
if (relation === 1) return `${key} 项目版本高于目标版本(${project[key]} > ${desired[key]})`;
|
|
98
|
+
if (relation === 0 && project[key] !== desired[key]) {
|
|
99
|
+
return `${key} 版本无法比较(${project[key]} / ${desired[key]})`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 校验现代 Plugin 状态后读取项目版本,避免旧 manifest 掩盖损坏状态。 */
|
|
106
|
+
function readValidatedProjectVersions(target) {
|
|
107
|
+
const store = new ProjectStore(target);
|
|
108
|
+
store.readLock();
|
|
109
|
+
store.readState();
|
|
110
|
+
return readProjectVersions(target);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 输出逐项目状态和计数;成功结果由 Flower 批量流程接续本地提交。 */
|
|
114
|
+
function printResult(rows, options, log) {
|
|
115
|
+
const labels = {
|
|
116
|
+
planned: "待更新",
|
|
117
|
+
updated: "已更新",
|
|
118
|
+
up_to_date: "已是最新版",
|
|
119
|
+
skipped: "跳过",
|
|
120
|
+
failed: "失败",
|
|
121
|
+
};
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const prefix = row.status === "updated" ? " ✓ " : " · ";
|
|
124
|
+
log(`${prefix}${labels[row.status]}: ${row.target}${row.reason ? ` (${row.reason})` : ""}`);
|
|
125
|
+
}
|
|
126
|
+
const counts = Object.fromEntries(Object.keys(labels).map((status) => [status, rows.filter((row) => row.status === status).length]));
|
|
127
|
+
log(` · 汇总: 待更新 ${counts.planned},已更新 ${counts.updated},已是最新版 ${counts.up_to_date},跳过 ${counts.skipped},失败 ${counts.failed}`);
|
|
128
|
+
if (options.dryRun) {
|
|
129
|
+
log(" · 写入:否");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const attempted = rows.filter((row) => row.attempted);
|
|
133
|
+
if (attempted.length > 0) {
|
|
134
|
+
log("<flower-update-result>");
|
|
135
|
+
log(`status: ${counts.failed || counts.skipped ? "partial" : "completed"}`);
|
|
136
|
+
log(`updated_targets: ${JSON.stringify(rows.filter((row) => row.status === "updated").map((row) => row.target))}`);
|
|
137
|
+
log(`attempted_targets: ${JSON.stringify(attempted.map((row) => row.target))}`);
|
|
138
|
+
if (counts.updated > 0) {
|
|
139
|
+
log("post_action: run_flower_batch_commit");
|
|
140
|
+
log("ai_instruction: 按所属 Git 仓库核对并精确本地提交已更新项目的 Flower/Trellis 产物,保留其它变更;异常仓库单独待处理,汇总简要结果,不推送。");
|
|
141
|
+
}
|
|
142
|
+
log("</flower-update-result>");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 批量更新显式指定或在根目录发现的 Flower 项目。
|
|
148
|
+
* @param {object} ctx CLI 解析上下文
|
|
149
|
+
* @param {{fetchTags?:Function,install?:Function,inspectCli?:Function,updateProject?:Function,log?:Function}} [options] 测试替身
|
|
150
|
+
* @returns {Promise<number>} 0 表示全部完成,1 表示存在未完成项目
|
|
151
|
+
*/
|
|
152
|
+
export async function updateAll(ctx, options = {}) {
|
|
153
|
+
if (hasHelpFlag(ctx.passthrough)) {
|
|
154
|
+
printUpdateAllHelp();
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const flags = ctx.passthrough;
|
|
158
|
+
const unknown = flags.filter((arg) => !["--dry-run", "--yes", "-y"].includes(arg));
|
|
159
|
+
if (unknown.length || ctx.forwarded?.length) {
|
|
160
|
+
throw new Error(`update-all 不支持参数: ${[...unknown, ...(ctx.forwarded || [])].join(" ")}`);
|
|
161
|
+
}
|
|
162
|
+
const dryRun = flags.includes("--dry-run");
|
|
163
|
+
if (!dryRun && !flags.includes("--yes") && !flags.includes("-y")) {
|
|
164
|
+
throw new Error("update-all 执行写入前需要 --yes;如需预览请使用 --dry-run");
|
|
165
|
+
}
|
|
166
|
+
if (!ctx.roots.length && !ctx.targets.length) {
|
|
167
|
+
throw new Error("update-all 至少需要一个 --root 或 --target");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const log = options.log || console.log;
|
|
171
|
+
const { projects, errors } = discoverFlowerProjects(ctx.roots, ctx.targets);
|
|
172
|
+
const rows = errors.map(({ target, reason }) => ({ target, status: "failed", reason }));
|
|
173
|
+
if (!projects.length && !errors.length) {
|
|
174
|
+
rows.push({ target: ctx.roots.join(", "), status: "failed", reason: "未发现已安装 Flower 的项目" });
|
|
175
|
+
}
|
|
176
|
+
const gitChecks = new Map();
|
|
177
|
+
for (const target of projects) {
|
|
178
|
+
const gitRoot = gitRepositoryRoot(target);
|
|
179
|
+
if (!gitRoot) {
|
|
180
|
+
rows.push({ target, status: "skipped", reason: "不是 Git 工作区" });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!gitChecks.has(gitRoot)) gitChecks.set(gitRoot, gitSafety(gitRoot));
|
|
184
|
+
const git = gitChecks.get(gitRoot);
|
|
185
|
+
if (!git.clean) {
|
|
186
|
+
rows.push({ target, status: "skipped", reason: git.reason === "dirty_worktree" ? `Git 工作区有 ${git.dirtyCount} 项变动` : `Git 检查失败(${git.reason})` });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const taskSafety = projectTaskSafety(target);
|
|
190
|
+
if (!taskSafety.safe) {
|
|
191
|
+
rows.push({ target, status: "skipped", reason: taskSafety.reason });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const version = readValidatedProjectVersions(target);
|
|
196
|
+
if (!version.flower || !version.trellis) {
|
|
197
|
+
rows.push({ target, status: "skipped", reason: "Flower/Trellis 项目版本证据缺失" });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
rows.push({ target, status: "candidate", version });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
rows.push({ target, status: "skipped", reason: `读取项目版本失败(${error.message})` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const local = { flower: flowerVersion(), trellis: trellisVersion() };
|
|
207
|
+
let tags = null;
|
|
208
|
+
if (projects.length && ctx.updateCheck !== false && !process.env.FLOWER_NO_UPDATE_CHECK) {
|
|
209
|
+
try {
|
|
210
|
+
tags = await (options.fetchTags || fetchPackageDistTags)();
|
|
211
|
+
} catch {
|
|
212
|
+
tags = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const recommendation = getUpdateRecommendation(local.flower, tags);
|
|
216
|
+
const desiredFlower = recommendation?.version || local.flower;
|
|
217
|
+
log(`update-all ${dryRun ? "dry-run" : "执行"}:`);
|
|
218
|
+
log(` · 远端版本: ${tags ? `已确认(${recommendation ? recommendation.version : "无更新"})` : "未确认,仅使用本机版本"}`);
|
|
219
|
+
log(` · 目标 Flower: ${desiredFlower}`);
|
|
220
|
+
log(` · 目标 Trellis: ${recommendation ? "安装新版 CLI 后核对" : local.trellis}`);
|
|
221
|
+
if (recommendation) log(` · 全局升级: ${recommendation.command}`);
|
|
222
|
+
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
if (row.status !== "candidate") continue;
|
|
225
|
+
const reason = versionBlockReason(row.version, { flower: desiredFlower, trellis: recommendation ? row.version.trellis : local.trellis });
|
|
226
|
+
if (reason) {
|
|
227
|
+
row.status = "skipped";
|
|
228
|
+
row.reason = reason;
|
|
229
|
+
} else if (recommendation || row.version.flower !== local.flower || row.version.trellis !== local.trellis) {
|
|
230
|
+
row.status = "planned";
|
|
231
|
+
} else {
|
|
232
|
+
row.status = "up_to_date";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!dryRun && rows.some((row) => row.status === "planned")) {
|
|
237
|
+
let installed = local;
|
|
238
|
+
const firstPlanned = rows.find((row) => row.status === "planned").target;
|
|
239
|
+
try {
|
|
240
|
+
if (recommendation) {
|
|
241
|
+
log(" · 正在安装 Flower 新版本");
|
|
242
|
+
const result = (options.install || installFlowerVersion)(recommendation.version, { cwd: firstPlanned });
|
|
243
|
+
if (result?.status !== 0) throw new Error(`全局安装失败(${processFailure(result)})`);
|
|
244
|
+
}
|
|
245
|
+
installed = (options.inspectCli || inspectInstalledCli)();
|
|
246
|
+
if (installed.flower !== desiredFlower || !installed.trellis || installed.trellis === "(未安装)") {
|
|
247
|
+
throw new Error(`已安装 CLI 版本不符(Flower ${installed.flower || "unknown"},Trellis ${installed.trellis || "unknown"})`);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
for (const row of rows) {
|
|
251
|
+
if (row.status === "planned") {
|
|
252
|
+
row.status = "failed";
|
|
253
|
+
row.reason = error.message;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
printResult(rows, { dryRun }, log);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const row of rows) {
|
|
261
|
+
if (row.status !== "planned") continue;
|
|
262
|
+
const reason = versionBlockReason(row.version, installed);
|
|
263
|
+
if (reason) {
|
|
264
|
+
row.status = "skipped";
|
|
265
|
+
row.reason = reason;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (row.version.flower === installed.flower && row.version.trellis === installed.trellis) {
|
|
269
|
+
row.status = "up_to_date";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
log(` · 正在更新: ${row.target}`);
|
|
273
|
+
row.attempted = true;
|
|
274
|
+
try {
|
|
275
|
+
const result = (options.updateProject || runProjectUpdate)(row.target, installed);
|
|
276
|
+
if (result?.status !== 0) throw new Error(processFailure(result));
|
|
277
|
+
const actual = readValidatedProjectVersions(row.target);
|
|
278
|
+
if (actual.flower !== installed.flower || actual.trellis !== installed.trellis) {
|
|
279
|
+
throw new Error(`更新后版本未追平(Flower ${actual.flower || "unknown"},Trellis ${actual.trellis || "unknown"})`);
|
|
280
|
+
}
|
|
281
|
+
row.status = "updated";
|
|
282
|
+
} catch (error) {
|
|
283
|
+
row.status = "failed";
|
|
284
|
+
row.reason = `项目更新失败(${error.message})`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
printResult(rows, { dryRun }, log);
|
|
290
|
+
return rows.some((row) => row.status === "failed" || row.status === "skipped") || projects.length === 0 ? 1 : 0;
|
|
291
|
+
}
|
package/src/constants.js
CHANGED
package/src/lib/cli-args.js
CHANGED
|
@@ -25,6 +25,8 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
25
25
|
let variant = null;
|
|
26
26
|
let target = cwd;
|
|
27
27
|
let targetExplicit = false;
|
|
28
|
+
const targets = [];
|
|
29
|
+
const roots = [];
|
|
28
30
|
let updateCheck = true;
|
|
29
31
|
let backupRetention = DEFAULT_UPDATE_BACKUP_RETENTION;
|
|
30
32
|
const skills = [];
|
|
@@ -60,10 +62,26 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
60
62
|
case "--variant":
|
|
61
63
|
variant = argv[++i] || null;
|
|
62
64
|
break;
|
|
63
|
-
case "--target":
|
|
65
|
+
case "--target": {
|
|
66
|
+
const value = argv[i + 1];
|
|
67
|
+
if (command === "update-all" && (!value || value.startsWith("-"))) {
|
|
68
|
+
targets.push(null);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
64
71
|
target = path.resolve(cwd, argv[++i] || ".");
|
|
65
72
|
targetExplicit = true;
|
|
73
|
+
if (command === "update-all") targets.push(target);
|
|
66
74
|
break;
|
|
75
|
+
}
|
|
76
|
+
case "--root": {
|
|
77
|
+
if (command !== "update-all") {
|
|
78
|
+
passthrough.push(a);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const value = argv[i + 1];
|
|
82
|
+
roots.push(!value || value.startsWith("-") ? null : path.resolve(cwd, argv[++i]));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
67
85
|
case "--no-update-check":
|
|
68
86
|
updateCheck = false;
|
|
69
87
|
break;
|
|
@@ -88,6 +106,8 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
88
106
|
ctx: {
|
|
89
107
|
target: path.resolve(target),
|
|
90
108
|
targetExplicit,
|
|
109
|
+
targets,
|
|
110
|
+
roots,
|
|
91
111
|
passthrough,
|
|
92
112
|
enhance,
|
|
93
113
|
enhanceOnly,
|
package/src/lib/self-check.js
CHANGED
|
@@ -124,11 +124,16 @@ async function fetchMissingReleaseNotes(target, projectEvidence, writeCache, ran
|
|
|
124
124
|
return releaseNotes;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
|
|
127
|
+
/**
|
|
128
|
+
* 检查目标目录 Git 工作区是否干净。
|
|
129
|
+
* @param {string} target 目标目录
|
|
130
|
+
* @returns {{clean:boolean,reason:string|null,dirtyCount?:number}} 检查结果
|
|
131
|
+
*/
|
|
132
|
+
export function gitSafety(target) {
|
|
129
133
|
const common = {
|
|
130
134
|
cwd: target,
|
|
131
135
|
encoding: "utf8",
|
|
136
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
132
137
|
shell: process.platform === "win32",
|
|
133
138
|
stdio: ["ignore", "pipe", "pipe"],
|
|
134
139
|
timeout: 1500,
|
|
@@ -147,8 +152,12 @@ function gitSafety(target) {
|
|
|
147
152
|
: { clean: true, reason: null, dirtyCount: 0 };
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
155
|
+
/**
|
|
156
|
+
* 查找 active 或 in_progress 的 Trellis 任务。
|
|
157
|
+
* @param {string} target 项目根目录
|
|
158
|
+
* @returns {boolean} 是否存在活动任务
|
|
159
|
+
*/
|
|
160
|
+
export function hasActiveTask(target) {
|
|
152
161
|
const tasksDir = path.join(target, ".trellis", "tasks");
|
|
153
162
|
const stack = [tasksDir];
|
|
154
163
|
while (stack.length) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRECTORIES = new Set([".git", ".trellis", ".flower", "node_modules"]);
|
|
6
|
+
const INACTIVE_TASK_STATUSES = new Set(["planning", "completed", "done"]);
|
|
7
|
+
|
|
8
|
+
/** 判断目录是否已有 Flower 安装记录。 */
|
|
9
|
+
function isFlowerProject(directory) {
|
|
10
|
+
const trellis = path.join(directory, ".trellis");
|
|
11
|
+
const hasLock = fs.existsSync(path.join(directory, ".flower", "plugin-lock.json"));
|
|
12
|
+
const hasManifest = fs.existsSync(path.join(trellis, ".flower-manifest.json"));
|
|
13
|
+
if (!hasLock && !hasManifest) return false;
|
|
14
|
+
try {
|
|
15
|
+
const stat = fs.lstatSync(trellis);
|
|
16
|
+
if (stat.isSymbolicLink()) throw new Error(".trellis 是软链接");
|
|
17
|
+
return stat.isDirectory();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === "ENOENT") return false;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 把用户输入解析为真实目录,避免重复路径与符号链接循环。 */
|
|
25
|
+
function resolveDirectory(input) {
|
|
26
|
+
if (!input) return { directory: null, reason: "缺少目录路径" };
|
|
27
|
+
try {
|
|
28
|
+
const directory = fs.realpathSync(path.resolve(input));
|
|
29
|
+
if (!fs.statSync(directory).isDirectory()) {
|
|
30
|
+
return { directory: null, reason: "目标不是目录" };
|
|
31
|
+
}
|
|
32
|
+
return { directory, reason: null };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { directory: null, reason: `无法读取目录(${error.code || error.message})` };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 在显式根目录与目标路径中发现 Flower 项目。
|
|
40
|
+
* @param {string[]} roots 允许扫描的父目录
|
|
41
|
+
* @param {string[]} targets 显式项目目录
|
|
42
|
+
* @returns {{projects:string[],errors:Array<{target:string,reason:string}>}} 去重项目与输入错误
|
|
43
|
+
*/
|
|
44
|
+
export function discoverFlowerProjects(roots, targets) {
|
|
45
|
+
const projects = new Set();
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
for (const input of targets) {
|
|
49
|
+
const { directory, reason } = resolveDirectory(input);
|
|
50
|
+
if (!directory) {
|
|
51
|
+
errors.push({ target: input || "--target", reason });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
if (isFlowerProject(directory)) projects.add(directory);
|
|
56
|
+
else errors.push({ target: input, reason: "目标不是已安装 Flower 项目" });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
errors.push({ target: input, reason: `无法读取安装记录(${error.code || error.message})` });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const input of roots) {
|
|
63
|
+
const { directory, reason } = resolveDirectory(input);
|
|
64
|
+
if (!directory) {
|
|
65
|
+
errors.push({ target: input || "--root", reason });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const stack = [directory];
|
|
69
|
+
while (stack.length) {
|
|
70
|
+
const current = stack.pop();
|
|
71
|
+
try {
|
|
72
|
+
if (isFlowerProject(current)) projects.add(current);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
errors.push({ target: current, reason: `无法读取安装记录(${error.code || error.message})` });
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const entries = fs.readdirSync(current, { withFileTypes: true })
|
|
78
|
+
.filter((entry) => entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name))
|
|
79
|
+
.sort((a, b) => b.name.localeCompare(a.name));
|
|
80
|
+
for (const entry of entries) stack.push(path.join(current, entry.name));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
errors.push({ target: current, reason: `无法扫描目录(${error.code || error.message})` });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { projects: [...projects].sort(), errors };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 读取项目所属 Git 根,用于共享写入前的安全检查。
|
|
92
|
+
* @param {string} target Flower 项目目录
|
|
93
|
+
* @returns {string|null} Git 根真实路径;无法读取时返回 null
|
|
94
|
+
*/
|
|
95
|
+
export function gitRepositoryRoot(target) {
|
|
96
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
97
|
+
cwd: target,
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
shell: process.platform === "win32",
|
|
100
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
+
timeout: 1500,
|
|
102
|
+
});
|
|
103
|
+
if (result.error || result.status !== 0) return null;
|
|
104
|
+
try {
|
|
105
|
+
return fs.realpathSync(String(result.stdout || "").trim());
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 严格检查批量更新目标的任务状态;不可读状态不能等同于没有活动任务。
|
|
113
|
+
* @param {string} target Flower 项目目录
|
|
114
|
+
* @returns {{safe:boolean,reason:string|null}} 是否允许进入更新队列
|
|
115
|
+
*/
|
|
116
|
+
export function projectTaskSafety(target) {
|
|
117
|
+
const tasksDir = path.join(target, ".trellis", "tasks");
|
|
118
|
+
let rootStat;
|
|
119
|
+
try {
|
|
120
|
+
rootStat = fs.lstatSync(tasksDir);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return error.code === "ENOENT"
|
|
123
|
+
? { safe: true, reason: null }
|
|
124
|
+
: { safe: false, reason: `无法读取任务目录(${error.code || error.message})` };
|
|
125
|
+
}
|
|
126
|
+
if (!rootStat.isDirectory()) return { safe: false, reason: "任务目录不是普通目录" };
|
|
127
|
+
|
|
128
|
+
const stack = [tasksDir];
|
|
129
|
+
while (stack.length) {
|
|
130
|
+
const directory = stack.pop();
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { safe: false, reason: `无法读取任务目录(${error.code || error.message})` };
|
|
136
|
+
}
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const absolute = path.join(directory, entry.name);
|
|
139
|
+
if (entry.isSymbolicLink()) return { safe: false, reason: `任务目录含软链接:${path.relative(target, absolute)}` };
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
stack.push(absolute);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (entry.name !== "task.json") continue;
|
|
145
|
+
if (!entry.isFile()) return { safe: false, reason: `任务记录不是普通文件:${path.relative(target, absolute)}` };
|
|
146
|
+
try {
|
|
147
|
+
const data = JSON.parse(fs.readFileSync(absolute, "utf8"));
|
|
148
|
+
if (data?.status === "in_progress" || data?.status === "active") {
|
|
149
|
+
return { safe: false, reason: "存在活动 Trellis 任务" };
|
|
150
|
+
}
|
|
151
|
+
if (!INACTIVE_TASK_STATUSES.has(data?.status)) {
|
|
152
|
+
return { safe: false, reason: `任务记录状态无法确认:${path.relative(target, absolute)}` };
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return { safe: false, reason: `任务记录损坏或不可读:${path.relative(target, absolute)} (${error.code || error.message})` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { safe: true, reason: null };
|
|
160
|
+
}
|