flower-trellis 0.6.6 → 0.6.7
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 +12 -4
- package/enhancements/0.6/.agents/skills/trellis-check-all/SKILL.md +6 -31
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/full-profile.md +10 -49
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/light-profile.md +9 -28
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/maven-evidence.md +14 -0
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/reporting-and-disposition.md +10 -1
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/verification.md +29 -0
- package/enhancements/0.6/.agents/skills/trellis-flower-update/SKILL.md +8 -5
- package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +1 -1
- package/enhancements/0.6/.agents/skills/trellis-worktree/SKILL.md +20 -8
- package/enhancements/0.6/.claude/skills/trellis-check-all/SKILL.md +6 -31
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/full-profile.md +10 -49
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/light-profile.md +9 -28
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/maven-evidence.md +14 -0
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/reporting-and-disposition.md +10 -1
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/verification.md +29 -0
- package/enhancements/0.6/.claude/skills/trellis-flower-update/SKILL.md +8 -5
- package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +1 -1
- package/enhancements/0.6/.claude/skills/trellis-worktree/SKILL.md +20 -8
- package/enhancements/0.6/scripts/worktree_setup.py +119 -17
- package/enhancements/MANIFEST.json +2 -2
- package/package.json +3 -3
- package/src/assets/flower_update_hook.py +135 -14
- package/src/builtin-plugins/skill-garden/content-adapter.js +4 -0
- package/src/builtin-plugins/skill-garden/project-metadata.js +41 -0
- package/src/commands/self-check.js +3 -1
- package/src/commands/self-update.js +3 -1
- package/src/commands/update-check.js +3 -3
- package/src/commands/worktree.js +36 -5
- package/src/lib/apply-enhancements.js +1 -1
- package/src/lib/manifest.js +46 -43
- package/src/lib/self-check.js +8 -4
- package/src/lib/worktree-flower-state.js +347 -0
- package/src/plugin/application-service.js +1 -44
- package/src/plugin/install/state-validation.js +47 -0
- package/src/plugin/install/transaction-writer.js +23 -2
- package/src/plugin/state/ignore-rules.js +40 -0
- package/src/plugin/state/project-store.js +3 -25
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-09-
|
|
2
|
+
"syncedAt": "2026-09-07T07:30:55.059Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "3e08ded44107ae8d3e1c04e5f1cdb3014655f038",
|
|
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.
|
|
3
|
+
"version": "0.6.7",
|
|
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.
|
|
73
|
+
"version": "0.6.7",
|
|
74
74
|
"source": "CHANGELOG.md",
|
|
75
|
-
"body": "### 🐛 修复 Bug Fixes\n\n* **
|
|
75
|
+
"body": "### ✨ 新功能 Features\n\n* **bootstrap:** 完善团队版本记录与 CLI 安装引导 ([e86217c](https://github.com/SilentFlower/flower-trellis/commit/e86217c5a121b35509ad972540c2c0adf3763b64))\n\n\n### 🐛 修复 Bug Fixes\n\n* **worktree:** 补齐 Flower 安装记录继承与版本诊断 ([17ad63e](https://github.com/SilentFlower/flower-trellis/commit/17ad63efd9f56ecf491c9402ef92469c70db3ea1))\n\n\n### ♻️ 重构 Refactor\n\n* **check-all:** 同步检查优化与回归验证 ([1409d42](https://github.com/SilentFlower/flower-trellis/commit/1409d421884cfe35a95342b6e68b3b7b80496e29))\n\n\n### 🧰 维护 Maintenance\n\n* **trellis:** 同步 Flower 0.6.6 工作流与 hooks ([11ae2b9](https://github.com/SilentFlower/flower-trellis/commit/11ae2b96b543da1a01e8a1871471d0752f1a04af))\n* **trellis:** 同步本地安装产物与 Flower 记录 ([5a78ca0](https://github.com/SilentFlower/flower-trellis/commit/5a78ca01434e26287caaed8b3e68321a1efd7cc7))",
|
|
76
76
|
"truncated": false
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
"""SessionStart
|
|
3
|
+
"""复用 SessionStart 升级入口,检测 CLI 缺失并提供对话安装引导。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
发现可执行更新时输出 `<flower-update>` 块。失败、离线、无更新或关闭检查时静默退出,
|
|
7
|
-
避免影响 Codex / Claude Code 正常启动。
|
|
5
|
+
只读检测,不执行 npm;CLI 已安装时沿用 self-check 更新检查。
|
|
8
6
|
"""
|
|
9
7
|
|
|
10
8
|
from __future__ import annotations
|
|
11
9
|
|
|
10
|
+
import argparse
|
|
12
11
|
import json
|
|
13
12
|
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
from pathlib import Path
|
|
@@ -18,6 +19,105 @@ from pathlib import Path
|
|
|
18
19
|
|
|
19
20
|
ACTIONABLE_STATUSES = {"update_available", "project_out_of_sync"}
|
|
20
21
|
SELF_CHECK_TIMEOUT_SECONDS = 30
|
|
22
|
+
BOOTSTRAP_STATUSES = {"cli_missing", "cli_unavailable", "project_version_unavailable", "prerequisites_missing"}
|
|
23
|
+
MAX_LOCK_BYTES = 2 * 1024 * 1024
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _executable_status(name: str) -> str:
|
|
27
|
+
"""检查 PATH,区分缺失与已有但不可执行的入口。"""
|
|
28
|
+
if shutil.which(name):
|
|
29
|
+
return "available"
|
|
30
|
+
names = [name]
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
names.extend(name + suffix for suffix in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";"))
|
|
33
|
+
for directory in os.get_exec_path():
|
|
34
|
+
if any(os.path.lexists(os.path.join(directory, candidate)) for candidate in names):
|
|
35
|
+
return "unavailable"
|
|
36
|
+
return "missing"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _locked_flower_version(project_dir: Path) -> str:
|
|
40
|
+
"""只读取受支持的内置锁条目,拒绝把未知版本拼入安装命令。"""
|
|
41
|
+
lock_path = project_dir / ".flower" / "plugin-lock.json"
|
|
42
|
+
if lock_path.parent.is_symlink() or lock_path.is_symlink() or not lock_path.is_file():
|
|
43
|
+
raise ValueError("项目缺少普通文件形式的 .flower/plugin-lock.json")
|
|
44
|
+
if lock_path.stat().st_size > MAX_LOCK_BYTES:
|
|
45
|
+
raise ValueError("项目锁超过读取上限")
|
|
46
|
+
lock = json.loads(lock_path.read_text(encoding="utf-8"))
|
|
47
|
+
if not isinstance(lock, dict) or type(lock.get("schemaVersion")) is not int or lock["schemaVersion"] != 1:
|
|
48
|
+
raise ValueError("项目锁 schema 不受支持")
|
|
49
|
+
plugins, roots = lock.get("plugins"), lock.get("roots")
|
|
50
|
+
if not isinstance(plugins, list) or not isinstance(roots, list) or any(not isinstance(root, str) for root in roots):
|
|
51
|
+
raise ValueError("项目锁 plugins/roots 结构无效")
|
|
52
|
+
if any(not isinstance(plugin, dict) for plugin in plugins):
|
|
53
|
+
raise ValueError("项目锁 Plugin 条目无效")
|
|
54
|
+
matches = [plugin for plugin in plugins if plugin.get("id") == "flower/skill-garden"]
|
|
55
|
+
if len(matches) != 1:
|
|
56
|
+
raise ValueError("项目锁缺少唯一的 flower/skill-garden 条目")
|
|
57
|
+
plugin = matches[0]
|
|
58
|
+
source = plugin.get("source")
|
|
59
|
+
if (
|
|
60
|
+
not isinstance(source, dict)
|
|
61
|
+
or source.get("id") != "flower"
|
|
62
|
+
or source.get("type") != "builtin"
|
|
63
|
+
or not isinstance(source.get("reference"), str)
|
|
64
|
+
or source.get("reference") not in {
|
|
65
|
+
"package:skill-garden:old", "package:skill-garden:0.5", "package:skill-garden:0.6",
|
|
66
|
+
}
|
|
67
|
+
):
|
|
68
|
+
raise ValueError("项目锁的 Skill-Garden 来源不是受支持的内置来源")
|
|
69
|
+
version = plugin.get("version")
|
|
70
|
+
match = re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?", version) if isinstance(version, str) and len(version) <= 256 else None
|
|
71
|
+
if not match or any(int(part) > 9007199254740991 for part in match.groups()[:3]):
|
|
72
|
+
raise ValueError("项目 Flower 版本不是完整有效的 SemVer")
|
|
73
|
+
if match[4] and any(part.isdigit() and len(part) > 1 and part.startswith("0") for part in match[4].split(".")):
|
|
74
|
+
raise ValueError("项目 Flower 预发布版本包含非法数字标识")
|
|
75
|
+
return version
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _check_cli(project_dir: Path) -> dict | None:
|
|
79
|
+
"""只读检查本机入口,返回安装或环境诊断;可用时返回空值。"""
|
|
80
|
+
if not project_dir.is_dir():
|
|
81
|
+
return {"status": "cli_unavailable", "reason": "目标项目目录不存在"}
|
|
82
|
+
cli_status = _executable_status("flower-trellis")
|
|
83
|
+
if cli_status == "available":
|
|
84
|
+
return None
|
|
85
|
+
if cli_status == "unavailable":
|
|
86
|
+
return {"status": "cli_unavailable", "reason": "PATH 中已有 Flower 入口但不可执行,请检查权限、链接与解释器"}
|
|
87
|
+
try:
|
|
88
|
+
version = _locked_flower_version(project_dir)
|
|
89
|
+
except (OSError, ValueError) as exc:
|
|
90
|
+
return {"status": "project_version_unavailable", "reason": f"Flower CLI 缺失,项目版本无法确认:{exc}"}
|
|
91
|
+
missing = [name for name in ("node", "npm") if _executable_status(name) != "available"]
|
|
92
|
+
if missing:
|
|
93
|
+
return {"status": "prerequisites_missing", "version": version, "reason": f"Flower CLI 缺失,请先准备可执行的 {', '.join(missing)}"}
|
|
94
|
+
return {"status": "cli_missing", "version": version, "command": f"npm install -g flower-trellis@{version}"}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _format_bootstrap(data: dict) -> str:
|
|
98
|
+
"""生成唯一的对话安装协议,检测脚本自身不执行安装。"""
|
|
99
|
+
lines = [
|
|
100
|
+
"<flower-cli-bootstrap>",
|
|
101
|
+
"priority: blocking_confirmation_required",
|
|
102
|
+
f"status: {data['status']}",
|
|
103
|
+
]
|
|
104
|
+
if data.get("version"):
|
|
105
|
+
lines.append(f"project_flower: {data['version']}")
|
|
106
|
+
if data.get("reason"):
|
|
107
|
+
lines.append("reason: " + json.dumps(data["reason"], ensure_ascii=False))
|
|
108
|
+
if data.get("command"):
|
|
109
|
+
lines.extend([
|
|
110
|
+
f"recommended_command: {data['command']}",
|
|
111
|
+
"ai_instruction: 告知成员本机缺少 Flower CLI,展示锁定版本和 recommended_command;说明全局 npm 安装会同步本机 Trellis 命令。先征得当前成员确认,确认前禁止执行 recommended_command;当前对话已明确授权这次安装时不重复确认。",
|
|
112
|
+
"verification: 安装返回成功后,在目标项目执行 flower-trellis -v 核对 CLI 首行版本,再执行 flower-trellis self-check --json --target <目标项目>;参数按所在 shell 引用。确认 CLI 可用后继续原请求,不把项目锁视为内容完整性证明,不隐式执行 init/update/self-update。",
|
|
113
|
+
])
|
|
114
|
+
else:
|
|
115
|
+
lines.append("ai_instruction: 说明诊断原因与缺失前提;版本未知时请维护者补齐有效项目锁,不依据旧 manifest 或猜测 latest 安装;入口异常先修复 PATH、权限或解释器。")
|
|
116
|
+
lines.extend([
|
|
117
|
+
"failure_policy: 成员拒绝后本次对话不重复追问;安装或验证失败如实报告,不自动提权、不循环安装。依赖 CLI 的操作说明尚未满足的前提;本次已处理的相同引导不重复执行。",
|
|
118
|
+
"</flower-cli-bootstrap>",
|
|
119
|
+
])
|
|
120
|
+
return "\n".join(lines)
|
|
21
121
|
|
|
22
122
|
|
|
23
123
|
def _debug(message: str) -> None:
|
|
@@ -45,6 +145,9 @@ def _project_dir(hook_input: dict) -> Path:
|
|
|
45
145
|
|
|
46
146
|
def _run_self_check(project_dir: Path) -> dict | None:
|
|
47
147
|
"""执行 flower-trellis self-check 并解析 JSON。"""
|
|
148
|
+
bootstrap = _check_cli(project_dir)
|
|
149
|
+
if bootstrap:
|
|
150
|
+
return bootstrap
|
|
48
151
|
try:
|
|
49
152
|
result = subprocess.run(
|
|
50
153
|
[
|
|
@@ -61,18 +164,19 @@ def _run_self_check(project_dir: Path) -> dict | None:
|
|
|
61
164
|
timeout=SELF_CHECK_TIMEOUT_SECONDS,
|
|
62
165
|
cwd=str(project_dir),
|
|
63
166
|
)
|
|
64
|
-
except
|
|
65
|
-
|
|
66
|
-
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
return {"status": "cli_unavailable", "reason": "Flower CLI 检查超时,不能据此判定未安装"}
|
|
169
|
+
except OSError as exc:
|
|
170
|
+
return {"status": "cli_unavailable", "reason": f"Flower 入口存在但执行失败,请检查 PATH、权限与解释器:{exc}"}
|
|
67
171
|
if result.returncode != 0:
|
|
68
|
-
_debug(result.stderr.strip()
|
|
69
|
-
return
|
|
172
|
+
_debug(result.stderr.strip())
|
|
173
|
+
return {"status": "cli_unavailable", "reason": f"Flower CLI 检查退出码 {result.returncode},请先诊断现有安装"}
|
|
70
174
|
try:
|
|
71
175
|
data = json.loads(result.stdout)
|
|
72
176
|
except json.JSONDecodeError as exc:
|
|
73
177
|
_debug(f"JSON 解析失败:{exc}")
|
|
74
|
-
return
|
|
75
|
-
return data if isinstance(data, dict) else
|
|
178
|
+
return {"status": "cli_unavailable", "reason": "Flower CLI 返回的检查结果不是有效 JSON"}
|
|
179
|
+
return data if isinstance(data, dict) and isinstance(data.get("status"), str) else {"status": "cli_unavailable", "reason": "Flower CLI 检查结果结构无效"}
|
|
76
180
|
|
|
77
181
|
|
|
78
182
|
def _json_bool(value: object) -> str:
|
|
@@ -219,15 +323,32 @@ def _emit_context(context: str, system_message: str) -> None:
|
|
|
219
323
|
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
220
324
|
|
|
221
325
|
|
|
222
|
-
def main() -> None:
|
|
326
|
+
def main(argv: list[str] | None = None) -> None:
|
|
327
|
+
"""处理宿主启动或手动只读检测入口并输出上下文。
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
argv: 显式参数列表,省略时读取进程参数。
|
|
331
|
+
Returns:
|
|
332
|
+
无返回值;通过标准输出返回宿主 JSON。
|
|
333
|
+
"""
|
|
334
|
+
parser = argparse.ArgumentParser(description="Flower CLI 安装与升级只读检测")
|
|
335
|
+
parser.add_argument("--bootstrap-only", action="store_true")
|
|
336
|
+
parser.add_argument("--target", type=Path)
|
|
337
|
+
args = parser.parse_args(argv)
|
|
338
|
+
if not args.bootstrap_only and (os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1" or os.environ.get("CODEX_NON_INTERACTIVE") == "1"):
|
|
339
|
+
return
|
|
223
340
|
try:
|
|
224
|
-
hook_input = json.loads(sys.stdin.read() or "{}")
|
|
341
|
+
hook_input = {} if args.bootstrap_only else json.loads(sys.stdin.read() or "{}")
|
|
225
342
|
if not isinstance(hook_input, dict):
|
|
226
343
|
hook_input = {}
|
|
227
344
|
except json.JSONDecodeError:
|
|
228
345
|
hook_input = {}
|
|
229
346
|
|
|
230
|
-
|
|
347
|
+
project_dir = args.target.resolve() if args.target else _project_dir(hook_input)
|
|
348
|
+
data = _check_cli(project_dir) if args.bootstrap_only else _run_self_check(project_dir)
|
|
349
|
+
if data and data.get("status") in BOOTSTRAP_STATUSES:
|
|
350
|
+
_emit_context(_format_bootstrap(data), "Flower CLI 需要安装确认或环境诊断")
|
|
351
|
+
return
|
|
231
352
|
if not data or data.get("status") not in ACTIONABLE_STATUSES:
|
|
232
353
|
return
|
|
233
354
|
_emit_context(_format_context(data), _system_message(data))
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ENHANCEMENT_SKILL_TARGETS,
|
|
7
7
|
} from "../../constants.js";
|
|
8
8
|
import { readLegacyManifestStatus } from "../../lib/manifest.js";
|
|
9
|
+
import { projectFlowerMetadata } from "./project-metadata.js";
|
|
9
10
|
import { shouldInstallName } from "../../lib/skill-filter.js";
|
|
10
11
|
import { listCanonicalTreeFiles, isVolatileTreeArtifact } from "../../plugin/integrity/canonical-tree.js";
|
|
11
12
|
import {
|
|
@@ -893,6 +894,9 @@ export function projectSkillGardenContent(options) {
|
|
|
893
894
|
}
|
|
894
895
|
}
|
|
895
896
|
}
|
|
897
|
+
const metadata = projectFlowerMetadata(projectRoot, resolved.id);
|
|
898
|
+
mutations.push(...metadata.mutations);
|
|
899
|
+
for (const [key, value] of metadata.payloads) payloads.set(key, value);
|
|
896
900
|
return {
|
|
897
901
|
mutations: mutations.sort((left, right) => compareUtf8(left.target, right.target)),
|
|
898
902
|
payloads,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { planLegacyManifestMigration } from "../../lib/manifest.js";
|
|
4
|
+
import { contentMutationKey } from "../../plugin/install/content-projector.js";
|
|
5
|
+
import { hashContent, hashFileIfExists } from "../../plugin/install/content-hash.js";
|
|
6
|
+
import { mergeFlowerIgnoreRules } from "../../plugin/state/ignore-rules.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 规划 Flower 共享记录与旧配置迁移,不声明用户配置的独占 ownership。
|
|
10
|
+
*
|
|
11
|
+
* @param {string} projectRoot 目标项目根
|
|
12
|
+
* @param {string} owner 内置 Plugin ID
|
|
13
|
+
* @returns {{mutations:object[],payloads:Map<string,Buffer>}} 事务目标及写入内容
|
|
14
|
+
*/
|
|
15
|
+
export function projectFlowerMetadata(projectRoot, owner) {
|
|
16
|
+
const writes = planLegacyManifestMigration(projectRoot);
|
|
17
|
+
const localIgnore = path.join(projectRoot, ".flower/.gitignore");
|
|
18
|
+
const localText = hashFileIfExists(localIgnore) === null ? "" : fs.readFileSync(localIgnore, "utf8");
|
|
19
|
+
writes.push({ path: ".flower/.gitignore", content: Buffer.from(mergeFlowerIgnoreRules(localText)) });
|
|
20
|
+
const rootIgnore = path.join(projectRoot, ".gitignore");
|
|
21
|
+
if (hashFileIfExists(rootIgnore) !== null) {
|
|
22
|
+
writes.push({ path: ".gitignore", content: Buffer.from(mergeFlowerIgnoreRules(fs.readFileSync(rootIgnore, "utf8"), { root: true })) });
|
|
23
|
+
}
|
|
24
|
+
const mutations = [];
|
|
25
|
+
const payloads = new Map();
|
|
26
|
+
for (const entry of writes) {
|
|
27
|
+
const mutation = {
|
|
28
|
+
owner,
|
|
29
|
+
target: entry.path,
|
|
30
|
+
operation: entry.content === null ? "remove" : "write",
|
|
31
|
+
beforeHash: hashFileIfExists(path.join(projectRoot, ...entry.path.split("/"))),
|
|
32
|
+
afterHash: entry.content === null ? null : hashContent(entry.content),
|
|
33
|
+
source: "flower:project-metadata",
|
|
34
|
+
allowUnownedWrite: true,
|
|
35
|
+
allowUnownedRemove: true,
|
|
36
|
+
};
|
|
37
|
+
mutations.push(mutation);
|
|
38
|
+
if (entry.content !== null) payloads.set(contentMutationKey(mutation), entry.content);
|
|
39
|
+
}
|
|
40
|
+
return { mutations, payloads };
|
|
41
|
+
}
|
|
@@ -14,7 +14,9 @@ function printSelfCheckHelp() {
|
|
|
14
14
|
--force-remote 强制检查远端版本
|
|
15
15
|
--target <dir> 要检查的项目目录
|
|
16
16
|
|
|
17
|
-
该命令供 SessionStart hook 和自动化读取,成功时始终输出 JSON
|
|
17
|
+
该命令供 SessionStart hook 和自动化读取,成功时始终输出 JSON。
|
|
18
|
+
project.flowerVersionStatus 区分 known/unknown;记录缺失且无已知更新时返回
|
|
19
|
+
project_unknown,不能以 CLI 最新推断项目也已更新。`);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -199,7 +199,9 @@ async function executeSelfUpdate(ctx) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (!shouldInstallFlower && !projectOnly && check.status !== "project_out_of_sync") {
|
|
202
|
-
console.log(
|
|
202
|
+
console.log(check.status === "project_unknown"
|
|
203
|
+
? " · 项目 Flower 版本无法确认,未执行更新;worktree 可先通过 prepare --inherit-flower 补齐安装记录"
|
|
204
|
+
: ` · 当前状态:${check.status},无需执行 self-update`);
|
|
203
205
|
return;
|
|
204
206
|
}
|
|
205
207
|
|
|
@@ -106,11 +106,11 @@ export async function updateCheck(ctx) {
|
|
|
106
106
|
const cachePath = updateCheckCachePath(ctx.target);
|
|
107
107
|
console.log(`settings: ${settingsPath(ctx.target)}`);
|
|
108
108
|
console.log(`cache: ${cachePath}`);
|
|
109
|
-
console.log(`legacy manifest: ${manifestPath(ctx.target)}`);
|
|
109
|
+
if (manifest) console.log(`legacy manifest (待迁移): ${manifestPath(ctx.target)}`);
|
|
110
110
|
console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
|
|
111
|
-
if (!manifest) console.log(" ·
|
|
111
|
+
if (!manifest && !fs.existsSync(settingsPath(ctx.target))) console.log(" · settings 不存在,当前显示默认策略");
|
|
112
112
|
if (!fs.existsSync(cachePath)) {
|
|
113
|
-
console.log(" · cache
|
|
113
|
+
console.log(" · cache 不存在,当前显示默认或旧记录兼容缓存");
|
|
114
114
|
}
|
|
115
115
|
return;
|
|
116
116
|
}
|
package/src/commands/worktree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { ENHANCEMENTS_ROOT } from "../lib/paths.js";
|
|
5
6
|
import {
|
|
6
7
|
resolveTrellisPythonCommand,
|
|
@@ -17,8 +18,9 @@ const VALUE_OPTIONS = new Set([
|
|
|
17
18
|
"--task-description",
|
|
18
19
|
"--developer",
|
|
19
20
|
"--plan-fingerprint",
|
|
21
|
+
"--source",
|
|
20
22
|
]);
|
|
21
|
-
const BOOLEAN_OPTIONS = new Set(["--json", "--dry-run", "--yes", "--inherit-route-prefs"]);
|
|
23
|
+
const BOOLEAN_OPTIONS = new Set(["--json", "--dry-run", "--yes", "--inherit-route-prefs", "--inherit-flower"]);
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* 识别 worktree 根级或子命令级帮助请求。
|
|
@@ -64,9 +66,12 @@ needs-migration 状态选择下一步。`);
|
|
|
64
66
|
用法:
|
|
65
67
|
flower-trellis worktree prepare [--target <dir>] [--developer <name>]
|
|
66
68
|
[--inherit-route-prefs] [--json]
|
|
69
|
+
[--inherit-flower --source <dir>]
|
|
67
70
|
|
|
68
71
|
prepare 不创建 Git worktree,只补齐目标分支本地的 developer、runtime 和 registry 状态。
|
|
69
|
-
仅在确实需要继承当前控制 worktree 的个人路由偏好时使用 --inherit-route-prefs
|
|
72
|
+
仅在确实需要继承当前控制 worktree 的个人路由偏好时使用 --inherit-route-prefs。
|
|
73
|
+
Flower 记录缺失时,用 --inherit-flower --source <同仓 worktree> 补齐可验证的独立安装信息。
|
|
74
|
+
即使 Trellis 已 ready-local,也可以运行此补配;已有目标记录不会被覆盖。`);
|
|
70
75
|
return;
|
|
71
76
|
}
|
|
72
77
|
if (command === "migrate") {
|
|
@@ -96,7 +101,9 @@ prepare 不创建 Git worktree,只补齐目标分支本地的 developer、runt
|
|
|
96
101
|
git worktree add <target> <existing-branch>
|
|
97
102
|
flower-trellis worktree status --target <target>
|
|
98
103
|
若 status 返回 needs-prepare,再运行:
|
|
99
|
-
flower-trellis worktree prepare --target <target> [--developer <name>]
|
|
104
|
+
flower-trellis worktree prepare --target <target> [--developer <name>]
|
|
105
|
+
create 自动将缺失的 Flower 安装信息纳入计划;目标内容不匹配时停止并回滚。
|
|
106
|
+
已有 worktree 缺少 Flower 记录时,使用 prepare --inherit-flower --source <source>。`);
|
|
100
107
|
return;
|
|
101
108
|
}
|
|
102
109
|
if (command === "remove") {
|
|
@@ -177,6 +184,15 @@ export function parseWorktreeArgs(args) {
|
|
|
177
184
|
if (command !== "prepare" && options.has("--inherit-route-prefs")) {
|
|
178
185
|
throw new Error("--inherit-route-prefs 只用于 worktree prepare");
|
|
179
186
|
}
|
|
187
|
+
if (command !== "prepare" && (options.has("--inherit-flower") || options.has("--source"))) {
|
|
188
|
+
throw new Error("--inherit-flower 和 --source 只用于 worktree prepare");
|
|
189
|
+
}
|
|
190
|
+
if (options.has("--inherit-flower") && !options.has("--source")) {
|
|
191
|
+
throw new Error("--inherit-flower 需要 --source 指定同仓来源");
|
|
192
|
+
}
|
|
193
|
+
if (options.has("--source") && !options.has("--inherit-flower")) {
|
|
194
|
+
throw new Error("--source 需要显式 --inherit-flower");
|
|
195
|
+
}
|
|
180
196
|
return { command, options, json: options.has("--json") };
|
|
181
197
|
}
|
|
182
198
|
|
|
@@ -194,7 +210,7 @@ export function worktreeEngineArgs(parsed, target, source) {
|
|
|
194
210
|
args.push("--source", source, "--target", target);
|
|
195
211
|
} else {
|
|
196
212
|
args.push("--target", target);
|
|
197
|
-
if (parsed.command === "prepare" && parsed.options.has("--inherit-route-prefs")) {
|
|
213
|
+
if (parsed.command === "prepare" && parsed.options.has("--inherit-route-prefs") && !parsed.options.has("--source")) {
|
|
198
214
|
args.push("--source", source);
|
|
199
215
|
}
|
|
200
216
|
}
|
|
@@ -244,6 +260,17 @@ export function printWorktreeResult(payload) {
|
|
|
244
260
|
if (payload.localStateTransfer?.routePreferences?.action) {
|
|
245
261
|
console.log(`route preferences: ${payload.localStateTransfer.routePreferences.action}`);
|
|
246
262
|
}
|
|
263
|
+
if (payload.localStateTransfer?.flower) {
|
|
264
|
+
const flower = payload.localStateTransfer.flower;
|
|
265
|
+
console.log(`flower transfer: ${flower.action}${flower.reason ? ` (${flower.reason})` : ""}`);
|
|
266
|
+
if (flower.source) console.log(`flower source: ${flower.source}`);
|
|
267
|
+
if (flower.paths?.length) console.log(`flower paths: ${flower.paths.join(", ")}`);
|
|
268
|
+
if (flower.validationPending) console.log("flower validation: checkout 后核验目标内容,失败回滚创建");
|
|
269
|
+
}
|
|
270
|
+
if (payload.flower) {
|
|
271
|
+
console.log(`flower: ${payload.flower.installation}; version: ${payload.flower.version || "unknown"}`);
|
|
272
|
+
if (payload.flower.reason) console.log(`flower reason: ${payload.flower.reason}`);
|
|
273
|
+
}
|
|
247
274
|
if (payload.requiresConfirmation && payload.confirmation?.fingerprint) {
|
|
248
275
|
console.log(`confirmation: 使用 --yes --plan-fingerprint ${payload.confirmation.fingerprint}`);
|
|
249
276
|
}
|
|
@@ -284,7 +311,11 @@ export async function worktree(ctx) {
|
|
|
284
311
|
[...invocation.args, script, ...worktreeEngineArgs(parsed, ctx.target, source)],
|
|
285
312
|
{
|
|
286
313
|
cwd: source,
|
|
287
|
-
env:
|
|
314
|
+
env: {
|
|
315
|
+
...process.env,
|
|
316
|
+
FLOWER_WORKTREE_NODE: process.execPath,
|
|
317
|
+
FLOWER_WORKTREE_HELPER: fileURLToPath(new URL("../lib/worktree-flower-state.js", import.meta.url)),
|
|
318
|
+
},
|
|
288
319
|
encoding: "utf8",
|
|
289
320
|
windowsHide: true,
|
|
290
321
|
},
|
|
@@ -6,7 +6,7 @@ import { formatPatchDiagnostic } from "./patch-conflicts.js";
|
|
|
6
6
|
* 叠加强化包的兼容 facade。
|
|
7
7
|
*
|
|
8
8
|
* 成功状态统一由 Plugin Runtime 写入 `.flower/`;旧
|
|
9
|
-
* `.trellis/.flower-manifest.json`
|
|
9
|
+
* `.trellis/.flower-manifest.json` 作为迁移证据读取,迁移成功后由同一事务删除。
|
|
10
10
|
*
|
|
11
11
|
* @param {string} target 目标项目根
|
|
12
12
|
* @param {{variant?:string|null,skills?:string[],dryRun?:boolean}} [opts] 兼容参数
|
package/src/lib/manifest.js
CHANGED
|
@@ -4,13 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { ProjectStore } from "../plugin/state/project-store.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 记录「flower 上一次为该项目铺设了哪些强化文件」,使升级(如 0.5/old → 0.6)时
|
|
10
|
-
* 能精确删除当前变体不再包含的过期 skill / command —— 只删自己铺过的路径,
|
|
11
|
-
* 绝不误删用户或 Trellis 本体的文件。
|
|
12
|
-
*
|
|
13
|
-
* 放在 .trellis/ 下,随项目的 Trellis 生命周期存在(uninstall 删 .trellis 时一并消失)。
|
|
7
|
+
* 旧安装清单的兼容读取入口;成功迁移后删除,现代状态由 ProjectStore 管理。
|
|
14
8
|
*/
|
|
15
9
|
const MANIFEST_REL = path.join(".trellis", ".flower-manifest.json");
|
|
16
10
|
const LEGACY_UPDATE_CHECK_CACHE_REL = path.join(".trellis", ".flower-update-check.tmp");
|
|
@@ -148,7 +142,7 @@ function normalizeLastRemote(value) {
|
|
|
148
142
|
* @param {object|null|undefined} value 原始 updateCheck 字段
|
|
149
143
|
* @returns {{enabled:boolean,policy:string,intervalHours:number}} 归一化后的策略
|
|
150
144
|
*/
|
|
151
|
-
function normalizeUpdateCheckPolicy(value) {
|
|
145
|
+
export function normalizeUpdateCheckPolicy(value) {
|
|
152
146
|
const raw = value && typeof value === "object" ? value : {};
|
|
153
147
|
const policy = UPDATE_POLICIES.has(raw.policy) ? raw.policy : DEFAULT_UPDATE_CHECK.policy;
|
|
154
148
|
const interval = Number(raw.intervalHours);
|
|
@@ -374,18 +368,51 @@ export function readManifest(target) {
|
|
|
374
368
|
* @returns {{status:"missing"|"valid"|"corrupt",manifest:object|null,error?:Error}} 迁移证据
|
|
375
369
|
*/
|
|
376
370
|
export function readLegacyManifestStatus(target) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
371
|
+
const result = readJsonFileStatus(manifestPath(target));
|
|
372
|
+
if (result.status === "valid" && !isPlainObject(result.value)) {
|
|
373
|
+
return { status: "corrupt", manifest: null, error: new Error("旧 manifest 必须是对象") };
|
|
374
|
+
}
|
|
375
|
+
return { status: result.status, manifest: result.value, ...(result.error ? { error: result.error } : {}) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 规划旧清单退出所需的配置写入及文件删除,保持 preflight 只读。
|
|
380
|
+
*
|
|
381
|
+
* @param {string} target 项目根
|
|
382
|
+
* @returns {Array<{path:string,content:Buffer|null}>} 固定目标及内容,null 表示删除
|
|
383
|
+
*/
|
|
384
|
+
export function planLegacyManifestMigration(target) {
|
|
385
|
+
const legacy = readLegacyManifestStatus(target);
|
|
386
|
+
if (legacy.status === "missing") return [];
|
|
387
|
+
if (legacy.status === "corrupt") throw new Error(`旧 flower manifest 损坏,拒绝删除:${manifestPath(target)}`, { cause: legacy.error });
|
|
388
|
+
const writes = [];
|
|
389
|
+
const settings = readSettingsStatus(target);
|
|
390
|
+
if (settings.status === "corrupt") throw new Error(`update-check settings 损坏,拒绝迁移:${settingsPath(target)}`, { cause: settings.error });
|
|
391
|
+
const updateCheck = legacy.manifest.updateCheck;
|
|
392
|
+
if (settings.status === "missing" && hasAnyOwn(updateCheck, UPDATE_CHECK_POLICY_KEYS)) {
|
|
393
|
+
writes.push({
|
|
394
|
+
path: ".flower/settings.json",
|
|
395
|
+
content: Buffer.from(JSON.stringify({ schemaVersion: 1, updateCheck: normalizeUpdateCheckPolicy(updateCheck) }, null, 2) + "\n"),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const cache = readJsonFileStatus(updateCheckCachePath(target));
|
|
399
|
+
if (cache.status === "corrupt" || (cache.status === "valid" && !isPlainObject(cache.value))) {
|
|
400
|
+
throw new Error(`update-check cache 损坏,拒绝迁移:${updateCheckCachePath(target)}`);
|
|
401
|
+
}
|
|
402
|
+
if (cache.status === "missing") {
|
|
403
|
+
const oldCache = readJsonFileStatus(path.join(target, LEGACY_UPDATE_CHECK_CACHE_REL));
|
|
404
|
+
if (oldCache.status === "corrupt" || (oldCache.status === "valid" && !isPlainObject(oldCache.value))) {
|
|
405
|
+
throw new Error("旧 update-check cache 损坏,拒绝丢失迁移证据");
|
|
406
|
+
}
|
|
407
|
+
if (oldCache.status === "valid" || hasLegacyUpdateCheckCache(updateCheck)) {
|
|
408
|
+
writes.push({
|
|
409
|
+
path: ".flower/update-check.tmp",
|
|
410
|
+
content: Buffer.from(JSON.stringify(normalizeUpdateCheckCache(oldCache.value || updateCheck), null, 2) + "\n"),
|
|
411
|
+
});
|
|
383
412
|
}
|
|
384
|
-
return { status: "valid", manifest };
|
|
385
|
-
} catch (error) {
|
|
386
|
-
if (error?.code === "ENOENT") return { status: "missing", manifest: null };
|
|
387
|
-
return { status: "corrupt", manifest: null, error };
|
|
388
413
|
}
|
|
414
|
+
writes.push({ path: ".trellis/.flower-manifest.json", content: null });
|
|
415
|
+
return writes;
|
|
389
416
|
}
|
|
390
417
|
|
|
391
418
|
/**
|
|
@@ -406,7 +433,7 @@ export function normalizeUpdateCheck(value) {
|
|
|
406
433
|
/**
|
|
407
434
|
* 读取启动更新检查配置。
|
|
408
435
|
*
|
|
409
|
-
*
|
|
436
|
+
* 返回现代 settings 策略与 tmp 缓存的合并视图,尚未迁移时兼容旧记录。
|
|
410
437
|
*
|
|
411
438
|
* @param {string} target 目标项目根
|
|
412
439
|
* @returns {ReturnType<typeof normalizeUpdateCheck>} 归一化后的配置
|
|
@@ -464,27 +491,3 @@ export function writeUpdateCheck(target, patch) {
|
|
|
464
491
|
}
|
|
465
492
|
return readUpdateCheck(target);
|
|
466
493
|
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* 写入 manifest。
|
|
470
|
-
*
|
|
471
|
-
* 若调用方没有显式传入 `updateCheck`,保留目标项目已有策略,避免全装重写时覆盖用户选择。
|
|
472
|
-
* 运行缓存会迁移到 `.flower-update-check.tmp`,不得继续写入 manifest。
|
|
473
|
-
*
|
|
474
|
-
* @param {string} target 目标项目根
|
|
475
|
-
* @param {object} data manifest 新内容
|
|
476
|
-
*/
|
|
477
|
-
export function writeManifest(target, data) {
|
|
478
|
-
const current = readManifest(target);
|
|
479
|
-
const next = { ...data };
|
|
480
|
-
if (hasLegacyUpdateCheckCache(current?.updateCheck)) {
|
|
481
|
-
migrateLegacyUpdateCheckCache(target, current.updateCheck);
|
|
482
|
-
}
|
|
483
|
-
if (Object.prototype.hasOwnProperty.call(data, "updateCheck")) {
|
|
484
|
-
migrateLegacyUpdateCheckCache(target, data.updateCheck);
|
|
485
|
-
next.updateCheck = normalizeUpdateCheckPolicy(data.updateCheck);
|
|
486
|
-
} else {
|
|
487
|
-
next.updateCheck = normalizeUpdateCheckPolicy(current?.updateCheck);
|
|
488
|
-
}
|
|
489
|
-
fs.writeFileSync(manifestPath(target), JSON.stringify(next, null, 2) + "\n");
|
|
490
|
-
}
|
package/src/lib/self-check.js
CHANGED
|
@@ -572,8 +572,9 @@ export async function buildSelfCheck(target, options = {}) {
|
|
|
572
572
|
const projectTrellis = readProjectTrellisVersion(absoluteTarget);
|
|
573
573
|
const skillGardenLock = pluginLock?.plugins.find(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
|
|
574
574
|
const projectFlower = skillGardenLock?.version || (
|
|
575
|
-
typeof manifest?.flowerVersion === "string" ? manifest.flowerVersion : null
|
|
575
|
+
typeof manifest?.flowerVersion === "string" && manifest.flowerVersion.trim() ? manifest.flowerVersion : null
|
|
576
576
|
);
|
|
577
|
+
const flowerVersionSource = skillGardenLock?.version ? "plugin-lock" : projectFlower ? "legacy-manifest" : null;
|
|
577
578
|
const projectOutOfSyncReasons = [];
|
|
578
579
|
if (projectFlower && projectFlower !== currentFlower) {
|
|
579
580
|
projectOutOfSyncReasons.push("flower_version_mismatch");
|
|
@@ -602,6 +603,8 @@ export async function buildSelfCheck(target, options = {}) {
|
|
|
602
603
|
},
|
|
603
604
|
project: {
|
|
604
605
|
flowerVersion: projectFlower,
|
|
606
|
+
flowerVersionStatus: projectFlower ? "known" : "unknown",
|
|
607
|
+
flowerVersionSource,
|
|
605
608
|
trellisVersion: projectTrellis,
|
|
606
609
|
manifestPresent: Boolean(manifest),
|
|
607
610
|
pluginStatePresent: Boolean(pluginLock && pluginState),
|
|
@@ -689,8 +692,8 @@ export async function buildSelfCheck(target, options = {}) {
|
|
|
689
692
|
}
|
|
690
693
|
return {
|
|
691
694
|
...base,
|
|
692
|
-
status: "skipped",
|
|
693
|
-
reason: "interval_not_elapsed",
|
|
695
|
+
status: projectFlower ? "skipped" : "project_unknown",
|
|
696
|
+
reason: projectFlower ? "interval_not_elapsed" : "project_flower_version_unknown",
|
|
694
697
|
remote: { ...base.remote, tags, fromCache: true, skipped: true },
|
|
695
698
|
};
|
|
696
699
|
}
|
|
@@ -744,7 +747,8 @@ export async function buildSelfCheck(target, options = {}) {
|
|
|
744
747
|
}
|
|
745
748
|
return {
|
|
746
749
|
...resultBase,
|
|
747
|
-
status: remoteStatus,
|
|
750
|
+
status: projectFlower ? remoteStatus : "project_unknown",
|
|
751
|
+
reason: projectFlower ? null : "project_flower_version_unknown",
|
|
748
752
|
remote: { ...resultBase.remote, tags },
|
|
749
753
|
};
|
|
750
754
|
}
|