@yottameta/yotta-chain 0.1.4 → 0.1.5

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.1.5 (2026-10-01)
4
+
5
+ - 安装器卫生批次:`bin/install.js` / `install.sh` 统一(未知参数报错 exit 2、`--help` / `--version`、残留清理白名单、嵌套载荷保留);由模板单一真源渲染,接入漂移门禁。
6
+
3
7
  ## v0.1.4 (2026-09-18)
4
8
 
5
9
  - JSON 输出新增 `scannedFiles`,显式列出实际读取的 package manifest / lock 文件;`files` 继续只表示产生发现的文件,消除 `files: []` 被误读成“未读取锁文件”的歧义。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-chain
3
- version: 0.1.4
3
+ version: 0.1.5
4
4
  description: 元链 —— 跨智能体的供应链依赖校验技能:零依赖自研引擎本地解析 npm(package.json / package-lock v1-v3 / yarn.lock / pnpm-lock.yaml / bun.lock / .npmrc)与 Python(requirements / pyproject.toml / poetry.lock / uv.lock / Pipfile)及 Maven pom.xml,检测依赖混淆(私有包名被公共仓库同名抢占 / 混合仓库 / 可疑仓库 URL / extra-index 回退)、lockfile 与清单不一致、缺失锁文件、未固定版本、typo-squat 仿冒命名,并生成 SBOM-lite(CycloneDX 1.5 子集)。触发:用户要在构建 / 发布 / CI 前检查项目依赖是否存在供应链风险、核对锁文件与清单是否一致、排查依赖混淆风险或生成 SBOM 时。边界:纯本地离线解析,不做在线 CVE 比对、不查询公共包仓库、不发送任何数据;结果只是「需人工复核的风险信号」,是否真实需人工核实;仅用于已获授权 / 自有资产 / 教学环境。
5
5
  license: MIT
6
6
  ---
package/bin/install.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * yotta-chain 跨平台安装器(YottaSkills)
4
+ *
4
5
  * 用法:
5
- * npx -y @yottameta/yotta-chain --agent <name> # 按智能体默认用户级目录安装(推荐)
6
- * npx -y @yottameta/yotta-chain --dir PATH # 装到指定目录(用户改了目录的智能体)
7
- * npx -y @yottameta/yotta-chain -g # 安装到全部已知智能体用户级目录
8
- * npx -y @yottameta/yotta-chain # 安装到检测到的项目级目录
9
- * npx -y @yottameta/yotta-chain --list # 列出智能体 -> 默认目录
6
+ * npx -y @yottameta/yotta-chain --agent <name> # 按智能体默认用户级目录安装(推荐)
7
+ * npx -y @yottameta/yotta-chain --dir <path> # 装到指定目录(用户改了目录的智能体)
8
+ * npx -y @yottameta/yotta-chain -g # 安装到全部已知智能体用户级目录
9
+ * npx -y @yottameta/yotta-chain # 安装到检测到的项目级目录
10
+ * npx -y @yottameta/yotta-chain --list # 列出智能体 -> 默认目录
11
+ * npx -y @yottameta/yotta-chain --dry-run # 只显示将写入的目标,不写文件
12
+ * npx -y @yottameta/yotta-chain --version # 显示版本
13
+ * npx -y @yottameta/yotta-chain --help # 显示帮助
14
+ *
15
+ * 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
10
16
  */
11
17
  'use strict';
12
18
  const fs = require('fs');
@@ -16,6 +22,21 @@ const os = require('os');
16
22
  const SKILL_NAME = 'yotta-chain';
17
23
  const PKG_ROOT = path.join(__dirname, '..');
18
24
 
