@yottameta/yotta-skills 0.23.3 → 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 +8 -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/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/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +1 -1
- package/skills.json +28 -1
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();
|