ruige-skill 1.1.8 → 1.2.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.
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import {
5
+ mkdtemp,
6
+ mkdir,
7
+ readFile,
8
+ readdir,
9
+ rm,
10
+ stat,
11
+ symlink,
12
+ } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, join, relative, resolve } from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
19
+ const failures = [];
20
+
21
+ function check(condition, message) {
22
+ if (!condition) failures.push(message);
23
+ }
24
+
25
+ async function readJson(path) {
26
+ return JSON.parse(await readFile(path, "utf8"));
27
+ }
28
+
29
+ async function markdownFiles(directory) {
30
+ const files = [];
31
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
32
+ const path = join(directory, entry.name);
33
+ if (entry.isDirectory()) files.push(...await markdownFiles(path));
34
+ else if (entry.name.endsWith(".md")) files.push(path);
35
+ }
36
+ return files;
37
+ }
38
+
39
+ async function validateManifest() {
40
+ const manifest = await readJson(join(root, "manifest.json"));
41
+ const knowledgeRoot = join(root, "knowledge");
42
+ const disk = await markdownFiles(knowledgeRoot);
43
+ const listed = new Map(manifest.files.map((item) => [item.path, item]));
44
+
45
+ check(manifest.skill === "rg", "manifest.skill 必须是 rg");
46
+ check(
47
+ manifest.knowledgeFiles === manifest.files.length,
48
+ "manifest.knowledgeFiles 与 files 数量不一致",
49
+ );
50
+ check(
51
+ disk.length === manifest.files.length,
52
+ `知识文件数量不一致:磁盘 ${disk.length},manifest ${manifest.files.length}`,
53
+ );
54
+
55
+ for (const path of disk) {
56
+ const name = relative(knowledgeRoot, path);
57
+ const item = listed.get(name);
58
+ if (!item) {
59
+ failures.push(`manifest 缺少:${name}`);
60
+ continue;
61
+ }
62
+ const content = await readFile(path);
63
+ const digest = createHash("sha256").update(content).digest("hex");
64
+ check(item.bytes === content.length, `文件大小不一致:${name}`);
65
+ check(item.sha256 === digest, `文件哈希不一致:${name}`);
66
+ }
67
+ }
68
+
69
+ async function validateMarkdownLinks() {
70
+ const files = [
71
+ join(root, "SKILL.md"),
72
+ join(root, "README.md"),
73
+ ...await markdownFiles(join(root, "references")),
74
+ ];
75
+ for (const file of files) {
76
+ const content = await readFile(file, "utf8");
77
+ for (const match of content.matchAll(/\[[^\]]*]\(([^)]+)\)/g)) {
78
+ const target = match[1].split("#")[0];
79
+ if (!target || /^[a-z]+:/i.test(target)) continue;
80
+ const destination = resolve(dirname(file), target);
81
+ try {
82
+ await stat(destination);
83
+ } catch {
84
+ failures.push(
85
+ `Markdown 链接不存在:${relative(root, file)} -> ${target}`,
86
+ );
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ async function validateRegressions() {
93
+ const suite = await readJson(join(root, "tests", "logic-regressions.json"));
94
+ check(Array.isArray(suite.cases), "回归测试 cases 必须是数组");
95
+ const ids = new Set();
96
+ for (const item of suite.cases ?? []) {
97
+ check(typeof item.id === "string" && item.id.length > 0, "回归案例缺少 id");
98
+ check(!ids.has(item.id), `回归案例 id 重复:${item.id}`);
99
+ ids.add(item.id);
100
+ check(
101
+ typeof item.prompt === "string"
102
+ || typeof item.task === "string"
103
+ || Array.isArray(item.turns),
104
+ `回归案例缺少输入:${item.id}`,
105
+ );
106
+ check(
107
+ Array.isArray(item.expect) && item.expect.length > 0,
108
+ `回归案例缺少 expect:${item.id}`,
109
+ );
110
+ check(
111
+ Array.isArray(item.reject) && item.reject.length > 0,
112
+ `回归案例缺少 reject:${item.id}`,
113
+ );
114
+ }
115
+ }
116
+
117
+ function run(command, args, options = {}) {
118
+ return spawnSync(command, args, {
119
+ cwd: root,
120
+ encoding: "utf8",
121
+ ...options,
122
+ });
123
+ }
124
+
125
+ async function validateRuntime() {
126
+ for (const file of [
127
+ "bin/ruige-skill.mjs",
128
+ "scripts/manage-career-archive.mjs",
129
+ "scripts/manage-prompt-library.mjs",
130
+ "scripts/validate-project.mjs",
131
+ ]) {
132
+ const result = run(process.execPath, ["--check", file]);
133
+ check(result.status === 0, `语法检查失败:${file}\n${result.stderr}`);
134
+ }
135
+
136
+ const noHit = run("bash", [
137
+ "scripts/search-knowledge.sh",
138
+ "肯定不存在的检索词_RG_TEST_987654321",
139
+ "3",
140
+ ]);
141
+ check(noHit.status === 0, "知识检索无命中时不应返回脚本错误");
142
+ check(noHit.stdout.includes("未找到匹配内容"), "知识检索无命中缺少明确提示");
143
+
144
+ const sandbox = await mkdtemp(join(tmpdir(), "ruige-skill-test-"));
145
+ try {
146
+ const env = { ...process.env, RUIGE_SKILL_HOME: sandbox };
147
+ const codexSkills = join(sandbox, ".codex", "skills");
148
+ await mkdir(codexSkills, { recursive: true });
149
+ await symlink(root, join(codexSkills, "rg.backup-test"), "dir");
150
+ const install = run(
151
+ process.execPath,
152
+ ["bin/ruige-skill.mjs", "install", "--agent", "codex"],
153
+ { env },
154
+ );
155
+ check(install.status === 0, `安装沙箱失败:${install.stderr}`);
156
+ check(
157
+ install.stdout.includes("已移出 1 个"),
158
+ "安装器没有移出可能被重复加载的旧 Skill 入口",
159
+ );
160
+
161
+ const statusResult = run(
162
+ process.execPath,
163
+ ["bin/ruige-skill.mjs", "status", "--agent", "codex"],
164
+ { env },
165
+ );
166
+ check(statusResult.status === 0, `状态检查失败:${statusResult.stderr}`);
167
+ check(statusResult.stdout.includes("正常,v"), "状态没有报告有效版本");
168
+ check(statusResult.stdout.includes("codex: 已连接"), "状态没有验证正确桥接");
169
+
170
+ const codexBridge = join(codexSkills, "rg");
171
+ const wrongTarget = join(sandbox, "wrong-skill");
172
+ await mkdir(wrongTarget, { recursive: true });
173
+ await rm(codexBridge);
174
+ await symlink(wrongTarget, codexBridge, "dir");
175
+ const wrongStatus = run(
176
+ process.execPath,
177
+ ["bin/ruige-skill.mjs", "status", "--agent", "codex"],
178
+ { env },
179
+ );
180
+ check(
181
+ wrongStatus.stdout.includes("codex: 错误链接"),
182
+ "状态检查没有识别指向错误位置的 Skill 链接",
183
+ );
184
+
185
+ const promptLibrary = join(sandbox, "prompt-library");
186
+ const initLibrary = run(
187
+ process.execPath,
188
+ ["scripts/manage-prompt-library.mjs", "init", "--dir", promptLibrary],
189
+ );
190
+ check(initLibrary.status === 0, `提示词资料库初始化失败:${initLibrary.stderr}`);
191
+ const addRevision = run(
192
+ process.execPath,
193
+ ["scripts/manage-prompt-library.mjs", "revision", "--dir", promptLibrary],
194
+ );
195
+ check(addRevision.status === 0, `提示词版本创建失败:${addRevision.stderr}`);
196
+ const checkLibrary = run(
197
+ process.execPath,
198
+ ["scripts/manage-prompt-library.mjs", "check", "--dir", promptLibrary],
199
+ );
200
+ check(checkLibrary.status === 0, `提示词资料库检查失败:${checkLibrary.stderr}`);
201
+ check(checkLibrary.stdout.includes("V001.md"), "提示词资料库没有保留第一版记录");
202
+ } finally {
203
+ await rm(sandbox, { recursive: true, force: true });
204
+ }
205
+ }
206
+
207
+ await validateManifest();
208
+ await validateMarkdownLinks();
209
+ await validateRegressions();
210
+ await validateRuntime();
211
+
212
+ if (failures.length) {
213
+ for (const failure of failures) console.error(`✗ ${failure}`);
214
+ process.exitCode = 1;
215
+ } else {
216
+ console.error("✓ 项目结构、知识清单、引用、回归定义和安装沙箱验证通过");
217
+ }