flower-trellis 0.6.5 → 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/overrides/conflicts.json +4 -4
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
- 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_session_start.py +1 -1
- 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
|
@@ -35,7 +35,7 @@ ROUTE_PREFERENCE_MODES = {
|
|
|
35
35
|
NOT_INHERITED_LOCAL_STATE = (
|
|
36
36
|
"session-state",
|
|
37
37
|
"auto-loop",
|
|
38
|
-
"flower-
|
|
38
|
+
"flower-control-state",
|
|
39
39
|
"platform-local-settings",
|
|
40
40
|
"cache-and-transaction-state",
|
|
41
41
|
)
|
|
@@ -496,6 +496,60 @@ def _registry_registration_conflicts(
|
|
|
496
496
|
return conflicts
|
|
497
497
|
|
|
498
498
|
|
|
499
|
+
def _flower_call(operation: str, **values: Any) -> dict[str, Any]:
|
|
500
|
+
"""调用 Flower 随包适配器,保持 Plugin schema 和写入逻辑的单一归属。"""
|
|
501
|
+
executable = os.environ.get("FLOWER_WORKTREE_NODE")
|
|
502
|
+
helper = os.environ.get("FLOWER_WORKTREE_HELPER")
|
|
503
|
+
if not executable or not helper:
|
|
504
|
+
if operation == "status":
|
|
505
|
+
return {"installation": "unchecked", "version": None, "source": None, "reason": "flower-cli-required"}
|
|
506
|
+
source = Path(values.get("source", "."))
|
|
507
|
+
flower_directory = source / ".flower"
|
|
508
|
+
if operation == "plan" and not flower_directory.is_symlink() and not any(
|
|
509
|
+
(flower_directory / name).exists() for name in ("plugins.json", "plugin-lock.json", "state.json")
|
|
510
|
+
):
|
|
511
|
+
return {"action": "unavailable", "paths": [], "reason": "source-installation-missing"}
|
|
512
|
+
raise WorktreeSetupError("flower-cli-required", "Flower 继承需要通过外部 flower-trellis worktree 命令执行")
|
|
513
|
+
try:
|
|
514
|
+
result = subprocess.run(
|
|
515
|
+
[executable, helper],
|
|
516
|
+
input=json.dumps({"operation": operation, **values}, ensure_ascii=False),
|
|
517
|
+
capture_output=True,
|
|
518
|
+
text=True,
|
|
519
|
+
timeout=60,
|
|
520
|
+
check=False,
|
|
521
|
+
)
|
|
522
|
+
payload = json.loads(result.stdout)
|
|
523
|
+
except (OSError, subprocess.SubprocessError, ValueError) as error:
|
|
524
|
+
raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器执行失败", error=str(error)) from error
|
|
525
|
+
if not isinstance(payload, dict) or payload.get("ok") is not True or result.returncode != 0:
|
|
526
|
+
details = payload if isinstance(payload, dict) else {}
|
|
527
|
+
raise WorktreeSetupError(
|
|
528
|
+
details.get("reason", "flower-adapter-failed"),
|
|
529
|
+
details.get("message", "Flower worktree 适配器返回非法结果"),
|
|
530
|
+
path=details.get("path"),
|
|
531
|
+
)
|
|
532
|
+
if not isinstance(payload.get("result"), dict):
|
|
533
|
+
raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器缺少结构化结果")
|
|
534
|
+
value = payload["result"]
|
|
535
|
+
if operation == "status":
|
|
536
|
+
valid = value.get("installation") in {"complete", "incomplete", "invalid"}
|
|
537
|
+
elif operation in {"plan", "apply"}:
|
|
538
|
+
valid = (
|
|
539
|
+
value.get("action") in {"inherited", "preserved", "unavailable"}
|
|
540
|
+
and isinstance(value.get("paths"), list)
|
|
541
|
+
and all(isinstance(item, str) for item in value["paths"])
|
|
542
|
+
and isinstance(value.get("digest"), str)
|
|
543
|
+
and value["digest"].startswith("sha256:")
|
|
544
|
+
and len(value["digest"]) == 71
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
valid = value.get("action") == "rolledBack"
|
|
548
|
+
if not valid:
|
|
549
|
+
raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器结果不符合操作契约")
|
|
550
|
+
return value
|
|
551
|
+
|
|
552
|
+
|
|
499
553
|
def _analyze(target_arg: str | None) -> dict[str, Any]:
|
|
500
554
|
"""生成 target-local readiness 诊断,且不写盘。"""
|
|
501
555
|
context = _worktree_context(_resolve_start(target_arg))
|
|
@@ -537,7 +591,7 @@ def _analyze(target_arg: str | None) -> dict[str, Any]:
|
|
|
537
591
|
conflicts.append({"path": relative, "reason": item["reason"]})
|
|
538
592
|
else:
|
|
539
593
|
item["state"] = "missing"
|
|
540
|
-
if relative in configured:
|
|
594
|
+
if relative in configured and relative != ".flower":
|
|
541
595
|
missing_configured.append(relative)
|
|
542
596
|
entries.append(item)
|
|
543
597
|
|
|
@@ -610,11 +664,14 @@ def _analyze(target_arg: str | None) -> dict[str, Any]:
|
|
|
610
664
|
},
|
|
611
665
|
"actions": actions,
|
|
612
666
|
"conflicts": conflicts,
|
|
667
|
+
"flower": _flower_call("status", target=str(target_root)),
|
|
613
668
|
}
|
|
614
669
|
|
|
615
670
|
|
|
616
671
|
def _developer_from_file(target_root: Path) -> str | None:
|
|
617
672
|
"""从目标本地 `.developer` 读取开发者名。"""
|
|
673
|
+
if (target_root / ".trellis").is_symlink() or (target_root / ".trellis/.developer").is_symlink():
|
|
674
|
+
return None
|
|
618
675
|
try:
|
|
619
676
|
lines = (target_root / ".trellis/.developer").read_text(encoding="utf-8").splitlines()
|
|
620
677
|
except OSError:
|
|
@@ -721,6 +778,7 @@ def _prepare_local(
|
|
|
721
778
|
*,
|
|
722
779
|
source: str | None = None,
|
|
723
780
|
inherit_route_prefs: bool = False,
|
|
781
|
+
inherit_flower: bool = False,
|
|
724
782
|
) -> dict[str, Any]:
|
|
725
783
|
"""初始化当前 worktree 自己的 gitignored 运行态并注册。"""
|
|
726
784
|
if plan["status"] == "needs-migration":
|
|
@@ -734,22 +792,23 @@ def _prepare_local(
|
|
|
734
792
|
context = _worktree_context(target_root)
|
|
735
793
|
source_context = None
|
|
736
794
|
source_developer = None
|
|
737
|
-
if inherit_route_prefs:
|
|
795
|
+
if inherit_route_prefs or inherit_flower:
|
|
738
796
|
if not source:
|
|
739
797
|
raise WorktreeSetupError(
|
|
740
798
|
"route-preferences-source-required",
|
|
741
|
-
"
|
|
799
|
+
"显式继承本地信息时需要 --source",
|
|
742
800
|
)
|
|
743
801
|
source_context = _worktree_context(_resolve_start(source))
|
|
744
802
|
if source_context["gitCommonDir"] != context["gitCommonDir"]:
|
|
745
803
|
raise WorktreeSetupError(
|
|
746
804
|
"route-preferences-repository-mismatch",
|
|
747
|
-
"
|
|
805
|
+
"继承来源与目标不属于同一 Git 仓库",
|
|
748
806
|
)
|
|
749
807
|
source_developer = _developer_from_file(source_context["targetRoot"])
|
|
750
808
|
changed_paths: list[str] = []
|
|
751
809
|
route_transfer = {"action": "notRequested", "values": {}}
|
|
752
|
-
|
|
810
|
+
flower_transfer: dict[str, Any] = {"action": "notRequested", "paths": []}
|
|
811
|
+
with _registry_lock(context["gitCommonDir"]), tempfile.TemporaryDirectory(prefix="flower-worktree-") as temporary:
|
|
753
812
|
current_developer = _developer_from_file(target_root)
|
|
754
813
|
registry = _load_registry(context["gitCommonDir"])
|
|
755
814
|
registration_conflicts = _registry_registration_conflicts(context, registry, task=None)
|
|
@@ -781,17 +840,42 @@ def _prepare_local(
|
|
|
781
840
|
source_developer,
|
|
782
841
|
normalized_developer,
|
|
783
842
|
)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
843
|
+
journal = str(Path(temporary) / "flower-receipt.json")
|
|
844
|
+
try:
|
|
845
|
+
if inherit_flower:
|
|
846
|
+
flower_input = {
|
|
847
|
+
"source": str(source_context["targetRoot"]),
|
|
848
|
+
"target": str(target_root),
|
|
849
|
+
"sourceDeveloper": source_developer,
|
|
850
|
+
"targetDeveloper": normalized_developer,
|
|
851
|
+
}
|
|
852
|
+
flower_transfer = _flower_call("plan", **flower_input)
|
|
853
|
+
flower_transfer = _flower_call(
|
|
854
|
+
"apply", **flower_input, expectedDigest=flower_transfer.get("digest"), journal=journal,
|
|
855
|
+
)
|
|
856
|
+
flower_transfer.pop("receipt", None)
|
|
857
|
+
changed_paths.extend(flower_transfer.get("paths", []))
|
|
858
|
+
runtime_sessions = target_root / ".trellis/.runtime/sessions"
|
|
859
|
+
if not runtime_sessions.is_dir():
|
|
860
|
+
runtime_sessions.mkdir(parents=True, exist_ok=True)
|
|
861
|
+
changed_paths.append(".trellis/.runtime/sessions")
|
|
862
|
+
if current_developer is None:
|
|
863
|
+
_write_developer(target_root, normalized_developer)
|
|
864
|
+
changed_paths.append(".trellis/.developer")
|
|
865
|
+
if route_transfer["action"] == "inherited":
|
|
866
|
+
_write_route_preferences(target_root, route_transfer["values"])
|
|
867
|
+
changed_paths.append(ROUTE_PREFERENCES_PATH)
|
|
868
|
+
registry_changed = _register_worktree(context, developer=normalized_developer)
|
|
869
|
+
except Exception as error:
|
|
870
|
+
if Path(journal).is_file():
|
|
871
|
+
try:
|
|
872
|
+
_flower_call("rollback", target=str(target_root), journal=journal)
|
|
873
|
+
except WorktreeSetupError as rollback_error:
|
|
874
|
+
raise WorktreeSetupError(
|
|
875
|
+
"flower-rollback-failed", "prepare 失败且 Flower 记录回滚不完整",
|
|
876
|
+
error=str(error), rollbackError=str(rollback_error),
|
|
877
|
+
) from error
|
|
878
|
+
raise
|
|
795
879
|
result = {
|
|
796
880
|
**_analyze(str(target_root)),
|
|
797
881
|
"status": "prepared" if changed_paths or registry_changed else "ready-local",
|
|
@@ -801,6 +885,7 @@ def _prepare_local(
|
|
|
801
885
|
}
|
|
802
886
|
result["localStateTransfer"] = {
|
|
803
887
|
"routePreferences": route_transfer,
|
|
888
|
+
"flower": flower_transfer,
|
|
804
889
|
"notInherited": list(NOT_INHERITED_LOCAL_STATE),
|
|
805
890
|
}
|
|
806
891
|
return result
|
|
@@ -1121,6 +1206,10 @@ def _create_plan(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1121
1206
|
if not developer:
|
|
1122
1207
|
raise WorktreeSetupError("developer-required", "create 需要 --developer 或来源 worktree 的本地身份")
|
|
1123
1208
|
route_transfer = _route_preference_transfer(source_root, target, source_developer, developer)
|
|
1209
|
+
flower_transfer = _flower_call(
|
|
1210
|
+
"plan", source=str(source_root), target=str(target), baseCommit=resolved_commit,
|
|
1211
|
+
sourceDeveloper=source_developer, targetDeveloper=developer,
|
|
1212
|
+
)
|
|
1124
1213
|
plan: dict[str, Any] = {
|
|
1125
1214
|
"status": "confirmation-required",
|
|
1126
1215
|
"changed": False,
|
|
@@ -1161,6 +1250,7 @@ def _create_plan(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1161
1250
|
"sourceName": source_developer,
|
|
1162
1251
|
},
|
|
1163
1252
|
"routePreferences": route_transfer,
|
|
1253
|
+
"flower": flower_transfer,
|
|
1164
1254
|
"initialized": ["session-runtime"],
|
|
1165
1255
|
"notInherited": list(NOT_INHERITED_LOCAL_STATE),
|
|
1166
1256
|
},
|
|
@@ -1239,6 +1329,16 @@ def _create(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1239
1329
|
if route_transfer["action"] == "inherited":
|
|
1240
1330
|
_write_route_preferences(target, route_transfer["values"])
|
|
1241
1331
|
|
|
1332
|
+
flower_transfer = plan["localStateTransfer"]["flower"]
|
|
1333
|
+
if flower_transfer.get("digest"):
|
|
1334
|
+
flower_transfer = _flower_call(
|
|
1335
|
+
"apply", source=str(source_root), target=str(target), baseCommit=base_commit,
|
|
1336
|
+
sourceDeveloper=plan["localStateTransfer"]["developer"]["sourceName"],
|
|
1337
|
+
targetDeveloper=developer, expectedDigest=flower_transfer["digest"],
|
|
1338
|
+
)
|
|
1339
|
+
flower_transfer.pop("receipt", None)
|
|
1340
|
+
plan["localStateTransfer"]["flower"] = flower_transfer
|
|
1341
|
+
|
|
1242
1342
|
task_script = target / ".trellis/scripts/task.py"
|
|
1243
1343
|
if not task_script.is_file():
|
|
1244
1344
|
raise WorktreeSetupError("task-script-missing", "目标分支缺少 `.trellis/scripts/task.py`")
|
|
@@ -1561,6 +1661,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1561
1661
|
action="store_true",
|
|
1562
1662
|
help="inherit normalized route preferences from --source",
|
|
1563
1663
|
)
|
|
1664
|
+
command_parser.add_argument("--inherit-flower", action="store_true", help="inherit verified Flower records from --source")
|
|
1564
1665
|
if command == "migrate":
|
|
1565
1666
|
command_parser.add_argument("--dry-run", action="store_true", help="validate migration without writing")
|
|
1566
1667
|
|
|
@@ -1603,6 +1704,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1603
1704
|
args.developer,
|
|
1604
1705
|
source=args.source,
|
|
1605
1706
|
inherit_route_prefs=args.inherit_route_prefs,
|
|
1707
|
+
inherit_flower=args.inherit_flower,
|
|
1606
1708
|
)
|
|
1607
1709
|
elif args.command == "migrate":
|
|
1608
1710
|
payload = _migrate(plan, dry_run=args.dry_run)
|
|
@@ -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": "### ✨ 新功能 Features\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": {
|
|
@@ -27,7 +27,7 @@ When executing the current task:
|
|
|
27
27
|
- Treat required steps, required references, phase boundaries, and output templates in applicable SKILL and WORKFLOW instructions as execution and delivery checks.
|
|
28
28
|
- Before a step, review its rules and required references. Reuse material already read in full and unchanged; search matches are not full reads.
|
|
29
29
|
- Preserve required heading levels, section order, and conditional sections in specified templates. General brevity or no-heading preferences apply to ordinary prose and do not justify flattening, shortening, or reshaping a specified template.
|
|
30
|
-
- Resolve conflicts by instruction hierarchy
|
|
30
|
+
- Resolve conflicts by instruction hierarchy. Follow the owning workflow's phase-transition and review requirements, checking the scope of prior authorization; do not infer approval of the final plan from permission to begin planning.
|
|
31
31
|
- Before claiming "read", "checked", or "complete", verify actual tool records and artifacts. Successful reading and compliant execution are separate facts.
|
|
32
32
|
- When corrected, review the applicable rules, execution records, and actual result before repairing it. If evidence is missing, state uncertainty. Do not invent causes such as "not read", "forgot", or "file missing", or consult unrelated rules in place of the relevant ones.
|
|
33
33
|
</trellis-astra-workflow-hint>"""
|
|
@@ -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
|
}
|