neo-skill 0.1.19 → 0.1.20

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.
@@ -43,17 +43,22 @@
43
43
  }
44
44
  ```
45
45
 
46
- ## 添加新 Tool
47
-
48
- 1. 创建 `{tool_id}.json` 文件
49
- 2. 更新 `index.json`,添加索引条目:
50
- ```json
51
- {
52
- "tool_id": {
53
- "file": "tool_id.json",
54
- "tags": ["..."],
55
- "keywords": ["..."]
56
- }
46
+ ## 如何添加新工具
47
+
48
+ 1. 创建工具详情文件 `{tool-name}.json`
49
+ 2. `index.json` 中添加索引条目
50
+ 3. 确保 tags 与 capability_tags 设计一致
51
+
52
+ ## 自动推荐机制
53
+
54
+ skill-creator 会**自动推荐**合适的第三方库,无需用户手动指定:
55
+
56
+ - **自动触发**:基于 `task_type` 和 `capability_tags` 自动匹配
57
+ - **智能排序**:返回 top-5 最匹配的工具库
58
+ - **多处展示**:推荐信息会在 Plan、Workflow Steps 和最终输出中展示
59
+ - **安全降级**:如果没有匹配的库,不影响 skill 生成
60
+
61
+ 详见:`docs/skill-creator-refactoring/LIBRARY_RECOMMENDATION.md`
57
62
  }
