huhaa-myskills 0.1.9 → 0.2.3
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/README.md +2 -2
- package/{service/bin → bin}/huhaa-myskills.mjs +116 -38
- package/package.json +19 -28
- package/packages/scanner/package.json +15 -0
- package/packages/scanner/test/scanner.test.mjs +140 -0
- package/packages/server/package.json +15 -0
- package/{service/packages → packages}/server/src/index.mjs +10 -23
- package/packages/server/test/server.test.mjs +135 -0
- package/packages/web/index.html +12 -0
- package/packages/web/package.json +20 -0
- package/packages/web/src/App.vue +407 -0
- package/packages/web/src/lib/api.js +43 -0
- package/packages/web/src/main.js +6 -0
- package/packages/web/src/stores/i18n.js +149 -0
- package/packages/web/src/stores/skills.js +276 -0
- package/packages/web/src/styles.css +325 -0
- package/packages/web/vite.config.js +17 -0
- package/{service/scripts → scripts}/build-web.mjs +2 -2
- package/scripts/prepare-publish.mjs +3 -0
- package/{service/scripts → scripts}/verify.mjs +6 -4
- package/install-and-sync.sh +0 -209
- package/service/scripts/prepare-publish.mjs +0 -44
- package/service/scripts/restore-publish.mjs +0 -17
- /package/{service/bin → bin}/lib/paths.mjs +0 -0
- /package/{service/bin → bin}/lib/port.mjs +0 -0
- /package/{service/config → config}/sources.example.yaml +0 -0
- /package/{service/packages → packages}/scanner/src/adapters/file-docs.mjs +0 -0
- /package/{service/packages → packages}/scanner/src/adapters/hermes-plugin.mjs +0 -0
- /package/{service/packages → packages}/scanner/src/adapters/markdown-skill.mjs +0 -0
- /package/{service/packages → packages}/scanner/src/adapters/mcp-config.mjs +0 -0
- /package/{service/packages → packages}/scanner/src/index.mjs +0 -0
- /package/{service/packages → packages}/scanner/src/types.d.ts +0 -0
- /package/{service/packages → packages}/scanner/src/utils.mjs +0 -0
- /package/{service/packages → packages}/web/README.md +0 -0
- /package/{service/packages → packages}/web/dist/assets/index-CTh5OdBd.js +0 -0
- /package/{service/packages → packages}/web/dist/assets/index-CtoiGnQ7.css +0 -0
- /package/{service/packages → packages}/web/dist/index.html +0 -0
- /package/{service/scripts → scripts}/sync-skills.sh +0 -0
package/README.md
CHANGED
|
@@ -182,10 +182,10 @@ npm install
|
|
|
182
182
|
npm start
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
### 方式 C:npm
|
|
185
|
+
### 方式 C:npm 全局安装(推荐)
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
|
-
npm install -g
|
|
188
|
+
npm install -g huhaa-myskills
|
|
189
189
|
huhaa-myskills init
|
|
190
190
|
huhaa-myskills start
|
|
191
191
|
```
|
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// huhaa-myskills CLI
|
|
3
|
-
//
|
|
4
|
-
// Subcommands:
|
|
5
|
-
// start — scan + start server + open browser
|
|
6
|
-
// scan — scan only, dump IR JSON to stdout
|
|
7
|
-
// init — write default sources.yaml to ~/.config/huhaa-myskills/
|
|
8
|
-
// purge — delete ~/.config/huhaa-myskills/ (zero-residue uninstall helper)
|
|
9
|
-
// dev — dev mode (P3+ wires Vite + nodemon)
|
|
10
|
-
//
|
|
11
|
-
// All user-side state lives under HUHAA_HOME (defaults to ~/.config/huhaa-myskills).
|
|
12
3
|
|
|
13
4
|
import fs from 'node:fs';
|
|
14
5
|
import path from 'node:path';
|
|
15
6
|
import { fileURLToPath } from 'node:url';
|
|
16
7
|
import { spawn } from 'node:child_process';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
17
10
|
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stateFile,
|
|
23
|
-
ensureHomeDir,
|
|
24
|
-
writeJson,
|
|
25
|
-
} from './lib/paths.mjs';
|
|
26
|
-
import { pickPort } from './lib/port.mjs';
|
|
27
|
-
|
|
28
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
// bin/ -> service/ -> repo root
|
|
30
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
-
const SERVICE_ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
const PKGS_ROOT = path.join(REPO_ROOT, 'packages');
|
|
32
15
|
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
// Load paths helper
|
|
19
|
+
const pathsLib = require('./lib/paths.mjs');
|
|
20
|
+
const { homeDir, configFile, cacheFile, stateFile, ensureHomeDir, writeJson } = pathsLib;
|
|
21
|
+
|
|
22
|
+
// Load port helper
|
|
23
|
+
const { pickPort } = require('./lib/port.mjs');
|
|
24
|
+
|
|
25
|
+
// Load version from package.json
|
|
26
|
+
function getVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const pkgPath = path.join(REPO_ROOT, 'package.json');
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
30
|
+
return pkg.version;
|
|
31
|
+
} catch {
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const VERSION = getVersion();
|
|
33
37
|
const cmd = process.argv[2] || 'start';
|
|
34
38
|
|
|
35
39
|
const handlers = {
|
|
@@ -40,9 +44,13 @@ const handlers = {
|
|
|
40
44
|
init: cmdInit,
|
|
41
45
|
purge: cmdPurge,
|
|
42
46
|
dev: cmdDev,
|
|
47
|
+
sync: cmdSync,
|
|
43
48
|
help: cmdHelp,
|
|
44
49
|
'--help': cmdHelp,
|
|
45
50
|
'-h': cmdHelp,
|
|
51
|
+
'--version': cmdVersion,
|
|
52
|
+
'-v': cmdVersion,
|
|
53
|
+
version: cmdVersion,
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
const fn = handlers[cmd] || cmdHelp;
|
|
@@ -51,13 +59,17 @@ fn().catch(err => {
|
|
|
51
59
|
process.exit(1);
|
|
52
60
|
});
|
|
53
61
|
|
|
54
|
-
// ----------------
|
|
62
|
+
// ---------------- commands ----------------
|
|
63
|
+
|
|
64
|
+
async function cmdVersion() {
|
|
65
|
+
console.log(`huhaa-myskills v${VERSION}`);
|
|
66
|
+
}
|
|
55
67
|
|
|
56
68
|
async function cmdHelp() {
|
|
57
|
-
console.log(`huhaa-myskills — local skill / plugin / MCP aggregation hub
|
|
69
|
+
console.log(`huhaa-myskills v${VERSION} — local skill / plugin / MCP aggregation hub
|
|
58
70
|
|
|
59
71
|
Usage:
|
|
60
|
-
huhaa-myskills <command>
|
|
72
|
+
huhaa-myskills <command> [options]
|
|
61
73
|
|
|
62
74
|
Commands:
|
|
63
75
|
start Scan + start server + open browser (default)
|
|
@@ -66,12 +78,17 @@ Commands:
|
|
|
66
78
|
duplicates Scan + print duplicate diagnostics by name/content/path
|
|
67
79
|
init Write default sources.yaml to ~/.config/huhaa-myskills/
|
|
68
80
|
purge Remove all user data under ~/.config/huhaa-myskills/
|
|
81
|
+
sync Sync current skills to selected editors
|
|
69
82
|
dev Dev mode (Vite + nodemon)
|
|
70
|
-
|
|
83
|
+
|
|
84
|
+
Options:
|
|
85
|
+
-v, --version Show version
|
|
86
|
+
-h, --help Show this message
|
|
71
87
|
|
|
72
88
|
Env:
|
|
73
89
|
HUHAA_HOME override user data dir (default: ~/.config/huhaa-myskills)
|
|
74
90
|
PORT override preferred port (default: 11520, falls back +10)
|
|
91
|
+
HUHAA_SYNC comma-separated list of editors to sync (e.g. "cursor,vscode")
|
|
75
92
|
|
|
76
93
|
Paths:
|
|
77
94
|
config ${configFile()}
|
|
@@ -88,7 +105,7 @@ async function cmdInit() {
|
|
|
88
105
|
console.log('[init] not overwriting. delete it first if you want to reset.');
|
|
89
106
|
return;
|
|
90
107
|
}
|
|
91
|
-
const tpl = path.join(
|
|
108
|
+
const tpl = path.join(REPO_ROOT, 'config', 'sources.example.yaml');
|
|
92
109
|
fs.copyFileSync(tpl, dst);
|
|
93
110
|
console.log(`[init] wrote ${dst}`);
|
|
94
111
|
console.log('[init] edit it to add / remove sources, then run: huhaa-myskills start');
|
|
@@ -106,8 +123,7 @@ async function cmdPurge() {
|
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
async function cmdScan() {
|
|
109
|
-
|
|
110
|
-
const { scan } = await import('@huhaa/scanner');
|
|
126
|
+
const { scan } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
|
|
111
127
|
await ensureConfigOrInit();
|
|
112
128
|
const items = await scan();
|
|
113
129
|
process.stdout.write(JSON.stringify(items, null, 2) + '\n');
|
|
@@ -115,7 +131,7 @@ async function cmdScan() {
|
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
async function cmdStats() {
|
|
118
|
-
const { scan } = await import('
|
|
134
|
+
const { scan } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
|
|
119
135
|
await ensureConfigOrInit();
|
|
120
136
|
const items = await scan();
|
|
121
137
|
|
|
@@ -178,7 +194,6 @@ async function cmdStats() {
|
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
181
|
-
// deterministic-ish sample: every Nth item so coverage spans both sources
|
|
182
197
|
const sampleN = Math.min(8, items.length);
|
|
183
198
|
if (sampleN > 0) {
|
|
184
199
|
const step = Math.max(1, Math.floor(items.length / sampleN));
|
|
@@ -200,7 +215,7 @@ async function cmdStats() {
|
|
|
200
215
|
}
|
|
201
216
|
|
|
202
217
|
async function cmdDuplicates() {
|
|
203
|
-
const { scan } = await import('
|
|
218
|
+
const { scan } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
|
|
204
219
|
await ensureConfigOrInit();
|
|
205
220
|
const items = await scan();
|
|
206
221
|
const byName = groupBy(items, it => `${it.source}:${it.name}`);
|
|
@@ -263,20 +278,17 @@ async function cmdStart() {
|
|
|
263
278
|
console.error(`[start] port ${preferred} busy, using ${port} instead.`);
|
|
264
279
|
}
|
|
265
280
|
|
|
266
|
-
// Persist actual port for any tooling that wants to know.
|
|
267
281
|
writeJson(stateFile(), { port, pid: process.pid, startedAt: new Date().toISOString() });
|
|
268
282
|
|
|
269
|
-
const { startServer } = await import('
|
|
283
|
+
const { startServer } = await import(path.join(PKGS_ROOT, 'server/src/index.mjs'));
|
|
270
284
|
await startServer({ port });
|
|
271
285
|
|
|
272
286
|
console.log(`\n HuHaa-MySkills running: http://localhost:${port}\n`);
|
|
273
287
|
console.log(` data dir: ${homeDir()}`);
|
|
274
288
|
console.log(` Ctrl+C to stop.\n`);
|
|
275
289
|
|
|
276
|
-
// Auto-open browser on macOS / linux / win
|
|
277
290
|
openBrowser(`http://localhost:${port}`);
|
|
278
291
|
|
|
279
|
-
// Cleanup state file on exit so a stale port number doesn't linger.
|
|
280
292
|
const cleanup = () => {
|
|
281
293
|
try { fs.unlinkSync(stateFile()); } catch {}
|
|
282
294
|
process.exit(0);
|
|
@@ -286,11 +298,77 @@ async function cmdStart() {
|
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
async function cmdDev() {
|
|
289
|
-
// P3+ wires Vite + nodemon. For now alias to start.
|
|
290
301
|
console.log('[dev] dev mode comes in P3. running start for now.');
|
|
291
302
|
await cmdStart();
|
|
292
303
|
}
|
|
293
304
|
|
|
305
|
+
async function cmdSync() {
|
|
306
|
+
const syncEnv = process.env.HUHAA_SYNC;
|
|
307
|
+
const editors = syncEnv
|
|
308
|
+
? syncEnv.split(',').map(e => e.trim().toLowerCase())
|
|
309
|
+
: null;
|
|
310
|
+
|
|
311
|
+
const { scan } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
|
|
312
|
+
await ensureConfigOrInit();
|
|
313
|
+
const items = await scan();
|
|
314
|
+
|
|
315
|
+
const skills = items.filter(it => it.raw && it.raw.trim());
|
|
316
|
+
|
|
317
|
+
console.log(`[sync] found ${skills.length} skills to sync`);
|
|
318
|
+
|
|
319
|
+
if (editors) {
|
|
320
|
+
console.log(`[sync] syncing to editors: ${editors.join(', ')}`);
|
|
321
|
+
await syncToEditors(skills, editors);
|
|
322
|
+
} else {
|
|
323
|
+
await interactiveSync(skills);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function syncToEditors(skills, editors) {
|
|
328
|
+
const supported = ['cursor', 'vscode', 'windsurf', 'zed', 'neovim', 'helix', 'sublime', 'vim', 'emacs'];
|
|
329
|
+
const validEditors = editors.filter(e => supported.includes(e));
|
|
330
|
+
|
|
331
|
+
if (validEditors.length === 0) {
|
|
332
|
+
console.error(`[sync] no valid editors. Supported: ${supported.join(', ')}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const editor of validEditors) {
|
|
337
|
+
await syncToEditor(skills, editor);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function syncToEditor(skills, editor) {
|
|
342
|
+
console.log(`[sync] syncing ${skills.length} skills to ${editor}...`);
|
|
343
|
+
|
|
344
|
+
const syncScript = path.join(REPO_ROOT, 'scripts', 'sync-skills.sh');
|
|
345
|
+
|
|
346
|
+
return new Promise((resolve) => {
|
|
347
|
+
const child = spawn('bash', [syncScript, '--editor', editor], {
|
|
348
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
child.on('close', (code) => {
|
|
352
|
+
if (code === 0) {
|
|
353
|
+
console.log(`[sync] ${editor}: done`);
|
|
354
|
+
} else {
|
|
355
|
+
console.error(`[sync] ${editor}: failed (exit ${code})`);
|
|
356
|
+
}
|
|
357
|
+
resolve();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function interactiveSync(skills) {
|
|
363
|
+
console.log('\nSupported editors:');
|
|
364
|
+
console.log(' [1] cursor [2] vscode [3] windsurf');
|
|
365
|
+
console.log(' [4] zed [5] neovim [6] helix');
|
|
366
|
+
console.log(' [7] sublime [8] vim [9] emacs');
|
|
367
|
+
console.log(' [0] all editors');
|
|
368
|
+
console.log('\nUse HUHAA_SYNC env to sync without prompt:');
|
|
369
|
+
console.log(' HUHAA_SYNC=cursor,vscode huhaa-myskills sync');
|
|
370
|
+
}
|
|
371
|
+
|
|
294
372
|
// ---------------- helpers ----------------
|
|
295
373
|
|
|
296
374
|
async function ensureConfigOrInit() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huhaa-myskills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local skill / plugin / MCP aggregation hub. Browse all your AI agent skills at http://localhost:11520.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,51 +13,42 @@
|
|
|
13
13
|
"url": "https://github.com/aquarkgn/HuHaa-MySkills/issues"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
|
-
"workspaces": [
|
|
17
|
-
"service/packages/*"
|
|
18
|
-
],
|
|
19
16
|
"engines": {
|
|
20
17
|
"node": ">=20.0.0"
|
|
21
18
|
},
|
|
22
19
|
"bin": {
|
|
23
|
-
"huhaa-myskills": "
|
|
20
|
+
"huhaa-myskills": "bin/huhaa-myskills.mjs"
|
|
24
21
|
},
|
|
25
22
|
"files": [
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"service/packages/web/dist/",
|
|
31
|
-
"service/scripts/",
|
|
23
|
+
"bin/",
|
|
24
|
+
"config/",
|
|
25
|
+
"packages/",
|
|
26
|
+
"scripts/",
|
|
32
27
|
"docs/",
|
|
33
28
|
"README.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
"install-and-sync.sh"
|
|
29
|
+
"LICENSE"
|
|
36
30
|
],
|
|
37
31
|
"scripts": {
|
|
38
|
-
"start": "node
|
|
39
|
-
"scan": "node
|
|
40
|
-
"stats": "node
|
|
41
|
-
"duplicates": "node
|
|
42
|
-
"init": "node
|
|
43
|
-
"purge": "node
|
|
44
|
-
"dev": "node
|
|
45
|
-
"sync": "bash
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
32
|
+
"start": "node bin/huhaa-myskills.mjs start",
|
|
33
|
+
"scan": "node bin/huhaa-myskills.mjs scan",
|
|
34
|
+
"stats": "node bin/huhaa-myskills.mjs stats",
|
|
35
|
+
"duplicates": "node bin/huhaa-myskills.mjs duplicates",
|
|
36
|
+
"init": "node bin/huhaa-myskills.mjs init",
|
|
37
|
+
"purge": "node bin/huhaa-myskills.mjs purge",
|
|
38
|
+
"dev": "node bin/huhaa-myskills.mjs dev",
|
|
39
|
+
"sync": "bash scripts/sync-skills.sh",
|
|
40
|
+
"build:web": "node scripts/build-web.mjs",
|
|
41
|
+
"test": "node --test packages/scanner/test/*.test.mjs packages/server/test/*.test.mjs",
|
|
42
|
+
"verify": "node scripts/verify.mjs",
|
|
43
|
+
"release": "npm version patch && git push --follow-tags"
|
|
50
44
|
},
|
|
51
45
|
"dependencies": {
|
|
52
|
-
"@huhaa/scanner": "file:service/packages/scanner",
|
|
53
|
-
"@huhaa/server": "file:service/packages/server",
|
|
54
46
|
"chokidar": "^4.0.3",
|
|
55
47
|
"fast-glob": "^3.3.2",
|
|
56
48
|
"fastify": "^5.0.0",
|
|
57
49
|
"yaml": "^2.6.0"
|
|
58
50
|
},
|
|
59
51
|
"devDependencies": {
|
|
60
|
-
"@huhaa/web": "file:service/packages/web",
|
|
61
52
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
62
53
|
"fuse.js": "^7.0.0",
|
|
63
54
|
"markdown-it": "^14.1.0",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huhaa/scanner",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs",
|
|
9
|
+
"./types": "./src/types.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"yaml": "^2.6.0",
|
|
13
|
+
"fast-glob": "^3.3.2"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { scan, getWatchTargets } from '../src/index.mjs';
|
|
8
|
+
|
|
9
|
+
function makeTempHome() {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'huhaa-scanner-test-'));
|
|
11
|
+
const home = path.join(root, 'home');
|
|
12
|
+
fs.mkdirSync(home, { recursive: true });
|
|
13
|
+
process.env.HUHAA_HOME = home;
|
|
14
|
+
return { root, home };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function write(file, text) {
|
|
18
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
fs.writeFileSync(file, text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function groupBy(items, fn) {
|
|
23
|
+
return items.reduce((acc, item) => {
|
|
24
|
+
const key = fn(item);
|
|
25
|
+
(acc[key] ||= []).push(item);
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('scan aggregates enabled sources, strips duplicate semantic skill exports, and redacts MCP secrets', async (t) => {
|
|
31
|
+
const { root, home } = makeTempHome();
|
|
32
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
33
|
+
|
|
34
|
+
const sources = path.join(home, 'sources.yaml');
|
|
35
|
+
const hermesRoot = path.join(root, 'hermes-skills');
|
|
36
|
+
const codexRoot = path.join(root, 'project-a');
|
|
37
|
+
const cursorRoot = path.join(root, 'project-b');
|
|
38
|
+
const mcpFile = path.join(root, 'mcp.json');
|
|
39
|
+
|
|
40
|
+
write(path.join(hermesRoot, 'devops', 'deploy-helper', 'SKILL.md'), `---
|
|
41
|
+
name: deploy-helper
|
|
42
|
+
description: Deploy helper for Docker services
|
|
43
|
+
triggers:
|
|
44
|
+
- deploy service
|
|
45
|
+
---
|
|
46
|
+
# Deploy helper
|
|
47
|
+
Use this when deploying Docker services.
|
|
48
|
+
`);
|
|
49
|
+
write(path.join(hermesRoot, '.hidden-export', 'deploy-helper', 'SKILL.md'), `---
|
|
50
|
+
name: deploy-helper
|
|
51
|
+
description: Duplicate hidden export
|
|
52
|
+
---
|
|
53
|
+
# Duplicate hidden export
|
|
54
|
+
`);
|
|
55
|
+
write(path.join(codexRoot, 'AGENTS.md'), '# Codex Instructions\n\nUse Node 20 and run tests.');
|
|
56
|
+
write(path.join(cursorRoot, '.cursorrules'), 'Always keep UI concise.');
|
|
57
|
+
write(mcpFile, JSON.stringify({
|
|
58
|
+
mcpServers: {
|
|
59
|
+
dangerous: {
|
|
60
|
+
command: 'node',
|
|
61
|
+
args: ['server.js'],
|
|
62
|
+
env: {
|
|
63
|
+
API_TOKEN: 'fake-token-value-for-redaction-test',
|
|
64
|
+
normal: 'visible',
|
|
65
|
+
},
|
|
66
|
+
url: 'https://example.com/mcp?token=secret-value',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}, null, 2));
|
|
70
|
+
|
|
71
|
+
write(sources, `sources:
|
|
72
|
+
hermes:
|
|
73
|
+
enabled: true
|
|
74
|
+
roots:
|
|
75
|
+
- ${JSON.stringify(hermesRoot)}
|
|
76
|
+
codex:
|
|
77
|
+
enabled: true
|
|
78
|
+
roots:
|
|
79
|
+
- ${JSON.stringify(codexRoot)}
|
|
80
|
+
cursor:
|
|
81
|
+
enabled: true
|
|
82
|
+
roots:
|
|
83
|
+
- ${JSON.stringify(cursorRoot)}
|
|
84
|
+
mcp-config:
|
|
85
|
+
enabled: true
|
|
86
|
+
files:
|
|
87
|
+
- ${JSON.stringify(mcpFile)}
|
|
88
|
+
limits:
|
|
89
|
+
maxFiles: 100
|
|
90
|
+
maxFileBytes: 1048576
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
const items = await scan();
|
|
94
|
+
const bySource = groupBy(items, it => it.source);
|
|
95
|
+
|
|
96
|
+
assert.equal(bySource.hermes?.length, 1, 'semantic duplicate hermes skill should collapse to one item');
|
|
97
|
+
assert.equal(bySource.codex?.length, 1);
|
|
98
|
+
assert.equal(bySource.cursor?.length, 1);
|
|
99
|
+
assert.equal(bySource['mcp-config']?.length, 1);
|
|
100
|
+
|
|
101
|
+
const skill = bySource.hermes[0];
|
|
102
|
+
assert.equal(skill.name, 'deploy-helper');
|
|
103
|
+
assert.equal(skill.editor, 'Hermes Agent');
|
|
104
|
+
assert.equal(skill.category, 'devops');
|
|
105
|
+
assert.deepEqual(skill.triggers, ['deploy service']);
|
|
106
|
+
assert.ok(!skill.paths.abs.includes('/.hidden-export/'), 'visible export should outrank hidden duplicate');
|
|
107
|
+
|
|
108
|
+
const mcp = bySource['mcp-config'][0];
|
|
109
|
+
assert.equal(mcp.kind, 'mcp');
|
|
110
|
+
assert.equal(mcp.name, 'dangerous');
|
|
111
|
+
assert.match(mcp.raw, /\[REDACTED\]/);
|
|
112
|
+
assert.doesNotMatch(mcp.raw, /fake-token-value-for-redaction-test/);
|
|
113
|
+
assert.doesNotMatch(mcp.raw, /secret-value/);
|
|
114
|
+
assert.match(mcp.raw, /visible/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('getWatchTargets includes config file plus configured source files and globs', async (t) => {
|
|
118
|
+
const { root, home } = makeTempHome();
|
|
119
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
120
|
+
|
|
121
|
+
const hermesRoot = path.join(root, 'skills');
|
|
122
|
+
const mcpFile = path.join(root, 'mcp.json');
|
|
123
|
+
fs.mkdirSync(hermesRoot, { recursive: true });
|
|
124
|
+
write(mcpFile, '{}');
|
|
125
|
+
write(path.join(home, 'sources.yaml'), `sources:
|
|
126
|
+
hermes:
|
|
127
|
+
enabled: true
|
|
128
|
+
roots:
|
|
129
|
+
- ${JSON.stringify(hermesRoot)}
|
|
130
|
+
mcp-config:
|
|
131
|
+
enabled: true
|
|
132
|
+
files:
|
|
133
|
+
- ${JSON.stringify(mcpFile)}
|
|
134
|
+
`);
|
|
135
|
+
|
|
136
|
+
const targets = await getWatchTargets();
|
|
137
|
+
assert.ok(targets.includes(path.join(home, 'sources.yaml')));
|
|
138
|
+
assert.ok(targets.includes(mcpFile));
|
|
139
|
+
assert.ok(targets.some(t => t.endsWith('/skills/**/SKILL.md')));
|
|
140
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huhaa/server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"fastify": "^5.0.0",
|
|
12
|
+
"chokidar": "^4.0.3",
|
|
13
|
+
"@huhaa/scanner": "*"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
// @huhaa/server — Fastify HTTP API + placeholder UI.
|
|
2
|
-
//
|
|
3
|
-
// Hard rules (locked from day 1):
|
|
4
|
-
// - bind 127.0.0.1 ONLY, never 0.0.0.0
|
|
5
|
-
// - server NEVER scans files; it reads in-memory IR populated by scanner
|
|
6
|
-
// - copy/open ONLY operate on paths that exist in the loaded IR snapshot
|
|
7
|
-
// (whitelist defense: a malicious request can't ask us to pbcopy / open
|
|
8
|
-
// arbitrary files because we resolve via skill id, not via raw path)
|
|
9
|
-
//
|
|
10
|
-
// API surface (P2):
|
|
11
|
-
// GET /api/health — liveness + counts
|
|
12
|
-
// GET /api/skills — list (no `raw` field — detail-only)
|
|
13
|
-
// GET /api/skills/:id — full IR item (with raw)
|
|
14
|
-
// GET /api/stats — by source / category / brand aggregates
|
|
15
|
-
// POST /api/copy — { id, what: 'path'|'dir'|'rel'|'name'|'raw'|'prompt' } -> pbcopy
|
|
16
|
-
// POST /api/open — { id, with?: 'cursor'|'finder'|'default' }
|
|
17
|
-
//
|
|
18
|
-
// P3 replaces the placeholder HTML with a built Vue SPA mounted at /.
|
|
19
|
-
|
|
20
2
|
import fs from 'node:fs';
|
|
21
3
|
import path from 'node:path';
|
|
22
4
|
import { fileURLToPath } from 'node:url';
|
|
23
5
|
import { spawn } from 'node:child_process';
|
|
24
6
|
import Fastify from 'fastify';
|
|
25
7
|
import chokidar from 'chokidar';
|
|
26
|
-
import { scan, getWatchTargets } from '@huhaa/scanner';
|
|
27
8
|
|
|
28
9
|
const PHASE = 'P6';
|
|
29
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const PACKAGE_JSON = path.resolve(SERVICE_ROOT, '..', 'package.json');
|
|
11
|
+
const PKGS_ROOT = path.resolve(__dirname, '..', '..');
|
|
12
|
+
const PACKAGE_JSON = path.resolve(PKGS_ROOT, '..', 'package.json');
|
|
33
13
|
const VERSION = readPackageVersion();
|
|
34
|
-
const WEB_DIST = path.join(
|
|
14
|
+
const WEB_DIST = path.join(PKGS_ROOT, 'web', 'dist');
|
|
15
|
+
|
|
16
|
+
async function importScanner() {
|
|
17
|
+
const { scan, getWatchTargets } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
|
|
18
|
+
return { scan, getWatchTargets };
|
|
19
|
+
}
|
|
35
20
|
|
|
36
21
|
function readPackageVersion() {
|
|
37
22
|
try {
|
|
@@ -46,6 +31,8 @@ export async function startServer({ port = 11520 } = {}) {
|
|
|
46
31
|
logger: { level: process.env.LOG_LEVEL || 'warn' },
|
|
47
32
|
});
|
|
48
33
|
|
|
34
|
+
const { scan, getWatchTargets } = await importScanner();
|
|
35
|
+
|
|
49
36
|
// In-memory IR. HTTP handlers only read this snapshot; reload/watch swaps it atomically.
|
|
50
37
|
let irCache = await scan();
|
|
51
38
|
let irById = new Map(irCache.map(it => [it.id, it]));
|