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.
- package/.shared/skill-creator/data_packs/tools/README.md +16 -11
- package/package.json +1 -1
- package/skills/skill-creator/references/architecture-rules.md +204 -0
- package/src/omni_skill/README.md +49 -0
- package/src/omni_skill/cli.py +64 -1
- package/src/omni_skill/doctor.py +323 -0
- package/src/omni_skill/install.py +352 -0
- package/src/skill_creator/core/builder.py +16 -0
- package/src/skill_creator/core/orchestrator.py +12 -0
- package/src/skill_creator/spec/model.py +2 -0
- package/src/skill_creator/targets/README.md +22 -1
- package/src/skill_creator/targets/claude.py +6 -1
- package/src/skill_creator/targets/common.py +83 -1
- package/src/skill_creator/targets/cursor.py +7 -0
- package/src/skill_creator/targets/github_skills.py +7 -1
- package/src/skill_creator/targets/windsurf.py +5 -10
|
@@ -43,17 +43,22 @@
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
1.
|
|
49
|
-
2.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
@@ -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`
|
package/src/omni_skill/README.md
CHANGED
|
@@ -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
|
|
package/src/omni_skill/cli.py
CHANGED
|
@@ -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 (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|