58
63
  ```
59
64
  3. 无需修改代码,系统会自动检索
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo-skill",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "A multi-assistant skill generator (Claude/Windsurf/Cursor/GitHub Skills) driven by a canonical SkillSpec.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -0,0 +1,204 @@
1
+ # Skill Creator 架构规则(强制约束)
2
+
3
+ ## 重要提示:AI 必须遵守以下规则
4
+
5
+ ---
6
+
7
+ ## 1. 禁止单文件脚本(Monolith)
8
+
9
+ ### 规则
10
+
11
+ **严格禁止**生成任何"单文件可执行脚本"作为最终 skill 产物。
12
+
13
+ ### 原因
14
+
15
+ - 违反分层索引原则
16
+ - 破坏可扩展性
17
+ - 难以维护和复用
18
+ - 无法利用数据包体系
19
+
20
+ ### 允许的形态
21
+
22
+ ✅ **Structured Skill**(唯一允许):
23
+ - 分层索引文件(index.json)
24
+ - 被索引的数据文件(items/*.json)
25
+ - Skill 主文档({{skill-id}}.md)
26
+ - 运行时依赖的最小闭包
27
+
28
+ ❌ **Monolith Script**(严格禁止):
29
+ - 单个 Python/Shell 脚本包含所有逻辑
30
+ - 不使用索引系统
31
+ - 硬编码数据和逻辑
32
+
33
+ ### AI 指令
34
+
35
+ **不得输出 Monolith**
36
+ **不得建议 Monolith 作为解决方案**
37
+ **即使更简单也必须使用分层索引结构**
38
+
39
+ ---
40
+
41
+ ## 2. Install Space 路径规范
42
+
43
+ ### 路径空间定义
44
+
45
+ #### Source Space(源文件空间)
46
+ - 仅用于构建/安装阶段
47
+ - 位置:`skills/`, `.shared/`, `src/`
48
+ - **禁止**:运行时依赖
49
+
50
+ #### Install Space(安装后运行空间)
51
+ - 仅用于运行时
52
+ - 位置:`.windsurf/`, `.claude/`, `.cursor/`, `.github/`
53
+ - **要求**:所有索引路径必须指向此空间
54
+
55
+ ### 路径写法规范
56
+
57
+ ✅ **正确**:使用 Install Space 占位符
58
+ ```markdown
59
+ - Data: `{{install_root}}/.windsurf/workflows/data/{{skill-id}}/`
60
+ - Index: `{{install_root}}/.windsurf/workflows/data/{{skill-id}}/index.json`
61
+ ```
62
+
63
+ ❌ **错误**:引用 Source Space 路径
64
+ ```markdown
65
+ - Data: `skills/{{skill-id}}/data/`
66
+ - Index: `.shared/skill-creator/data_packs/`
67
+ - Relative: `../data/`, `../../skills/`
68
+ ```
69
+
70
+ ### 验证
71
+
72
+ 使用 `omni-skill doctor --skill <skill-id>` 验证路径正确性。
73
+
74
+ ---
75
+
76
+ ## 3. 三方库规则
77
+
78
+ ### 自动采用,无需确认
79
+
80
+ **规则**:
81
+ - ✅ 自动判断并采用三方库
82
+ - ❌ 不要求用户确认
83
+ - ❌ 不要求用户选择 A/B/C
84
+
85
+ ### 结果汇报(强制)
86
+
87
+ **必须在最终输出中列出**:
88
+
89
+ ```markdown
90
+ ## 使用的第三方库
91
+
92
+ ### 1. requests
93
+ - **版本策略**:>=2.28.0
94
+ - **用途**:发起 HTTP 请求
95
+ - **安装方式**:`pip install requests>=2.28.0`
96
+ - **文档链接**:
97
+ - PyPI: https://pypi.org/project/requests/
98
+ - Docs: https://requests.readthedocs.io/
99
+
100
+ ### 2. feedparser
101
+ - **版本策略**:>=6.0.0
102
+ - **用途**:解析 RSS/Atom feed
103
+ - **安装方式**:`pip install feedparser>=6.0.0`
104
+ - **文档链接**:
105
+ - PyPI: https://pypi.org/project/feedparser/
106
+ - Docs: https://feedparser.readthedocs.io/
107
+
108
+ ### Fallback 方案(可选)
109
+ - 如果不使用 `requests`,可使用标准库 `urllib`
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 4. 依赖闭包原则
115
+
116
+ ### 最小闭包
117
+
118
+ **要求**:
119
+ - ✅ 只拷贝该 skill 需要的最小闭包
120
+ - ❌ 不原封不动复制整个源目录
121
+ - ❌ 不拷贝未被引用的文件
122
+
123
+ ### 闭包内容
124
+
125
+ 每个 skill 的依赖闭包包括:
126
+ 1. Skill 主文档({{skill-id}}.md)
127
+ 2. 索引文件(index.json 等)
128
+ 3. 索引命中的 items
129
+ 4. 必需不变量(universal schema, output packs, minimal checklists)
130
+ 5. 三方库声明(requirements.txt)
131
+ 6. References/Scripts/Assets
132
+
133
+ ---
134
+
135
+ ## 5. 生成流程
136
+
137
+ ### 步骤
138
+
139
+ 1. **分析需求**:提取任务类型、输出形态、约束条件
140
+ 2. **收集信息**:对话式收集(≤10 问)
141
+ 3. **设计系统**:分层索引 + 文件化数据包
142
+ 4. **生成 SkillSpec**:写入 `skills/<name>/skillspec.json`
143
+ 5. **生成输出**:为所有 AI 目标生成文档
144
+ 6. **验证**:Schema 校验 + Dry-run
145
+
146
+ ### 禁止事项
147
+
148
+ ❌ 不生成单文件脚本
149
+ ❌ 不在文档中引用 Source Space 路径
150
+ ❌ 不要求用户确认三方库选型
151
+ ❌ 不复制整个源目录
152
+
153
+ ---
154
+
155
+ ## 6. 验收标准
156
+
157
+ ### 功能验收
158
+
159
+ ✅ Skill 产物自包含
160
+ ✅ 索引路径只指向 Install Space
161
+ ✅ 使用分层索引结构
162
+ ✅ 只拷贝最小闭包文件
163
+
164
+ ### 质量验收
165
+
166
+ ✅ 可追溯性:Install manifest 记录完整
167
+ ✅ 可复现性:相同输入产生相同输出
168
+ ✅ 可维护性:代码结构清晰,文档完整
169
+
170
+ ---
171
+
172
+ ## 7. 示例
173
+
174
+ ### 正确的 Skill 结构
175
+
176
+ ```
177
+ skills/review-gate/
178
+ ├── skillspec.json # Skill 定义
179
+ ├── references/ # 规则文档
180
+ │ └── checklist-rules.md
181
+ ├── scripts/ # 确定性脚本
182
+ │ └── validate.py
183
+ └── assets/ # 数据文件
184
+ └── templates/
185
+ ```
186
+
187
+ ### 正确的索引引用
188
+
189
+ ```json
190
+ {
191
+ "version": "1.0",
192
+ "items": {
193
+ "checklist-1": {
194
+ "file": "{{install_root}}/.windsurf/workflows/data/review-gate/checklists/checklist-1.json"
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ **本规则为强制约束,所有 AI 生成的 skill 必须遵守。**
203
+
204
+ 详见:`docs/ARCHITECTURE_RULES.md`
@@ -52,6 +52,55 @@ omni-skill install ./skills
52
52
 
53
53
  **Note:** Unlike `init`, the `install` command always generates outputs for all AI targets to ensure maximum compatibility.
54
54
 
55
+ ### `omni-skill install-skill <skill-id>`
56
+ Install skill with dependency closure (new architecture).
57
+
58
+ **Usage:**
59
+ ```bash
60
+ omni-skill install-skill review-gate --target windsurf
61
+ omni-skill install-skill skill-creator --target claude
62
+ ```
63
+
64
+ **What it does:**
65
+ 1. Resolves skill dependency closure (minimal files)
66
+ 2. Materializes files to Install Space
67
+ 3. Generates install manifest
68
+ 4. Validates paths (no source path leakage)
69
+
70
+ **Architecture:**
71
+ - Uses Source Space (skills/) for build
72
+ - Copies to Install Space (.windsurf/, .claude/, etc.) for runtime
73
+ - Only copies minimal closure (not entire source directory)
74
+
75
+ ### `omni-skill doctor --skill <skill-id>`
76
+ Diagnose skill installation and dependencies.
77
+
78
+ **Usage:**
79
+ ```bash
80
+ omni-skill doctor --skill review-gate --target windsurf
81
+ ```
82
+
83
+ **What it checks:**
84
+ - Install manifest exists
85
+ - All referenced files exist
86
+ - No source path leakage
87
+ - Index files are valid
88
+ - Dependency closure is complete
89
+
90
+ **Output:**
91
+ ```
92
+ === Skill Diagnostic Report ===
93
+ Skill ID: review-gate
94
+ Install Root: .windsurf/workflows/data/review-gate
95
+
96
+ --- Path Validation ---
97
+ ✓ No source path leakage detected
98
+ ✓ All index paths point to Install Space
99
+ ✓ All referenced files exist
100
+
101
+ === Diagnostic Complete ===
102
+ ```
103
+
55
104
  ### `omni-skill update`
56
105
  Update npm package and re-initialize skills.
57
106
 
@@ -7,6 +7,8 @@ from pathlib import Path
7
7
  from typing import Dict, List, Optional, Set
8
8
 
9
9
  from skill_creator.cli import cmd_generate
10
+ from .install import SkillInstaller
11
+ from .doctor import SkillDoctor
10
12
 
11
13
  STATE_FILE = ".neo-skill.json"
12
14
 
@@ -310,9 +312,36 @@ def _cmd_init(args: argparse.Namespace) -> int:
310
312
  return _handle_init(resolved["selected"], "init")
311
313
 
312
314
 
315
+ def _cmd_install_new(args: argparse.Namespace) -> int:
316
+ """
317
+ Install skill with dependency closure (new architecture).
318
+ Usage: omni-skill install <skill-id> [--target <agent>]
319
+ """
320
+ cwd = Path.cwd().resolve()
321
+ pkg_root = _get_pkg_root()
322
+
323
+ skill_id = args.skill_id
324
+ target = getattr(args, 'target', 'windsurf')
325
+
326
+ # 使用新的 SkillInstaller
327
+ installer = SkillInstaller(source_root=pkg_root, install_root=cwd)
328
+
329
+ try:
330
+ manifest = installer.install(skill_id, target)
331
+ print(f"\n✓ Successfully installed {skill_id}")
332
+ print(f" Install root: {manifest.install_root}")
333
+ print(f" Files: {len(manifest.files)}")
334
+ if manifest.dependencies.get('libraries'):
335
+ print(f" Libraries: {', '.join(manifest.dependencies['libraries'])}")
336
+ return 0
337
+ except Exception as e:
338
+ print(f"\n✗ Installation failed: {e}")
339
+ return 1
340
+
341
+
313
342
  def _cmd_install(args: argparse.Namespace) -> int:
314
343
  """
315
- Install skill(s) from a local directory.
344
+ Install skill(s) from a local directory (legacy).
316
345
  Usage: omni-skill install <path-to-skill-or-skills-dir>
317
346
 
318
347
  Note: install command always generates outputs for all AI targets.
@@ -377,6 +406,28 @@ def _print_init_help() -> None:
377
406
  print(" omni-skill init --ai all")
378
407
 
379
408
 
409
+ def _cmd_doctor(args: argparse.Namespace) -> int:
410
+ """
411
+ Diagnose skill installation and dependencies.
412
+ Usage: omni-skill doctor --skill <skill-id> [--target <agent>]
413
+ """
414
+ cwd = Path.cwd().resolve()
415
+
416
+ skill_id = args.skill_id
417
+ target = getattr(args, 'target', 'windsurf')
418
+
419
+ # 使用 SkillDoctor
420
+ doctor = SkillDoctor(install_root=cwd)
421
+
422
+ report = doctor.diagnose(skill_id, target)
423
+ print(report.format())
424
+
425
+ # 返回错误码
426
+ if report.has_errors():
427
+ return 1
428
+ return 0
429
+
430
+
380
431
  def build_parser() -> argparse.ArgumentParser:
381
432
  p = argparse.ArgumentParser(
382
433
  prog="omni-skill",
@@ -392,6 +443,18 @@ def build_parser() -> argparse.ArgumentParser:
392
443
  p_install = sub.add_parser("install", help="Install skill(s) from a local directory")
393
444
  p_install.add_argument("path", help="Path to skill directory or skills directory")
394
445
  p_install.set_defaults(func=_cmd_install)
446
+
447
+ # 新增:install-skill 命令(使用新架构)
448
+ p_install_new = sub.add_parser("install-skill", help="Install skill with dependency closure (new)")
449
+ p_install_new.add_argument("skill_id", help="Skill ID to install")
450
+ p_install_new.add_argument("--target", default="windsurf", help="Target AI (windsurf/claude/cursor/github)")
451
+ p_install_new.set_defaults(func=_cmd_install_new)
452
+
453
+ # 新增:doctor 命令
454
+ p_doctor = sub.add_parser("doctor", help="Diagnose skill installation")
455
+ p_doctor.add_argument("--skill", dest="skill_id", required=True, help="Skill ID to diagnose")
456
+ p_doctor.add_argument("--target", default="windsurf", help="Target AI (windsurf/claude/cursor/github)")
457
+ p_doctor.set_defaults(func=_cmd_doctor)
395
458
 
396
459
  p_update = sub.add_parser("update", help="Update npm package and re-initialize skills")
397
460
  p_update.set_defaults(func=_cmd_update)
@@ -0,0 +1,323 @@
1
+ """
2
+ Doctor module: 诊断 skill 安装状态和依赖完整性
3
+
4
+ 遵循架构规则:
5
+ - 检测 source path 泄漏
6
+ - 验证索引路径指向 Install Space
7
+ - 检查依赖闭包完整性
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import List, Optional, Set
15
+
16
+
17
+ @dataclass
18
+ class DiagnosticIssue:
19
+ """诊断问题"""
20
+ severity: str # error, warning, info
21
+ category: str # path_leakage, missing_file, invalid_index, etc.
22
+ message: str
23
+ file: Optional[str] = None
24
+ line: Optional[int] = None
25
+
26
+
27
+ @dataclass
28
+ class DiagnosticReport:
29
+ """诊断报告"""
30
+ skill_id: str
31
+ install_root: Optional[str] = None
32
+ installed_at: Optional[str] = None
33
+ total_files: int = 0
34
+ files: List[str] = field(default_factory=list)
35
+ dependencies: dict = field(default_factory=dict)
36
+ issues: List[DiagnosticIssue] = field(default_factory=list)
37
+
38
+ def has_errors(self) -> bool:
39
+ """是否有错误"""
40
+ return any(issue.severity == "error" for issue in self.issues)
41
+
42
+ def has_warnings(self) -> bool:
43
+ """是否有警告"""
44
+ return any(issue.severity == "warning" for issue in self.issues)
45
+
46
+ def format(self) -> str:
47
+ """格式化输出"""
48
+ lines = [
49
+ "=== Skill Diagnostic Report ===",
50
+ "",
51
+ f"Skill ID: {self.skill_id}",
52
+ ]
53
+
54
+ if self.install_root:
55
+ lines.append(f"Install Root: {self.install_root}")
56
+ if self.installed_at:
57
+ lines.append(f"Installed At: {self.installed_at}")
58
+
59
+ lines.extend([
60
+ "",
61
+ "--- Dependency Closure ---"
62
+ ])
63
+
64
+ if self.files:
65
+ for file in self.files[:10]: # 只显示前 10 个
66
+ lines.append(f"✓ {file}")
67
+ if len(self.files) > 10:
68
+ lines.append(f"... and {len(self.files) - 10} more files")
69
+
70
+ lines.append(f"\nTotal files: {self.total_files}")
71
+
72
+ if self.dependencies:
73
+ lines.extend([
74
+ "",
75
+ "--- Dependencies ---"
76
+ ])
77
+
78
+ if "libraries" in self.dependencies and self.dependencies["libraries"]:
79
+ lines.append("Libraries:")
80
+ for lib in self.dependencies["libraries"]:
81
+ lines.append(f" - {lib}")
82
+
83
+ if "data_packs" in self.dependencies and self.dependencies["data_packs"]:
84
+ lines.append("\nData Packs:")
85
+ for pack in self.dependencies["data_packs"]:
86
+ lines.append(f" - {pack}")
87
+
88
+ lines.extend([
89
+ "",
90
+ "--- Issues ---"
91
+ ])
92
+
93
+ if not self.issues:
94
+ lines.append("None")
95
+ else:
96
+ # 按严重程度分组
97
+ errors = [i for i in self.issues if i.severity == "error"]
98
+ warnings = [i for i in self.issues if i.severity == "warning"]
99
+ infos = [i for i in self.issues if i.severity == "info"]
100
+
101
+ if errors:
102
+ lines.append("\nErrors:")
103
+ for issue in errors:
104
+ lines.append(f" ❌ [{issue.category}] {issue.message}")
105
+ if issue.file:
106
+ lines.append(f" File: {issue.file}")
107
+
108
+ if warnings:
109
+ lines.append("\nWarnings:")
110
+ for issue in warnings:
111
+ lines.append(f" ⚠️ [{issue.category}] {issue.message}")
112
+ if issue.file:
113
+ lines.append(f" File: {issue.file}")
114
+
115
+ if infos:
116
+ lines.append("\nInfo:")
117
+ for issue in infos:
118
+ lines.append(f" ℹ️ [{issue.category}] {issue.message}")
119
+
120
+ lines.extend([
121
+ "",
122
+ "=== Diagnostic Complete ==="
123
+ ])
124
+
125
+ return "\n".join(lines)
126
+
127
+
128
+ class SkillDoctor:
129
+ """Skill 诊断器"""
130
+
131
+ def __init__(self, install_root: Path):
132
+ self.install_root = install_root
133
+
134
+ def diagnose(self, skill_id: str, target: str = "windsurf") -> DiagnosticReport:
135
+ """
136
+ 诊断 skill 安装状态
137
+
138
+ Args:
139
+ skill_id: skill ID
140
+ target: 目标 AI 助手
141
+
142
+ Returns:
143
+ DiagnosticReport
144
+ """
145
+ report = DiagnosticReport(skill_id=skill_id)
146
+
147
+ # 1. 查找 install manifest
148
+ manifest_path = self._find_manifest(skill_id, target)
149
+ if not manifest_path:
150
+ report.issues.append(DiagnosticIssue(
151
+ severity="error",
152
+ category="missing_manifest",
153
+ message=f"Install manifest not found for skill '{skill_id}'. Please run 'omni-skill install' first."
154
+ ))
155
+ return report
156
+
157
+ # 2. 读取 manifest
158
+ try:
159
+ with open(manifest_path, 'r', encoding='utf-8') as f:
160
+ manifest = json.load(f)
161
+
162
+ report.install_root = manifest.get("install_root")
163
+ report.installed_at = manifest.get("installed_at")
164
+ report.dependencies = manifest.get("dependencies", {})
165
+
166
+ files = manifest.get("files", [])
167
+ report.total_files = len(files)
168
+ report.files = [f["target"] for f in files[:10]]
169
+
170
+ except Exception as e:
171
+ report.issues.append(DiagnosticIssue(
172
+ severity="error",
173
+ category="invalid_manifest",
174
+ message=f"Failed to read manifest: {e}",
175
+ file=str(manifest_path)
176
+ ))
177
+ return report
178
+
179
+ # 3. 检查文件完整性
180
+ self._check_file_integrity(manifest, report)
181
+
182
+ # 4. 检查 source path 泄漏
183
+ self._check_path_leakage(skill_id, target, report)
184
+
185
+ # 5. 检查索引可解析性
186
+ self._check_index_validity(skill_id, target, report)
187
+
188
+ return report
189
+
190
+ def _find_manifest(self, skill_id: str, target: str) -> Optional[Path]:
191
+ """查找 install manifest"""
192
+ if target == "windsurf":
193
+ manifest_path = self.install_root / ".windsurf" / "workflows" / "data" / skill_id / ".install_manifest.json"
194
+ elif target == "claude":
195
+ manifest_path = self.install_root / ".claude" / "skills" / skill_id / "resources" / ".install_manifest.json"
196
+ elif target == "cursor":
197
+ manifest_path = self.install_root / ".cursor" / "commands" / "data" / skill_id / ".install_manifest.json"
198
+ elif target == "github":
199
+ manifest_path = self.install_root / ".github" / "skills" / skill_id / ".install_manifest.json"
200
+ else:
201
+ return None
202
+
203
+ return manifest_path if manifest_path.exists() else None
204
+
205
+ def _check_file_integrity(self, manifest: dict, report: DiagnosticReport) -> None:
206
+ """检查文件完整性"""
207
+ files = manifest.get("files", [])
208
+
209
+ for file_info in files:
210
+ target = file_info.get("target")
211
+ if not target:
212
+ continue
213
+
214
+ target_path = self.install_root / target
215
+ if not target_path.exists():
216
+ report.issues.append(DiagnosticIssue(
217
+ severity="error",
218
+ category="missing_file",
219
+ message=f"Referenced file does not exist",
220
+ file=target
221
+ ))
222
+
223
+ def _check_path_leakage(self, skill_id: str, target: str, report: DiagnosticReport) -> None:
224
+ """检查 source path 泄漏"""
225
+ # 查找 skill 主文档
226
+ if target == "windsurf":
227
+ skill_doc = self.install_root / ".windsurf" / "workflows" / f"{skill_id}.md"
228
+ elif target == "claude":
229
+ skill_doc = self.install_root / ".claude" / "skills" / skill_id / "SKILL.md"
230
+ elif target == "cursor":
231
+ skill_doc = self.install_root / ".cursor" / "commands" / f"{skill_id}.md"
232
+ elif target == "github":
233
+ skill_doc = self.install_root / ".github" / "skills" / skill_id / "SKILL.md"
234
+ else:
235
+ return
236
+
237
+ if not skill_doc.exists():
238
+ report.issues.append(DiagnosticIssue(
239
+ severity="warning",
240
+ category="missing_skill_doc",
241
+ message=f"Skill document not found",
242
+ file=str(skill_doc)
243
+ ))
244
+ return
245
+
246
+ # 检查文档内容
247
+ try:
248
+ content = skill_doc.read_text(encoding='utf-8')
249
+
250
+ # 检测禁止的路径模式
251
+ forbidden_patterns = [
252
+ "skills/",
253
+ ".shared/",
254
+ "../",
255
+ "../../",
256
+ "/home/",
257
+ "/Users/",
258
+ "C:/",
259
+ "D:/",
260
+ ]
261
+
262
+ for pattern in forbidden_patterns:
263
+ if pattern in content:
264
+ report.issues.append(DiagnosticIssue(
265
+ severity="error",
266
+ category="path_leakage",
267
+ message=f"Source path leakage detected: '{pattern}' found in skill document",
268
+ file=str(skill_doc)
269
+ ))
270
+
271
+ except Exception as e:
272
+ report.issues.append(DiagnosticIssue(
273
+ severity="warning",
274
+ category="read_error",
275
+ message=f"Failed to read skill document: {e}",
276
+ file=str(skill_doc)
277
+ ))
278
+
279
+ def _check_index_validity(self, skill_id: str, target: str, report: DiagnosticReport) -> None:
280
+ """检查索引可解析性"""
281
+ # 查找索引文件
282
+ if target == "windsurf":
283
+ data_dir = self.install_root / ".windsurf" / "workflows" / "data" / skill_id
284
+ elif target == "claude":
285
+ data_dir = self.install_root / ".claude" / "skills" / skill_id / "resources"
286
+ elif target == "cursor":
287
+ data_dir = self.install_root / ".cursor" / "commands" / "data" / skill_id
288
+ elif target == "github":
289
+ data_dir = self.install_root / ".github" / "skills" / skill_id
290
+ else:
291
+ return
292
+
293
+ if not data_dir.exists():
294
+ return
295
+
296
+ # 查找所有 index.json
297
+ for index_file in data_dir.rglob("index.json"):
298
+ try:
299
+ with open(index_file, 'r', encoding='utf-8') as f:
300
+ index_data = json.load(f)
301
+
302
+ # 检查索引项是否存在
303
+ items = index_data.get("items", {})
304
+ for item_id, item_info in items.items():
305
+ if isinstance(item_info, dict):
306
+ item_file = item_info.get("file")
307
+ if item_file:
308
+ item_path = index_file.parent / item_file
309
+ if not item_path.exists():
310
+ report.issues.append(DiagnosticIssue(
311
+ severity="error",
312
+ category="missing_indexed_file",
313
+ message=f"Indexed file not found: {item_file}",
314
+ file=str(index_file)
315
+ ))
316
+
317
+ except Exception as e:
318
+ report.issues.append(DiagnosticIssue(
319
+ severity="warning",
320
+ category="invalid_index",
321
+ message=f"Failed to parse index: {e}",
322
+ file=str(index_file)
323
+ ))
@@ -0,0 +1,352 @@
1
+ """
2
+ Install module: 依赖闭包解析、物化拷贝、manifest 生成
3
+
4
+ 遵循架构规则:
5
+ - Source Space vs Install Space 严格分离
6
+ - 最小闭包原则
7
+ - 禁止 source path 泄漏
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import shutil
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Set
18
+
19
+
20
+ @dataclass
21
+ class FileMapping:
22
+ """文件映射:Source -> Install"""
23
+ source: Path
24
+ target: Path
25
+ hash: str = ""
26
+
27
+ def to_dict(self) -> dict:
28
+ return {
29
+ "source": str(self.source),
30
+ "target": str(self.target),
31
+ "hash": self.hash
32
+ }
33
+
34
+
35
+ @dataclass
36
+ class InstallManifest:
37
+ """安装清单"""
38
+ version: str = "1.0"
39
+ skill_id: str = ""
40
+ install_root: str = ""
41
+ installed_at: str = ""
42
+ files: List[FileMapping] = field(default_factory=list)
43
+ dependencies: Dict[str, List[str]] = field(default_factory=dict)
44
+
45
+ def to_dict(self) -> dict:
46
+ return {
47
+ "version": self.version,
48
+ "skill_id": self.skill_id,
49
+ "install_root": self.install_root,
50
+ "installed_at": self.installed_at,
51
+ "files": [f.to_dict() for f in self.files],
52
+ "dependencies": self.dependencies
53
+ }
54
+
55
+ def save(self, path: Path) -> None:
56
+ """保存 manifest 到文件"""
57
+ path.parent.mkdir(parents=True, exist_ok=True)
58
+ with open(path, 'w', encoding='utf-8') as f:
59
+ json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
60
+
61
+
62
+ class DependencyClosureResolver:
63
+ """依赖闭包解析器"""
64
+
65
+ def __init__(self, source_root: Path):
66
+ self.source_root = source_root
67
+ self.visited: Set[Path] = set()
68
+ self.closure: Set[Path] = set()
69
+
70
+ def resolve(self, skill_dir: Path) -> Set[Path]:
71
+ """
72
+ 解析 skill 的依赖闭包
73
+
74
+ Args:
75
+ skill_dir: skill 源目录(Source Space)
76
+
77
+ Returns:
78
+ 依赖闭包文件集合
79
+ """
80
+ self.visited.clear()
81
+ self.closure.clear()
82
+
83
+ # 1. 添加 skill 主要文件
84
+ self._add_skill_files(skill_dir)
85
+
86
+ # 2. 解析 skillspec.json 中的引用
87
+ skillspec = skill_dir / "skillspec.json"
88
+ if skillspec.exists():
89
+ self._resolve_skillspec(skillspec)
90
+
91
+ # 3. 添加必需不变量
92
+ self._add_universal_invariants()
93
+
94
+ return self.closure
95
+
96
+ def _add_skill_files(self, skill_dir: Path) -> None:
97
+ """添加 skill 的主要文件"""
98
+ # skillspec.json
99
+ skillspec = skill_dir / "skillspec.json"
100
+ if skillspec.exists():
101
+ self.closure.add(skillspec)
102
+
103
+ # references/
104
+ refs_dir = skill_dir / "references"
105
+ if refs_dir.exists():
106
+ for ref_file in refs_dir.rglob("*"):
107
+ if ref_file.is_file():
108
+ self.closure.add(ref_file)
109
+
110
+ # scripts/
111
+ scripts_dir = skill_dir / "scripts"
112
+ if scripts_dir.exists():
113
+ for script_file in scripts_dir.rglob("*"):
114
+ if script_file.is_file():
115
+ self.closure.add(script_file)
116
+
117
+ # assets/
118
+ assets_dir = skill_dir / "assets"
119
+ if assets_dir.exists():
120
+ for asset_file in assets_dir.rglob("*"):
121
+ if asset_file.is_file():
122
+ self.closure.add(asset_file)
123
+
124
+ def _resolve_skillspec(self, skillspec: Path) -> None:
125
+ """解析 skillspec.json 中的引用"""
126
+ if skillspec in self.visited:
127
+ return
128
+ self.visited.add(skillspec)
129
+
130
+ try:
131
+ with open(skillspec, 'r', encoding='utf-8') as f:
132
+ spec = json.load(f)
133
+
134
+ # 解析 references
135
+ for ref in spec.get("references", []):
136
+ ref_path = skillspec.parent / ref
137
+ if ref_path.exists():
138
+ self.closure.add(ref_path)
139
+
140
+ # 解析 scripts
141
+ for script in spec.get("scripts", []):
142
+ script_path = skillspec.parent / script
143
+ if script_path.exists():
144
+ self.closure.add(script_path)
145
+
146
+ # 解析 assets
147
+ for asset in spec.get("assets", []):
148
+ asset_path = skillspec.parent / asset
149
+ if asset_path.exists():
150
+ self.closure.add(asset_path)
151
+
152
+ # 解析 libraries(如果有)
153
+ libraries = spec.get("libraries", [])
154
+ if libraries:
155
+ # 生成 requirements.txt
156
+ self._generate_requirements(skillspec.parent, libraries)
157
+
158
+ except Exception as e:
159
+ print(f"Warning: Failed to parse {skillspec}: {e}")
160
+
161
+ def _generate_requirements(self, skill_dir: Path, libraries: List) -> None:
162
+ """生成 requirements.txt"""
163
+ req_file = skill_dir / "requirements.txt"
164
+
165
+ lines = []
166
+ for lib in libraries:
167
+ if isinstance(lib, dict):
168
+ name = lib.get('name', '')
169
+ version = lib.get('version', '')
170
+ if version:
171
+ lines.append(f"{name}{version}")
172
+ else:
173
+ lines.append(name)
174
+ else:
175
+ lines.append(str(lib))
176
+
177
+ if lines:
178
+ with open(req_file, 'w', encoding='utf-8') as f:
179
+ f.write('\n'.join(lines) + '\n')
180
+ self.closure.add(req_file)
181
+
182
+ def _add_universal_invariants(self) -> None:
183
+ """添加必需不变量"""
184
+ shared_dir = self.source_root / ".shared" / "skill-creator" / "data_packs"
185
+
186
+ # universal/
187
+ universal_dir = shared_dir / "universal"
188
+ if universal_dir.exists():
189
+ # schema
190
+ schema = universal_dir / "schema.skill.json"
191
+ if schema.exists():
192
+ self.closure.add(schema)
193
+
194
+ # output_packs/
195
+ output_packs_dir = universal_dir / "output_packs"
196
+ if output_packs_dir.exists():
197
+ for pack_file in output_packs_dir.rglob("*.json"):
198
+ self.closure.add(pack_file)
199
+
200
+ # minimal_checklists/
201
+ checklists_dir = universal_dir / "minimal_checklists"
202
+ if checklists_dir.exists():
203
+ for checklist_file in checklists_dir.rglob("*.json"):
204
+ self.closure.add(checklist_file)
205
+
206
+
207
+ class SkillInstaller:
208
+ """Skill 安装器:物化拷贝到 Install Space"""
209
+
210
+ def __init__(self, source_root: Path, install_root: Path):
211
+ self.source_root = source_root
212
+ self.install_root = install_root
213
+ self.resolver = DependencyClosureResolver(source_root)
214
+
215
+ def install(self, skill_id: str, target: str = "windsurf") -> InstallManifest:
216
+ """
217
+ 安装 skill 到 Install Space
218
+
219
+ Args:
220
+ skill_id: skill ID
221
+ target: 目标 AI 助手 (windsurf/claude/cursor/github)
222
+
223
+ Returns:
224
+ InstallManifest
225
+ """
226
+ skill_dir = self.source_root / "skills" / skill_id
227
+ if not skill_dir.exists():
228
+ raise FileNotFoundError(f"Skill not found: {skill_dir}")
229
+
230
+ # 1. 解析依赖闭包
231
+ closure = self.resolver.resolve(skill_dir)
232
+ print(f"Resolved {len(closure)} files in dependency closure")
233
+
234
+ # 2. 确定 install 目标目录
235
+ install_target_root = self._get_install_target_root(target, skill_id)
236
+
237
+ # 3. 物化拷贝
238
+ manifest = InstallManifest(
239
+ skill_id=skill_id,
240
+ install_root=str(install_target_root.relative_to(self.install_root)),
241
+ installed_at=datetime.utcnow().isoformat() + "Z"
242
+ )
243
+
244
+ for source_file in closure:
245
+ target_file = self._map_to_install_space(source_file, skill_id, target)
246
+ self._copy_file(source_file, target_file)
247
+
248
+ # 计算哈希
249
+ file_hash = self._compute_hash(target_file)
250
+
251
+ mapping = FileMapping(
252
+ source=source_file.relative_to(self.source_root),
253
+ target=target_file.relative_to(self.install_root),
254
+ hash=file_hash
255
+ )
256
+ manifest.files.append(mapping)
257
+
258
+ # 4. 收集依赖信息
259
+ manifest.dependencies = self._collect_dependencies(skill_dir)
260
+
261
+ # 5. 保存 manifest
262
+ manifest_path = install_target_root / ".install_manifest.json"
263
+ manifest.save(manifest_path)
264
+
265
+ print(f"Installed {skill_id} to {install_target_root}")
266
+ print(f"Manifest saved to {manifest_path}")
267
+
268
+ return manifest
269
+
270
+ def _get_install_target_root(self, target: str, skill_id: str) -> Path:
271
+ """获取 install 目标根目录"""
272
+ if target == "windsurf":
273
+ return self.install_root / ".windsurf" / "workflows" / "data" / skill_id
274
+ elif target == "claude":
275
+ return self.install_root / ".claude" / "skills" / skill_id / "resources"
276
+ elif target == "cursor":
277
+ return self.install_root / ".cursor" / "commands" / "data" / skill_id
278
+ elif target == "github":
279
+ return self.install_root / ".github" / "skills" / skill_id
280
+ else:
281
+ raise ValueError(f"Unknown target: {target}")
282
+
283
+ def _map_to_install_space(self, source_file: Path, skill_id: str, target: str) -> Path:
284
+ """映射 Source Space 路径到 Install Space 路径"""
285
+ install_target_root = self._get_install_target_root(target, skill_id)
286
+
287
+ # 计算相对路径
288
+ try:
289
+ # 尝试相对于 skills/{skill_id}
290
+ rel_path = source_file.relative_to(self.source_root / "skills" / skill_id)
291
+ return install_target_root / rel_path
292
+ except ValueError:
293
+ pass
294
+
295
+ try:
296
+ # 尝试相对于 .shared/skill-creator/data_packs
297
+ rel_path = source_file.relative_to(self.source_root / ".shared" / "skill-creator" / "data_packs")
298
+ return install_target_root / "data_packs" / rel_path
299
+ except ValueError:
300
+ pass
301
+
302
+ # 兜底:使用文件名
303
+ return install_target_root / source_file.name
304
+
305
+ def _copy_file(self, source: Path, target: Path) -> None:
306
+ """拷贝文件"""
307
+ target.parent.mkdir(parents=True, exist_ok=True)
308
+ shutil.copy2(source, target)
309
+
310
+ def _compute_hash(self, file_path: Path) -> str:
311
+ """计算文件哈希"""
312
+ sha256 = hashlib.sha256()
313
+ with open(file_path, 'rb') as f:
314
+ for chunk in iter(lambda: f.read(4096), b''):
315
+ sha256.update(chunk)
316
+ return f"sha256:{sha256.hexdigest()}"
317
+
318
+ def _collect_dependencies(self, skill_dir: Path) -> Dict[str, List[str]]:
319
+ """收集依赖信息"""
320
+ deps = {
321
+ "libraries": [],
322
+ "data_packs": []
323
+ }
324
+
325
+ # 读取 skillspec.json
326
+ skillspec = skill_dir / "skillspec.json"
327
+ if skillspec.exists():
328
+ try:
329
+ with open(skillspec, 'r', encoding='utf-8') as f:
330
+ spec = json.load(f)
331
+
332
+ # 收集 libraries
333
+ libraries = spec.get("libraries", [])
334
+ for lib in libraries:
335
+ if isinstance(lib, dict):
336
+ name = lib.get('name', '')
337
+ if name:
338
+ deps["libraries"].append(name)
339
+ else:
340
+ deps["libraries"].append(str(lib))
341
+
342
+ except Exception as e:
343
+ print(f"Warning: Failed to parse {skillspec}: {e}")
344
+
345
+ # 收集 data_packs(固定的必需不变量)
346
+ deps["data_packs"] = [
347
+ "universal/output_packs",
348
+ "universal/minimal_checklists",
349
+ "universal/schema"
350
+ ]
351
+
352
+ return deps
@@ -133,6 +133,22 @@ class SkillBuilder:
133
133
  }
134
134
  ]
135
135
 
136
+ # 添加三方库推荐信息到第一个步骤的 notes
137
+ if steps and ctx.plan and ctx.plan.libraries:
138
+ lib_notes = ["\n\n**推荐的第三方库**:"]
139
+ for lib in ctx.plan.libraries:
140
+ lib_notes.append(f"- `{lib.name}`: {lib.purpose}")
141
+ if lib.pypi_link:
142
+ lib_notes.append(f" - 安装: `pip install {lib.name}`")
143
+ if lib.docs_link:
144
+ lib_notes.append(f" - 文档: {lib.docs_link}")
145
+
146
+ # 将库信息添加到第一个步骤的 notes
147
+ if steps[0].get("notes"):
148
+ steps[0]["notes"] += "\n".join(lib_notes)
149
+ else:
150
+ steps[0]["notes"] = "\n".join(lib_notes)
151
+
136
152
  return steps
137
153
 
138
154
  def _generate_triggers(self, ctx: SkillCreatorContext) -> list[str]:
@@ -162,6 +162,18 @@ class PlanOrchestrator:
162
162
  summary_parts.append("")
163
163
  summary_parts.append(f"**输出形式**: {self._get_pack_description(output_pack)}")
164
164
 
165
+ # 添加推荐的三方库信息
166
+ libraries = self._get_recommended_libraries(ctx)
167
+ if libraries:
168
+ summary_parts.append("")
169
+ summary_parts.append("**推荐的第三方库**:")
170
+ for lib in libraries:
171
+ summary_parts.append(f"- `{lib.name}`: {lib.purpose}")
172
+ if lib.reason:
173
+ summary_parts.append(f" - 推荐理由: {lib.reason}")
174
+ if lib.pypi_link:
175
+ summary_parts.append(f" - PyPI: {lib.pypi_link}")
176
+
165
177
  return "\n".join(summary_parts)
166
178
 
167
179
  def _get_pack_description(self, pack: str) -> str:
@@ -63,6 +63,7 @@ class SkillSpec:
63
63
  references: List[str] = field(default_factory=list)
64
64
  scripts: List[str] = field(default_factory=list)
65
65
  assets: List[str] = field(default_factory=list)
66
+ libraries: List[Any] = field(default_factory=list) # Recommended third-party libraries
66
67
 
67
68
  @staticmethod
68
69
  def from_path(path: Path) -> "SkillSpec":
@@ -84,6 +85,7 @@ class SkillSpec:
84
85
  references=[str(x) for x in _as_list(d.get("references"))],
85
86
  scripts=[str(x) for x in _as_list(d.get("scripts"))],
86
87
  assets=[str(x) for x in _as_list(d.get("assets"))],
88
+ libraries=_as_list(d.get("libraries")),
87
89
  )
88
90
 
89
91
  def validate_basic(self) -> List[str]:
@@ -8,7 +8,28 @@
8
8
  - **claude.py**:生成 `.claude/skills/<skill>/SKILL.md`(兼容输出)。
9
9
  - **cursor.py**:生成 `.cursor/commands/<skill>.md`(兼容输出)。
10
10
  - **github_skills.py**:生成 `.github/skills/<skill>/SKILL.md`(兼容输出)。
11
- - **common.py**:渲染共用逻辑。
11
+ - **common.py**:公共渲染函数。
12
+
13
+ ## 公共逻辑复用
14
+
15
+ 所有 target 都使用 `common.py` 中的公共函数,避免代码重复:
16
+
17
+ - `_render_steps()` - 渲染 workflow steps
18
+ - `_render_resources()` - 渲染 references/scripts/assets
19
+ - `_render_libraries()` - 渲染推荐的第三方库信息
20
+ - `_render_prerequisites()` - 渲染 Prerequisites 部分(包含三方库)
21
+ - `_render_footer()` - 渲染页脚
22
+
23
+ ### 三方库信息展示
24
+
25
+ **所有 target 都会展示推荐的三方库信息**,确保用户知道需要安装哪些依赖:
26
+
27
+ - ✅ **windsurf.py** - 在 Prerequisites 部分展示(包含 Python 版本检查)
28
+ - ✅ **claude.py** - 在 Prerequisites 部分展示
29
+ - ✅ **cursor.py** - 在 Prerequisites 部分展示
30
+ - ✅ **github_skills.py** - 在 Prerequisites 部分展示
31
+
32
+ 这确保了无论用户使用哪个 AI 助手,都能获得完整的依赖信息和安装指导。
12
33
 
13
34
  ## 约定
14
35
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from ..spec.model import SkillSpec
4
4
  from ..util.frontmatter import dump_frontmatter
5
- from .common import _render_steps, _render_resources, _render_footer
5
+ from .common import _render_steps, _render_resources, _render_footer, _render_prerequisites
6
6
 
7
7
 
8
8
  def render_claude_skill_md(spec: SkillSpec) -> str:
@@ -16,6 +16,11 @@ def render_claude_skill_md(spec: SkillSpec) -> str:
16
16
  body.append(spec.description)
17
17
  body.append("")
18
18
 
19
+ # Prerequisites (三方库信息)
20
+ prerequisites = _render_prerequisites(spec, include_python_check=False)
21
+ if prerequisites:
22
+ body.append(prerequisites)
23
+
19
24
  if spec.triggers:
20
25
  body.append("## When to use (examples of user requests)")
21
26
  for t in spec.triggers:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import List
4
+ from typing import Any, List
5
5
 
6
6
  from ..spec.model import SkillSpec, WorkflowStep
7
7
 
@@ -41,6 +41,88 @@ def _render_resources(spec: SkillSpec) -> str:
41
41
  return "\n".join(lines)
42
42
 
43
43
 
44
+ def _render_libraries(spec: SkillSpec) -> str:
45
+ """
46
+ 渲染推荐的第三方库信息(公共逻辑)。
47
+ 所有 target 都应该展示三方库信息。
48
+ """
49
+ if not hasattr(spec, 'libraries') or not spec.libraries:
50
+ return ""
51
+
52
+ lines = []
53
+ lines.append("**推荐的第三方库**:")
54
+ lines.append("")
55
+
56
+ for lib in spec.libraries:
57
+ if isinstance(lib, dict):
58
+ lib_name = lib.get('name', '')
59
+ lib_purpose = lib.get('purpose', '')
60
+ lib_pypi = lib.get('pypi_link', '')
61
+ lib_docs = lib.get('docs_link', '')
62
+
63
+ lines.append(f"- `{lib_name}`: {lib_purpose}")
64
+ if lib_pypi:
65
+ lines.append(f" - 安装: `pip install {lib_name}`")
66
+ lines.append(f" - PyPI: {lib_pypi}")
67
+ if lib_docs:
68
+ lines.append(f" - 文档: {lib_docs}")
69
+ else:
70
+ lines.append(f"- `{lib}`")
71
+
72
+ lines.append("")
73
+
74
+ # 生成一键安装命令
75
+ lib_names = []
76
+ for lib in spec.libraries:
77
+ if isinstance(lib, dict):
78
+ lib_names.append(lib.get('name', ''))
79
+ else:
80
+ lib_names.append(str(lib))
81
+
82
+ if lib_names:
83
+ lines.append("```bash")
84
+ lines.append("# 安装所有依赖")
85
+ lines.append("pip install " + " ".join(lib_names))
86
+ lines.append("```")
87
+ lines.append("")
88
+
89
+ return "\n".join(lines)
90
+
91
+
92
+ def _render_prerequisites(spec: SkillSpec, include_python_check: bool = True) -> str:
93
+ """
94
+ 渲染 Prerequisites 部分(公共逻辑)。
95
+
96
+ Args:
97
+ spec: SkillSpec 对象
98
+ include_python_check: 是否包含 Python 版本检查
99
+
100
+ Returns:
101
+ Prerequisites 部分的 Markdown 文本
102
+ """
103
+ has_prerequisites = spec.scripts or (hasattr(spec, 'libraries') and spec.libraries)
104
+ if not has_prerequisites:
105
+ return ""
106
+
107
+ lines = []
108
+ lines.append("## Prerequisites")
109
+ lines.append("")
110
+
111
+ # Python 版本检查(可选)
112
+ if include_python_check and spec.scripts:
113
+ lines.append("```bash")
114
+ lines.append("python3 --version || python --version")
115
+ lines.append("```")
116
+ lines.append("")
117
+
118
+ # 推荐的第三方库
119
+ lib_section = _render_libraries(spec)
120
+ if lib_section:
121
+ lines.append(lib_section)
122
+
123
+ return "\n".join(lines)
124
+
125
+
44
126
  def _render_footer() -> str:
