@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/install.sh CHANGED
@@ -1,31 +1,30 @@
1
1
  #!/usr/bin/env bash
2
- # yotta-skills(元阁)多智能体安装脚本(YottaSkills)
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
- install_to() {
67
- local base="$1"
68
- local dest
69
- local item
70
- case "$base" in
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
- mkdir -p "$base"
77
- dest="$base/$SKILL_NAME"
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
- if [ -e "$dest" ] && [ ! -d "$dest" ]; then
82
- echo "安装失败:技能路径已存在且不是目录:$dest" >&2; return 1
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
- mkdir -p "$dest"
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
- # 只安装技能本体与运行时需要的 MCP/清单资产;只清副本内的固定子路径。
87
- for item in .git .github .gitignore .npmignore bin lib test package.json package-lock.json install.sh; do
88
- if [ -e "$dest/$item" ] && [ ! -L "$dest/$item" ]; then
89
- rm -rf -- "$dest/$item"
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
- for item in SKILL.md skill-manifest.json references scripts assets skills.json scan-policy.json LICENSE NOTICE README.md README.zh-CN.md CHANGELOG.md USER_GUIDE.md; do
93
- if [ -e "$SOURCE_DIR/$item" ]; then
94
- cp -RP "$SOURCE_DIR/$item" "$dest/"
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
- find "$dest" -type d -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
98
- find "$dest" -type f -name '*.pyc' -delete 2>/dev/null || true
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 assume_yes=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
- --agent) shift; agent="${1:-}" ;;
128
- --dir) shift; dir="${1:-}" ;;
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 [ "$show_list" = "1" ]; then list; return; fi
138
- if [ -n "$dir" ]; then install_to "$dir"; echo "完成。"; return; fi
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。请用 --dir <路径> 指定技能目录。" >&2; exit 2
290
+ echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
291
+ exit 2
143
292
  fi
144
293
  first="${dirs%% *}"
145
- install_to "$(resolve_user "$first")"; echo "完成。"; return
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
- if [ "$assume_yes" != "1" ]; then
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 install_to "$(resolve_user "$rel")"; done
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 "完成。"; return
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 install_to "$d"; installed=1; fi
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> / -g 装到用户级,或 --dir 指定。"
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
+ };
@@ -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.stderr || result.stdout || 'npm pack 失败').trim();
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.stderr || result.stdout || 'npm pack 失败').trim();
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();