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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/{service/bin → bin}/huhaa-myskills.mjs +116 -38
  3. package/package.json +19 -28
  4. package/packages/scanner/package.json +15 -0
  5. package/packages/scanner/test/scanner.test.mjs +140 -0
  6. package/packages/server/package.json +15 -0
  7. package/{service/packages → packages}/server/src/index.mjs +10 -23
  8. package/packages/server/test/server.test.mjs +135 -0
  9. package/packages/web/index.html +12 -0
  10. package/packages/web/package.json +20 -0
  11. package/packages/web/src/App.vue +407 -0
  12. package/packages/web/src/lib/api.js +43 -0
  13. package/packages/web/src/main.js +6 -0
  14. package/packages/web/src/stores/i18n.js +149 -0
  15. package/packages/web/src/stores/skills.js +276 -0
  16. package/packages/web/src/styles.css +325 -0
  17. package/packages/web/vite.config.js +17 -0
  18. package/{service/scripts → scripts}/build-web.mjs +2 -2
  19. package/scripts/prepare-publish.mjs +3 -0
  20. package/{service/scripts → scripts}/verify.mjs +6 -4
  21. package/install-and-sync.sh +0 -209
  22. package/service/scripts/prepare-publish.mjs +0 -44
  23. package/service/scripts/restore-publish.mjs +0 -17
  24. /package/{service/bin → bin}/lib/paths.mjs +0 -0
  25. /package/{service/bin → bin}/lib/port.mjs +0 -0
  26. /package/{service/config → config}/sources.example.yaml +0 -0
  27. /package/{service/packages → packages}/scanner/src/adapters/file-docs.mjs +0 -0
  28. /package/{service/packages → packages}/scanner/src/adapters/hermes-plugin.mjs +0 -0
  29. /package/{service/packages → packages}/scanner/src/adapters/markdown-skill.mjs +0 -0
  30. /package/{service/packages → packages}/scanner/src/adapters/mcp-config.mjs +0 -0
  31. /package/{service/packages → packages}/scanner/src/index.mjs +0 -0
  32. /package/{service/packages → packages}/scanner/src/types.d.ts +0 -0
  33. /package/{service/packages → packages}/scanner/src/utils.mjs +0 -0
  34. /package/{service/packages → packages}/web/README.md +0 -0
  35. /package/{service/packages → packages}/web/dist/assets/index-CTh5OdBd.js +0 -0
  36. /package/{service/packages → packages}/web/dist/assets/index-CtoiGnQ7.css +0 -0
  37. /package/{service/packages → packages}/web/dist/index.html +0 -0
  38. /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 github:aquarkgn/HuHaa-MySkills
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
- homeDir,
20
- configFile,
21
- cacheFile,
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
- // ---------------- handlers ----------------
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
- help Show this message
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(SERVICE_ROOT, 'config', 'sources.example.yaml');
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
- // P0 placeholder. P1 wires real adapters.
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('@huhaa/scanner');
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('@huhaa/scanner');
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('@huhaa/server');
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.1.9",
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": "service/bin/huhaa-myskills.mjs"
20
+ "huhaa-myskills": "bin/huhaa-myskills.mjs"
24
21
  },
25
22
  "files": [
26
- "service/bin/",
27
- "service/config/",
28
- "service/packages/scanner/src/",
29
- "service/packages/server/src/",
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 service/bin/huhaa-myskills.mjs start",
39
- "scan": "node service/bin/huhaa-myskills.mjs scan",
40
- "stats": "node service/bin/huhaa-myskills.mjs stats",
41
- "duplicates": "node service/bin/huhaa-myskills.mjs duplicates",
42
- "init": "node service/bin/huhaa-myskills.mjs init",
43
- "purge": "node service/bin/huhaa-myskills.mjs purge",
44
- "dev": "node service/bin/huhaa-myskills.mjs dev",
45
- "sync": "bash service/scripts/sync-skills.sh",
46
- "sync:remote": "curl -fsSL https://raw.githubusercontent.com/aquarkgn/HuHaa-MySkills/main/install-and-sync.sh | bash -s -- --sync-only",
47
- "build:web": "node service/scripts/build-web.mjs",
48
- "test": "node --test service/packages/scanner/test/*.test.mjs service/packages/server/test/*.test.mjs",
49
- "verify": "node service/scripts/verify.mjs"
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
- // server/src -> server -> packages -> service
31
- const SERVICE_ROOT = path.resolve(__dirname, '..', '..', '..');
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(SERVICE_ROOT, 'packages', 'web', 'dist');
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]));