25
+ // 顶层开发文件 / 目录:只在安装包顶层跳过;技能包内嵌套同名载荷必须保留。
26
+ // 与 install.sh 同口径:开发件与运行时不相关目录一律不落目标。
27
+ const TOP_SKIP = [
28
+ 'package.json', 'package-lock.json', 'bin', 'lib', 'test',
29
+ '.github', '.git', '.gitignore', '.npmignore', '.gitattributes',
30
+ '.yotta', '.tmp', 'install.sh', 'node_modules',
31
+ ];
32
+ const TOP_SKIP_SET = new Set(TOP_SKIP);
33
+ // 缓存 / 编译产物:任意层级跳过,安装时同样清理。
34
+ const CACHE_DIRS = new Set(['__pycache__', '.pytest_cache', '.mypy_cache']);
35
+
36
+ class UsageError extends Error {}
37
+ class TargetError extends Error {}
38
+ class InstallError extends Error {}
39
+
19
40
  // 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
20
41
  // 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
21
42
  // Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
@@ -39,6 +60,26 @@ const AGENT_DIRS = {
39
60
  agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
40
61
  };
41
62
 
63
+ const PROJECT_DIRS = [
64
+ '.claude/skills',
65
+ '.cursor/skills',
66
+ '.codex/skills',
67
+ '.config/goose/skills',
68
+ '.config/agents/skills',
69
+ '.opencode/skills',
70
+ '.codeium/windsurf/skills',
71
+ '.workbuddy/skills',
72
+ '.kiro/skills',
73
+ '.traecli/skills',
74
+ '.gemini/skills',
75
+ '.trae-cn/skills',
76
+ '.qwen/skills',
77
+ '.comate/skills',
78
+ '.codebuddy/skills',
79
+ '.kimi/skills',
80
+ '.agents/skills',
81
+ ];
82
+
42
83
  // Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