45
127
  ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
46
128
  return f"\n---\nGenerated by neo-skill at {ts}.\n"
@@ -1,11 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from ..spec.model import SkillSpec
4
+ from .common import _render_prerequisites
4
5
 
5
6
 
6
7
  def render_cursor_command_md(spec: SkillSpec) -> str:
7
8
  # Cursor's command palette prompts are typically Markdown instructions.
8
9
  lines = [f"# {spec.name}", "", spec.description, ""]
10
+
11
+ # Prerequisites (三方库信息)
12
+ prerequisites = _render_prerequisites(spec, include_python_check=False)
13
+ if prerequisites:
14
+ lines.append(prerequisites)
15
+
9
16
  if spec.triggers:
10
17
  lines.append("## When to use")
11
18
  lines.extend([f"- {t}" for t in spec.triggers])
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from ..spec.model import SkillSpec
4
4
  from ..util.frontmatter import dump_frontmatter
5
- from .common import _render_steps, _render_resources, _render_footer
5
+ from .common import _render_steps, _render_resources, _render_footer, _render_prerequisites
6
6
 
7
7
 
8
8
  def render_github_skill_md(spec: SkillSpec) -> str:
@@ -22,6 +22,12 @@ def render_github_skill_md(spec: SkillSpec) -> str:
22
22
  body.append("## Overview")
