skill-guide 0.2.0

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/skill-guide.js ADDED
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { execFileSync, spawnSync } = require('child_process');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const ROOT = __dirname;
10
+ const SCANNER = path.join(ROOT, 'scan-skills.js');
11
+ const args = process.argv.slice(2);
12
+
13
+ function hasFlag(flag) {
14
+ return args.includes(flag);
15
+ }
16
+
17
+ function getArgValue(flag) {
18
+ const idx = args.indexOf(flag);
19
+ if (idx === -1) return null;
20
+ return args[idx + 1] || null;
21
+ }
22
+
23
+ function usage() {
24
+ return [
25
+ 'Usage:',
26
+ ' skill-guide [--open] [--output <file>] [--format html|json] [--lang en|zh] [--refresh]',
27
+ ' skill-guide --search <query> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
28
+ ' skill-guide --skill <name> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
29
+ ' skill-guide --full [--open] [--output <file>] [--format html|json] [--lang en|zh]',
30
+ ' skill-guide --doctor [--refresh]',
31
+ '',
32
+ 'Examples:',
33
+ ' npx skill-guide --open',
34
+ ' npx skill-guide --search security --open',
35
+ ' npx skill-guide --skill tdd --lang zh --open',
36
+ ' npx skill-guide --doctor',
37
+ ].join('\n');
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // i18n labels
42
+ // ---------------------------------------------------------------------------
43
+ const LABELS = {
44
+ en: {
45
+ yourAgentSkills: 'Your Agent Skills',
46
+ yourClaudeSkills: 'Your Claude Code Skills',
47
+ yourCodexSkills: 'Your Codex Skills',
48
+ skillsScanned: 'skills scanned',
49
+ noSources: 'No skill sources found',
50
+ discovery: 'Discovery',
51
+ toolSelection: 'Tool Selection',
52
+ skillDeepDive: 'Skill Deep Dive',
53
+ completeManual: 'Complete Manual',
54
+ categoryMap: 'Category Map',
55
+ highlights: 'Highlights',
56
+ quickReference: 'Quick Reference',
57
+ completeReference: 'Complete Reference',
58
+ comparisonReference: 'Comparison Reference',
59
+ whenToUse: 'When to Use',
60
+ howItWorks: 'How It Works',
61
+ limitations: 'Limitations',
62
+ matchResults: 'Match Results',
63
+ skillsCount: '{count} skills',
64
+ noSkills: 'No skills found.',
65
+ name: 'Name',
66
+ category: 'Category',
67
+ description: 'Description',
68
+ triggers: 'Triggers',
69
+ },
70
+ zh: {
71
+ yourAgentSkills: '你的 Agent Skills 技能库',
72
+ yourClaudeSkills: '你的 Claude Code 技能库',
73
+ yourCodexSkills: '你的 Codex 技能库',
74
+ skillsScanned: '个技能已扫描',
75
+ noSources: '未找到技能来源',
76
+ discovery: '技能发现',
77
+ toolSelection: '工具选择',
78
+ skillDeepDive: '技能深入',
79
+ completeManual: '完整技能手册',
80
+ categoryMap: '分类概览',
81
+ highlights: '精选推荐',
82
+ quickReference: '快速参考',
83
+ completeReference: '完整参考',
84
+ comparisonReference: '对比参考',
85
+ whenToUse: '何时使用',
86
+ howItWorks: '运作原理',
87
+ limitations: '使用限制',
88
+ matchResults: '匹配结果',
89
+ skillsCount: '{count} 个技能',
90
+ noSkills: '未找到技能。',
91
+ name: '名称',
92
+ category: '分类',
93
+ description: '描述',
94
+ triggers: '触发词',
95
+ },
96
+ };
97
+
98
+ function lang() {
99
+ const l = getArgValue('--lang');
100
+ if (l === 'zh') return 'zh';
101
+ return 'en';
102
+ }
103
+
104
+ function t(key) {
105
+ return LABELS[lang()][key] || LABELS.en[key] || key;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Content translation (EN -> ZH via phrase glossary)
110
+ // ---------------------------------------------------------------------------
111
+ const GLOSSARY_ZH = [
112
+ ['comprehensive accessibility audit', '全面的无障碍审计'],
113
+ ['structured design feedback', '结构化设计反馈'],
114
+ ['systematic debugging workflow', '系统性调试工作流'],
115
+ ['infrastructure-first security audit', '基础设施优先安全审计'],
116
+ ['every important claim needs a source', '每个重要论点需要附带来源'],
117
+ ['prefer recent data and call out stale data', '优先使用最新数据并标注过期数据'],
118
+ ['include contrarian evidence and downside cases', '包含反面证据和下行情况'],
119
+ ['translate findings into a decision', '将发现转化为决策建议'],
120
+ ['not just a summary', '而非仅罗列事实'],
121
+ ['all numbers are sourced or labeled as estimates', '所有数字有来源或标注为估算'],
122
+ ['old data is flagged', '过期数据已标记'],
123
+ ['the recommendation follows from the evidence', '建议基于证据得出'],
124
+ ['risks and counterarguments are included', '包含风险和反面论据'],
125
+ ['default structure', '默认结构'],
126
+ ['executive summary', '执行摘要'],
127
+ ['key findings', '核心发现'],
128
+ ['risks and caveats', '风险与注意事项'],
129
+ ['competitive analysis', '竞争分析'],
130
+ ['market research', '市场调研'],
131
+ ['market sizing', '市场规模估算'],
132
+ ['investor research', '投资者研究'],
133
+ ['due diligence', '尽职调查'],
134
+ ['industry intelligence', '行业情报'],
135
+ ['source attribution', '来源引用'],
136
+ ['decision-oriented', '决策导向'],
137
+ ['technology trend', '技术趋势'],
138
+ ['investor dossier', '投资者档案'],
139
+ ['portfolio company', '投资组合公司'],
140
+ ['integration complexity', '集成复杂度'],
141
+ ['adoption signals', '采纳信号'],
142
+ ['code review', '代码审查'],
143
+ ['code quality', '代码质量'],
144
+ ['unit test', '单元测试'],
145
+ ['regression test', '回归测试'],
146
+ ['e2e testing', '端到端测试'],
147
+ ['security audit', '安全审计'],
148
+ ['slide presentation', '幻灯片演示'],
149
+ ['code example', '代码示例'],
150
+ ['design specification', '设计规范'],
151
+ ['best practice', '最佳实践'],
152
+ ['trade-off', '权衡取舍'],
153
+ ['lock-in risk', '锁定风险'],
154
+ ['red flag', '红旗警告'],
155
+ ['check size', '投资规模'],
156
+ ['fund size', '基金规模'],
157
+ ['action recommendation', '行动建议'],
158
+ ['fund research', '基金研究'],
159
+ ['vendor research', '供应商调研'],
160
+ ['technology scan', '技术扫描'],
161
+ ['business decision', '商业决策'],
162
+ ['product strategy', '产品策略'],
163
+ ['user interface', '用户界面'],
164
+ ['design system', '设计系统'],
165
+ ['project overview', '项目概述'],
166
+ ['use case', '使用场景'],
167
+ ['skill content', '技能内容'],
168
+ ['how it works', '运作原理'],
169
+ ['when to use', '何时使用'],
170
+ ['how to use', '使用方法'],
171
+ ['use when', '当以下情况时使用'],
172
+ ['use this', '使用此'],
173
+ ['researching a market', '研究市场'],
174
+ ['comparing competitors', '对比竞争对手'],
175
+ ['preparing investor', '准备投资者'],
176
+ ['building estimates', '构建估算'],
177
+ ['quality gate', '质量把关'],
178
+ ['before delivering', '交付前'],
179
+ ['research standard', '调研标准'],
180
+ ['output format', '输出格式'],
181
+ ['common research mode', '常用调研模式'],
182
+ ['public thesis', '公开投资理念'],
183
+ ['recent activity', '近期动态'],
184
+ ['obvious mismatch', '明显不匹配'],
185
+ ['relevant portfolio', '相关投资组合'],
186
+ ['security compliance', '安全合规'],
187
+ ['operational risk', '运营风险'],
188
+ ['visual hierarchy', '视觉层次'],
189
+ ['color scheme', '色彩方案'],
190
+ ['brand guideline', '品牌规范'],
191
+ ['design principle', '设计原则'],
192
+ ['test driven', '测试驱动'],
193
+ ['continuous integration', '持续集成'],
194
+ ['code coverage', '代码覆盖率'],
195
+ ['dependency management', '依赖管理'],
196
+ ['error handling', '错误处理'],
197
+ ['performance optimization', '性能优化'],
198
+ ['data validation', '数据验证'],
199
+ ['API reference', 'API 参考'],
200
+ ['quick reference', '快速参考'],
201
+ ['step by step', '逐步'],
202
+ ['front matter', '前置元数据'],
203
+ ['security vulnerability', '安全漏洞'],
204
+ ['threat model', '威胁建模'],
205
+ ['supply chain', '供应链'],
206
+ ['CI/CD pipeline', 'CI/CD 流水线'],
207
+ ['landing page', '落地页'],
208
+ ['admin panel', '管理后台'],
209
+ ['slide deck', '幻灯片组'],
210
+ ['conference talk', '会议演讲'],
211
+ ['teaching material', '教学材料'],
212
+ ['tech stack', '技术栈'],
213
+ ['project structure', '项目结构'],
214
+ ['file structure', '文件结构'],
215
+ ['data flow', '数据流'],
216
+ ['user flow', '用户流程'],
217
+ ['state management', '状态管理'],
218
+ ['route config', '路由配置'],
219
+ ['database schema', '数据库结构'],
220
+ ['data model', '数据模型'],
221
+ ['SEO audit', 'SEO 审计'],
222
+ ['presentation', '演示文稿'],
223
+ ['dashboard', '仪表板'],
224
+ ['screenshot', '截图'],
225
+ ['checklist', '检查清单'],
226
+ ['workflow', '工作流'],
227
+ ['algorithm', '算法'],
228
+ ['framework', '框架'],
229
+ ['template', '模板'],
230
+ ['component', '组件'],
231
+ ['interface', '接口'],
232
+ ['abstraction', '抽象'],
233
+ ['refactoring', '重构'],
234
+ ['debugging', '调试'],
235
+ ['profiling', '性能分析'],
236
+ ['benchmark', '基准测试'],
237
+ ['deployment', '部署'],
238
+ ['migration', '迁移'],
239
+ ['automation', '自动化'],
240
+ ['orchestration', '编排'],
241
+ ['scalability', '可扩展性'],
242
+ ['reliability', '可靠性'],
243
+ ['monitoring', '监控'],
244
+ ['observability', '可观测性'],
245
+ ['resilience', '韧性'],
246
+ ['encryption', '加密'],
247
+ ['authentication', '认证'],
248
+ ['authorization', '授权'],
249
+ ['compliance', '合规'],
250
+ ['governance', '治理'],
251
+ ['vulnerability', '漏洞'],
252
+ ['architecture', '架构'],
253
+ ['infrastructure', '基础设施'],
254
+ ['microservice', '微服务'],
255
+ ['serverless', '无服务器'],
256
+ ['containerization', '容器化'],
257
+ ['concurrency', '并发'],
258
+ ['asynchronous', '异步'],
259
+ ['synchronous', '同步'],
260
+ ['idiomatic', '惯用'],
261
+ ['boilerplate', '样板代码'],
262
+ ['implement', '实现'],
263
+ ['integrate', '集成'],
264
+ ['configure', '配置'],
265
+ ['troubleshoot', '排查'],
266
+ ['diagnose', '诊断'],
267
+ ['repository', '仓库'],
268
+ ['endpoint', '端点'],
269
+ ['middleware', '中间件'],
270
+ ['dependency', '依赖'],
271
+ ['plugin', '插件'],
272
+ ['extension', '扩展'],
273
+ ['webhook', 'Webhook'],
274
+ ['payload', '负载'],
275
+ ['schema', '模式'],
276
+ ['fixture', '测试夹具'],
277
+ ['snapshot', '快照'],
278
+ ['mock', '模拟'],
279
+ ['keyword', '关键词'],
280
+ ['security', '安全'],
281
+ ['performance', '性能'],
282
+ ['testing', '测试'],
283
+ ['documentation', '文档'],
284
+ ['development', '开发'],
285
+ ['quality', '质量'],
286
+ ['collection', '收集'],
287
+ ['recommendation', '建议'],
288
+ ['implications', '影响'],
289
+ ['summary', '摘要'],
290
+ ['overview', '概述'],
291
+ ['examples', '示例'],
292
+ ['sources', '来源'],
293
+ ['middleware', '中间件'],
294
+ ];
295
+
296
+ let _compiledGlossary = null;
297
+ function getCompiledGlossary() {
298
+ if (_compiledGlossary) return _compiledGlossary;
299
+ _compiledGlossary = GLOSSARY_ZH.map(([en, zh]) => ({
300
+ re: new RegExp('\\b' + en.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'gi'),
301
+ zh,
302
+ }));
303
+ return _compiledGlossary;
304
+ }
305
+
306
+ function translateContent(text) {
307
+ if (lang() !== 'zh' || !text) return text;
308
+ let result = text;
309
+ for (const { re, zh } of getCompiledGlossary()) {
310
+ result = result.replace(re, zh);
311
+ }
312
+ return result;
313
+ }
314
+
315
+ function te(text) {
316
+ return escapeHtml(translateContent(text));
317
+ }
318
+
319
+ function parseMode() {
320
+ if (hasFlag('--help') || hasFlag('-h')) return { mode: 'help' };
321
+ if (hasFlag('--doctor')) return { mode: 'doctor' };
322
+ if (hasFlag('--full') || args[0] === 'all') return { mode: 'full' };
323
+
324
+ const skill = getArgValue('--skill');
325
+ if (skill) return { mode: 'skill', value: skill };
326
+
327
+ const search = getArgValue('--search');
328
+ if (search) return { mode: 'search', value: search };
329
+
330
+ const valueFlags = new Set(['--output', '--skill', '--search', '--format', '--lang']);
331
+ const positional = args.find((arg, index) => !arg.startsWith('-') && !valueFlags.has(args[index - 1]));
332
+ if (positional) return { mode: 'skill', value: positional };
333
+
334
+ return { mode: 'list' };
335
+ }
336
+
337
+ function scannerArgsFor(mode) {
338
+ const scannerArgs = [];
339
+ if (hasFlag('--refresh')) scannerArgs.push('--refresh');
340
+
341
+ if (mode.mode === 'list' || mode.mode === 'doctor') {
342
+ scannerArgs.push('--list');
343
+ } else if (mode.mode === 'skill') {
344
+ scannerArgs.push('--skill', mode.value);
345
+ } else if (mode.mode === 'search') {
346
+ scannerArgs.push('--search', mode.value);
347
+ } else if (mode.mode === 'full') {
348
+ scannerArgs.push('--full');
349
+ }
350
+
351
+ return scannerArgs;
352
+ }
353
+
354
+ function runScanner(mode) {
355
+ const args = scannerArgsFor(mode);
356
+ if (mode.mode === 'full') {
357
+ const result = spawnSync(process.execPath, [SCANNER, ...args], {
358
+ cwd: process.cwd(),
359
+ encoding: 'utf8',
360
+ stdio: ['ignore', 'pipe', 'pipe'],
361
+ maxBuffer: 50 * 1024 * 1024,
362
+ });
363
+ if (result.status !== 0) {
364
+ process.stderr.write(result.stderr || '');
365
+ process.exit(result.status || 1);
366
+ }
367
+ return JSON.parse(result.stdout);
368
+ }
369
+ const output = execFileSync(process.execPath, [SCANNER, ...args], {
370
+ cwd: process.cwd(),
371
+ encoding: 'utf8',
372
+ stdio: ['ignore', 'pipe', 'pipe'],
373
+ });
374
+ return JSON.parse(output);
375
+ }
376
+
377
+ function defaultOutputPath(mode) {
378
+ const date = new Date().toISOString().slice(0, 10);
379
+ const suffix = mode.mode === 'search' ? 'selection' : mode.mode === 'skill' ? mode.value : mode.mode;
380
+ const safeSuffix = String(suffix).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
381
+ return path.join(process.cwd(), `skill-guide-${safeSuffix || 'list'}-${date}.html`);
382
+ }
383
+
384
+ function escapeHtml(value) {
385
+ return String(value ?? '')
386
+ .replace(/&/g, '&amp;')
387
+ .replace(/</g, '&lt;')
388
+ .replace(/>/g, '&gt;')
389
+ .replace(/"/g, '&quot;')
390
+ .replace(/'/g, '&#39;');
391
+ }
392
+
393
+ function truncate(value, length) {
394
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
395
+ return text.length > length ? `${text.slice(0, length - 1)}...` : text;
396
+ }
397
+
398
+ function renderMd(text) {
399
+ if (!text) return '';
400
+ let html = escapeHtml(translateContent(text));
401
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
402
+ html = html.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
403
+ html = html.replace(/`([^`]+?)`/g, '<code>$1</code>');
404
+ html = html.replace(/^&gt;\s?(.+)$/gm, '<blockquote>$1</blockquote>');
405
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
406
+ html = html.replace(/^\d+\.\s(.+)$/gm, '<li>$1</li>');
407
+ html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
408
+ html = html.replace(/<\/blockquote>\n<blockquote>/g, '<br>');
409
+ return html;
410
+ }
411
+
412
+ function titleForSources(sources) {
413
+ const labels = Object.keys(sources || {}).filter((key) => sources[key] > 0);
414
+ const hasClaude = labels.some((label) => label.startsWith('claude'));
415
+ const hasCodex = labels.some((label) => label.startsWith('codex'));
416
+ if (hasClaude && hasCodex) return t('yourAgentSkills');
417
+ if (hasCodex) return t('yourCodexSkills');
418
+ if (hasClaude) return t('yourClaudeSkills');
419
+ return t('yourAgentSkills');
420
+ }
421
+
422
+ function sourceSummary(sources) {
423
+ const isZh = lang() === 'zh';
424
+ const labels = isZh ? {
425
+ 'claude-user': 'Claude',
426
+ 'codex-user': 'Codex',
427
+ 'openai-system': 'OpenAI 系统',
428
+ 'cc-switch': 'cc-switch',
429
+ 'claude-plugin': 'Claude 插件',
430
+ 'codex-plugin': 'Codex 插件',
431
+ } : {
432
+ 'claude-user': 'Claude',
433
+ 'codex-user': 'Codex',
434
+ 'openai-system': 'OpenAI system',
435
+ 'cc-switch': 'cc-switch',
436
+ 'claude-plugin': 'Claude plugins',
437
+ 'codex-plugin': 'Codex plugins',
438
+ };
439
+ return Object.entries(sources || {})
440
+ .filter(([, count]) => count > 0)
441
+ .map(([source, count]) => `${count} ${labels[source] || source}`)
442
+ .join(' · ');
443
+ }
444
+
445
+ function groupBy(items, key) {
446
+ return items.reduce((acc, item) => {
447
+ const value = item[key] || 'other';
448
+ if (!acc[value]) acc[value] = [];
449
+ acc[value].push(item);
450
+ return acc;
451
+ }, {});
452
+ }
453
+
454
+ function categoryBadge(category) {
455
+ return `<span class="badge badge-${escapeHtml(category)}">${escapeHtml(category)}</span>`;
456
+ }
457
+
458
+ function sourceBadges(sources) {
459
+ return (sources || []).map((source) => `<span class="source">${escapeHtml(source)}</span>`).join('');
460
+ }
461
+
462
+ function renderCover(data, mode) {
463
+ const title = titleForSources(data.sources);
464
+ const subtitle = sourceSummary(data.sources) || t('noSources');
465
+ const modeLabel = {
466
+ list: t('discovery'),
467
+ search: t('toolSelection'),
468
+ skill: t('skillDeepDive'),
469
+ full: t('completeManual'),
470
+ }[mode.mode] || t('discovery');
471
+
472
+ return `<section class="slide cover">
473
+ <div class="rv center">
474
+ <div class="kicker" data-i18n="label">${escapeHtml(modeLabel)}</div>
475
+ <h1><span class="grad" data-i18n="label">${escapeHtml(title)}</span></h1>
476
+ <p class="sub">${escapeHtml(data.totalCount || 0)} ${t('skillsScanned')} · ${escapeHtml(subtitle)}</p>
477
+ <div class="stats">${Object.entries(data.sources || {}).map(([source, count]) => `<div class="stat"><b>${count}</b><span data-i18n="label">${escapeHtml(source)}</span></div>`).join('')}</div>
478
+ </div>
479
+ </section>`;
480
+ }
481
+
482
+ function renderCategorySlide(skills) {
483
+ const groups = groupBy(skills, 'category');
484
+ const cards = Object.entries(groups)
485
+ .sort((a, b) => b[1].length - a[1].length)
486
+ .map(([category, items]) => `<article class="card">
487
+ <h3>${escapeHtml(category)}</h3>
488
+ <p>${t('skillsCount').replace('{count}', items.length)}</p>
489
+ <div class="chips">${items.slice(0, 8).map((skill) => `<span>${escapeHtml(skill.name)}</span>`).join('')}</div>
490
+ </article>`).join('');
491
+
492
+ return `<section class="slide">
493
+ <div class="rv wide">
494
+ <h2 data-i18n="label">${t('categoryMap')}</h2>
495
+ <div class="grid">${cards || `<p class="empty">${t('noSkills')}</p>`}</div>
496
+ </div>
497
+ </section>`;
498
+ }
499
+
500
+ function renderHighlights(skills) {
501
+ const highlights = [...skills]
502
+ .sort((a, b) => ((b.triggers || []).length + (b.sources || []).length) - ((a.triggers || []).length + (a.sources || []).length))
503
+ .slice(0, 8);
504
+
505
+ return `<section class="slide">
506
+ <div class="rv wide">
507
+ <h2 data-i18n="label">${t('highlights')}</h2>
508
+ <div class="list">${highlights.map((skill, index) => `<article class="row">
509
+ <strong>${index + 1}</strong>
510
+ <div>
511
+ <h3>${escapeHtml(skill.name)}</h3>
512
+ <p data-i18n="desc">${te(truncate(skill.description, 180))}</p>
513
+ <div>${categoryBadge(skill.category)}${sourceBadges(skill.sources)}</div>
514
+ </div>
515
+ </article>`).join('')}</div>
516
+ </div>
517
+ </section>`;
518
+ }
519
+
520
+ function renderReference(skills, title) {
521
+ const refTitle = title || t('quickReference');
522
+ const rows = skills.map((skill) => `<tr>
523
+ <td>${escapeHtml(skill.name)}</td>
524
+ <td>${categoryBadge(skill.category)}</td>
525
+ <td data-i18n="desc">${te(truncate(skill.description, 160))}</td>
526
+ <td>${escapeHtml((skill.triggers || []).slice(0, 4).join(', '))}</td>
527
+ </tr>`).join('');
528
+
529
+ return `<section class="slide">
530
+ <div class="rv wide">
531
+ <h2 data-i18n="label">${escapeHtml(refTitle)}</h2>
532
+ <div class="table-wrap"><table>
533
+ <thead><tr><th>${t('name')}</th><th>${t('category')}</th><th>${t('description')}</th><th>${t('triggers')}</th></tr></thead>
534
+ <tbody>${rows}</tbody>
535
+ </table></div>
536
+ </div>
537
+ </section>`;
538
+ }
539
+
540
+ function renderSkillDetails(skills) {
541
+ return skills.map((skill) => `<section class="slide">
542
+ <div class="rv wide detail">
543
+ <h2>${escapeHtml(skill.name)}</h2>
544
+ <div class="sub-md" data-i18n="desc">${renderMd(skill.description)}</div>
545
+ <div class="meta">${categoryBadge(skill.category)}${sourceBadges(skill.sources)}${(skill.allowedTools || []).map((tool) => `<code>${escapeHtml(tool)}</code>`).join('')}</div>
546
+ ${skill.whenToUse ? `<h3 data-i18n="label">${t('whenToUse')}</h3><div class="md-content" data-i18n="when-to-use">${renderMd(skill.whenToUse)}</div>` : ''}
547
+ ${skill.howItWorks ? `<h3 data-i18n="label">${t('howItWorks')}</h3><div class="md-content" data-i18n="how-it-works">${renderMd(skill.howItWorks)}</div>` : ''}
548
+ ${skill.limitations ? `<h3 data-i18n="label">${t('limitations')}</h3><div class="md-content" data-i18n="limitations">${renderMd(skill.limitations)}</div>` : ''}
549
+ ${(skill.sections || []).length ? `<div class="steps">${skill.sections.slice(0, 8).map((section, index) => `<article><b>${index + 1}</b><span data-i18n="section-title">${te(section.title)}</span><div class="md-content" data-i18n="section-body">${renderMd(section.summary)}</div></article>`).join('')}</div>` : ''}
550
+ </div>
551
+ </section>`).join('');
552
+ }
553
+
554
+ function renderSelection(data, mode) {
555
+ return `<section class="slide">
556
+ <div class="rv wide">
557
+ <h2 data-i18n="label">${t('matchResults')}</h2>
558
+ <p class="quote">${escapeHtml(mode.value || '')}</p>
559
+ <div class="list">${data.skills.slice(0, 12).map((skill, index) => `<article class="row">
560
+ <strong>${index + 1}</strong>
561
+ <div>
562
+ <h3>${escapeHtml(skill.name)}</h3>
563
+ <p data-i18n="desc">${te(truncate(skill.description, 220))}</p>
564
+ <div>${categoryBadge(skill.category)}${sourceBadges(skill.sources)}</div>
565
+ </div>
566
+ </article>`).join('')}</div>
567
+ </div>
568
+ </section>${renderReference(data.skills.slice(0, 20), t('comparisonReference'))}`;
569
+ }
570
+
571
+ function renderSlides(data, mode) {
572
+ if (data.error) {
573
+ return `${renderCover(data, mode)}<section class="slide"><div class="rv center"><h2>Error</h2><p class="sub">${escapeHtml(data.error)}</p></div></section>`;
574
+ }
575
+
576
+ if (mode.mode === 'search') return `${renderCover(data, mode)}${renderSelection(data, mode)}`;
577
+ if (mode.mode === 'skill') return `${renderCover(data, mode)}${renderSkillDetails(data.skills)}`;
578
+ if (mode.mode === 'full') return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderSkillDetails(data.skills)}${renderReference(data.skills, t('completeReference'))}`;
579
+ return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderHighlights(data.skills)}${renderReference(data.skills)}`;
580
+ }
581
+
582
+ function renderHtml(data, mode) {
583
+ const slides = renderSlides(data, mode);
584
+ const htmlLang = lang();
585
+ return `<!DOCTYPE html>
586
+ <html lang="${htmlLang}">
587
+ <head>
588
+ <meta charset="UTF-8">
589
+ <meta name="viewport" content="width=device-width,initial-scale=1">
590
+ <title>${escapeHtml(titleForSources(data.sources))} - skill-guide</title>
591
+ <style>
592
+ :root{--bg:#0F172A;--bg2:#1E293B;--card:#1E293B;--card-h:#273347;--t:#F8FAFC;--muted:#94A3B8;--accent:#22C55E;--accent2:#34D399;--ab:#818cf8;--ap:#c084fc;--am:#6ee7b7;--r:12px;--border:#334155;--shadow:0 4px 24px rgba(0,0,0,.35);--mono:"SF Mono","Fira Code","Cascadia Code",monospace}
593
+ *{box-sizing:border-box}html{scroll-snap-type:y mandatory;scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--t);font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
594
+ a.skip{position:absolute;top:-100%;left:16px;background:var(--accent);color:#0F172A;padding:8px 16px;border-radius:var(--r);z-index:999;font-weight:600;text-decoration:none}a.skip:focus{top:8px}
595
+ .slide{min-height:100vh;min-height:100dvh;scroll-snap-align:start;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:clamp(24px,5vw,64px)}.slide::after{content:"";position:absolute;inset:0;background:radial-gradient(ellipse at 20% 50%,rgba(34,197,94,.04),transparent 60%),radial-gradient(ellipse at 80% 80%,rgba(129,140,248,.04),transparent 50%);pointer-events:none}
596
+ .center,.wide{position:relative;z-index:1}.center{text-align:center;max-width:960px}.wide{width:min(1120px,100%)}
597
+ h1{font-size:clamp(36px,6vw,72px);line-height:1.05;margin:0 0 16px;font-weight:800;letter-spacing:-.02em;color:var(--t)}h1 .grad{background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;color:transparent}
598
+ h2{font-size:clamp(24px,3.5vw,44px);line-height:1.1;margin:0 0 24px;text-align:center;letter-spacing:-.01em;color:var(--t)}h3{margin:0 0 6px;font-size:16px;font-weight:600}
599
+ .sub{font-size:clamp(15px,1.8vw,20px);line-height:1.6;color:var(--muted);margin:0 auto 24px;max-width:860px}
600
+ .kicker{text-transform:uppercase;letter-spacing:.16em;color:var(--accent);font-size:11px;font-weight:700;margin-bottom:16px;font-family:var(--mono)}
601
+ .stats{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:8px}.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:12px 20px;min-width:120px;transition:border-color .2s}.stat:hover{border-color:var(--accent)}.stat b{display:block;font-size:26px;color:var(--t)}.stat span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-family:var(--mono)}
602
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
603
+ .card,.row{background:var(--card);border:1px solid var(--border);border-radius:var(--r);transition:border-color .2s,transform .15s;cursor:default}.card:hover,.row:hover{border-color:var(--accent);transform:translateY(-1px)}
604
+ .card{padding:18px}.card p,.row p,.detail p{color:var(--muted);line-height:1.55;font-size:14px}
605
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;overflow:hidden;max-height:72px}.chips span,.badge,.source{display:inline-flex;align-items:center;border-radius:6px;padding:3px 8px;font-size:12px;font-weight:600}.chips span{background:var(--bg);color:var(--muted);border:1px solid var(--border)}.badge{background:rgba(34,197,94,.12);color:var(--accent2);border:1px solid rgba(34,197,94,.2);margin-right:4px}.source{background:rgba(129,140,248,.1);color:var(--ab);border:1px solid rgba(129,140,248,.18);margin-right:4px}
606
+ .list{display:flex;flex-direction:column;gap:10px}.row{display:grid;grid-template-columns:44px 1fr;gap:12px;padding:16px}.row strong{font-size:24px;color:var(--accent);line-height:1;font-family:var(--mono);font-weight:700}
607
+ .table-wrap{max-height:72vh;overflow:auto;border-radius:var(--r);border:1px solid var(--border);background:var(--card)}table{border-collapse:collapse;width:100%;font-size:13px}th{position:sticky;top:0;background:var(--bg2);color:var(--accent);text-align:left;font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em}th,td{padding:10px 14px;border-bottom:1px solid var(--border)}tr:hover td{background:rgba(34,197,94,.04)}
608
+ .meta{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin:14px 0 22px}.meta code{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-family:var(--mono);font-size:12px;color:var(--muted)}
609
+ .detail{text-align:center}.detail h3{margin-top:20px;color:var(--accent);font-family:var(--mono);font-size:13px;text-transform:uppercase;letter-spacing:.1em}
610
+ .steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-top:18px;text-align:left}.steps article{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:14px;transition:border-color .2s}.steps article:hover{border-color:var(--accent)}.steps b{display:inline-grid;place-items:center;width:26px;height:26px;border-radius:6px;background:var(--accent);color:#0F172A;margin-right:8px;font-size:13px;font-weight:700}.steps span{font-weight:600;color:var(--t)}.steps p{font-size:13px;margin-top:4px}
611
+ .quote{font-size:18px;color:var(--muted);text-align:center;background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;font-family:var(--mono)}.empty{text-align:center;color:var(--muted)}
612
+ .md-content{color:var(--muted);line-height:1.6;font-size:14px;text-align:left}.md-content strong{color:var(--t);font-weight:600}.md-content code{background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-family:var(--mono);font-size:12px;color:var(--accent2)}.md-content blockquote{border-left:3px solid var(--accent);margin:8px 0;padding:6px 12px;color:var(--muted);font-style:italic}.md-content ul{margin:6px 0;padding-left:20px}.md-content li{margin:3px 0}.sub-md{color:var(--muted);line-height:1.6;font-size:clamp(15px,1.8vw,20px);margin:0 auto 24px;max-width:860px;text-align:center}.sub-md strong{color:var(--t);font-weight:600}.sub-md code{background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-family:var(--mono);font-size:12px;color:var(--accent2)}
613
+ .rv{opacity:0;transform:translateY(18px);transition:opacity .4s ease,transform .4s ease}.rv.v{opacity:1;transform:none}
614
+ nav.progress{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:6px;z-index:10;padding:6px 12px;background:var(--card);border:1px solid var(--border);border-radius:999px}nav.progress button{width:8px;height:8px;border-radius:50%;border:none;background:var(--border);cursor:pointer;padding:0;transition:background .2s,transform .15s}nav.progress button.active{background:var(--accent);transform:scale(1.3)}nav.progress button:hover{background:var(--muted)}nav.progress button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
615
+ .shortcut{position:fixed;bottom:16px;right:16px;font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--card);border:1px solid var(--border);border-radius:6px;padding:4px 8px;z-index:10;opacity:.5}
616
+ @media(prefers-reduced-motion:reduce){.rv{opacity:1;transform:none;transition:none}.card:hover,.row:hover,.steps article:hover{transform:none}}
617
+ @media(max-width:760px){.slide{padding:20px 14px}.row{grid-template-columns:1fr}.row strong{font-size:18px}.table-wrap{max-height:60vh}nav.progress{bottom:10px}nav.progress button{width:10px;height:10px}.shortcut{display:none}.stats{gap:8px}.stat{min-width:90px;padding:10px 14px}.stat b{font-size:22px}.grid{grid-template-columns:1fr}}
618
+ </style>
619
+ </head>
620
+ <body>
621
+ <a class="skip" href="#main">${lang() === 'zh' ? '跳到主要内容' : 'Skip to content'}</a>
622
+ <main id="main">
623
+ ${slides}
624
+ </main>
625
+ <nav class="progress" aria-label="${lang() === 'zh' ? '幻灯片导航' : 'Slide navigation'}"></nav>
626
+ <div class="shortcut" aria-hidden="true">↓ ↑ Space</div>
627
+ <script>
628
+ const seen=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting)e.target.classList.add('v')}),{threshold:.12});
629
+ document.querySelectorAll('.rv').forEach(el=>seen.observe(el));
630
+ const slides=[...document.querySelectorAll('.slide')];
631
+ const nav=document.querySelector('nav.progress');
632
+ slides.forEach((_,i)=>{const b=document.createElement('button');b.setAttribute('aria-label','${htmlLang==='zh'?'幻灯片':'Slide'} '+(i+1));b.addEventListener('click',()=>slides[i]?.scrollIntoView());nav.appendChild(b)});
633
+ function updateNav(){const i=slides.findIndex(s=>{const r=s.getBoundingClientRect();return r.top>-10&&r.top<innerHeight/2});nav.querySelectorAll('button').forEach((b,j)=>b.classList.toggle('active',j===i))}
634
+ document.addEventListener('scroll',updateNav,{passive:true});updateNav();
635
+ document.addEventListener('keydown',e=>{const i=slides.findIndex(s=>{const r=s.getBoundingClientRect();return r.top>-10&&r.top<innerHeight/2});if(['ArrowDown','ArrowRight',' '].includes(e.key)){e.preventDefault();slides[Math.min(i+1,slides.length-1)]?.scrollIntoView()}if(['ArrowUp','ArrowLeft'].includes(e.key)){e.preventDefault();slides[Math.max(i-1,0)]?.scrollIntoView()}});
636
+ </script>
637
+ </body>
638
+ </html>
639
+ `;
640
+ }
641
+
642
+ function openFile(file) {
643
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
644
+ const argsForOpen = process.platform === 'win32' ? ['/c', 'start', '', file] : [file];
645
+ spawnSync(command, argsForOpen, { stdio: 'ignore', detached: true });
646
+ }
647
+
648
+ function skillRoots() {
649
+ const home = os.homedir();
650
+ const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
651
+ return [
652
+ { label: 'Claude Code skills path', source: 'claude-user', path: path.join(home, '.claude', 'skills') },
653
+ { label: 'Codex skills path', source: 'codex-user', path: path.join(codexHome, 'skills') },
654
+ { label: 'OpenAI system skills path', source: 'openai-system', path: path.join(codexHome, 'skills', '.system') },
655
+ { label: 'cc-switch skills path', source: 'cc-switch', path: path.join(home, '.cc-switch', 'skills') },
656
+ { label: 'Claude plugin path', source: 'claude-plugin', path: path.join(home, '.claude', 'plugins', 'marketplaces') },
657
+ { label: 'Codex plugin path', source: 'codex-plugin', path: path.join(codexHome, 'plugins', 'cache') },
658
+ ];
659
+ }
660
+
661
+ function walkForSkillFiles(dir, maxDepth, currentDepth = 0) {
662
+ if (currentDepth > maxDepth) return [];
663
+ let entries;
664
+ try {
665
+ entries = fs.readdirSync(dir, { withFileTypes: true });
666
+ } catch (_) {
667
+ return [];
668
+ }
669
+
670
+ const files = [];
671
+ for (const entry of entries) {
672
+ const full = path.join(dir, entry.name);
673
+ if (!entry.isDirectory()) continue;
674
+ const skillFile = path.join(full, 'SKILL.md');
675
+ if (fs.existsSync(skillFile)) files.push(skillFile);
676
+ files.push(...walkForSkillFiles(full, maxDepth, currentDepth + 1));
677
+ }
678
+ return files;
679
+ }
680
+
681
+ function readFrontmatter(content) {
682
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
683
+ if (!match) return null;
684
+ const fields = {};
685
+ for (const line of match[1].split('\n')) {
686
+ const kv = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)/);
687
+ if (kv) fields[kv[1]] = kv[2].trim();
688
+ }
689
+ return fields;
690
+ }
691
+
692
+ function doctorDetails(data) {
693
+ const roots = skillRoots();
694
+ const skillFiles = roots.flatMap((root) => walkForSkillFiles(root.path, root.source.includes('plugin') ? 4 : 2));
695
+ const malformed = [];
696
+ for (const file of skillFiles) {
697
+ let frontmatter;
698
+ try {
699
+ frontmatter = readFrontmatter(fs.readFileSync(file, 'utf8'));
700
+ } catch (_) {
701
+ frontmatter = null;
702
+ }
703
+ if (!frontmatter || !frontmatter.name || !frontmatter.description) malformed.push(file);
704
+ }
705
+
706
+ const duplicateNames = new Map();
707
+ for (const skill of data.skills || []) {
708
+ if ((skill.sources || []).length > 1) duplicateNames.set(skill.name, skill.sources);
709
+ }
710
+
711
+ return { roots, malformed, duplicateNames };
712
+ }
713
+
714
+ function printDoctor(data) {
715
+ const details = doctorDetails(data);
716
+ const lines = [
717
+ 'Skill Guide Doctor',
718
+ `Node.js: ${process.version}`,
719
+ `Home: ${os.homedir()}`,
720
+ `CODEX_HOME: ${process.env.CODEX_HOME || path.join(os.homedir(), '.codex')}`,
721
+ `Total skills: ${data.totalCount || 0}`,
722
+ 'Paths:',
723
+ ];
724
+ for (const root of details.roots) {
725
+ lines.push(` ${root.label}: ${fs.existsSync(root.path) ? 'exists' : 'missing'} (${root.path})`);
726
+ }
727
+ lines.push(`Duplicate skill names: ${details.duplicateNames.size}`);
728
+ lines.push(`Malformed skill files: ${details.malformed.length}`);
729
+ lines.push(`Suggested Claude Code install: ${path.join(os.homedir(), '.claude', 'skills', 'skill-guide')}`);
730
+ lines.push(`Suggested Codex install: ${path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'skills', 'skill-guide')}`);
731
+ lines.push(
732
+ 'Sources:',
733
+ );
734
+ for (const [source, count] of Object.entries(data.sources || {})) {
735
+ lines.push(` ${source}: ${count}`);
736
+ }
737
+ lines.push('Status: OK');
738
+ return lines.join('\n');
739
+ }
740
+
741
+ function main() {
742
+ const mode = parseMode();
743
+ if (mode.mode === 'help') {
744
+ process.stdout.write(`${usage()}\n`);
745
+ return;
746
+ }
747
+
748
+ const data = runScanner(mode);
749
+ if (mode.mode === 'doctor') {
750
+ process.stdout.write(`${printDoctor(data)}\n`);
751
+ return;
752
+ }
753
+
754
+ const format = getArgValue('--format') || 'html';
755
+ if (!['html', 'json'].includes(format)) {
756
+ process.stderr.write('Error: --format must be "html" or "json"\n');
757
+ process.exit(1);
758
+ }
759
+
760
+ if (format === 'json') {
761
+ const serialized = JSON.stringify(data, null, 2);
762
+ const jsonOutput = getArgValue('--output');
763
+ if (jsonOutput) {
764
+ const outputPath = path.resolve(jsonOutput);
765
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
766
+ fs.writeFileSync(outputPath, `${serialized}\n`, 'utf8');
767
+ process.stdout.write(`Generated ${outputPath}\n`);
768
+ } else {
769
+ process.stdout.write(`${serialized}\n`);
770
+ }
771
+ return;
772
+ }
773
+
774
+ const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
775
+ fs.mkdirSync(path.dirname(output), { recursive: true });
776
+ fs.writeFileSync(output, renderHtml(data, mode), 'utf8');
777
+
778
+ if (hasFlag('--open') && !hasFlag('--no-open')) openFile(output);
779
+ process.stdout.write(`Generated ${output}\n`);
780
+ }
781
+
782
+ main();