43
84
  function codexUserDir() {
44
85
  const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
@@ -57,107 +98,239 @@ function resolveUserDir(rel) {
57
98
  return path.join(os.homedir(), rel);
58
99
  }
59
100
 
60
- function installTo(dest) {
61
- const target = path.join(dest, SKILL_NAME);
62
- fs.mkdirSync(target, { recursive: true });
63
- copyDir(PKG_ROOT, target, new Set(['package.json', 'bin', 'node_modules', '.git']));
64
- console.log('installed -> ' + target);
101
+ function displayDir(rel) {
102
+ if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
103
+ return '~/' + rel;
104
+ }
105
+
106
+ function skillVersion() {
107
+ try {
108
+ const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
109
+ return data.version || null;
110
+ } catch (_) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function usage() {
116
+ console.log(SKILL_NAME + ' 安装器(YottaSkills)');
117
+ console.log('');
118
+ console.log('用法:');
119
+ console.log(' node bin/install.js --agent <name> 按智能体默认用户级目录安装(推荐)');
120
+ console.log(' node bin/install.js --dir <path> 装到指定目录(用户改了目录的智能体)');
121
+ console.log(' node bin/install.js -g 安装到全部已知智能体用户级目录');
122
+ console.log(' node bin/install.js 安装到检测到的项目级目录');
123
+ console.log('');
124
+ console.log('参数:');
125
+ console.log(' --agent <name> 智能体键名,见 --list');
126
+ console.log(' --dir <path> 自定义技能目录');
127
+ console.log(' -g, --global 安装到全部已知用户级目录');
128
+ console.log(' --list, -l 列出支持的智能体目录');
129
+ console.log(' --dry-run 只显示将写入的目标,不写文件');
130
+ console.log(' --version, -v 显示版本');
131
+ console.log(' --help, -h 显示帮助');
132
+ console.log(' --yes, -y 兼容参数(-g 不再强制要求)');
133
+ console.log('');
134
+ console.log('退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误');
135
+ }
136
+
137
+ function parseArgs(argv) {
138
+ const opts = { help: false, version: false, list: false, global: false, dryRun: false, yes: false, dir: null, agent: null };
139
+ for (let i = 0; i < argv.length; i++) {
140
+ const arg = argv[i];
141
+ if (arg === '--help' || arg === '-h') opts.help = true;
142
+ else if (arg === '--version' || arg === '-v') opts.version = true;
143
+ else if (arg === '--list' || arg === '-l') opts.list = true;
144
+ else if (arg === '--global' || arg === '-g') opts.global = true;
145
+ else if (arg === '--dry-run') opts.dryRun = true;
146
+ else if (arg === '--yes' || arg === '-y') opts.yes = true;
147
+ else if (arg === '--dir') {
148
+ const value = argv[++i];
149
+ if (!value) throw new UsageError('--dir 需要一个非空路径');
150
+ opts.dir = value;
151
+ } else if (arg === '--agent') {
152
+ const value = argv[++i];
153
+ if (!value) throw new UsageError('--agent 需要一个非空名称');
154
+ opts.agent = String(value).toLowerCase();
155
+ } else {
156
+ throw new UsageError('未知参数: ' + arg);
157
+ }
158
+ }
159
+ if (!opts.help && !opts.version) {
160
+ const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
161
+ if (selected > 1) throw new UsageError('--dir / --agent / -g 只能选一个');
162
+ }
163
+ return opts;
164
+ }
165
+
166
+ function printList() {
167
+ console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
168
+ for (const [key, value] of Object.entries(AGENT_DIRS)) {
169
+ const resolved = value.dirs.map(displayDir);
170
+ console.log(' ' + key.padEnd(10) + value.label.padEnd(18) + resolved.join('、'));
171
+ }
172
+ console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
173
+ console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
174
+ }
175
+
176
+ function assertSafeTarget(target) {
177
+ const rel = path.relative(PKG_ROOT, target);
178
+ if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
179
+ throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
180
+ }
65
181
  }
66
182
 
67
- function copyDir(src, dst, skip) {
183
+ function shouldSkipCache(name, isFile) {
184
+ if (CACHE_DIRS.has(name)) return true;
185
+ if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
186
+ return false;
187
+ }
188
+
189
+ function copyDir(src, dst, topLevel) {
68
190
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
69
- if (skip.has(entry.name)) continue;
191
+ // 只跳过安装包顶层开发文件;template/ 等嵌套同名载荷必须保留。
192
+ if (topLevel && TOP_SKIP_SET.has(entry.name)) continue;
193
+ if (shouldSkipCache(entry.name, entry.isFile())) continue;
70
194
  const s = path.join(src, entry.name);
71
195
  const d = path.join(dst, entry.name);
72
196
  if (entry.isDirectory()) {
73
197
  fs.mkdirSync(d, { recursive: true });
74
- copyDir(s, d, skip);
198
+ copyDir(s, d, false);
75
199
  } else if (entry.isFile()) {
76
200
  fs.copyFileSync(s, d);
77
201
  }
78
202
  }
79
203
  }
80
204
 
81
- function displayDir(rel) {
82
- if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
83
- return '~/' + rel;
205
+ // 清理旧版安装残留(fail-closed 白名单):
206
+ // 仅在目标目录已存在且含 SKILL.md 时触发;只删顶层开发项 + 任意层级缓存;
207
+ // 不整目录删除、不跟随符号链接。
208
+ function cleanResidue(target) {
209
+ const removed = [];
210
+ let stat = null;
211
+ try { stat = fs.statSync(target); } catch (_) { return removed; }
212
+ if (!stat.isDirectory()) return removed;
213
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
214
+ for (const name of TOP_SKIP) {
215
+ const p = path.join(target, name);
216
+ let entry = null;
217
+ try { entry = fs.lstatSync(p); } catch (_) { continue; }
218
+ if (entry.isSymbolicLink()) continue;
219
+ fs.rmSync(p, { recursive: true, force: true });
220
+ removed.push(name);
221
+ }
222
+ const walk = (dir) => {
223
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
224
+ const p = path.join(dir, entry.name);
225
+ if (entry.isSymbolicLink()) continue;
226
+ if (entry.isDirectory()) {
227
+ if (CACHE_DIRS.has(entry.name)) {
228
+ fs.rmSync(p, { recursive: true, force: true });
229
+ removed.push(path.relative(target, p));
230
+ } else {
231
+ walk(p);
232
+ }
233
+ } else if (entry.isFile() && (entry.name.endsWith('.pyc') || entry.name.endsWith('.pyo'))) {
234
+ fs.unlinkSync(p);
235
+ removed.push(path.relative(target, p));
236
+ }
237
+ }
238
+ };
239
+ walk(target);
240
+ return removed;
84
241
  }
85
242
 
86
- function main() {
87
- const args = process.argv.slice(2);
88
- const isGlobal = args.includes('-g') || args.includes('--global');
89
- const list = args.includes('--list') || args.includes('-l');
90
- let explicitDir = null;
91
- const di = args.indexOf('--dir');
92
- if (di !== -1 && args[di + 1]) explicitDir = args[di + 1];
93
- let agent = null;
94
- const ai = args.indexOf('--agent');
95
- if (ai !== -1 && args[ai + 1]) agent = String(args[ai + 1]).toLowerCase();
96
-
97
- if (list) {
98
- console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
99
- for (const [key, v] of Object.entries(AGENT_DIRS)) {
100
- const resolved = v.dirs.map(displayDir);
101
- console.log(' ' + key.padEnd(10) + v.label.padEnd(18) + resolved.join('、'));
243
+ function installTo(dest, opts) {
244
+ if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
245
+ const target = path.resolve(dest, SKILL_NAME);
246
+ assertSafeTarget(target);
247
+ if (opts.dryRun) {
248
+ console.log('[dry-run] 将安装到 -> ' + target);
249
+ return target;
250
+ }
251
+ try {
252
+ for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
253
+ fs.mkdirSync(target, { recursive: true });
254
+ copyDir(PKG_ROOT, target, true);
255
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
256
+ throw new InstallError('安装结果缺少 SKILL.md');
257
+ }
258
+ } catch (err) {
259
+ if (err instanceof UsageError || err instanceof TargetError || err instanceof InstallError) throw err;
260
+ throw new InstallError('安装到 ' + target + ' 失败: ' + err.message);
261
+ }
262
+ console.log('installed -> ' + target);
263
+ return target;
264
+ }
265
+
266
+ function run() {
267
+ const opts = parseArgs(process.argv.slice(2));
268
+ if (opts.help) { usage(); return; }
269
+ if (opts.version) {
270
+ const version = skillVersion();
271
+ if (!version) {
272
+ console.error(SKILL_NAME + ' 版本未知(未找到 package.json)');
273
+ process.exitCode = 1;
274
+ return;
102
275
  }
103
- console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
104
- console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
276
+ console.log(SKILL_NAME + ' v' + version);
105
277
  return;
106
278
  }
279
+ if (opts.list) { printList(); return; }
107
280
 
108
- if (explicitDir) { installTo(explicitDir); return; }
281
+ if (opts.dir) { installTo(opts.dir, opts); return; }
109
282
 
110
- if (agent) {
111
- const info = AGENT_DIRS[agent];
283
+ if (opts.agent) {
284
+ const info = AGENT_DIRS[opts.agent];
112
285
  if (!info) {
113
- console.log('未收录智能体: ' + agent + '。请用 --dir <路径> 指定技能目录。');
114
- console.log('可用: ' + Object.keys(AGENT_DIRS).join(', '));
115
- return;
286
+ throw new UsageError('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';自定义目录请用 --dir <路径>。');
116
287
  }
117
- installTo(resolveUserDir(info.dirs[0]));
288
+ installTo(resolveUserDir(info.dirs[0]), opts);
118
289
  console.log('完成。');
119
290
  return;
120
291
  }
121
292
 
122
- if (isGlobal) {
293
+ if (opts.global) {
123
294
  const seen = new Set();
124
- for (const v of Object.values(AGENT_DIRS)) {
125
- for (const d of v.dirs) {
126
- if (seen.has(d)) continue;
127
- seen.add(d);
128
- installTo(resolveUserDir(d));
295
+ for (const value of Object.values(AGENT_DIRS)) {
296
+ for (const rel of value.dirs) {
297
+ if (seen.has(rel)) continue;
298
+ seen.add(rel);
299
+ installTo(resolveUserDir(rel), opts);
129
300
  }
130
301
  }
131
- console.log('完成。');
302
+ if (!opts.dryRun) console.log('完成。');
132
303
  return;
133
304
  }
134
305
 
135
- const PROJECT_DIRS = [
136
- '.claude/skills',
137
- '.cursor/skills',
138
- '.codex/skills',
139
- '.config/goose/skills',
140
- '.config/agents/skills',
141
- '.opencode/skills',
142
- '.codeium/windsurf/skills',
143
- '.workbuddy/skills',
144
- '.kiro/skills',
145
- '.traecli/skills',
146
- '.gemini/skills',
147
- '.trae-cn/skills',
148
- '.qwen/skills',
149
- '.comate/skills',
150
- '.codebuddy/skills',
151
- '.kimi/skills',
152
- '.agents/skills',
153
- ];
154
- let installedAny = false;
155
- for (const d of PROJECT_DIRS) {
156
- if (fs.existsSync(d)) { installTo(d); installedAny = true; }
306
+ const dirs = PROJECT_DIRS.filter((d) => fs.existsSync(d));
307
+ if (!dirs.length) {
308
+ throw new TargetError('未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。');
157
309
  }
158
- if (!installedAny) {
159
- console.log('未检测到项目级智能体目录。可手动复制,或用 --agent <name> / -g 装到用户级。');
310
+ for (const dir of dirs) installTo(dir, opts);
311
+ if (!opts.dryRun) console.log('完成。');
312
+ }
313
+
314
+ function main() {
315
+ try {
316
+ run();
317
+ } catch (err) {
318
+ if (err instanceof UsageError) {
319
+ console.error('用法错误: ' + err.message);
320
+ usage();
321
+ process.exitCode = 2;
322
+ } else if (err instanceof TargetError) {
323
+ console.error('目标错误: ' + err.message);
324
+ process.exitCode = 4;
325
+ } else if (err instanceof InstallError) {
326
+ console.error('安装失败: ' + err.message);
327
+ console.error('修复建议: 检查目录权限与磁盘空间后重试,或用 --dir 换一个目录。');
328
+ process.exitCode = 1;
329
+ } else {
330
+ console.error('未知错误: ' + (err && err.message ? err.message : String(err)));
331
+ process.exitCode = 1;
332
+ }
160
333
  }
161
334
  }
162
335
 
163
- main();
336
+ main();
package/install.sh CHANGED
@@ -1,28 +1,30 @@
1
1
  #!/usr/bin/env bash
2
- # yotta-chain 多智能体安装脚本(YottaSkills 模板)
2
+ # yotta-chain 多智能体安装脚本(YottaSkills)
3
3
  # 用法:
4
4
  # bash install.sh --agent <name> # 按智能体默认用户级目录安装
5
5
  # bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
6
6
  # bash install.sh -g # 装到全部已知智能体用户级目录
7
- # bash install.sh # 检测并安装到已存在的项目级目录
7
+ # bash install.sh # 检测并安装到已存在的项目级目录
8
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 目标错误
9
13
  set -euo pipefail
10
14
 
11
15
  SKILL_NAME="yotta-chain"
12
16
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ _IS_WINDOWS=0
13
18
  case "$(uname -s)" in
14
19
  MINGW*|MSYS*)
15
20
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
21
+ _IS_WINDOWS=1
16
22
  ;;
23
+ CYGWIN*) _IS_WINDOWS=1 ;;
17
24
  esac
18
25
 
19
26
  # 智能体 -> 用户级默认目录(--agent 装到第一个)
20
27
  # .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
21
- # 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
22
- _IS_WINDOWS=0
23
- case "$(uname -s)" in
24
- MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
25
- esac
26
28
  dirs_for() {
27
29
  case "$1" in
28
30
  claude) echo ".claude/skills" ;;
@@ -60,17 +62,153 @@ resolve_user() {
60
62
  esac
61
63
  }
62
64
 
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)"
70
+ fi
71
+ if [ -z "$v" ]; then
72
+ v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
73
+ fi
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")"
118
+ fi
119
+ case "$dest_real" in
120
+ "$src_real"|"$src_real"/*)
121
+ echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
122
+ return 1
123
+ ;;
124
+ esac
125
+ return 0
126
+ }
127
+
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"
141
+ fi
142
+ done
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
170
+ fi
171
+ done
172
+ }
173
+
63
174
  install_to() {
64
- mkdir -p "$1/$SKILL_NAME"
65
- cp -r "$SOURCE_DIR/." "$1/$SKILL_NAME/"
66
- rm -rf "$1/$SKILL_NAME/.git"
67
- echo "installed -> $1/$SKILL_NAME"
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
205
+ echo "installed -> $dest"
68
206
  }
69
207
 
70
208
  list() {
71
209
  echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
210
+ local a dirs first
72
211
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
73
- local dirs first
74
212
  dirs="$(dirs_for "$a")"
75
213
  first="${dirs%% *}"
76
214
  case "$first" in
@@ -89,44 +227,101 @@ list() {
89
227
  }
90
228
 
91
229
  main() {
92
- 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
93
231
  while [ $# -gt 0 ]; do
94
232
  case "$1" in
95
- --agent) shift; agent="${1:-}" ;;
96
- --dir) shift; dir="${1:-}" ;;
97
- -g|--global) global=1 ;;
233
+ --help|-h) show_help=1 ;;
234
+ --version|-v) show_version=1 ;;
98
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
+ ;;
99
255
  *) echo "未知参数: $1" >&2; exit 2 ;;
100
256
  esac
101
257
  shift
102
258
  done
103
259
 
104
- if [ "$show_list" = "1" ]; then list; return; fi
105
- 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
106
287
  if [ -n "$agent" ]; then
107
288
  local dirs first
108
289
  if ! dirs="$(dirs_for "$agent")"; then
109
- echo "未收录智能体: $agent。请用 --dir <路径> 指定技能目录。" >&2; exit 2
290
+ echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
291
+ exit 2
110
292
  fi
111
293
  first="${dirs%% *}"
112
- install_to "$(resolve_user "$first")"; echo "完成。"; return
294
+ install_to "$(resolve_user "$first")" "$dry" || exit 1
295
+ [ "$dry" = "1" ] || echo "完成。"
296
+ return 0
113
297
  fi
114
298
  if [ "$global" = "1" ]; then
115
- echo "安装到全部已知智能体用户级目录..."
116
- local dirs rel
299
+ local a dirs rel seen=" "
117
300
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
118
- dirs="$(dirs_for "$a")"
119
- 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
120
309
  done
121
- echo "完成。"; return
310
+ [ "$dry" = "1" ] || echo "完成。"
311
+ return 0
122
312
  fi
123
313
  local installed=0 d
124
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
125
- 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
126
319
  done
127
320
  if [ "$installed" = "0" ]; then
128
- echo "未检测到项目级智能体目录。可用 --agent <name> / -g 装到用户级,或 --dir 指定。"
321
+ echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
322
+ exit 4
129
323
  fi
324
+ [ "$dry" = "1" ] || echo "完成。"
130
325
  }
131
326
 
132
- main "$@"
327
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-chain",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Yuanlian — a zero-dependency supply-chain dependency validator for AI agents: detects dependency confusion (private package names resolved from public registries, mixed registries, suspicious registry URLs), lockfile consistency issues (missing entries, unsatisfied ranges, dangling references, missing integrity), missing lockfiles, unpinned versions, SNAPSHOT deps and typosquatting across npm (package.json + package-lock v1/v2/v3), Python (requirements.txt, pyproject.toml, poetry.lock, Pipfile/Pipfile.lock) and Maven (pom.xml); generates SBOM-lite (CycloneDX 1.5 subset). Pure Python 3.8+ standard library, fully local and offline, no online CVE lookups. Triggers when the user needs to check a project dependencies for supply-chain risks before build/release, verify a lockfile matches its manifest, detect dependency-confusion exposure, or generate an SBOM. Boundaries: local and offline; no CVE databases, no package-registry queries, no data leaves the machine; findings are review signals to be verified by humans; authorized / own-asset / educational use only.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -36,7 +36,7 @@ try:
36
36
  except Exception:
37
37
  pass
38
38
 
39
- VERSION = "0.1.4"
39
+ VERSION = "0.1.5"
40
40
 
41
41
  SEVERITY_ORDER = ["info", "low", "medium", "high"]
42
42
  SEVERITY_RANK = {s: i for i, s in enumerate(SEVERITY_ORDER)}