flower-trellis 0.5.2-beta.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -1
- package/enhancements/0.6/overrides/conflicts.json +22 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/constants-selector.py +6 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/helpers-selector.py +150 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/imports-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/imports-selector.py +8 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/output-content.py +7 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/output-selector.py +13 -0
- package/enhancements/0.6/overrides/patches/scripts/session-context-update-boundary/patch.json +53 -0
- package/enhancements/MANIFEST.json +9 -2
- package/package.json +3 -3
- package/src/builtin-plugins/skill-garden/content-adapter.js +29 -6
- package/src/builtin-plugins/skill-garden/provider.js +13 -2
- package/src/lib/patch-conflicts.js +28 -2
- package/src/lib/patch-engine.js +41 -3
- package/src/lib/platform-patch-adapters.js +8 -7
- package/src/lib/trellis-python-command.js +95 -0
|
@@ -1101,6 +1101,28 @@
|
|
|
1101
1101
|
},
|
|
1102
1102
|
"owner": "trellis-route",
|
|
1103
1103
|
"reason": "Claude workflow meta 必须与 shared route owner 模型一致。"
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
"id": "session-context-no-legacy-update-check",
|
|
1107
|
+
"severity": "error",
|
|
1108
|
+
"target": ".trellis/scripts/common/session_context.py",
|
|
1109
|
+
"whenOperations": [
|
|
1110
|
+
"session-context-update-imports",
|
|
1111
|
+
"session-context-update-constants",
|
|
1112
|
+
"session-context-update-helpers",
|
|
1113
|
+
"session-context-update-output"
|
|
1114
|
+
],
|
|
1115
|
+
"assertion": {
|
|
1116
|
+
"type": "absent-literal",
|
|
1117
|
+
"values": [
|
|
1118
|
+
"def _get_update_hint(",
|
|
1119
|
+
"def _mark_update_check_attempted(",
|
|
1120
|
+
"def _update_marker_path(",
|
|
1121
|
+
"[\"trellis\", \"--version\"]"
|
|
1122
|
+
]
|
|
1123
|
+
},
|
|
1124
|
+
"owner": "flower-update-hook",
|
|
1125
|
+
"reason": "默认 Session Context 只负责项目上下文;更新检查必须统一由 Flower SessionStart Hook 进入。"
|
|
1104
1126
|
}
|
|
1105
1127
|
]
|
|
1106
1128
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
def _read_project_version(repo_root: Path) -> str | None:
|
|
2
|
+
try:
|
|
3
|
+
version = (repo_root / DIR_WORKFLOW / ".version").read_text(
|
|
4
|
+
encoding="utf-8"
|
|
5
|
+
).strip()
|
|
6
|
+
except OSError:
|
|
7
|
+
return None
|
|
8
|
+
return version or None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fetch_trellis_version_output() -> str | None:
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run(
|
|
14
|
+
["trellis", "--version"],
|
|
15
|
+
capture_output=True,
|
|
16
|
+
text=True,
|
|
17
|
+
encoding="utf-8",
|
|
18
|
+
errors="replace",
|
|
19
|
+
timeout=_UPDATE_CHECK_TIMEOUT_SECONDS,
|
|
20
|
+
)
|
|
21
|
+
except (OSError, subprocess.SubprocessError, TimeoutError):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
if result.returncode != 0:
|
|
25
|
+
return None
|
|
26
|
+
output = f"{result.stdout}\n{result.stderr}".strip()
|
|
27
|
+
return output or None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_available_update_version(output: str) -> str | None:
|
|
31
|
+
update_match = re.search(
|
|
32
|
+
r"Trellis update available:\s*"
|
|
33
|
+
r"(?P<current>\S+)\s*(?:→|->)\s*(?P<latest>\S+)",
|
|
34
|
+
output,
|
|
35
|
+
)
|
|
36
|
+
if update_match:
|
|
37
|
+
return update_match.group("latest").strip()
|
|
38
|
+
candidates = _VERSION_TOKEN_RE.findall(output)
|
|
39
|
+
return candidates[-1] if candidates else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_available_update_version() -> str | None:
|
|
43
|
+
output = _fetch_trellis_version_output()
|
|
44
|
+
if not output:
|
|
45
|
+
return None
|
|
46
|
+
return _extract_available_update_version(output)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_version(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None] | None:
|
|
50
|
+
match = _VERSION_RE.match(version)
|
|
51
|
+
if not match:
|
|
52
|
+
return None
|
|
53
|
+
major, minor, patch, prerelease = match.groups()
|
|
54
|
+
numbers = (int(major), int(minor or "0"), int(patch or "0"))
|
|
55
|
+
prerelease_parts = tuple(prerelease.split(".")) if prerelease else None
|
|
56
|
+
return numbers, prerelease_parts
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _compare_prerelease(
|
|
60
|
+
left: tuple[str, ...] | None,
|
|
61
|
+
right: tuple[str, ...] | None,
|
|
62
|
+
) -> int:
|
|
63
|
+
if left is None and right is None:
|
|
64
|
+
return 0
|
|
65
|
+
if left is None:
|
|
66
|
+
return 1
|
|
67
|
+
if right is None:
|
|
68
|
+
return -1
|
|
69
|
+
|
|
70
|
+
for left_part, right_part in zip(left, right):
|
|
71
|
+
if left_part == right_part:
|
|
72
|
+
continue
|
|
73
|
+
left_numeric = left_part.isdigit()
|
|
74
|
+
right_numeric = right_part.isdigit()
|
|
75
|
+
if left_numeric and right_numeric:
|
|
76
|
+
left_int = int(left_part)
|
|
77
|
+
right_int = int(right_part)
|
|
78
|
+
return (left_int > right_int) - (left_int < right_int)
|
|
79
|
+
if left_numeric:
|
|
80
|
+
return -1
|
|
81
|
+
if right_numeric:
|
|
82
|
+
return 1
|
|
83
|
+
return (left_part > right_part) - (left_part < right_part)
|
|
84
|
+
|
|
85
|
+
return (len(left) > len(right)) - (len(left) < len(right))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _compare_versions(left: str, right: str) -> int | None:
|
|
89
|
+
parsed_left = _parse_version(left)
|
|
90
|
+
parsed_right = _parse_version(right)
|
|
91
|
+
if parsed_left is None or parsed_right is None:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
left_numbers, left_prerelease = parsed_left
|
|
95
|
+
right_numbers, right_prerelease = parsed_right
|
|
96
|
+
if left_numbers != right_numbers:
|
|
97
|
+
return (left_numbers > right_numbers) - (left_numbers < right_numbers)
|
|
98
|
+
return _compare_prerelease(left_prerelease, right_prerelease)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _update_marker_path(repo_root: Path) -> Path:
|
|
102
|
+
context_key = resolve_context_key()
|
|
103
|
+
if not context_key:
|
|
104
|
+
terminal_key = os.environ.get("TERM_SESSION_ID", "").strip()
|
|
105
|
+
context_key = terminal_key or f"ppid-{os.getppid()}"
|
|
106
|
+
safe_key = re.sub(r"[^A-Za-z0-9._-]+", "_", context_key).strip("._-")
|
|
107
|
+
if not safe_key:
|
|
108
|
+
safe_key = "session"
|
|
109
|
+
return (
|
|
110
|
+
repo_root
|
|
111
|
+
/ DIR_WORKFLOW
|
|
112
|
+
/ ".runtime"
|
|
113
|
+
/ f"update-check-{safe_key[:160]}.marker"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _mark_update_check_attempted(repo_root: Path) -> bool:
|
|
118
|
+
marker_path = _update_marker_path(repo_root)
|
|
119
|
+
if marker_path.exists():
|
|
120
|
+
return False
|
|
121
|
+
try:
|
|
122
|
+
marker_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
marker_path.write_text("checked\n", encoding="utf-8")
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_update_hint(repo_root: Path) -> str | None:
|
|
130
|
+
marker_path = _update_marker_path(repo_root)
|
|
131
|
+
if marker_path.exists():
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
current_version = _read_project_version(repo_root)
|
|
135
|
+
if not current_version:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
latest_version = _resolve_available_update_version()
|
|
139
|
+
if not latest_version:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
_mark_update_check_attempted(repo_root)
|
|
143
|
+
comparison = _compare_versions(current_version, latest_version)
|
|
144
|
+
if comparison is None or comparison >= 0:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
f"Trellis update available: {current_version} -> {latest_version}, "
|
|
149
|
+
"run trellis upgrade"
|
|
150
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
def output_text(repo_root: Path | None = None) -> None:
|
|
2
|
+
"""Output context in text format.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
6
|
+
"""
|
|
7
|
+
if repo_root is None:
|
|
8
|
+
repo_root = get_repo_root()
|
|
9
|
+
update_hint = _get_update_hint(repo_root)
|
|
10
|
+
if update_hint:
|
|
11
|
+
print(update_hint)
|
|
12
|
+
print("")
|
|
13
|
+
print(get_context_text(repo_root))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "session-context-update-boundary",
|
|
4
|
+
"purpose": "control_plane_integrity",
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "session-context-update-imports",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targets": [{
|
|
10
|
+
"kind": "file",
|
|
11
|
+
"path": ".trellis/scripts/common/session_context.py",
|
|
12
|
+
"missing": "error",
|
|
13
|
+
"markerStyle": "hash"
|
|
14
|
+
}],
|
|
15
|
+
"selector": { "type": "literal", "source": "imports-selector.py" },
|
|
16
|
+
"content": { "source": "imports-content.py" }
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "session-context-update-constants",
|
|
20
|
+
"operation": "remove",
|
|
21
|
+
"targets": [{
|
|
22
|
+
"kind": "file",
|
|
23
|
+
"path": ".trellis/scripts/common/session_context.py",
|
|
24
|
+
"missing": "error",
|
|
25
|
+
"markerStyle": "hash"
|
|
26
|
+
}],
|
|
27
|
+
"selector": { "type": "literal", "source": "constants-selector.py" }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "session-context-update-helpers",
|
|
31
|
+
"operation": "remove",
|
|
32
|
+
"targets": [{
|
|
33
|
+
"kind": "file",
|
|
34
|
+
"path": ".trellis/scripts/common/session_context.py",
|
|
35
|
+
"missing": "error",
|
|
36
|
+
"markerStyle": "hash"
|
|
37
|
+
}],
|
|
38
|
+
"selector": { "type": "literal", "source": "helpers-selector.py" }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "session-context-update-output",
|
|
42
|
+
"operation": "replace",
|
|
43
|
+
"targets": [{
|
|
44
|
+
"kind": "file",
|
|
45
|
+
"path": ".trellis/scripts/common/session_context.py",
|
|
46
|
+
"missing": "error",
|
|
47
|
+
"markerStyle": "hash"
|
|
48
|
+
}],
|
|
49
|
+
"selector": { "type": "literal", "source": "output-selector.py" },
|
|
50
|
+
"content": { "source": "output-content.py" }
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-07-
|
|
2
|
+
"syncedAt": "2026-07-29T11:34:14.834Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "9c35c52409d53198738405861b730d942de7efb0",
|
|
5
5
|
"common": {
|
|
6
6
|
"codexSkills": [
|
|
7
7
|
"craft-rpa",
|
|
@@ -161,6 +161,13 @@
|
|
|
161
161
|
"scripts/runtime-state-integrity/active-task-set-content.py",
|
|
162
162
|
"scripts/runtime-state-integrity/active-task-set-selector.py",
|
|
163
163
|
"scripts/runtime-state-integrity/patch.json",
|
|
164
|
+
"scripts/session-context-update-boundary/constants-selector.py",
|
|
165
|
+
"scripts/session-context-update-boundary/helpers-selector.py",
|
|
166
|
+
"scripts/session-context-update-boundary/imports-content.py",
|
|
167
|
+
"scripts/session-context-update-boundary/imports-selector.py",
|
|
168
|
+
"scripts/session-context-update-boundary/output-content.py",
|
|
169
|
+
"scripts/session-context-update-boundary/output-selector.py",
|
|
170
|
+
"scripts/session-context-update-boundary/patch.json",
|
|
164
171
|
"scripts/task-start-brief-gate/degraded-content.py",
|
|
165
172
|
"scripts/task-start-brief-gate/degraded-selector.py",
|
|
166
173
|
"scripts/task-start-brief-gate/guard-content.py",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.5.2
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"commit-and-tag-version": "^12.7.3"
|
|
69
69
|
},
|
|
70
70
|
"flowerReleaseNotes": {
|
|
71
|
-
"version": "0.5.2
|
|
71
|
+
"version": "0.5.2",
|
|
72
72
|
"source": "CHANGELOG.md",
|
|
73
|
-
"body": "### ✨ 新功能 Features\n\n* **0.6:** 同步 Push 门禁与意图识别 ([8abf3b1](https://github.com/SilentFlower/flower-trellis/commit/8abf3b1439d7a5dd2af0d698c4094aa6c7207fb3))\n* **brief:** 同步 Brief 显式预授权流程 ([7c179af](https://github.com/SilentFlower/flower-trellis/commit/7c179affd7389cac053d28104e0d624773159c72))\n* **spec-router:** 同步章节感知加载能力 ([0d51a24](https://github.com/SilentFlower/flower-trellis/commit/0d51a24e6136bb9b167cd5716eeb9958b5d8412e))\n* **trellis:** 补全 Meta 架构并修复 Auto-Loop 自恢复 ([943179f](https://github.com/SilentFlower/flower-trellis/commit/943179f42e3e3d068bf98f8ff60b2aa324acc4dd))\n\n\n### 🐛 修复 Bug Fixes\n\n* **trellis:** 移除旧更新检查 marker ([a33f376](https://github.com/SilentFlower/flower-trellis/commit/a33f376d7efe15441cba8fbe0b0a67c6f1a15f6c))",
|
|
73
|
+
"body": "### ✨ 新功能 Features\n\n* **0.6:** 同步 Push 门禁与意图识别 ([8abf3b1](https://github.com/SilentFlower/flower-trellis/commit/8abf3b1439d7a5dd2af0d698c4094aa6c7207fb3))\n* **brief:** 同步 Brief 显式预授权流程 ([7c179af](https://github.com/SilentFlower/flower-trellis/commit/7c179affd7389cac053d28104e0d624773159c72))\n* **spec-router:** 同步章节感知加载能力 ([0d51a24](https://github.com/SilentFlower/flower-trellis/commit/0d51a24e6136bb9b167cd5716eeb9958b5d8412e))\n* **trellis:** 补全 Meta 架构并修复 Auto-Loop 自恢复 ([943179f](https://github.com/SilentFlower/flower-trellis/commit/943179f42e3e3d068bf98f8ff60b2aa324acc4dd))\n* **plugin:** 建立契约与项目状态存储 ([3c664c3](https://github.com/SilentFlower/flower-trellis/commit/3c664c337806849d7bf040f269eb0f1ebed115f0))\n* **plugin:** 支持 GitHub 外部格式与交互来源管理 ([8701039](https://github.com/SilentFlower/flower-trellis/commit/8701039ea8f8018e7e8d017c0ac5f545b9b2549b))\n* **plugin:** 新增 GitLab 市场外部来源 ([3688744](https://github.com/SilentFlower/flower-trellis/commit/3688744d7663952d437f4bc4e5c8187798c45453))\n* **plugin:** 新增插件能力授权策略 ([32ec993](https://github.com/SilentFlower/flower-trellis/commit/32ec993de77686fb411451c4165ca17367cf5e33))\n* **plugin:** 新增插件运行时生命周期 ([0d7c5b1](https://github.com/SilentFlower/flower-trellis/commit/0d7c5b1ae4821e8cbadcd857608399c2e725c560))\n* **plugin:** 添加作者工具与 CI 契约 ([4e34b87](https://github.com/SilentFlower/flower-trellis/commit/4e34b878f37fddb3732b2264b90f79b3610c82d0))\n* **plugin:** 迁移 skill-garden 到插件运行时 ([2e9fd18](https://github.com/SilentFlower/flower-trellis/commit/2e9fd18123e3164a68971725269619401b2625b4))\n* 增加 Check-All 下一步引导 ([fa488f8](https://github.com/SilentFlower/flower-trellis/commit/fa488f83c85983b8b8c545b85e4e42d20b290bdf))\n\n\n### 🐛 修复 Bug Fixes\n\n* **trellis:** 移除旧更新检查 marker ([a33f376](https://github.com/SilentFlower/flower-trellis/commit/a33f376d7efe15441cba8fbe0b0a67c6f1a15f6c))\n* **plugin:** 优化外部来源交互与 archive 安全检测 ([4699a22](https://github.com/SilentFlower/flower-trellis/commit/4699a2211cb280e5a085c852950fda52af28c456))\n* **plugin:** 修复远程更新并完成集成验收 ([3db8320](https://github.com/SilentFlower/flower-trellis/commit/3db83208a6da12c3bbcad1730e144271ff02a96a))\n* **trellis:** 优化 auto-loop 重试预算与跟踪状态 ([19d7a1a](https://github.com/SilentFlower/flower-trellis/commit/19d7a1ae5302af56085e1053706442ad7cb347b0))\n* **trellis:** 支持跳过 Check-All 并修复 update -y ([1bd0a12](https://github.com/SilentFlower/flower-trellis/commit/1bd0a126f91f6b915d2253f1a497bda40fc41edb))\n* 修复 Windows Trellis Patch 命令漂移 ([5892e7f](https://github.com/SilentFlower/flower-trellis/commit/5892e7f7629be0a7ee075a3758f594a220c7db1f))\n\n\n### ♻️ 重构 Refactor\n\n* **trellis:** 拆分 Check-All 检查档位并同步快照 ([ef1a8c4](https://github.com/SilentFlower/flower-trellis/commit/ef1a8c434244c06cf565c449173c6d6763a0a160))\n\n\n### 🧰 维护 Maintenance\n\n* **trellis:** 同步 Flower 0.5.1-beta.1 清单 ([d3dfc27](https://github.com/SilentFlower/flower-trellis/commit/d3dfc2759927b0759b10e2ae246b68438dc3d339))\n* **trellis:** 执行 Flower 自更新同步 ([f79ce33](https://github.com/SilentFlower/flower-trellis/commit/f79ce33bcc409129a054f1b59a016cdbe1ea991b))",
|
|
74
74
|
"truncated": false
|
|
75
75
|
},
|
|
76
76
|
"optionalDependencies": {
|
|
@@ -20,6 +20,7 @@ import { PKG_ROOT } from "../../lib/paths.js";
|
|
|
20
20
|
import { injectWorkflow } from "../../lib/workflow-inject.js";
|
|
21
21
|
import { applyCodexTweaks } from "../../lib/codex-tweaks.js";
|
|
22
22
|
import { applyClaudeTweaks } from "../../lib/claude-tweaks.js";
|
|
23
|
+
import { materializeTrellisPythonText } from "../../lib/trellis-python-command.js";
|
|
23
24
|
import { describeInstalledCommonSkillSync } from "../../lib/skill-catalog.js";
|
|
24
25
|
import {
|
|
25
26
|
PLUGIN_RUNTIME_ERROR_CODES,
|
|
@@ -46,6 +47,7 @@ const SCRIPT_ALIASES = Object.freeze({
|
|
|
46
47
|
"intent-routing", "auto-loop", "auto-loop-runner", "trellis-auto-loop",
|
|
47
48
|
],
|
|
48
49
|
});
|
|
50
|
+
const TRELLIS_TEXT_EXTENSIONS = new Set([".json", ".md", ".toml", ".txt", ".yaml", ".yml"]);
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* 列出目标目录中的普通文件。
|
|
@@ -66,7 +68,7 @@ function listExistingFiles(directory) {
|
|
|
66
68
|
*/
|
|
67
69
|
export function projectSkillGardenContent(options) {
|
|
68
70
|
const { projectRoot, resolved, pluginPackage } = options;
|
|
69
|
-
const { variantDir, skills } = pluginPackage.skillGarden;
|
|
71
|
+
const { variantDir, skills, pythonCommand } = pluginPackage.skillGarden;
|
|
70
72
|
const payloads = new Map();
|
|
71
73
|
const mutations = [];
|
|
72
74
|
const directoryRemovals = [];
|
|
@@ -86,20 +88,24 @@ export function projectSkillGardenContent(options) {
|
|
|
86
88
|
* @param {Buffer} content 最终字节
|
|
87
89
|
* @param {string} source 来源说明
|
|
88
90
|
* @param {"exclusive"|"shared"} ownership 所有权
|
|
91
|
+
* @param {boolean} materializePython 是否按目标项目命令物化明确文本载荷
|
|
89
92
|
*/
|
|
90
|
-
function addFile(target, content, source, ownership = "exclusive") {
|
|
93
|
+
function addFile(target, content, source, ownership = "exclusive", materializePython = false) {
|
|
91
94
|
assertSafePosixRelativePath(target, "skill-garden 投影目标");
|
|
92
95
|
const absoluteTarget = path.join(projectRoot, ...target.split("/"));
|
|
96
|
+
const payload = materializePython && TRELLIS_TEXT_EXTENSIONS.has(path.extname(target))
|
|
97
|
+
? Buffer.from(materializeTrellisPythonText(content.toString("utf8"), pythonCommand))
|
|
98
|
+
: content;
|
|
93
99
|
const mutation = {
|
|
94
100
|
owner: resolved.id,
|
|
95
101
|
target,
|
|
96
102
|
operation: "write",
|
|
97
103
|
beforeHash: hashFileIfExists(absoluteTarget),
|
|
98
|
-
afterHash: hashContent(
|
|
104
|
+
afterHash: hashContent(payload),
|
|
99
105
|
source,
|
|
100
106
|
allowUnownedWrite: true,
|
|
101
107
|
};
|
|
102
|
-
payloads.set(contentMutationKey(mutation),
|
|
108
|
+
payloads.set(contentMutationKey(mutation), payload);
|
|
103
109
|
mutations.push(mutation);
|
|
104
110
|
paths.set(target, {
|
|
105
111
|
path: target,
|
|
@@ -116,13 +122,26 @@ export function projectSkillGardenContent(options) {
|
|
|
116
122
|
* @param {string} targetRoot 目标目录
|
|
117
123
|
* @param {string} sourceLabel 来源标签
|
|
118
124
|
* @param {"exclusive"|"shared"} ownership 所有权
|
|
125
|
+
* @param {boolean} materializePython 是否按目标项目命令物化明确文本载荷
|
|
119
126
|
*/
|
|
120
|
-
function addTree(
|
|
127
|
+
function addTree(
|
|
128
|
+
sourceRoot,
|
|
129
|
+
targetRoot,
|
|
130
|
+
sourceLabel,
|
|
131
|
+
ownership = "exclusive",
|
|
132
|
+
materializePython = false,
|
|
133
|
+
) {
|
|
121
134
|
const desired = new Set();
|
|
122
135
|
for (const file of listCanonicalTreeFiles(sourceRoot)) {
|
|
123
136
|
const target = `${targetRoot}/${file.path}`;
|
|
124
137
|
desired.add(file.path);
|
|
125
|
-
addFile(
|
|
138
|
+
addFile(
|
|
139
|
+
target,
|
|
140
|
+
fs.readFileSync(file.absolutePath),
|
|
141
|
+
`${sourceLabel}:${file.path}`,
|
|
142
|
+
ownership,
|
|
143
|
+
materializePython,
|
|
144
|
+
);
|
|
126
145
|
}
|
|
127
146
|
for (const file of listExistingFiles(path.join(projectRoot, ...targetRoot.split("/")))) {
|
|
128
147
|
if (desired.has(file.path)) continue;
|
|
@@ -192,6 +211,8 @@ export function projectSkillGardenContent(options) {
|
|
|
192
211
|
path.join(sourceRoot, entry.name),
|
|
193
212
|
`${target.root}/${entry.name}`,
|
|
194
213
|
`skill-garden:${pluginPackage.skillGarden.variant}:${target.source}:${entry.name}`,
|
|
214
|
+
"exclusive",
|
|
215
|
+
pluginPackage.skillGarden.variant === "0.6",
|
|
195
216
|
);
|
|
196
217
|
installed.add(entry.name);
|
|
197
218
|
}
|
|
@@ -208,6 +229,8 @@ export function projectSkillGardenContent(options) {
|
|
|
208
229
|
`.claude/commands/trellis/${file.name}`,
|
|
209
230
|
fs.readFileSync(path.join(commandRoot, file.name)),
|
|
210
231
|
`skill-garden:${pluginPackage.skillGarden.variant}:command:${file.name}`,
|
|
232
|
+
"exclusive",
|
|
233
|
+
pluginPackage.skillGarden.variant === "0.6",
|
|
211
234
|
);
|
|
212
235
|
installed.add(name);
|
|
213
236
|
}
|
|
@@ -9,6 +9,7 @@ import { validatePluginManifest } from "../../plugin/schemas/plugin-manifest.js"
|
|
|
9
9
|
import { markRuntimeBuiltinProvider } from "../../plugin/runtime-extensions.js";
|
|
10
10
|
import { BuiltinSourceProvider } from "../../plugin/sources/builtin-provider.js";
|
|
11
11
|
import { flowerPatchAdapters } from "../../lib/platform-patch-adapters.js";
|
|
12
|
+
import { resolveTrellisPythonCommand } from "../../lib/trellis-python-command.js";
|
|
12
13
|
import { projectSkillGardenContent } from "./content-adapter.js";
|
|
13
14
|
|
|
14
15
|
/** 内置 skill-garden canonical Plugin ID。 */
|
|
@@ -97,6 +98,7 @@ function runtimeManifest(variant) {
|
|
|
97
98
|
*/
|
|
98
99
|
function ensureSkillGardenReady(provider) {
|
|
99
100
|
if (provider.snapshot) return;
|
|
101
|
+
provider.pythonCommand = resolveTrellisPythonCommand(provider.projectRoot);
|
|
100
102
|
provider.snapshot = resolveEnhancementSnapshot(provider.projectRoot, provider.variantOverride);
|
|
101
103
|
provider.manifest = runtimeManifest(provider.snapshot.variant);
|
|
102
104
|
provider.integrity = stablePayloadDigest([
|
|
@@ -145,6 +147,7 @@ export class SkillGardenBuiltinProvider {
|
|
|
145
147
|
this.manifest = null;
|
|
146
148
|
this.integrity = null;
|
|
147
149
|
this.resolutionManifest = null;
|
|
150
|
+
this.pythonCommand = null;
|
|
148
151
|
if (fs.existsSync(path.join(this.projectRoot, ".trellis", ".version"))) ensureSkillGardenReady(this);
|
|
149
152
|
markRuntimeBuiltinProvider(this);
|
|
150
153
|
}
|
|
@@ -208,11 +211,13 @@ export class SkillGardenBuiltinProvider {
|
|
|
208
211
|
compatibilityFile: path.join(this.snapshot.variantDir, "overrides", "compatibility.json"),
|
|
209
212
|
conflictsFile: path.join(this.snapshot.variantDir, "overrides", "conflicts.json"),
|
|
210
213
|
},
|
|
214
|
+
textMaterialization: { trellisPythonCommand: this.pythonCommand.command },
|
|
211
215
|
},
|
|
212
216
|
{
|
|
213
217
|
id: "flower",
|
|
214
218
|
patchesDir: path.join(PKG_ROOT, "src", "patches", "platforms"),
|
|
215
219
|
bundlesDir: path.join(PKG_ROOT, "src", "patches", "bundles"),
|
|
220
|
+
textMaterialization: { trellisPythonCommand: this.pythonCommand.command },
|
|
216
221
|
},
|
|
217
222
|
] : [];
|
|
218
223
|
return {
|
|
@@ -220,10 +225,16 @@ export class SkillGardenBuiltinProvider {
|
|
|
220
225
|
manifest: this.resolutionManifest,
|
|
221
226
|
integrity: acceptedIntegrity,
|
|
222
227
|
catalogs,
|
|
223
|
-
systemAdapters: this.snapshot.variant === "0.6"
|
|
228
|
+
systemAdapters: this.snapshot.variant === "0.6"
|
|
229
|
+
? flowerPatchAdapters(this.pythonCommand.command)
|
|
230
|
+
: {},
|
|
224
231
|
patchOptions: { skills: this.skills },
|
|
225
232
|
allowContentPatchOverlap: true,
|
|
226
|
-
skillGarden: {
|
|
233
|
+
skillGarden: {
|
|
234
|
+
...this.snapshot,
|
|
235
|
+
skills: this.skills,
|
|
236
|
+
pythonCommand: this.pythonCommand.command,
|
|
237
|
+
},
|
|
227
238
|
};
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolvePatchCatalogPolicy } from "./patch-engine.js";
|
|
4
|
+
import { materializeTrellisPythonText } from "./trellis-python-command.js";
|
|
4
5
|
|
|
5
6
|
const SEVERITIES = new Set(["error", "warning", "info"]);
|
|
6
7
|
const ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -155,6 +156,27 @@ function validateConflicts(raw) {
|
|
|
155
156
|
return raw;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
function materializeConflictAssertions(conflicts, command) {
|
|
160
|
+
if (command === undefined) return conflicts;
|
|
161
|
+
return {
|
|
162
|
+
...conflicts,
|
|
163
|
+
rules: conflicts.rules.map((rule) => ({
|
|
164
|
+
...rule,
|
|
165
|
+
assertion: rule.assertion.type === "max-occurrences"
|
|
166
|
+
? {
|
|
167
|
+
...rule.assertion,
|
|
168
|
+
value: materializeTrellisPythonText(rule.assertion.value, command),
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
...rule.assertion,
|
|
172
|
+
values: rule.assertion.values.map((value) =>
|
|
173
|
+
materializeTrellisPythonText(value, command)
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
function parseVersion(value) {
|
|
159
181
|
if (typeof value !== "string") return null;
|
|
160
182
|
const match = value.match(SEMVER_RE);
|
|
@@ -253,7 +275,7 @@ export function loadPatchPolicy(overridesDir, catalogId = "skill-garden") {
|
|
|
253
275
|
/**
|
|
254
276
|
* 按 catalog descriptor 加载并校验其声明式 Patch policy。
|
|
255
277
|
*
|
|
256
|
-
* @param {Array<{id:string,policy?:{compatibilityFile?:string,conflictsFile?:string}}>} catalogs catalog 描述符
|
|
278
|
+
* @param {Array<{id:string,policy?:{compatibilityFile?:string,conflictsFile?:string},textMaterialization?:{trellisPythonCommand:string}}>} catalogs catalog 描述符
|
|
257
279
|
* @returns {Array<{catalog:string,compatibility?:object,conflicts?:object}>} 已校验的 policy 列表
|
|
258
280
|
*/
|
|
259
281
|
export function loadPatchPolicies(catalogs) {
|
|
@@ -271,10 +293,14 @@ export function loadPatchPolicies(catalogs) {
|
|
|
271
293
|
));
|
|
272
294
|
}
|
|
273
295
|
if (paths.conflictsFile) {
|
|
274
|
-
|
|
296
|
+
const conflicts = validateConflicts(readJson(
|
|
275
297
|
paths.conflictsFile,
|
|
276
298
|
`${catalogId} conflict policy`,
|
|
277
299
|
));
|
|
300
|
+
policy.conflicts = materializeConflictAssertions(
|
|
301
|
+
conflicts,
|
|
302
|
+
catalog.textMaterialization?.trellisPythonCommand,
|
|
303
|
+
);
|
|
278
304
|
}
|
|
279
305
|
if (!policy.compatibility && !policy.conflicts) {
|
|
280
306
|
throw new Error(`catalog ${catalogId} policy 至少声明一个文件`);
|
package/src/lib/patch-engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { preserveFirstBackup } from "./backup.js";
|
|
5
5
|
import { shouldInstallName } from "./skill-filter.js";
|
|
6
|
+
import { materializeTrellisPythonText } from "./trellis-python-command.js";
|
|
6
7
|
|
|
7
8
|
const PATCH_SCHEMA_VERSION = 2;
|
|
8
9
|
const BUNDLE_SCHEMA_VERSION = 1;
|
|
@@ -691,9 +692,45 @@ function resolveCatalogPolicy(catalog, catalogFiles) {
|
|
|
691
692
|
return policy;
|
|
692
693
|
}
|
|
693
694
|
|
|
695
|
+
function resolveCatalogPythonCommand(catalog) {
|
|
696
|
+
if (catalog.textMaterialization === undefined) return null;
|
|
697
|
+
assertPlainObject(catalog.textMaterialization, `catalog ${catalog.id} textMaterialization`);
|
|
698
|
+
const keys = Object.keys(catalog.textMaterialization);
|
|
699
|
+
if (keys.length !== 1 || keys[0] !== "trellisPythonCommand") {
|
|
700
|
+
throw new Error(`catalog ${catalog.id} textMaterialization 只支持 trellisPythonCommand`);
|
|
701
|
+
}
|
|
702
|
+
const command = catalog.textMaterialization.trellisPythonCommand;
|
|
703
|
+
if (typeof command !== "string" || !command.trim()) {
|
|
704
|
+
throw new Error(`catalog ${catalog.id} textMaterialization.trellisPythonCommand 必须是非空字符串`);
|
|
705
|
+
}
|
|
706
|
+
return command;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function materializeOperationPythonCommand(operation, command) {
|
|
710
|
+
if (!command) return operation;
|
|
711
|
+
const selectorText = typeof operation.selectorText === "string"
|
|
712
|
+
? materializeTrellisPythonText(operation.selectorText, command)
|
|
713
|
+
: operation.selectorText;
|
|
714
|
+
return {
|
|
715
|
+
...operation,
|
|
716
|
+
selector: {
|
|
717
|
+
...operation.selector,
|
|
718
|
+
...(typeof operation.selector.text === "string" ? { text: selectorText } : {}),
|
|
719
|
+
},
|
|
720
|
+
selectorText,
|
|
721
|
+
content: typeof operation.content === "string"
|
|
722
|
+
? materializeTrellisPythonText(operation.content, command)
|
|
723
|
+
: operation.content,
|
|
724
|
+
baselines: operation.baselines.map((baseline) =>
|
|
725
|
+
materializeTrellisPythonText(baseline, command)
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
694
730
|
function loadCatalog(catalog, skills, allowedSelectors) {
|
|
695
731
|
assertPlainObject(catalog, "Patch catalog");
|
|
696
732
|
assertId(catalog.id, "Patch catalog.id");
|
|
733
|
+
const pythonCommand = resolveCatalogPythonCommand(catalog);
|
|
697
734
|
const patchesDir = path.resolve(catalog.patchesDir);
|
|
698
735
|
const bundlesDir = path.resolve(catalog.bundlesDir);
|
|
699
736
|
const catalogRoot = path.dirname(patchesDir);
|
|
@@ -742,9 +779,10 @@ function loadCatalog(catalog, skills, allowedSelectors) {
|
|
|
742
779
|
qualifiedId: qualifyId(catalog.id, raw.id),
|
|
743
780
|
markerIdentity,
|
|
744
781
|
};
|
|
745
|
-
patch.operations = raw.operations.map((operation) =>
|
|
746
|
-
normalizeOperation(operation, leafDir, patch, seenOperationIds, allowedSelectors)
|
|
747
|
-
|
|
782
|
+
patch.operations = raw.operations.map((operation) => materializeOperationPythonCommand(
|
|
783
|
+
normalizeOperation(operation, leafDir, patch, seenOperationIds, allowedSelectors),
|
|
784
|
+
pythonCommand,
|
|
785
|
+
));
|
|
748
786
|
patchByRef.set(ref, patch);
|
|
749
787
|
catalogFiles.push(file, ...listRecursive(leafDir, (candidate) => candidate !== file));
|
|
750
788
|
}
|
|
@@ -25,9 +25,9 @@ function findHookCommand(config, event, needle) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function resolveCommand(name, config) {
|
|
28
|
+
function resolveCommand(name, config, pythonCommand) {
|
|
29
29
|
const codexBase = findHookCommand(config, "UserPromptSubmit", CODEX_WORKFLOW_HOOK) ||
|
|
30
|
-
|
|
30
|
+
`${pythonCommand} -X utf8 ${CODEX_WORKFLOW_HOOK}`;
|
|
31
31
|
if (name === "codex-session-start") {
|
|
32
32
|
return codexBase.replace(CODEX_WORKFLOW_HOOK, CODEX_SESSION_START);
|
|
33
33
|
}
|
|
@@ -35,14 +35,14 @@ function resolveCommand(name, config) {
|
|
|
35
35
|
return codexBase.replace(CODEX_WORKFLOW_HOOK, FLOWER_UPDATE_HOOK_REL);
|
|
36
36
|
}
|
|
37
37
|
const claudeBase = findHookCommand(config, "UserPromptSubmit", CLAUDE_WORKFLOW_HOOK) ||
|
|
38
|
-
|
|
38
|
+
`${pythonCommand} ${CLAUDE_WORKFLOW_HOOK}`;
|
|
39
39
|
if (name === "claude-flower-update") {
|
|
40
40
|
return claudeBase.replace(CLAUDE_WORKFLOW_HOOK, FLOWER_UPDATE_HOOK_REL);
|
|
41
41
|
}
|
|
42
42
|
throw new Error(`未知 Hook commandResolver:${name}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function applyJsonHookCommand({ value, operation }) {
|
|
45
|
+
function applyJsonHookCommand({ value, operation, pythonCommand = "python3" }) {
|
|
46
46
|
let config;
|
|
47
47
|
try {
|
|
48
48
|
config = value.trim() ? JSON.parse(value) : {};
|
|
@@ -86,7 +86,7 @@ function applyJsonHookCommand({ value, operation }) {
|
|
|
86
86
|
}
|
|
87
87
|
let command;
|
|
88
88
|
try {
|
|
89
|
-
command = resolveCommand(content.commandResolver, config);
|
|
89
|
+
command = resolveCommand(content.commandResolver, config, pythonCommand);
|
|
90
90
|
} catch (error) {
|
|
91
91
|
return { error: error.message };
|
|
92
92
|
}
|
|
@@ -304,11 +304,12 @@ function applyTomlSection({ value, operation }) {
|
|
|
304
304
|
/**
|
|
305
305
|
* Flower 自有 Patch Adapter 注册表。
|
|
306
306
|
*
|
|
307
|
+
* @param {string} [pythonCommand] 目标 Trellis 项目实际使用的 Python 命令
|
|
307
308
|
* @returns {Record<string, Function>} selector type 到受控 Adapter 的映射
|
|
308
309
|
*/
|
|
309
|
-
export function flowerPatchAdapters() {
|
|
310
|
+
export function flowerPatchAdapters(pythonCommand = "python3") {
|
|
310
311
|
return {
|
|
311
|
-
"json-hook-command": applyJsonHookCommand,
|
|
312
|
+
"json-hook-command": (context) => applyJsonHookCommand({ ...context, pythonCommand }),
|
|
312
313
|
"yaml-key": applyYamlKey,
|
|
313
314
|
"toml-section": applyTomlSection,
|
|
314
315
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TARGET_EVIDENCE_FILES = Object.freeze([
|
|
5
|
+
".codex/hooks.json",
|
|
6
|
+
".claude/settings.json",
|
|
7
|
+
".trellis/workflow.md",
|
|
8
|
+
]);
|
|
9
|
+
const GENERATED_COMMAND_RE = /(?:^|[\s`"'([{])(py -3|python3|python)(?=\s+(?:-X\s+utf8\s+)?(?:\.\/)?(?:\.trellis\/scripts|\.codex\/hooks|\.claude\/hooks)\/)/m;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 校验可用于 Trellis 文本物化的 Python 命令。
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} value 待校验命令
|
|
15
|
+
* @param {string} label 错误字段说明
|
|
16
|
+
* @returns {string} 去除首尾空白后的命令
|
|
17
|
+
*/
|
|
18
|
+
function normalizePythonCommand(value, label) {
|
|
19
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
20
|
+
throw new Error(`${label} 必须是非空字符串`);
|
|
21
|
+
}
|
|
22
|
+
const command = value.trim();
|
|
23
|
+
if (command.includes("\0") || /[\r\n]/.test(command)) {
|
|
24
|
+
throw new Error(`${label} 不能包含换行或 NUL`);
|
|
25
|
+
}
|
|
26
|
+
return command;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 从 Trellis 已生成文件中识别实际使用的 Python 命令。
|
|
31
|
+
*
|
|
32
|
+
* @param {string} targetRoot 目标项目根目录
|
|
33
|
+
* @returns {{command:string,source:string}|null} 命令及证据来源;没有证据时返回 null
|
|
34
|
+
*/
|
|
35
|
+
function resolveGeneratedCommand(targetRoot) {
|
|
36
|
+
for (const relative of TARGET_EVIDENCE_FILES) {
|
|
37
|
+
const file = path.join(targetRoot, ...relative.split("/"));
|
|
38
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) continue;
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
value = fs.readFileSync(file, "utf8");
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const match = value.match(GENERATED_COMMAND_RE);
|
|
46
|
+
if (match) return { command: match[1], source: relative };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 解析目标 Trellis 项目实际使用的 Python 命令。
|
|
53
|
+
*
|
|
54
|
+
* 已生成目标文件是最高优先级证据;仅在目标中没有证据时才读取环境变量和平台回退。
|
|
55
|
+
*
|
|
56
|
+
* @param {string} target 目标 Trellis 项目根目录
|
|
57
|
+
* @param {{env?:Record<string,string|undefined>,platform?:string}} [options] 环境与平台覆盖,主要用于测试
|
|
58
|
+
* @returns {{command:string,source:string}} 命令及解析来源
|
|
59
|
+
*/
|
|
60
|
+
export function resolveTrellisPythonCommand(target, options = {}) {
|
|
61
|
+
const targetRoot = path.resolve(target);
|
|
62
|
+
const generated = resolveGeneratedCommand(targetRoot);
|
|
63
|
+
if (generated) return generated;
|
|
64
|
+
|
|
65
|
+
const env = options.env || process.env;
|
|
66
|
+
if (env.TRELLIS_PYTHON_CMD !== undefined) {
|
|
67
|
+
return {
|
|
68
|
+
command: normalizePythonCommand(env.TRELLIS_PYTHON_CMD, "TRELLIS_PYTHON_CMD"),
|
|
69
|
+
source: "env:TRELLIS_PYTHON_CMD",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const platform = options.platform || process.platform;
|
|
74
|
+
return {
|
|
75
|
+
command: platform === "win32" ? "python" : "python3",
|
|
76
|
+
source: `platform:${platform}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 按 Trellis 模板渲染规则物化文本中的 canonical `python3` 命令。
|
|
82
|
+
*
|
|
83
|
+
* @param {string} value canonical Trellis 文本
|
|
84
|
+
* @param {string} command 目标项目 Python 命令
|
|
85
|
+
* @returns {string} 已物化文本
|
|
86
|
+
*/
|
|
87
|
+
export function materializeTrellisPythonText(value, command) {
|
|
88
|
+
if (typeof value !== "string") throw new Error("Trellis Python 文本必须是字符串");
|
|
89
|
+
const normalizedCommand = normalizePythonCommand(command, "Trellis Python 命令");
|
|
90
|
+
if (normalizedCommand === "python3") return value;
|
|
91
|
+
return value
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((line) => line.startsWith("#!") ? line : line.replaceAll("python3", normalizedCommand))
|
|
94
|
+
.join("\n");
|
|
95
|
+
}
|