@yottameta/yotta-skills 0.23.2 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/SKILL.md +36 -1
- package/bin/yotta-skills.js +203 -14
- package/install.sh +215 -57
- package/lib/deps.js +173 -0
- package/lib/install-pipeline.js +5 -0
- package/lib/npm-pack.js +8 -2
- package/lib/registry-fetch-child.js +29 -0
- package/lib/registry-fetch.js +448 -0
- package/lib/untar.js +179 -0
- package/package.json +1 -1
- package/references/install-flow.md +37 -4
- package/references/skill-list.md +29 -29
- package/scan-policy.json +26 -26
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +1 -1
- package/skills.json +55 -28
package/install.sh
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# yotta-skills
|
|
3
|
-
# 用途:把「元阁安装器技能」本身装进智能体/目录(它自带 SKILL.md,让代理能调用元阁)。
|
|
4
|
-
# 注意:这**不是**把 27 个 yotta-* 技能一并装齐——装全家需再跑
|
|
5
|
-
# `node bin/yotta-skills.js install --agent <name>`(或 --dir <path>)。
|
|
2
|
+
# yotta-skills 多智能体安装脚本(YottaSkills)
|
|
6
3
|
# 用法:
|
|
7
|
-
# bash install.sh --agent <name> #
|
|
4
|
+
# bash install.sh --agent <name> # 按智能体默认用户级目录安装
|
|
8
5
|
# bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
|
|
9
6
|
# bash install.sh -g # 装到全部已知智能体用户级目录
|
|
10
|
-
# bash install.sh
|
|
7
|
+
# bash install.sh # 检测并安装到已存在的项目级目录
|
|
11
8
|
# bash install.sh --list # 列出智能体 -> 默认目录
|
|
9
|
+
# bash install.sh --dry-run # 只显示将写入的目标,不写文件
|
|
10
|
+
# bash install.sh --version # 显示版本
|
|
11
|
+
# bash install.sh --help # 显示帮助
|
|
12
|
+
# 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
|
|
12
13
|
set -euo pipefail
|
|
13
14
|
|
|
14
15
|
SKILL_NAME="yotta-skills"
|
|
15
16
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
_IS_WINDOWS=0
|
|
16
18
|
case "$(uname -s)" in
|
|
17
19
|
MINGW*|MSYS*)
|
|
18
20
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
|
|
21
|
+
_IS_WINDOWS=1
|
|
19
22
|
;;
|
|
23
|
+
CYGWIN*) _IS_WINDOWS=1 ;;
|
|
20
24
|
esac
|
|
21
25
|
|
|
22
26
|
# 智能体 -> 用户级默认目录(--agent 装到第一个)
|
|
23
27
|
# .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
|
|
24
|
-
# 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
|
|
25
|
-
_IS_WINDOWS=0
|
|
26
|
-
case "$(uname -s)" in
|
|
27
|
-
MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
|
|
28
|
-
esac
|
|
29
28
|
dirs_for() {
|
|
30
29
|
case "$1" in
|
|
31
30
|
claude) echo ".claude/skills" ;;
|
|
@@ -63,46 +62,153 @@ resolve_user() {
|
|
|
63
62
|
esac
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
local
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
""|"/") echo "安装失败:拒绝不安全的目标目录:'$base'" >&2; return 1 ;;
|
|
72
|
-
esac
|
|
73
|
-
if [ -L "$base" ]; then
|
|
74
|
-
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2; return 1
|
|
65
|
+
skill_version() {
|
|
66
|
+
local pkg="$SOURCE_DIR/package.json" v=""
|
|
67
|
+
[ -f "$pkg" ] || return 0
|
|
68
|
+
if command -v awk >/dev/null 2>&1; then
|
|
69
|
+
v="$(awk -F'"' '{for (i=1;i<=NF;i++) if ($i=="version") {print $(i+2); exit}}' "$pkg" 2>/dev/null || true)"
|
|
75
70
|
fi
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if [ -L "$dest" ]; then
|
|
79
|
-
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2; return 1
|
|
71
|
+
if [ -z "$v" ]; then
|
|
72
|
+
v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
|
|
80
73
|
fi
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
printf '%s' "$v"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
usage() {
|
|
78
|
+
echo "$SKILL_NAME 安装脚本(YottaSkills)"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "用法:"
|
|
81
|
+
echo " bash install.sh --agent <name> 按智能体默认用户级目录安装(推荐)"
|
|
82
|
+
echo " bash install.sh --dir <path> 装到指定目录(用户改了目录的智能体)"
|
|
83
|
+
echo " bash install.sh -g 安装到全部已知智能体用户级目录"
|
|
84
|
+
echo " bash install.sh 安装到检测到的项目级目录"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "参数:"
|
|
87
|
+
echo " --agent <name> 智能体键名,见 --list"
|
|
88
|
+
echo " --dir <path> 自定义技能目录"
|
|
89
|
+
echo " -g, --global 安装到全部已知用户级目录"
|
|
90
|
+
echo " --list, -l 列出支持的智能体目录"
|
|
91
|
+
echo " --dry-run 只显示将写入的目标,不写文件"
|
|
92
|
+
echo " --version, -v 显示版本"
|
|
93
|
+
echo " --help, -h 显示帮助"
|
|
94
|
+
echo " --yes, -y 兼容参数(-g 不再强制要求)"
|
|
95
|
+
echo ""
|
|
96
|
+
echo "退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolve_path() {
|
|
100
|
+
# 与 SOURCE_DIR 同一风格(Windows Git Bash 用 pwd -W),供路径包含判断使用
|
|
101
|
+
local p="$1"
|
|
102
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
103
|
+
(cd "$p" 2>/dev/null && pwd -W)
|
|
104
|
+
else
|
|
105
|
+
(cd "$p" 2>/dev/null && pwd -P)
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assert_safe_target() {
|
|
110
|
+
local dest="$1" src_real dest_real parent
|
|
111
|
+
src_real="$(resolve_path "$SOURCE_DIR")" || return 0
|
|
112
|
+
if [ -d "$dest" ]; then
|
|
113
|
+
dest_real="$(resolve_path "$dest")" || return 0
|
|
114
|
+
else
|
|
115
|
+
parent="$(dirname "$dest")"
|
|
116
|
+
dest_real="$(resolve_path "$parent")" || return 0
|
|
117
|
+
dest_real="$dest_real/$(basename "$dest")"
|
|
83
118
|
fi
|
|
84
|
-
|
|
119
|
+
case "$dest_real" in
|
|
120
|
+
"$src_real"|"$src_real"/*)
|
|
121
|
+
echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
|
|
122
|
+
return 1
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
85
127
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
# 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
|
|
129
|
+
# 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
|
|
130
|
+
DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules"
|
|
131
|
+
clean_residue() {
|
|
132
|
+
local dest="$1" name p
|
|
133
|
+
[ -d "$dest" ] || return 0
|
|
134
|
+
[ -f "$dest/SKILL.md" ] || return 0
|
|
135
|
+
for name in $DEV_SKIP; do
|
|
136
|
+
p="$dest/$name"
|
|
137
|
+
[ -L "$p" ] && continue
|
|
138
|
+
if [ -e "$p" ]; then
|
|
139
|
+
rm -rf -- "$p"
|
|
140
|
+
echo "已清理残留: $name"
|
|
90
141
|
fi
|
|
91
142
|
done
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
find "$dest" -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' \) -prune -exec rm -rf -- {} + 2>/dev/null || true
|
|
144
|
+
find "$dest" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete 2>/dev/null || true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# 复制技能本体:顶层跳过开发件与运行时不相关目录(嵌套同名保留);
|
|
148
|
+
# 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
|
|
149
|
+
copy_tree() {
|
|
150
|
+
local src="$1" dst="$2" top="$3" entry base
|
|
151
|
+
mkdir -p "$dst"
|
|
152
|
+
for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
|
|
153
|
+
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
154
|
+
base="$(basename "$entry")"
|
|
155
|
+
case "$base" in
|
|
156
|
+
__pycache__|.pytest_cache|.mypy_cache) continue ;;
|
|
157
|
+
esac
|
|
158
|
+
if [ "$top" = "1" ]; then
|
|
159
|
+
case " $DEV_SKIP " in
|
|
160
|
+
*" $base "*) continue ;;
|
|
161
|
+
esac
|
|
162
|
+
fi
|
|
163
|
+
if [ -d "$entry" ] && [ ! -L "$entry" ]; then
|
|
164
|
+
copy_tree "$entry" "$dst/$base" 0 || return 1
|
|
165
|
+
else
|
|
166
|
+
case "$base" in
|
|
167
|
+
*.pyc|*.pyo) continue ;;
|
|
168
|
+
esac
|
|
169
|
+
cp -RP "$entry" "$dst/$base" || return 1
|
|
95
170
|
fi
|
|
96
171
|
done
|
|
97
|
-
|
|
98
|
-
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
install_to() {
|
|
175
|
+
local base="$1" dry="$2" dest
|
|
176
|
+
dest="$base/$SKILL_NAME"
|
|
177
|
+
if [ -L "$base" ]; then
|
|
178
|
+
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
if [ -e "$base" ] && [ ! -d "$base" ]; then
|
|
182
|
+
echo "安装失败:目标路径已存在且不是目录:$base" >&2
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
if [ -L "$dest" ]; then
|
|
186
|
+
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
if [ -e "$dest" ] && [ ! -d "$dest" ]; then
|
|
190
|
+
echo "安装失败:技能路径已存在且不是目录:$dest" >&2
|
|
191
|
+
return 1
|
|
192
|
+
fi
|
|
193
|
+
assert_safe_target "$dest" || return 1
|
|
194
|
+
if [ "$dry" = "1" ]; then
|
|
195
|
+
echo "[dry-run] 将安装到 -> $dest"
|
|
196
|
+
return 0
|
|
197
|
+
fi
|
|
198
|
+
clean_residue "$dest"
|
|
199
|
+
mkdir -p "$dest" || return 1
|
|
200
|
+
copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
|
|
201
|
+
if [ ! -f "$dest/SKILL.md" ]; then
|
|
202
|
+
echo "安装失败:结果缺少 SKILL.md" >&2
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
99
205
|
echo "installed -> $dest"
|
|
100
206
|
}
|
|
101
207
|
|
|
102
208
|
list() {
|
|
103
209
|
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
210
|
+
local a dirs first
|
|
104
211
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
105
|
-
local dirs first
|
|
106
212
|
dirs="$(dirs_for "$a")"
|
|
107
213
|
first="${dirs%% *}"
|
|
108
214
|
case "$first" in
|
|
@@ -121,49 +227,101 @@ list() {
|
|
|
121
227
|
}
|
|
122
228
|
|
|
123
229
|
main() {
|
|
124
|
-
local agent="" dir="" global=0 show_list=0
|
|
230
|
+
local agent="" dir="" global=0 show_list=0 show_help=0 show_version=0 dry=0
|
|
125
231
|
while [ $# -gt 0 ]; do
|
|
126
232
|
case "$1" in
|
|
127
|
-
--
|
|
128
|
-
--
|
|
129
|
-
-g|--global) global=1 ;;
|
|
130
|
-
-y|--yes) assume_yes=1 ;;
|
|
233
|
+
--help|-h) show_help=1 ;;
|
|
234
|
+
--version|-v) show_version=1 ;;
|
|
131
235
|
--list|-l) show_list=1 ;;
|
|
236
|
+
-g|--global) global=1 ;;
|
|
237
|
+
--dry-run) dry=1 ;;
|
|
238
|
+
--yes|-y) : ;;
|
|
239
|
+
--agent)
|
|
240
|
+
shift
|
|
241
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
242
|
+
echo "用法错误: --agent 需要一个非空名称" >&2
|
|
243
|
+
exit 2
|
|
244
|
+
fi
|
|
245
|
+
agent="$1"
|
|
246
|
+
;;
|
|
247
|
+
--dir)
|
|
248
|
+
shift
|
|
249
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
250
|
+
echo "用法错误: --dir 需要一个非空路径" >&2
|
|
251
|
+
exit 2
|
|
252
|
+
fi
|
|
253
|
+
dir="$1"
|
|
254
|
+
;;
|
|
132
255
|
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
133
256
|
esac
|
|
134
257
|
shift
|
|
135
258
|
done
|
|
136
259
|
|
|
137
|
-
if [ "$
|
|
138
|
-
if [
|
|
260
|
+
if [ "$show_help" = "1" ]; then usage; return 0; fi
|
|
261
|
+
if [ "$show_version" = "1" ]; then
|
|
262
|
+
local v
|
|
263
|
+
v="$(skill_version)"
|
|
264
|
+
if [ -z "$v" ]; then
|
|
265
|
+
echo "$SKILL_NAME 版本未知(未找到 package.json)" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
echo "$SKILL_NAME v$v"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
if [ "$show_list" = "1" ]; then list; return 0; fi
|
|
272
|
+
|
|
273
|
+
local selected=0
|
|
274
|
+
[ -n "$dir" ] && selected=$((selected + 1))
|
|
275
|
+
[ -n "$agent" ] && selected=$((selected + 1))
|
|
276
|
+
[ "$global" = "1" ] && selected=$((selected + 1))
|
|
277
|
+
if [ "$selected" -gt 1 ]; then
|
|
278
|
+
echo "用法错误: --dir / --agent / -g 只能选一个" >&2
|
|
279
|
+
exit 2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
if [ -n "$dir" ]; then
|
|
283
|
+
install_to "$dir" "$dry" || exit 1
|
|
284
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
139
287
|
if [ -n "$agent" ]; then
|
|
140
288
|
local dirs first
|
|
141
289
|
if ! dirs="$(dirs_for "$agent")"; then
|
|
142
|
-
echo "未收录智能体: $agent
|
|
290
|
+
echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
|
|
291
|
+
exit 2
|
|
143
292
|
fi
|
|
144
293
|
first="${dirs%% *}"
|
|
145
|
-
install_to "$(resolve_user "$first")"
|
|
294
|
+
install_to "$(resolve_user "$first")" "$dry" || exit 1
|
|
295
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
296
|
+
return 0
|
|
146
297
|
fi
|
|
147
298
|
if [ "$global" = "1" ]; then
|
|
148
|
-
|
|
149
|
-
echo "批量安装到全部已知智能体目录会写入多个宿主目录,需要显式确认:请加 --yes(或用 --agent / --dir 指定单一目标)。" >&2
|
|
150
|
-
exit 2
|
|
151
|
-
fi
|
|
152
|
-
echo "安装到全部已知智能体用户级目录..."
|
|
153
|
-
local dirs rel
|
|
299
|
+
local a dirs rel seen=" "
|
|
154
300
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
155
|
-
dirs="$(dirs_for "$a")"
|
|
156
|
-
for rel in $dirs; do
|
|
301
|
+
dirs="$(dirs_for "$a")" || continue
|
|
302
|
+
for rel in $dirs; do
|
|
303
|
+
case "$seen" in
|
|
304
|
+
*" $rel "*) continue ;;
|
|
305
|
+
esac
|
|
306
|
+
seen="$seen$rel "
|
|
307
|
+
install_to "$(resolve_user "$rel")" "$dry" || exit 1
|
|
308
|
+
done
|
|
157
309
|
done
|
|
158
|
-
echo "完成。"
|
|
310
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
311
|
+
return 0
|
|
159
312
|
fi
|
|
160
313
|
local installed=0 d
|
|
161
314
|
for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
|
|
162
|
-
if [ -d "$d" ]; then
|
|
315
|
+
if [ -d "$d" ]; then
|
|
316
|
+
install_to "$d" "$dry" || exit 1
|
|
317
|
+
installed=1
|
|
318
|
+
fi
|
|
163
319
|
done
|
|
164
320
|
if [ "$installed" = "0" ]; then
|
|
165
|
-
echo "未检测到项目级智能体目录。可用 --agent <name>
|
|
321
|
+
echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
|
|
322
|
+
exit 4
|
|
166
323
|
fi
|
|
324
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
167
325
|
}
|
|
168
326
|
|
|
169
327
|
main "$@"
|
package/lib/deps.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* lib/deps.js —— 依赖缺口的统一人话提示(元阁 0.24.0,方案 B)。
|
|
4
|
+
*
|
|
5
|
+
* 约定:按需触发、不常驻;每条提示回答四件事:
|
|
6
|
+
* 需要什么 / 为什么 / 怎么办(一条可复制修复)/ 不影响使用。
|
|
7
|
+
* 依赖矩阵:Node 18+ 必需;npm 与系统 tar 是回退通道(内置拉包 / 内置解包为主);
|
|
8
|
+
* 元信装前扫描需要 Python 3.8+,优先指向宿主自带 Python(--python / YOTTA_SKILLS_PYTHON)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const NEED = {
|
|
12
|
+
node: { label: 'Node 18+', need: '>=18', why: '运行元阁 CLI 本体' },
|
|
13
|
+
npm: { label: 'npm', need: '随 Node 安装', why: '拉包回退通道(内置拉包失败时的备用)' },
|
|
14
|
+
python: { label: 'Python 3.8+', need: '>=3.8', why: '元信装前安全扫描' },
|
|
15
|
+
tar: { label: 'tar', need: '可用即可', why: '解包回退通道(内置解包失败时的备用)' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function fixFor(dep, platform) {
|
|
19
|
+
const p = platform || process.platform;
|
|
20
|
+
if (dep === 'node' || dep === 'npm') {
|
|
21
|
+
if (p === 'win32') return 'winget install OpenJS.NodeJS.LTS';
|
|
22
|
+
if (p === 'darwin') return 'brew install node';
|
|
23
|
+
return 'sudo apt install nodejs npm(或到 https://nodejs.org/ 下载 LTS)';
|
|
24
|
+
}
|
|
25
|
+
if (dep === 'python') {
|
|
26
|
+
if (p === 'win32') {
|
|
27
|
+
return '优先用 --python <路径> 或 YOTTA_SKILLS_PYTHON 指向宿主自带 Python;否则 winget install Python.Python.3.12';
|
|
28
|
+
}
|
|
29
|
+
if (p === 'darwin') {
|
|
30
|
+
return '优先用 --python <路径> 或 YOTTA_SKILLS_PYTHON 指向宿主自带 Python;否则 brew install python';
|
|
31
|
+
}
|
|
32
|
+
return '优先用 --python <路径> 或 YOTTA_SKILLS_PYTHON 指向宿主自带 Python;否则 sudo apt install python3';
|
|
33
|
+
}
|
|
34
|
+
if (dep === 'tar') {
|
|
35
|
+
if (p === 'win32') return 'Windows 10+ 自带 tar(缺失时可 winget install bsdtar)';
|
|
36
|
+
if (p === 'darwin') return 'macOS 自带 tar;或 brew install gnu-tar';
|
|
37
|
+
return 'sudo apt install tar';
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 统一三行模板:
|
|
44
|
+
* 需要 <依赖>(用途:<为什么>)—— 装好后重跑:<原命令>
|
|
45
|
+
* 修复:<一条可复制命令>
|
|
46
|
+
* 或者:继续用宿主自带技能,不影响正常使用。<补充>
|
|
47
|
+
*/
|
|
48
|
+
function describe(dep, options) {
|
|
49
|
+
const meta = NEED[dep] || { label: dep, need: '', why: '' };
|
|
50
|
+
const opts = options || {};
|
|
51
|
+
const command = opts.command || 'yotta-skills <原命令>';
|
|
52
|
+
const status = opts.missing
|
|
53
|
+
? '—— 未找到 ' + meta.label + ';装好后重跑:' + command
|
|
54
|
+
: '—— 装好后重跑:' + command;
|
|
55
|
+
const extra = opts.extra ? ' ' + opts.extra : '';
|
|
56
|
+
return [
|
|
57
|
+
'需要 ' + meta.label + '(用途:' + meta.why + ')' + status,
|
|
58
|
+
'修复:' + (opts.fix || fixFor(dep)),
|
|
59
|
+
'或者:继续用宿主自带技能,不影响正常使用。' + extra,
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function semverAtLeast(version, minimum) {
|
|
64
|
+
const got = String(version || '').match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
65
|
+
const need = String(minimum || '').match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
66
|
+
if (!got || !need) return null;
|
|
67
|
+
for (let i = 1; i <= 3; i += 1) {
|
|
68
|
+
const a = Number(got[i] || 0);
|
|
69
|
+
const b = Number(need[i] || 0);
|
|
70
|
+
if (a > b) return true;
|
|
71
|
+
if (a < b) return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Node 版本自检;version 可注入用于测试。 */
|
|
77
|
+
function nodeCheck(version) {
|
|
78
|
+
const current = version || process.versions.node;
|
|
79
|
+
return {
|
|
80
|
+
name: 'node',
|
|
81
|
+
found: true,
|
|
82
|
+
version: current,
|
|
83
|
+
ok: semverAtLeast(current, '18.0.0') === true,
|
|
84
|
+
need: NEED.node.need,
|
|
85
|
+
why: NEED.node.why,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** npm 缺失错误识别(spawn ENOENT / 命令不存在的中英文提示)。 */
|
|
90
|
+
function isNpmMissing(raw) {
|
|
91
|
+
const text = String(raw || '');
|
|
92
|
+
return /ENOENT|EINVAL|npm\.cmd'? ?(无法|不能被)|不是内部或外部命令|not recognized|系统找不到指定/i.test(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function versionLine(proc) {
|
|
96
|
+
if (!proc) return null;
|
|
97
|
+
const text = String((proc.stdout || '') + (proc.stderr || '')).trim();
|
|
98
|
+
if (!text) return null;
|
|
99
|
+
return text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0].slice(0, 80) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* doctor 依赖自检块(只告警不失败)。
|
|
104
|
+
* detected: { node:{version,ok}, npm:{found,version}, python:{found,version,path}, tar:{found,version} }
|
|
105
|
+
*/
|
|
106
|
+
function dependencyReport(detected) {
|
|
107
|
+
const d = detected || {};
|
|
108
|
+
const items = [];
|
|
109
|
+
|
|
110
|
+
const node = d.node || {};
|
|
111
|
+
const nodeVersion = node.version || process.versions.node;
|
|
112
|
+
const nodeOk = node.ok !== undefined ? !!node.ok : semverAtLeast(nodeVersion, '18.0.0') === true;
|
|
113
|
+
items.push({
|
|
114
|
+
name: 'node',
|
|
115
|
+
found: true,
|
|
116
|
+
version: 'v' + String(nodeVersion),
|
|
117
|
+
ok: nodeOk,
|
|
118
|
+
need: NEED.node.need,
|
|
119
|
+
why: NEED.node.why,
|
|
120
|
+
optional: false,
|
|
121
|
+
fix: nodeOk ? null : fixFor('node'),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const npm = d.npm || {};
|
|
125
|
+
items.push({
|
|
126
|
+
name: 'npm',
|
|
127
|
+
found: !!npm.found,
|
|
128
|
+
version: npm.version || null,
|
|
129
|
+
ok: !!npm.found,
|
|
130
|
+
need: NEED.npm.need,
|
|
131
|
+
why: NEED.npm.why,
|
|
132
|
+
optional: true,
|
|
133
|
+
fix: npm.found ? null : fixFor('npm'),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const py = d.python || {};
|
|
137
|
+
const pyVersionOk = py.found ? semverAtLeast(py.version || '', '3.8.0') : false;
|
|
138
|
+
items.push({
|
|
139
|
+
name: 'python',
|
|
140
|
+
found: !!py.found,
|
|
141
|
+
version: py.version || null,
|
|
142
|
+
ok: pyVersionOk === true,
|
|
143
|
+
need: NEED.python.need,
|
|
144
|
+
why: NEED.python.why,
|
|
145
|
+
optional: false,
|
|
146
|
+
fix: pyVersionOk === true ? null : fixFor('python'),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const tar = d.tar || {};
|
|
150
|
+
items.push({
|
|
151
|
+
name: 'tar',
|
|
152
|
+
found: !!tar.found,
|
|
153
|
+
version: tar.version || null,
|
|
154
|
+
ok: !!tar.found,
|
|
155
|
+
need: NEED.tar.need,
|
|
156
|
+
why: NEED.tar.why,
|
|
157
|
+
optional: true,
|
|
158
|
+
fix: tar.found ? null : fixFor('tar'),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return items;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
NEED,
|
|
166
|
+
fixFor,
|
|
167
|
+
describe,
|
|
168
|
+
nodeCheck,
|
|
169
|
+
isNpmMissing,
|
|
170
|
+
versionLine,
|
|
171
|
+
dependencyReport,
|
|
172
|
+
semverAtLeast,
|
|
173
|
+
};
|
package/lib/install-pipeline.js
CHANGED
|
@@ -151,6 +151,7 @@ function createInstaller(deps) {
|
|
|
151
151
|
const scanResult = deps.scanTarget(gate.engine, extracted.pkgDir, {
|
|
152
152
|
slug: skill.slug,
|
|
153
153
|
version: packed.resolved,
|
|
154
|
+
opts,
|
|
154
155
|
});
|
|
155
156
|
if (!scanResult.ok) {
|
|
156
157
|
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'block', error: scanResult.error });
|
|
@@ -338,6 +339,8 @@ function createInstaller(deps) {
|
|
|
338
339
|
verdict: scan.verdict,
|
|
339
340
|
decision: scan.decision,
|
|
340
341
|
scan_policy: scan.policy || null,
|
|
342
|
+
fetch_channel: packed.channel || null,
|
|
343
|
+
extract_channel: extracted.channel || null,
|
|
341
344
|
npm_registry_fallback: Boolean(packed.registryFallback),
|
|
342
345
|
snapshot,
|
|
343
346
|
});
|
|
@@ -351,6 +354,8 @@ function createInstaller(deps) {
|
|
|
351
354
|
package: skill.pkg,
|
|
352
355
|
version: packed.resolved || skill.version,
|
|
353
356
|
decision: 'allow',
|
|
357
|
+
fetch_channel: packed.channel || null,
|
|
358
|
+
extract_channel: extracted.channel || null,
|
|
354
359
|
snapshot,
|
|
355
360
|
lifecycle: {
|
|
356
361
|
setup: lifecycleSummary(lifecycle.setup),
|
package/lib/npm-pack.js
CHANGED
|
@@ -24,6 +24,12 @@ function briefOf(raw) {
|
|
|
24
24
|
return lines.slice(-4).join(' | ');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** 把 spawn 层错误(如 ENOENT:npm 缺失)并入原始错误文本,供上层人话提示识别。 */
|
|
28
|
+
function rawOf(result) {
|
|
29
|
+
if (result && result.error && result.error.message) return String(result.error.message).trim();
|
|
30
|
+
return String((result && result.stderr) || (result && result.stdout) || 'npm pack 失败').trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
function findTarball(stdout, packDir, readdirSync) {
|
|
28
34
|
const lines = String(stdout || '')
|
|
29
35
|
.split(/\r?\n/)
|
|
@@ -71,14 +77,14 @@ function createPackRunner(deps) {
|
|
|
71
77
|
let result = attempt(baseArgs);
|
|
72
78
|
let registryFallback = false;
|
|
73
79
|
if (result.status !== 0) {
|
|
74
|
-
const raw = (result
|
|
80
|
+
const raw = rawOf(result);
|
|
75
81
|
if (!fallbackDisabled() && !hasRegistryFlag(baseArgs) && isNotFound(raw)) {
|
|
76
82
|
result = attempt([...baseArgs, '--registry', OFFICIAL_REGISTRY]);
|
|
77
83
|
registryFallback = result.status === 0;
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
if (result.status !== 0) {
|
|
81
|
-
const raw = (result
|
|
87
|
+
const raw = rawOf(result);
|
|
82
88
|
const brief = briefOf(raw) || 'npm pack 失败';
|
|
83
89
|
const hint = isNotFound(raw) ? '\n ' + FALLBACK_HINT : '';
|
|
84
90
|
return { error: brief + hint, detail: raw };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* lib/registry-fetch-child.js —— 内置拉包的子进程入口(内部工具,勿直接调用)。
|
|
4
|
+
*
|
|
5
|
+
* 父进程(lib/registry-fetch.js fetchPackageSync)经 stdin 传入 JSON:
|
|
6
|
+
* { skill: {slug, pkg, version}, opts: {pin, registry}, packDir }
|
|
7
|
+
* stdout 最后一行输出 JSON:{ ok: true, result } 或 { ok: false, error }。
|
|
8
|
+
* 这样安装管线保持同步契约,网络 I/O 仍走同一套异步核心。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fetchLib = require('./registry-fetch');
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
let payload;
|
|
15
|
+
try {
|
|
16
|
+
payload = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
process.stdout.write(JSON.stringify({ ok: false, error: '入参解析失败: ' + error.message }) + '\n');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const result = await fetchLib.fetchPackage(payload.skill, payload.opts || {}, payload.packDir);
|
|
23
|
+
process.stdout.write(JSON.stringify(result.ok ? { ok: true, result } : { ok: false, error: result.error }) + '\n');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
process.stdout.write(JSON.stringify({ ok: false, error: '内置拉包异常: ' + (error && error.message ? error.message : String(error)) }) + '\n');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main();
|