skillhelper 0.4.2
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/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/lib/atomic-write.mjs +72 -0
- package/bin/lib/paths.mjs +65 -0
- package/bin/lib/port.mjs +23 -0
- package/bin/skillhelper.mjs +720 -0
- package/build/build-site.mjs +35 -0
- package/build/build-web.mjs +29 -0
- package/build/pack-smoke.mjs +195 -0
- package/build/prepare-publish.mjs +3 -0
- package/build/verify.mjs +157 -0
- package/config/sources.example.yaml +126 -0
- package/docs/Frontend-Spec.md +1207 -0
- package/docs/scan_skills_rules.md +432 -0
- package/package.json +95 -0
- package/packages/scanner/package.json +15 -0
- package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
- package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
- package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/file-docs.mjs +158 -0
- package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
- package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
- package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
- package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
- package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
- package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
- package/packages/scanner/src/config/editor-tiers.mjs +191 -0
- package/packages/scanner/src/core/atomic-write.mjs +11 -0
- package/packages/scanner/src/core/descriptor.mjs +143 -0
- package/packages/scanner/src/core/path-hash.mjs +89 -0
- package/packages/scanner/src/core/registry.mjs +128 -0
- package/packages/scanner/src/hash/path-hash.mjs +8 -0
- package/packages/scanner/src/icon/brand-map.mjs +268 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
- package/packages/scanner/src/index.mjs +450 -0
- package/packages/scanner/src/types.d.ts +144 -0
- package/packages/scanner/src/utils.mjs +194 -0
- package/packages/server/package.json +15 -0
- package/packages/server/src/index.mjs +1071 -0
- package/packages/server/src/labels.mjs +64 -0
- package/packages/server/src/langdetect.mjs +34 -0
- package/packages/server/src/llm-client.mjs +42 -0
- package/packages/server/src/translate-cache.mjs +132 -0
- package/packages/server/src/translator.mjs +53 -0
- package/packages/web/README.md +36 -0
- package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
- package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
- package/packages/web/dist/brand-icon.png +0 -0
- package/packages/web/dist/brand-logo.png +0 -0
- package/packages/web/dist/favicon-16x16.png +0 -0
- package/packages/web/dist/favicon-192x192.png +0 -0
- package/packages/web/dist/favicon-32x32.png +0 -0
- package/packages/web/dist/favicon-512x512.png +0 -0
- package/packages/web/dist/favicon-source.svg +5 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/favicon.svg +5 -0
- package/packages/web/dist/icons/hermes-128.png +0 -0
- package/packages/web/dist/icons/hermes-192.png +0 -0
- package/packages/web/dist/icons/hermes-256.png +0 -0
- package/packages/web/dist/icons/hermes-32.png +0 -0
- package/packages/web/dist/icons/hermes-512.png +0 -0
- package/packages/web/dist/icons/hermes-64.png +0 -0
- package/packages/web/dist/icons/hermes.png +0 -0
- package/packages/web/dist/index.html +37 -0
- package/packages/web/dist/robots.txt +3 -0
- package/packages/web/dist/site.webmanifest +24 -0
- package/packages/web/public/brand-icon.png +0 -0
- package/packages/web/public/brand-logo.png +0 -0
- package/packages/web/public/favicon-16x16.png +0 -0
- package/packages/web/public/favicon-192x192.png +0 -0
- package/packages/web/public/favicon-32x32.png +0 -0
- package/packages/web/public/favicon-512x512.png +0 -0
- package/packages/web/public/favicon-source.svg +5 -0
- package/packages/web/public/favicon.ico +0 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/icons/hermes-128.png +0 -0
- package/packages/web/public/icons/hermes-192.png +0 -0
- package/packages/web/public/icons/hermes-256.png +0 -0
- package/packages/web/public/icons/hermes-32.png +0 -0
- package/packages/web/public/icons/hermes-512.png +0 -0
- package/packages/web/public/icons/hermes-64.png +0 -0
- package/packages/web/public/icons/hermes.png +0 -0
- package/packages/web/public/robots.txt +3 -0
- package/packages/web/public/site.webmanifest +24 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/sync-skills.sh +605 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 官网落地页构建/开发脚本,结构对齐 build/build-web.mjs。
|
|
3
|
+
// - 默认:vite build(生产构建,base='/SkillsHelper/')
|
|
4
|
+
// - --dev:vite(本地开发服务器,base='/')
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
12
|
+
const siteRoot = path.join(repoRoot, 'packages', 'site');
|
|
13
|
+
const viteBin = path.join(repoRoot, 'node_modules', 'vite', 'bin', 'vite.js');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(viteBin)) {
|
|
16
|
+
console.error(`[build:site] vite not found at ${viteBin}`);
|
|
17
|
+
console.error('[build:site] run npm install first.');
|
|
18
|
+
process.exit(127);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isDev = process.argv.includes('--dev');
|
|
22
|
+
const args = isDev ? [] : ['build'];
|
|
23
|
+
|
|
24
|
+
const result = spawnSync(process.execPath, [viteBin, ...args], {
|
|
25
|
+
cwd: siteRoot,
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
console.error(result.error.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
9
|
+
const webRoot = path.join(repoRoot, 'packages', 'web');
|
|
10
|
+
const viteBin = path.join(repoRoot, 'node_modules', 'vite', 'bin', 'vite.js');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(viteBin)) {
|
|
13
|
+
console.error(`[build:web] vite not found at ${viteBin}`);
|
|
14
|
+
console.error('[build:web] run npm install first; GitHub/npm prepare should install dev dependencies before this script runs.');
|
|
15
|
+
process.exit(127);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = spawnSync(process.execPath, [viteBin, 'build'], {
|
|
19
|
+
cwd: webRoot,
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: process.env,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
console.error(result.error.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
12
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'skillhelper-pack-smoke-'));
|
|
13
|
+
const packDir = path.join(tempRoot, 'pack');
|
|
14
|
+
const installRoot = path.join(tempRoot, 'install');
|
|
15
|
+
const home = path.join(tempRoot, 'home');
|
|
16
|
+
const childOutput = [];
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
18
|
+
|
|
19
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
20
|
+
fs.mkdirSync(installRoot, { recursive: true });
|
|
21
|
+
fs.mkdirSync(home, { recursive: true });
|
|
22
|
+
|
|
23
|
+
function run(name, args, options = {}) {
|
|
24
|
+
console.log(`\n[pack-smoke] $ ${name} ${args.join(' ')}`);
|
|
25
|
+
const res = spawnSync(name, args, {
|
|
26
|
+
cwd: repoRoot,
|
|
27
|
+
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
28
|
+
env: options.env || process.env,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
});
|
|
31
|
+
if (res.status !== 0) {
|
|
32
|
+
if (options.capture) {
|
|
33
|
+
process.stdout.write(res.stdout || '');
|
|
34
|
+
process.stderr.write(res.stderr || '');
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`${name} ${args.join(' ')} failed with exit ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function write(file, text) {
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
fs.writeFileSync(file, text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getFreePort() {
|
|
47
|
+
return await new Promise((resolve, reject) => {
|
|
48
|
+
const server = net.createServer();
|
|
49
|
+
server.once('error', reject);
|
|
50
|
+
server.listen(0, '127.0.0.1', () => {
|
|
51
|
+
const address = server.address();
|
|
52
|
+
server.close(() => resolve(address.port));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitForHealth(base, timeoutMs) {
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
let lastError = '';
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${base}/api/health`);
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
if (res.ok) {
|
|
65
|
+
const body = JSON.parse(text);
|
|
66
|
+
if (body.ok === true) return body;
|
|
67
|
+
}
|
|
68
|
+
lastError = `${res.status}: ${text}`;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
lastError = err.message;
|
|
71
|
+
}
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`health check timed out: ${lastError}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function waitForChildClose(child) {
|
|
78
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
79
|
+
await new Promise(resolve => {
|
|
80
|
+
const timer = setTimeout(resolve, 2000);
|
|
81
|
+
child.once('close', () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function assertTarballEntries(tarball) {
|
|
89
|
+
const res = run('tar', ['-tf', tarball], { capture: true });
|
|
90
|
+
const entries = res.stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
91
|
+
assert.ok(entries.length > 0, 'tarball should not be empty');
|
|
92
|
+
|
|
93
|
+
const forbidden = entries.filter(entry =>
|
|
94
|
+
entry.includes('/node_modules/') ||
|
|
95
|
+
entry.includes('/.vite/') ||
|
|
96
|
+
entry.endsWith('/node_modules') ||
|
|
97
|
+
entry.endsWith('/.vite')
|
|
98
|
+
);
|
|
99
|
+
assert.deepEqual(forbidden, [], 'tarball must not contain node_modules or .vite cache');
|
|
100
|
+
|
|
101
|
+
for (const required of [
|
|
102
|
+
'package/packages/web/dist/index.html',
|
|
103
|
+
'package/packages/web/dist/favicon.ico',
|
|
104
|
+
'package/packages/web/dist/site.webmanifest',
|
|
105
|
+
'package/packages/web/public/favicon.ico',
|
|
106
|
+
'package/packages/server/src/index.mjs',
|
|
107
|
+
'package/packages/scanner/src/index.mjs',
|
|
108
|
+
]) {
|
|
109
|
+
assert.ok(entries.includes(required), `tarball missing ${required}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function assertInstalledCli(tarball) {
|
|
114
|
+
run('npm', ['install', '--prefix', installRoot, '--silent', '--omit=dev', tarball]);
|
|
115
|
+
|
|
116
|
+
const bin = path.join(installRoot, 'node_modules', '.bin', 'skillhelper');
|
|
117
|
+
const version = run(bin, ['--version'], { capture: true });
|
|
118
|
+
assert.match(version.stdout, new RegExp(`^skillhelper v${escapeRegExp(pkg.version)}`, 'm'));
|
|
119
|
+
|
|
120
|
+
write(path.join(home, '.hermes', 'skills', 'pack-smoke', 'SKILL.md'), `---
|
|
121
|
+
name: pack-smoke
|
|
122
|
+
description: Pack smoke skill
|
|
123
|
+
---
|
|
124
|
+
# Pack smoke skill
|
|
125
|
+
`);
|
|
126
|
+
write(path.join(home, 'sources.yaml'), `limits:
|
|
127
|
+
maxFiles: 100
|
|
128
|
+
maxFileBytes: 1048576
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
const port = await getFreePort();
|
|
132
|
+
const base = `http://127.0.0.1:${port}`;
|
|
133
|
+
const env = {
|
|
134
|
+
...process.env,
|
|
135
|
+
HOME: home,
|
|
136
|
+
SKILLHELPER_HOME: home,
|
|
137
|
+
SKILLHELPER_NO_OPEN: '1',
|
|
138
|
+
LOG_LEVEL: 'error',
|
|
139
|
+
PORT: String(port),
|
|
140
|
+
};
|
|
141
|
+
const child = spawn(bin, ['start', '--foreground'], {
|
|
142
|
+
cwd: installRoot,
|
|
143
|
+
env,
|
|
144
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
|
+
});
|
|
146
|
+
child.stdout.on('data', data => childOutput.push(data.toString()));
|
|
147
|
+
child.stderr.on('data', data => childOutput.push(data.toString()));
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const health = await waitForHealth(base, 15000);
|
|
151
|
+
assert.equal(health.ok, true);
|
|
152
|
+
assert.equal(health.version, pkg.version);
|
|
153
|
+
|
|
154
|
+
const icon = await fetch(`${base}/favicon.ico`);
|
|
155
|
+
const bytes = Buffer.from(await icon.arrayBuffer());
|
|
156
|
+
assert.equal(icon.ok, true);
|
|
157
|
+
assert.equal((icon.headers.get('content-type') || '').split(';')[0], 'image/x-icon');
|
|
158
|
+
assert.deepEqual([...bytes.subarray(0, 4)], [0x00, 0x00, 0x01, 0x00]);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const tail = childOutput.join('').trim().split(/\r?\n/).slice(-60).join('\n');
|
|
161
|
+
throw new Error(`${err.message}\n\nCLI output:\n${tail}`);
|
|
162
|
+
} finally {
|
|
163
|
+
if (child.exitCode == null && child.signalCode == null) {
|
|
164
|
+
child.kill('SIGTERM');
|
|
165
|
+
}
|
|
166
|
+
await waitForChildClose(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
try {
|
|
172
|
+
run('npm', ['pack', '--pack-destination', packDir]);
|
|
173
|
+
const tarballs = fs.readdirSync(packDir)
|
|
174
|
+
.filter(name => name.endsWith('.tgz'))
|
|
175
|
+
.map(name => path.join(packDir, name));
|
|
176
|
+
assert.equal(tarballs.length, 1, 'npm pack should create exactly one tarball');
|
|
177
|
+
const tarball = tarballs[0];
|
|
178
|
+
|
|
179
|
+
assertTarballEntries(tarball);
|
|
180
|
+
await assertInstalledCli(tarball);
|
|
181
|
+
console.log('\n[pack-smoke] PASS tarball contents + installed CLI smoke');
|
|
182
|
+
} finally {
|
|
183
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function escapeRegExp(text) {
|
|
188
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch(err => {
|
|
192
|
+
console.error('\n[pack-smoke] FAIL');
|
|
193
|
+
console.error(err && err.stack || err);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
package/build/verify.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
11
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'skillhelper-verify-'));
|
|
12
|
+
const oldHome = process.env.SKILLHELPER_HOME;
|
|
13
|
+
const oldUserHome = process.env.HOME;
|
|
14
|
+
process.env.SKILLHELPER_HOME = path.join(tempRoot, 'home');
|
|
15
|
+
process.env.HOME = process.env.SKILLHELPER_HOME;
|
|
16
|
+
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
18
|
+
const testFiles = [
|
|
19
|
+
'packages/scanner/test/scanner.test.mjs',
|
|
20
|
+
'packages/server/test/server.test.mjs',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function run(name, args) {
|
|
24
|
+
console.log(`\n[verify] $ ${name} ${args.join(' ')}`);
|
|
25
|
+
const res = spawnSync(name, args, {
|
|
26
|
+
cwd: repoRoot,
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
env: process.env,
|
|
29
|
+
});
|
|
30
|
+
if (res.status !== 0) throw new Error(`${name} ${args.join(' ')} failed with exit ${res.status}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function write(file, text) {
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
fs.writeFileSync(file, text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
try {
|
|
40
|
+
run('npm', ['run', 'build:web']);
|
|
41
|
+
run('npm', ['run', 'typecheck:web']);
|
|
42
|
+
run('npm', ['run', 'test:web']);
|
|
43
|
+
run('node', ['--test', ...testFiles]);
|
|
44
|
+
|
|
45
|
+
const skillRoot = path.join(process.env.HOME, '.hermes', 'skills');
|
|
46
|
+
write(path.join(skillRoot, 'demo', 'verify-skill', 'SKILL.md'), `---
|
|
47
|
+
name: verify-skill
|
|
48
|
+
description: Verify smoke skill
|
|
49
|
+
triggers:
|
|
50
|
+
- verify smoke
|
|
51
|
+
---
|
|
52
|
+
# Verify smoke skill
|
|
53
|
+
Use this item to prove the local server works.
|
|
54
|
+
`);
|
|
55
|
+
write(path.join(process.env.SKILLHELPER_HOME, 'sources.yaml'), `sources:
|
|
56
|
+
hermes:
|
|
57
|
+
enabled: true
|
|
58
|
+
roots:
|
|
59
|
+
- ${JSON.stringify(skillRoot)}
|
|
60
|
+
limits:
|
|
61
|
+
maxFiles: 100
|
|
62
|
+
maxFileBytes: 1048576
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
const { startServer } = await import(path.join(repoRoot, 'packages/server/src/index.mjs'));
|
|
66
|
+
const { app } = await startServer({ port: 0 });
|
|
67
|
+
const address = app.server.address();
|
|
68
|
+
const port = address.port;
|
|
69
|
+
const base = `http://127.0.0.1:${port}`;
|
|
70
|
+
console.log(`\n[verify] server listening on ${base}`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const health = await fetchJson(`${base}/api/health`);
|
|
74
|
+
assert.equal(health.ok, true);
|
|
75
|
+
assert.equal(health.items, 1);
|
|
76
|
+
assert.equal(health.version, pkg.version);
|
|
77
|
+
|
|
78
|
+
const skills = await fetchJson(`${base}/api/skills`);
|
|
79
|
+
assert.equal(skills.length, 1);
|
|
80
|
+
assert.equal(skills[0].name, 'verify-skill');
|
|
81
|
+
assert.equal(skills[0].raw, undefined, '/api/skills must strip raw content');
|
|
82
|
+
|
|
83
|
+
const detail = await fetchJson(`${base}/api/skills/${skills[0].id}`);
|
|
84
|
+
assert.match(detail.raw, /Verify smoke skill/);
|
|
85
|
+
|
|
86
|
+
const stats = await fetchJson(`${base}/api/stats`);
|
|
87
|
+
assert.equal(stats.total, 1);
|
|
88
|
+
assert.equal(stats.byBrand.hermes, 1);
|
|
89
|
+
|
|
90
|
+
const reload = await fetchJson(`${base}/api/reload`, { method: 'POST' });
|
|
91
|
+
assert.equal(reload.ok, true);
|
|
92
|
+
assert.equal(reload.items, 1);
|
|
93
|
+
|
|
94
|
+
const index = await fetchText(`${base}/`);
|
|
95
|
+
assert.match(index.body, /<div id="app"><\/div>/);
|
|
96
|
+
const jsMatch = index.body.match(/\/assets\/[^"']+\.js/);
|
|
97
|
+
const cssMatch = index.body.match(/\/assets\/[^"']+\.css/);
|
|
98
|
+
assert.ok(jsMatch, 'index.html should reference built JS asset');
|
|
99
|
+
assert.ok(cssMatch, 'index.html should reference built CSS asset');
|
|
100
|
+
|
|
101
|
+
const js = await fetchText(`${base}${jsMatch[0]}`);
|
|
102
|
+
assert.equal(js.contentType.split(';')[0], 'text/javascript');
|
|
103
|
+
assert.ok(js.body.length > 1000);
|
|
104
|
+
|
|
105
|
+
const css = await fetchText(`${base}${cssMatch[0]}`);
|
|
106
|
+
assert.equal(css.contentType.split(';')[0], 'text/css');
|
|
107
|
+
assert.ok(css.body.includes('topbar') || css.body.includes('sidebar') || css.body.includes('detail'));
|
|
108
|
+
|
|
109
|
+
const manifest = await fetchJson(`${base}/site.webmanifest`);
|
|
110
|
+
assert.equal(manifest.name, '呼哈哈-技能助手');
|
|
111
|
+
assert.ok(manifest.icons.some(icon => icon.sizes === '192x192'));
|
|
112
|
+
assert.ok(manifest.icons.some(icon => icon.sizes === '512x512'));
|
|
113
|
+
|
|
114
|
+
const favicon = await fetchBuffer(`${base}/favicon.ico`);
|
|
115
|
+
assert.equal(favicon.contentType.split(';')[0], 'image/x-icon');
|
|
116
|
+
assert.deepEqual([...favicon.body.subarray(0, 4)], [0x00, 0x00, 0x01, 0x00]);
|
|
117
|
+
} finally {
|
|
118
|
+
await app.close();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
run('node', ['build/pack-smoke.mjs']);
|
|
122
|
+
console.log('\n[verify] PASS build + tests + HTTP/API/static/package smoke checks');
|
|
123
|
+
} finally {
|
|
124
|
+
if (oldHome == null) delete process.env.SKILLHELPER_HOME;
|
|
125
|
+
else process.env.SKILLHELPER_HOME = oldHome;
|
|
126
|
+
if (oldUserHome == null) delete process.env.HOME;
|
|
127
|
+
else process.env.HOME = oldUserHome;
|
|
128
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchJson(url, init) {
|
|
133
|
+
const res = await fetch(url, init);
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
assert.equal(res.ok, true, `${url} returned ${res.status}: ${text}`);
|
|
136
|
+
return JSON.parse(text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function fetchText(url, init) {
|
|
140
|
+
const res = await fetch(url, init);
|
|
141
|
+
const body = await res.text();
|
|
142
|
+
assert.equal(res.ok, true, `${url} returned ${res.status}: ${body.slice(0, 200)}`);
|
|
143
|
+
return { body, contentType: res.headers.get('content-type') || '' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function fetchBuffer(url, init) {
|
|
147
|
+
const res = await fetch(url, init);
|
|
148
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
149
|
+
assert.equal(res.ok, true, `${url} returned ${res.status}: ${body.toString('utf8', 0, 200)}`);
|
|
150
|
+
return { body, contentType: res.headers.get('content-type') || '' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch(err => {
|
|
154
|
+
console.error('\n[verify] FAIL');
|
|
155
|
+
console.error(err && err.stack || err);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SkillHelper sources.yaml
|
|
2
|
+
#
|
|
3
|
+
# This file is copied to ~/.config/skillhelper/sources.yaml by `npm run init`.
|
|
4
|
+
# Paths support ~ expansion and simple glob expansion in roots.
|
|
5
|
+
#
|
|
6
|
+
# Environment overrides:
|
|
7
|
+
# - SKILLHELPER_HOME: user data directory
|
|
8
|
+
# - PORT: preferred HTTP port
|
|
9
|
+
#
|
|
10
|
+
# Safety:
|
|
11
|
+
# - adapters only read files; they do not execute tools or commands
|
|
12
|
+
# - MCP configs are redacted before entering raw/preview
|
|
13
|
+
# - copy/open API calls only operate on scanned item ids, not arbitrary paths
|
|
14
|
+
|
|
15
|
+
sources:
|
|
16
|
+
hermes:
|
|
17
|
+
enabled: true
|
|
18
|
+
roots:
|
|
19
|
+
- ~/.hermes/skills
|
|
20
|
+
- ~/.hermes/profiles/*/skills
|
|
21
|
+
|
|
22
|
+
claude-code:
|
|
23
|
+
enabled: true
|
|
24
|
+
roots:
|
|
25
|
+
- ~/.claude/skills
|
|
26
|
+
|
|
27
|
+
codex:
|
|
28
|
+
enabled: true
|
|
29
|
+
files:
|
|
30
|
+
- ~/.codex/AGENTS.md
|
|
31
|
+
roots: []
|
|
32
|
+
globs:
|
|
33
|
+
- AGENTS.md
|
|
34
|
+
|
|
35
|
+
cursor:
|
|
36
|
+
enabled: true
|
|
37
|
+
files: []
|
|
38
|
+
roots:
|
|
39
|
+
- ~/.cursor
|
|
40
|
+
globs:
|
|
41
|
+
- rules/**/*.{md,mdc}
|
|
42
|
+
- .cursorrules
|
|
43
|
+
|
|
44
|
+
mcp-config:
|
|
45
|
+
enabled: true
|
|
46
|
+
files:
|
|
47
|
+
- ~/.cursor/mcp.json
|
|
48
|
+
- ~/.codex/config.toml
|
|
49
|
+
- ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
50
|
+
- ~/.hermes/config.yaml
|
|
51
|
+
|
|
52
|
+
hermes-plugin:
|
|
53
|
+
enabled: true
|
|
54
|
+
roots:
|
|
55
|
+
- ~/.hermes/plugins
|
|
56
|
+
- ~/.hermes/profiles/*/plugins
|
|
57
|
+
|
|
58
|
+
codex-plugin:
|
|
59
|
+
enabled: true
|
|
60
|
+
roots:
|
|
61
|
+
- ~/.codex/plugins/cache
|
|
62
|
+
|
|
63
|
+
project-runbook:
|
|
64
|
+
enabled: true
|
|
65
|
+
# Add your project roots here. The default scans ~/Project/*.
|
|
66
|
+
# Each root scans docs/RUNBOOK-*.md plus common AI-agent instruction files.
|
|
67
|
+
roots:
|
|
68
|
+
- ~/Project/*
|
|
69
|
+
globs:
|
|
70
|
+
- docs/RUNBOOK-*.md
|
|
71
|
+
- AGENTS.md
|
|
72
|
+
- CLAUDE.md
|
|
73
|
+
- .cursorrules
|
|
74
|
+
- .cursor/rules/**/*.{md,mdc}
|
|
75
|
+
|
|
76
|
+
skills:
|
|
77
|
+
enabled: true
|
|
78
|
+
roots:
|
|
79
|
+
- ~/skills
|
|
80
|
+
- ~/Skills
|
|
81
|
+
globs:
|
|
82
|
+
- '*.md'
|
|
83
|
+
- '*/SKILL.md'
|
|
84
|
+
- '**/SKILL.md'
|
|
85
|
+
|
|
86
|
+
mcp:
|
|
87
|
+
enabled: true
|
|
88
|
+
roots:
|
|
89
|
+
- ~/mcp
|
|
90
|
+
- ~/MCP
|
|
91
|
+
globs:
|
|
92
|
+
- '*.md'
|
|
93
|
+
- '*.json'
|
|
94
|
+
- '*.yaml'
|
|
95
|
+
- '*.yml'
|
|
96
|
+
- '**/manifest.*'
|
|
97
|
+
|
|
98
|
+
skill:
|
|
99
|
+
enabled: true
|
|
100
|
+
roots:
|
|
101
|
+
- ~/skill
|
|
102
|
+
globs:
|
|
103
|
+
- '*.md'
|
|
104
|
+
- '*/SKILL.md'
|
|
105
|
+
- '**/SKILL.md'
|
|
106
|
+
|
|
107
|
+
obsidian:
|
|
108
|
+
enabled: false
|
|
109
|
+
roots: []
|
|
110
|
+
tagFilter: '#skill'
|
|
111
|
+
|
|
112
|
+
ui:
|
|
113
|
+
port: 11520
|
|
114
|
+
autoOpen: true
|
|
115
|
+
defaultEditor: cursor
|
|
116
|
+
groupBy: [editor, category, product]
|
|
117
|
+
|
|
118
|
+
limits:
|
|
119
|
+
maxFiles: 5000
|
|
120
|
+
maxFileBytes: 1048576
|
|
121
|
+
|
|
122
|
+
exclude:
|
|
123
|
+
- '**/node_modules/**'
|
|
124
|
+
- '**/.git/**'
|
|
125
|
+
- '**/dist/**'
|
|
126
|
+
- '**/.venv/**'
|