23
23
  body.append(spec.description)
24
24
  body.append("")
25
+
26
+ # Prerequisites (三方库信息)
27
+ prerequisites = _render_prerequisites(spec, include_python_check=False)
28
+ if prerequisites:
29
+ body.append(prerequisites)
30
+
25
31
  if spec.triggers:
26
32
  body.append("## Triggers")
27
33
  for t in spec.triggers:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from ..spec.model import SkillSpec
4
+ from .common import _render_prerequisites
4
5
 
5
6
 
6
7
  def render_windsurf_workflow_md(spec: SkillSpec) -> str:
@@ -26,16 +27,10 @@ def render_windsurf_workflow_md(spec: SkillSpec) -> str:
26
27
  "",
27
28
  ]
28
29
 
29
- # Prerequisites (if scripts exist)
30
- if spec.scripts:
31
- lines.extend([
32
- "## Prerequisites",
33
- "",
34
- "```bash",
35
- "python3 --version || python --version",
36
- "```",
37
- "",
38
- ])
30
+ # Prerequisites (使用公共逻辑)
31
+ prerequisites = _render_prerequisites(spec, include_python_check=True)
32
+ if prerequisites:
33
+ lines.append(prerequisites)
39
34
 
40
35
  # Triggers
41
36
  if spec.triggers: