huhaa-myskills 0.3.6 → 0.3.7

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 (118) hide show
  1. package/bin/huhaa-myskills.mjs +88 -17
  2. package/build/pack-smoke.mjs +195 -0
  3. package/build/verify.mjs +18 -1
  4. package/package.json +12 -4
  5. package/packages/server/src/index.mjs +44 -18
  6. package/packages/server/src/labels.mjs +1 -1
  7. package/packages/web/dist/assets/index-DBR3X1hb.js +304 -0
  8. package/packages/web/dist/assets/index-DxHxmeVA.css +1 -0
  9. package/packages/web/dist/favicon.ico +0 -0
  10. package/packages/web/dist/index.html +4 -4
  11. package/packages/web/dist/site.webmanifest +4 -4
  12. package/packages/web/public/favicon.ico +0 -0
  13. package/packages/web/public/site.webmanifest +4 -4
  14. package/scripts/install-and-sync.sh +3 -3
  15. package/packages/scanner/test/icon.test.mjs +0 -212
  16. package/packages/scanner/test/scan-tier.test.mjs +0 -74
  17. package/packages/scanner/test/scanner.test.mjs +0 -136
  18. package/packages/scanner/test/skill-adapter.test.mjs +0 -70
  19. package/packages/scanner/test/tier-editor.test.mjs +0 -67
  20. package/packages/scanner/tests/directory-skill.test.mjs +0 -93
  21. package/packages/scanner/tests/scanner-integration.test.mjs +0 -139
  22. package/packages/scanner/verify-tier-scan.mjs +0 -72
  23. package/packages/server/test/langdetect.test.mjs +0 -48
  24. package/packages/server/test/llm-client.test.mjs +0 -101
  25. package/packages/server/test/server.test.mjs +0 -150
  26. package/packages/server/test/translate-cache.test.mjs +0 -96
  27. package/packages/server/test/translate-raw.test.mjs +0 -113
  28. package/packages/server/test/translator.test.mjs +0 -30
  29. package/packages/web/dist/assets/index-BbSpJilK.js +0 -249
  30. package/packages/web/dist/assets/index-DtwcYsHQ.css +0 -1
  31. package/packages/web/index.html +0 -37
  32. package/packages/web/package-lock.json +0 -2989
  33. package/packages/web/package.json +0 -33
  34. package/packages/web/postcss.config.js +0 -6
  35. package/packages/web/src/App.test.ts +0 -115
  36. package/packages/web/src/App.tsx +0 -267
  37. package/packages/web/src/components/ComingSoon.tsx +0 -14
  38. package/packages/web/src/components/Sidebar/SidebarMenu.tsx +0 -83
  39. package/packages/web/src/components/Sidebar/SkillItem.tsx +0 -44
  40. package/packages/web/src/components/Sidebar/TierGroup.tsx +0 -143
  41. package/packages/web/src/components/Sidebar/__tests__/verify-frontend.test.ts +0 -158
  42. package/packages/web/src/components/layout/Sidebar.test.tsx +0 -291
  43. package/packages/web/src/components/layout/Sidebar.tsx +0 -196
  44. package/packages/web/src/components/layout/Topbar.tsx +0 -76
  45. package/packages/web/src/components/ui/ActionButton.tsx +0 -48
  46. package/packages/web/src/components/ui/OfficialBrandIcon.tsx +0 -101
  47. package/packages/web/src/components/ui/button.tsx +0 -48
  48. package/packages/web/src/components/ui/card.tsx +0 -45
  49. package/packages/web/src/components/views/CliCommandView.test.tsx +0 -328
  50. package/packages/web/src/components/views/CliCommandView.tsx +0 -588
  51. package/packages/web/src/components/views/CommandIcon.test.tsx +0 -36
  52. package/packages/web/src/components/views/CommandIcon.tsx +0 -18
  53. package/packages/web/src/components/views/DashboardView.test.tsx +0 -165
  54. package/packages/web/src/components/views/DashboardView.tsx +0 -176
  55. package/packages/web/src/components/views/EditorPlaceholder.tsx +0 -25
  56. package/packages/web/src/components/views/OtherSkillsView.test.tsx +0 -126
  57. package/packages/web/src/components/views/OtherSkillsView.tsx +0 -383
  58. package/packages/web/src/components/views/SettingsView.tsx +0 -125
  59. package/packages/web/src/components/views/SkillDetail.test.tsx +0 -132
  60. package/packages/web/src/components/views/SkillDetail.tsx +0 -264
  61. package/packages/web/src/components/views/SkillsView.icon.test.tsx +0 -67
  62. package/packages/web/src/components/views/SkillsView.test.ts +0 -28
  63. package/packages/web/src/components/views/SkillsView.tsx +0 -224
  64. package/packages/web/src/components/views/cli/SubcommandRelatedItemsPanel.tsx +0 -288
  65. package/packages/web/src/data/command-help/claude.json +0 -328
  66. package/packages/web/src/data/command-help/code.json +0 -297
  67. package/packages/web/src/data/command-help/codex.json +0 -444
  68. package/packages/web/src/data/command-help/gstack.json +0 -177
  69. package/packages/web/src/data/command-help/hermes.json +0 -321
  70. package/packages/web/src/data/commands.json +0 -1468
  71. package/packages/web/src/hooks/QUICK_REFERENCE.md +0 -165
  72. package/packages/web/src/hooks/README.md +0 -378
  73. package/packages/web/src/hooks/__tests__/getSkillIcons.test.ts +0 -317
  74. package/packages/web/src/hooks/__tests__/useLiveReload.test.ts +0 -84
  75. package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +0 -422
  76. package/packages/web/src/hooks/getSkillIcons.ts +0 -270
  77. package/packages/web/src/hooks/useLiveReload.ts +0 -38
  78. package/packages/web/src/hooks/useOtherSkills.ts +0 -387
  79. package/packages/web/src/hooks/useSidebarMenu.ts +0 -74
  80. package/packages/web/src/hooks/useTheme.ts +0 -36
  81. package/packages/web/src/index.css +0 -161
  82. package/packages/web/src/lib/api.test.ts +0 -47
  83. package/packages/web/src/lib/api.ts +0 -212
  84. package/packages/web/src/lib/cli-enrichments/gstackList.test.ts +0 -71
  85. package/packages/web/src/lib/cli-enrichments/gstackList.ts +0 -91
  86. package/packages/web/src/lib/cli-enrichments/index.ts +0 -45
  87. package/packages/web/src/lib/cli-enrichments/skillDocParser.test.ts +0 -47
  88. package/packages/web/src/lib/cli-enrichments/skillDocParser.ts +0 -124
  89. package/packages/web/src/lib/cli-enrichments/types.ts +0 -52
  90. package/packages/web/src/lib/cn.ts +0 -7
  91. package/packages/web/src/lib/commandHelp.test.ts +0 -41
  92. package/packages/web/src/lib/commandHelp.ts +0 -64
  93. package/packages/web/src/lib/commands.test.ts +0 -71
  94. package/packages/web/src/lib/commands.ts +0 -74
  95. package/packages/web/src/lib/editors.test.ts +0 -34
  96. package/packages/web/src/lib/editors.ts +0 -80
  97. package/packages/web/src/lib/i18n.ts +0 -87
  98. package/packages/web/src/lib/iconBlacklist.ts +0 -37
  99. package/packages/web/src/lib/idb.ts +0 -64
  100. package/packages/web/src/lib/markdown.test.ts +0 -27
  101. package/packages/web/src/lib/markdown.ts +0 -14
  102. package/packages/web/src/main.tsx +0 -13
  103. package/packages/web/src/types/index.ts +0 -74
  104. package/packages/web/src/types/other-skill.ts +0 -88
  105. package/packages/web/src/types/skill.ts +0 -136
  106. package/packages/web/src/types.ts +0 -122
  107. package/packages/web/src/vite-env.d.ts +0 -1
  108. package/packages/web/tailwind.config.ts +0 -66
  109. package/packages/web/tsconfig.json +0 -28
  110. package/packages/web/vite.config.ts +0 -36
  111. package/packages/web/vitest.config.ts +0 -40
  112. package/packages/web/vitest.setup.ts +0 -1
  113. package/scripts/activate-data-sources.sh +0 -223
  114. package/scripts/brand-completion.py +0 -586
  115. package/scripts/digest-commands.mjs +0 -389
  116. package/scripts/digest-commands.test.mjs +0 -147
  117. package/scripts/init-nextjs-project.sh +0 -421
  118. package/scripts/start-dev.sh +0 -72
