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.
- package/bin/huhaa-myskills.mjs +88 -17
- package/build/pack-smoke.mjs +195 -0
- package/build/verify.mjs +18 -1
- package/package.json +12 -4
- package/packages/server/src/index.mjs +44 -18
- package/packages/server/src/labels.mjs +1 -1
- package/packages/web/dist/assets/index-DBR3X1hb.js +304 -0
- package/packages/web/dist/assets/index-DxHxmeVA.css +1 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/index.html +4 -4
- package/packages/web/dist/site.webmanifest +4 -4
- package/packages/web/public/favicon.ico +0 -0
- package/packages/web/public/site.webmanifest +4 -4
- package/scripts/install-and-sync.sh +3 -3
- package/packages/scanner/test/icon.test.mjs +0 -212
- package/packages/scanner/test/scan-tier.test.mjs +0 -74
- package/packages/scanner/test/scanner.test.mjs +0 -136
- package/packages/scanner/test/skill-adapter.test.mjs +0 -70
- package/packages/scanner/test/tier-editor.test.mjs +0 -67
- package/packages/scanner/tests/directory-skill.test.mjs +0 -93
- package/packages/scanner/tests/scanner-integration.test.mjs +0 -139
- package/packages/scanner/verify-tier-scan.mjs +0 -72
- package/packages/server/test/langdetect.test.mjs +0 -48
- package/packages/server/test/llm-client.test.mjs +0 -101
- package/packages/server/test/server.test.mjs +0 -150
- package/packages/server/test/translate-cache.test.mjs +0 -96
- package/packages/server/test/translate-raw.test.mjs +0 -113
- package/packages/server/test/translator.test.mjs +0 -30
- package/packages/web/dist/assets/index-BbSpJilK.js +0 -249
- package/packages/web/dist/assets/index-DtwcYsHQ.css +0 -1
- package/packages/web/index.html +0 -37
- package/packages/web/package-lock.json +0 -2989
- package/packages/web/package.json +0 -33
- package/packages/web/postcss.config.js +0 -6
- package/packages/web/src/App.test.ts +0 -115
- package/packages/web/src/App.tsx +0 -267
- package/packages/web/src/components/ComingSoon.tsx +0 -14
- package/packages/web/src/components/Sidebar/SidebarMenu.tsx +0 -83
- package/packages/web/src/components/Sidebar/SkillItem.tsx +0 -44
- package/packages/web/src/components/Sidebar/TierGroup.tsx +0 -143
- package/packages/web/src/components/Sidebar/__tests__/verify-frontend.test.ts +0 -158
- package/packages/web/src/components/layout/Sidebar.test.tsx +0 -291
- package/packages/web/src/components/layout/Sidebar.tsx +0 -196
- package/packages/web/src/components/layout/Topbar.tsx +0 -76
- package/packages/web/src/components/ui/ActionButton.tsx +0 -48
- package/packages/web/src/components/ui/OfficialBrandIcon.tsx +0 -101
- package/packages/web/src/components/ui/button.tsx +0 -48
- package/packages/web/src/components/ui/card.tsx +0 -45
- package/packages/web/src/components/views/CliCommandView.test.tsx +0 -328
- package/packages/web/src/components/views/CliCommandView.tsx +0 -588
- package/packages/web/src/components/views/CommandIcon.test.tsx +0 -36
- package/packages/web/src/components/views/CommandIcon.tsx +0 -18
- package/packages/web/src/components/views/DashboardView.test.tsx +0 -165
- package/packages/web/src/components/views/DashboardView.tsx +0 -176
- package/packages/web/src/components/views/EditorPlaceholder.tsx +0 -25
- package/packages/web/src/components/views/OtherSkillsView.test.tsx +0 -126
- package/packages/web/src/components/views/OtherSkillsView.tsx +0 -383
- package/packages/web/src/components/views/SettingsView.tsx +0 -125
- package/packages/web/src/components/views/SkillDetail.test.tsx +0 -132
- package/packages/web/src/components/views/SkillDetail.tsx +0 -264
- package/packages/web/src/components/views/SkillsView.icon.test.tsx +0 -67
- package/packages/web/src/components/views/SkillsView.test.ts +0 -28
- package/packages/web/src/components/views/SkillsView.tsx +0 -224
- package/packages/web/src/components/views/cli/SubcommandRelatedItemsPanel.tsx +0 -288
- package/packages/web/src/data/command-help/claude.json +0 -328
- package/packages/web/src/data/command-help/code.json +0 -297
- package/packages/web/src/data/command-help/codex.json +0 -444
- package/packages/web/src/data/command-help/gstack.json +0 -177
- package/packages/web/src/data/command-help/hermes.json +0 -321
- package/packages/web/src/data/commands.json +0 -1468
- package/packages/web/src/hooks/QUICK_REFERENCE.md +0 -165
- package/packages/web/src/hooks/README.md +0 -378
- package/packages/web/src/hooks/__tests__/getSkillIcons.test.ts +0 -317
- package/packages/web/src/hooks/__tests__/useLiveReload.test.ts +0 -84
- package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +0 -422
- package/packages/web/src/hooks/getSkillIcons.ts +0 -270
- package/packages/web/src/hooks/useLiveReload.ts +0 -38
- package/packages/web/src/hooks/useOtherSkills.ts +0 -387
- package/packages/web/src/hooks/useSidebarMenu.ts +0 -74
- package/packages/web/src/hooks/useTheme.ts +0 -36
- package/packages/web/src/index.css +0 -161
- package/packages/web/src/lib/api.test.ts +0 -47
- package/packages/web/src/lib/api.ts +0 -212
- package/packages/web/src/lib/cli-enrichments/gstackList.test.ts +0 -71
- package/packages/web/src/lib/cli-enrichments/gstackList.ts +0 -91
- package/packages/web/src/lib/cli-enrichments/index.ts +0 -45
- package/packages/web/src/lib/cli-enrichments/skillDocParser.test.ts +0 -47
- package/packages/web/src/lib/cli-enrichments/skillDocParser.ts +0 -124
- package/packages/web/src/lib/cli-enrichments/types.ts +0 -52
- package/packages/web/src/lib/cn.ts +0 -7
- package/packages/web/src/lib/commandHelp.test.ts +0 -41
- package/packages/web/src/lib/commandHelp.ts +0 -64
- package/packages/web/src/lib/commands.test.ts +0 -71
- package/packages/web/src/lib/commands.ts +0 -74
- package/packages/web/src/lib/editors.test.ts +0 -34
- package/packages/web/src/lib/editors.ts +0 -80
- package/packages/web/src/lib/i18n.ts +0 -87
- package/packages/web/src/lib/iconBlacklist.ts +0 -37
- package/packages/web/src/lib/idb.ts +0 -64
- package/packages/web/src/lib/markdown.test.ts +0 -27
- package/packages/web/src/lib/markdown.ts +0 -14
- package/packages/web/src/main.tsx +0 -13
- package/packages/web/src/types/index.ts +0 -74
- package/packages/web/src/types/other-skill.ts +0 -88
- package/packages/web/src/types/skill.ts +0 -136
- package/packages/web/src/types.ts +0 -122
- package/packages/web/src/vite-env.d.ts +0 -1
- package/packages/web/tailwind.config.ts +0 -66
- package/packages/web/tsconfig.json +0 -28
- package/packages/web/vite.config.ts +0 -36
- package/packages/web/vitest.config.ts +0 -40
- package/packages/web/vitest.setup.ts +0 -1
- package/scripts/activate-data-sources.sh +0 -223
- package/scripts/brand-completion.py +0 -586
- package/scripts/digest-commands.mjs +0 -389
- package/scripts/digest-commands.test.mjs +0 -147
- package/scripts/init-nextjs-project.sh +0 -421
- package/scripts/start-dev.sh +0 -72
package/bin/huhaa-myskills.mjs
CHANGED
|
@@ -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} —
|
|
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(`✓ 已停止
|
|
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(
|
|
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(`\
|
|
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(
|
|
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(`✓
|
|
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(`✓
|
|
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
|
-
|
|
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
|
|
476
|
-
|
|
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
|
|
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:${
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
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
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
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
|
|
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> ·
|