@@ -12,6 +12,7 @@ const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = path.dirname(__filename);
13
13
  const REPO_ROOT = path.resolve(__dirname, '..');
14
14
  const PKGS_ROOT = path.join(REPO_ROOT, 'packages');
15
+ const DISPLAY_NAME = 'HuHaa AI 助手';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
 
@@ -69,7 +70,7 @@ async function cmdVersion() {
69
70
  }
70
71
 
71
72
  async function cmdHelp() {
72
- console.log(`huhaa-myskills v${VERSION} — local skill / plugin / MCP aggregation hub
73
+ console.log(`huhaa-myskills v${VERSION} — ${DISPLAY_NAME}:本地技能 / 插件 / MCP 聚合中心
73
74
 
74
75
  Usage:
75
76
  huhaa-myskills <command> [options]
@@ -96,6 +97,7 @@ Env:
96
97
  HUHAA_HOME override user data dir (default: ~/.config/huhaa-myskills)
97
98
  PORT override preferred port (default: 11520, falls back +10)
98
99
  HUHAA_SYNC comma-separated list of editors to sync (e.g. "cursor,vscode")
100
+ HUHAA_NO_OPEN set to 1/true/yes to skip opening the browser
99
101
 
100
102
  Paths:
101
103
  config ${configFile()}
@@ -143,7 +145,7 @@ async function cmdStop() {
143
145
  }
144
146
  try {
145
147
  process.kill(state.pid, 'SIGTERM');
146
- console.log(`✓ 已停止 HuHaa-MySkills (pid ${state.pid}, port ${state.port ?? '未知'})`);
148
+ console.log(`✓ 已停止 ${DISPLAY_NAME} (pid ${state.pid}, port ${state.port ?? '未知'})`);
147
149
  } catch (err) {
148
150
  console.error(`[stop] 停止失败: ${err.message}`);
149
151
  return false;
@@ -191,7 +193,7 @@ async function cmdUninstall() {
191
193
  const question = (q) => new Promise(resolve => rl.question(q, resolve));
192
194
 
193
195
  const dir = homeDir();
194
- console.log('\n[uninstall] 即将卸载 HuHaa-MySkills,将执行以下操作:');
196
+ console.log(`\n[uninstall] 即将卸载 ${DISPLAY_NAME},将执行以下操作:`);
195
197
  console.log(' 1. 停止运行中的后台服务');
196
198
  console.log(` 2. 删除用户数据目录: ${dir}`);
197
199
  console.log(' 3. 卸载全局 npm 包: huhaa-myskills');
@@ -275,7 +277,7 @@ async function cmdStats() {
275
277
 
276
278
  const sortDesc = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]);
277
279
 
278
- console.log(`\nHuHaa-MySkills scan stats`);
280
+ console.log(`\n${DISPLAY_NAME} scan stats`);
279
281
  console.log(`==========================`);
280
282
  console.log(`total items: ${items.length}`);
281
283
  console.log(`parse errors: ${errors.length}`);
@@ -341,7 +343,7 @@ async function cmdDuplicates() {
341
343
  const contentDups = [...byContent.entries()].filter(([k, arr]) => !k.endsWith(':00000000') && arr.length > 1);
342
344
  const pathDups = [...byAbs.entries()].filter(([k, arr]) => k && arr.length > 1);
343
345
 
344
- console.log('\nHuHaa-MySkills duplicate diagnostics');
346
+ console.log(`\n${DISPLAY_NAME} duplicate diagnostics`);
345
347
  console.log('====================================');
346
348
  console.log(`total items: ${items.length}`);
347
349
  console.log(`duplicate names: ${nameDups.length} groups`);
@@ -393,7 +395,7 @@ async function cmdStart() {
393
395
  const stateData = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
394
396
  if (stateData.pid && isProcessRunning(stateData.pid)) {
395
397
  const logFile = path.join(homeDir(), 'huhaa.log');
396
- console.log(`✓ HuHaa-MySkills 已在运行: http://localhost:${stateData.port}`);
398
+ console.log(`✓ ${DISPLAY_NAME} 已在运行: http://localhost:${stateData.port}`);
397
399
  console.log(`📝 日志: ${logFile}`);
398
400
  return;
399
401
  }
@@ -428,7 +430,7 @@ async function startBackgroundServer(port, logFile, homeDirectory) {
428
430
  try {
429
431
  const stateData = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
430
432
  if (stateData.pid && isProcessRunning(stateData.pid)) {
431
- console.log(`✓ HuHaa-MySkills 已在运行: http://localhost:${stateData.port}`);
433
+ console.log(`✓ ${DISPLAY_NAME} 已在运行: http://localhost:${stateData.port}`);
432
434
  console.log(`📝 日志: ${logFile}`);
433
435
  return;
434
436
  }
@@ -449,15 +451,39 @@ async function startBackgroundServer(port, logFile, homeDirectory) {
449
451
  }
450
452
  });
451
453
 
454
+ let childExited = false;
455
+ let exitInfo = '';
456
+ child.once('exit', (code, signal) => {
457
+ childExited = true;
458
+ exitInfo = signal ? `signal ${signal}` : `exit ${code}`;
459
+ });
460
+ child.once('error', (err) => {
461
+ childExited = true;
462
+ exitInfo = err.message;
463
+ });
464
+
452
465
  child.unref();
453
466
 
454
- // 等待子进程启动,检查端口是否开放
455
- await new Promise(resolve => setTimeout(resolve, 1000));
467
+ const healthy = await waitForHealth(port, 10000, () => childExited);
456
468
 
457
- // 关闭日志文件描述符(子进程已继承)
458
- fs.closeSync(logFd);
469
+ try { fs.closeSync(logFd); } catch {}
459
470
 
460
- console.log(`✓ HuHaa-MySkills 已在后台运行: http://localhost:${port}`);
471
+ if (!healthy) {
472
+ if (!childExited) {
473
+ try { process.kill(child.pid, 'SIGTERM'); } catch {}
474
+ }
475
+ try { fs.unlinkSync(stateFile()); } catch {}
476
+ const tail = readLogTail(logFile, 40);
477
+ console.error(`[start] ${DISPLAY_NAME} 后台启动失败${exitInfo ? `(${exitInfo})` : ''}`);
478
+ console.error(`日志: ${logFile}`);
479
+ if (tail) {
480
+ console.error('\n最近日志:');
481
+ console.error(tail);
482
+ }
483
+ process.exit(1);
484
+ }
485
+
486
+ console.log(`✓ ${DISPLAY_NAME} 已在后台运行: http://localhost:${port}`);
461
487
  console.log(`📝 日志: ${logFile}`);
462
488
  console.log(`💡 查看日志: tail -f ${logFile}`);
463
489
  console.log(`💡 停止服务: huhaa-myskills stop`);
@@ -472,18 +498,56 @@ function isProcessRunning(pid) {
472
498
  }
473
499
  }
474
500
 
475
- async function runServer(port, logFile, homeDirectory) {
476
- writeJson(stateFile(), { port, pid: process.pid, startedAt: new Date().toISOString() });
501
+ async function waitForHealth(port, timeoutMs, shouldStop) {
502
+ const deadline = Date.now() + timeoutMs;
503
+ while (Date.now() < deadline) {
504
+ if (shouldStop && shouldStop()) return false;
505
+ if (await fetchHealth(port)) return true;
506
+ await delay(250);
507
+ }
508
+ return false;
509
+ }
477
510
 
511
+ async function fetchHealth(port) {
512
+ const controller = new AbortController();
513
+ const timer = setTimeout(() => controller.abort(), 500);
514
+ try {
515
+ const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: controller.signal });
516
+ if (!res.ok) return false;
517
+ const data = await res.json();
518
+ return data && data.ok === true;
519
+ } catch {
520
+ return false;
521
+ } finally {
522
+ clearTimeout(timer);
523
+ }
524
+ }
525
+
526
+ function readLogTail(file, lines = 40) {
527
+ try {
528
+ const text = fs.readFileSync(file, 'utf8');
529
+ return text.trimEnd().split(/\r?\n/).slice(-lines).join('\n');
530
+ } catch {
531
+ return '';
532
+ }
533
+ }
534
+
535
+ function delay(ms) {
536
+ return new Promise(resolve => setTimeout(resolve, ms));
537
+ }
538
+
539
+ async function runServer(port, logFile, homeDirectory) {
478
540
  const { startServer } = await import(path.join(PKGS_ROOT, 'server/src/index.mjs'));
479
- await startServer({ port });
541
+ const server = await startServer({ port });
542
+ const actualPort = server.port || port;
543
+ writeJson(stateFile(), { port: actualPort, pid: process.pid, startedAt: new Date().toISOString() });
480
544
 
481
- console.log(`\n HuHaa-MySkills 运行中: http://localhost:${port}\n`);
545
+ console.log(`\n ${DISPLAY_NAME} 运行中: http://localhost:${actualPort}\n`);
482
546
  console.log(` 数据目录: ${homeDirectory}`);
483
547
  console.log(` 日志: ${logFile}`);
484
548
  console.log(` 按 Ctrl+C 停止。\n`);
485
549
 
486
- openBrowser(`http://localhost:${port}`);
550
+ openBrowser(`http://localhost:${actualPort}`);
487
551
 
488
552
  const cleanup = () => {
489
553
  try { fs.unlinkSync(stateFile()); } catch {}
@@ -636,6 +700,8 @@ async function ensureConfigOrInit() {
636
700
  }
637
701
 
638
702
  function openBrowser(url) {
703
+ if (shouldSkipBrowserOpen()) return;
704
+
639
705
  const platform = process.platform;
640
706
  let cmd, args;
641
707
  if (platform === 'darwin') { cmd = 'open'; args = [url]; }
@@ -647,3 +713,8 @@ function openBrowser(url) {
647
713
  // best-effort; user can copy-paste the URL
648
714
  }
649
715
  }
716
+
717
+ function shouldSkipBrowserOpen() {
718
+ const value = process.env.HUHAA_NO_OPEN;
719
+ return value === '1' || value?.toLowerCase() === 'true' || value?.toLowerCase() === 'yes';
720
+ }
@@ -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(), 'huhaa-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', 'huhaa-myskills');
117
+ const version = run(bin, ['--version'], { capture: true });
118
+ assert.match(version.stdout, new RegExp(`^huhaa-myskills 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
+ HUHAA_HOME: home,
137
+ HUHAA_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 CHANGED
@@ -106,10 +106,20 @@ limits:
106
106
  assert.equal(css.contentType.split(';')[0], 'text/css');
107
107
  assert.ok(css.body.includes('topbar') || css.body.includes('sidebar') || css.body.includes('detail'));
108
108
 
109
- console.log('\n[verify] PASS build + tests + HTTP/API/static smoke checks');
109
+ const manifest = await fetchJson(`${base}/site.webmanifest`);
110
+ assert.equal(manifest.name, 'HuHaa AI 助手');
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]);
110
117
  } finally {
111
118
  await app.close();
112
119
  }
120
+
121
+ run('node', ['build/pack-smoke.mjs']);
122
+ console.log('\n[verify] PASS build + tests + HTTP/API/static/package smoke checks');
113
123
  } finally {
114
124
  if (oldHome == null) delete process.env.HUHAA_HOME;
115
125
  else process.env.HUHAA_HOME = oldHome;
@@ -133,6 +143,13 @@ async function fetchText(url, init) {
133
143
  return { body, contentType: res.headers.get('content-type') || '' };
134
144
  }
135
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
+
136
153
  main().catch(err => {
137
154
  console.error('\n[verify] FAIL');
138
155
  console.error(err && err.stack || err);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "huhaa-myskills",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
- "description": "Local skill / plugin / MCP aggregation hub. Browse all your AI agent skills at http://localhost:11520.",
5
+ "description": "HuHaa AI 助手:本地技能 / 插件 / MCP 聚合中心。",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/aquarkgn/HuHaa-MySkills#readme",
8
8
  "repository": {
@@ -23,9 +23,15 @@
23
23
  "bin/",
24
24
  "build/",
25
25
  "config/",
26
- "packages/",
27
- "scripts/",
28
26
  "docs/",
27
+ "packages/scanner/package.json",
28
+ "packages/scanner/src/",
29
+ "packages/server/package.json",
30
+ "packages/server/src/",
31
+ "packages/web/dist/",
32
+ "packages/web/public/",
33
+ "scripts/install-and-sync.sh",
34
+ "scripts/sync-skills.sh",
29
35
  "README.md",
30
36
  "LICENSE"
31
37
  ],
@@ -46,6 +52,7 @@
46
52
  "typecheck:web": "tsc --noEmit -p packages/web/tsconfig.json",
47
53
  "test:web": "vitest run --root packages/web",
48
54
  "prepack": "node build/build-web.mjs",
55
+ "pack:smoke": "node build/pack-smoke.mjs",
49
56
  "test": "node --test packages/scanner/test/*.test.mjs packages/server/test/*.test.mjs",
50
57
  "verify": "node build/verify.mjs",
51
58
  "release": "npm version patch && git push"
@@ -55,6 +62,7 @@
55
62
  "fast-glob": "^3.3.2",
56
63
  "fastify": "^5.0.0",
57
64
  "google-translate-api-x": "^10.7.3",
65
+ "undici": "^6.27.0",
58
66
  "yaml": "^2.6.0"
59
67
  },
60
68
  "devDependencies": {
@@ -22,6 +22,17 @@ const PKGS_ROOT = path.resolve(__dirname, '..', '..');
22
22
  const PACKAGE_JSON = path.resolve(PKGS_ROOT, '..', 'package.json');
23
23
  const VERSION = readPackageVersion();
24
24
  const WEB_DIST = path.join(PKGS_ROOT, 'web', 'dist');
25
+ const WEB_PUBLIC = path.join(PKGS_ROOT, 'web', 'public');
26
+ const ROOT_STATIC_FILES = [
27
+ 'favicon.ico',
28
+ 'favicon.svg',
29
+ 'favicon-16x16.png',
30
+ 'favicon-32x32.png',
31
+ 'favicon-192x192.png',
32
+ 'favicon-512x512.png',
33
+ 'site.webmanifest',
34
+ 'robots.txt',
35
+ ];
25
36
 
26
37
  async function importScanner() {
27
38
  const { scan, getWatchTargets } = await import(path.join(PKGS_ROOT, 'scanner/src/index.mjs'));
@@ -624,22 +635,10 @@ export async function startServer({ port = 11520 } = {}) {
624
635
  return fs.createReadStream(abs);
625
636
  });
626
637
 
627
- // PWA manifest(index.html 引用 /site.webmanifest)——返回有效 JSON,
628
- // 避免 SPA fallback 返回 HTML 导致浏览器报 Manifest 语法错误
629
- app.get('/site.webmanifest', async (req, reply) => {
630
- reply.type('application/manifest+json; charset=utf-8');
631
- reply.header('cache-control', 'no-cache, no-store, must-revalidate');
632
- return {
633
- name: 'HuHaa-MySkills',
634
- short_name: 'HuHaa',
635
- description: '本地技能 / 插件 / MCP 聚合中心',
636
- start_url: '/',
637
- display: 'standalone',
638
- theme_color: '#3B82F6',
639
- background_color: '#ffffff',
640
- icons: [],
641
- };
642
- });
638
+ // 根路径静态资源必须在 SPA fallback 前注册,否则浏览器会拿到 HTML 而不是图标。
639
+ for (const file of ROOT_STATIC_FILES) {
640
+ app.get(`/${file}`, async (req, reply) => serveRootStatic(file, reply));
641
+ }
643
642
 
644
643
  // Serve built Vue SPA if present; fallback to P2 placeholder when the user
645
644
  // has not run `npm run build:web` yet.
@@ -674,6 +673,29 @@ export async function startServer({ port = 11520 } = {}) {
674
673
 
675
674
  // ─────────────────────────────── helpers ─────────────────────────────────
676
675
 
676
+ function findRootStaticFile(file) {
677
+ for (const base of [WEB_DIST, WEB_PUBLIC]) {
678
+ const abs = path.join(base, file);
679
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) return abs;
680
+ }
681
+ return null;
682
+ }
683
+
684
+ async function serveRootStatic(file, reply) {
685
+ const abs = findRootStaticFile(file);
686
+ if (!abs) {
687
+ reply.code(404);
688
+ return { error: 'static file not found' };
689
+ }
690
+ reply.type(contentTypeFor(abs));
691
+ if (file.endsWith('.webmanifest') || file === 'robots.txt') {
692
+ reply.header('cache-control', 'no-cache, no-store, must-revalidate');
693
+ } else {
694
+ reply.header('cache-control', 'public, max-age=86400');
695
+ }
696
+ return fs.createReadStream(abs);
697
+ }
698
+
677
699
  function stripRaw({ raw, ...rest }) {
678
700
  return rest;
679
701
  }
@@ -726,11 +748,15 @@ export function splitSegments(text) {
726
748
  }
727
749
 
728
750
  function contentTypeFor(abs) {
751
+ if (abs.endsWith('.html')) return 'text/html; charset=utf-8';
729
752
  if (abs.endsWith('.js')) return 'text/javascript; charset=utf-8';
730
753
  if (abs.endsWith('.css')) return 'text/css; charset=utf-8';
731
754
  if (abs.endsWith('.json')) return 'application/json; charset=utf-8';
755
+ if (abs.endsWith('.webmanifest')) return 'application/manifest+json; charset=utf-8';
756
+ if (abs.endsWith('.txt')) return 'text/plain; charset=utf-8';
732
757
  if (abs.endsWith('.svg')) return 'image/svg+xml';
733
758
  if (abs.endsWith('.png')) return 'image/png';
759
+ if (abs.endsWith('.ico')) return 'image/x-icon';
734
760
  if (abs.endsWith('.jpg') || abs.endsWith('.jpeg')) return 'image/jpeg';
735
761
  if (abs.endsWith('.woff2')) return 'font/woff2';
736
762
  return 'application/octet-stream';
@@ -894,7 +920,7 @@ function placeholderHtml({ port, items }) {
894
920
  <html lang="zh-CN">
895
921
  <head>
896
922
  <meta charset="utf-8">
897
- <title>HuHaa-MySkills · P2</title>
923
+ <title>HuHaa AI 助手 · P2</title>
898
924
  <style>
899
925
  :root { color-scheme: light dark; }
900
926
  body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
@@ -925,7 +951,7 @@ function placeholderHtml({ port, items }) {
925
951
  </style>
926
952
  </head>
927
953
  <body>
928
- <h1>HuHaa-MySkills <span style="font-size:13px;color:#888">P2</span></h1>
954
+ <h1>HuHaa AI 助手 <span style="font-size:13px;color:#888">P2</span></h1>
929
955
  <div class="meta">
930
956
  本地聚合中枢 · ${total} 条 skill 已加载 · 显示前 ${sample.length} 条 ·
931
957
  <a href="/api/skills">/api/skills</a> ·
@@ -38,7 +38,7 @@ export const LABELS = {
38
38
  'social-media': '社交媒体',
39
39
  'mcp': 'MCP 协议',
40
40
  'note-taking': '笔记',
41
- 'huhaa-myskills': 'HuHaa-MySkills',
41
+ 'huhaa-myskills': 'HuHaa AI 助手',
42
42
  'autonomous-ai-agents': '自主 AI 代理',
43
43
  'kanban': '看板',
44
44
  'eslint': 'ESLint',