create-dwy 0.16.0 → 0.17.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/CHANGELOG.md +17 -0
- package/bin/index.js +1 -1
- package/package.json +3 -2
- package/src/searchable-select.js +419 -0
- package/src/sync-all.js +313 -65
- package/src/sync-global-skills.js +167 -0
- package/src/sync.js +59 -43
- package/templates/ai-tools/CHANGELOG.md +210 -0
- package/templates/ai-tools/VERSION +1 -0
- package/templates/ai-tools/hook-manifests/hooks.json +7 -0
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
- package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
- package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
- package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
- package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +65 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +43 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +17 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +16 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +16 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/scripts/appstore_config.sh +140 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +82 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +2 -2
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/assets/cover-style-master-v1.png +0 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates/topic-backlog.template.md +50 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates//351/200/211/351/242/230/346/261/240.template.md +16 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates/channel-profile.template.md +52 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates//344/272/247/345/223/201/345/215/241.template.md +132 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/build_facts_html.py +430 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/templates//344/272/213/345/256/236/350/265/204/346/226/231/345/272/223.template.md +55 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//344/272/213/345/256/236.template.md +82 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//345/215/225/351/233/206/346/226/207/346/241/243.template.md +175 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//350/204/232/346/234/254.template.md +69 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//351/252/250/346/236/266-v1.1.md +174 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/345/214/205.template.md +1 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/350/257/264/346/230/216.md +15 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//346/210/220/347/211/207/345/214/205.template.md +49 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow//346/211/247/350/241/214/346/270/205/345/215/225.template.md +60 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/knowledge-town-paper-film-v1.json +80 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/sticker-asset-production.md +30 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/build_cue_timeline.py +75 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_cue_timeline.py +68 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_style_preset.py +22 -0
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
package/src/sync-all.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { confirm, isCancel
|
|
3
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
4
|
+
import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SKILL_DESTINATIONS,
|
|
7
|
+
copySkillsToGlobalDirs,
|
|
8
|
+
copySkillsToProjectPlatforms,
|
|
9
|
+
listSkillDestOptions,
|
|
10
|
+
normalizeSkillScope,
|
|
11
|
+
resolveProjectSkillDir,
|
|
12
|
+
} from './sync-global-skills.js'
|
|
4
13
|
import { chalk } from './utils.js'
|
|
5
14
|
import {
|
|
6
15
|
interactiveSelect,
|
|
@@ -27,10 +36,10 @@ import { syncOpenCode } from './sync-opencode.js'
|
|
|
27
36
|
import { ensureSkillsInstalled } from './skills-install.js'
|
|
28
37
|
|
|
29
38
|
const PLATFORM_OPTIONS = [
|
|
30
|
-
{ value: 'claude', label: 'Claude Code',
|
|
31
|
-
{ value: 'codex', label: 'Codex',
|
|
32
|
-
{ value: 'cursor', label: 'Cursor',
|
|
33
|
-
{ value: 'opencode', label: 'OpenCode',
|
|
39
|
+
{ value: 'claude', label: 'Claude Code', description: '支持 rules / skills / commands / hooks' },
|
|
40
|
+
{ value: 'codex', label: 'Codex', description: '支持 rules / skills / hooks' },
|
|
41
|
+
{ value: 'cursor', label: 'Cursor', description: '支持 rules' },
|
|
42
|
+
{ value: 'opencode', label: 'OpenCode', description: '支持 rules / skills / commands' },
|
|
34
43
|
]
|
|
35
44
|
|
|
36
45
|
const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
|
|
@@ -54,6 +63,24 @@ const TYPE_LABELS = {
|
|
|
54
63
|
hooks: 'hooks',
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
/** 完整同步:skills / rules / commands / hooks 都走平台写入 */
|
|
67
|
+
export const SYNC_MODE_ALL = 'all'
|
|
68
|
+
/** 仅 Skills:不写、不删 rules / commands / hooks */
|
|
69
|
+
export const SYNC_MODE_SKILLS = 'skills'
|
|
70
|
+
export const DEFAULT_SYNC_MODE = SYNC_MODE_ALL
|
|
71
|
+
|
|
72
|
+
const SKILL_ONLY_CATEGORIES = [{ key: 'skills', label: 'Skills' }]
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 规范化同步范围。未知值回退完整同步,避免误进仅 Skills 删不该删的东西。
|
|
76
|
+
*
|
|
77
|
+
* @param {unknown} raw
|
|
78
|
+
* @returns {'all' | 'skills'}
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeSyncMode(raw) {
|
|
81
|
+
return raw === SYNC_MODE_SKILLS ? SYNC_MODE_SKILLS : DEFAULT_SYNC_MODE
|
|
82
|
+
}
|
|
83
|
+
|
|
57
84
|
function unionSets(...sets) {
|
|
58
85
|
return new Set(sets.flatMap(set => [...set]))
|
|
59
86
|
}
|
|
@@ -143,6 +170,132 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
|
|
|
143
170
|
return defaults.length > 0 ? defaults : ['claude', 'codex']
|
|
144
171
|
}
|
|
145
172
|
|
|
173
|
+
/**
|
|
174
|
+
* 开头选同步范围。仅 Skills 时后面不再问 rules / commands / hooks。
|
|
175
|
+
*
|
|
176
|
+
* @param {'all' | 'skills'} initialValue
|
|
177
|
+
* @returns {Promise<'all' | 'skills' | null>}
|
|
178
|
+
*/
|
|
179
|
+
async function promptSyncMode(initialValue) {
|
|
180
|
+
const result = await searchableSelect({
|
|
181
|
+
message: '选择同步范围',
|
|
182
|
+
options: [
|
|
183
|
+
{
|
|
184
|
+
value: SYNC_MODE_ALL,
|
|
185
|
+
label: '完整同步',
|
|
186
|
+
description: '同步 Skills / Rules / Commands / Hooks',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
value: SYNC_MODE_SKILLS,
|
|
190
|
+
label: '仅 Skills',
|
|
191
|
+
description: '只处理 skill,不改动已有 Rules / Commands / Hooks',
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
initialValue,
|
|
195
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
196
|
+
})
|
|
197
|
+
if (isCancel(result)) return null
|
|
198
|
+
return normalizeSyncMode(result)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
|
|
203
|
+
* 禁止调用完整平台 sync,否则空 rules/commands 会删掉项目已有项。
|
|
204
|
+
*
|
|
205
|
+
* @param {{ projectDir: string, selectedPlatforms: string[], skills: Array<{ name: string, sourcePath: string }>, templateSkillNames: Set<string>, staleSkillNames: Set<string>, skillScope: { destinations: string[] } }} opts
|
|
206
|
+
*/
|
|
207
|
+
async function syncSkillsOnly({
|
|
208
|
+
projectDir,
|
|
209
|
+
selectedPlatforms,
|
|
210
|
+
skills,
|
|
211
|
+
templateSkillNames,
|
|
212
|
+
staleSkillNames,
|
|
213
|
+
skillScope,
|
|
214
|
+
}) {
|
|
215
|
+
const writeProject = skillScope.destinations.includes('project')
|
|
216
|
+
if (!writeProject) return
|
|
217
|
+
|
|
218
|
+
const skillPlatforms = selectedPlatforms.filter(platform => resolveProjectSkillDir(projectDir, platform))
|
|
219
|
+
if (skillPlatforms.length === 0) {
|
|
220
|
+
console.log(chalk.yellow('已选平台不支持项目内 skills,跳过项目写入。'))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(chalk.blue('\nSyncing skills only (rules / commands / hooks untouched)...\n'))
|
|
225
|
+
await copySkillsToProjectPlatforms({
|
|
226
|
+
projectDir,
|
|
227
|
+
platforms: skillPlatforms,
|
|
228
|
+
skills,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const selectedNames = new Set(skills.map(item => item.name))
|
|
232
|
+
const toRemove = new Set(
|
|
233
|
+
[...templateSkillNames, ...staleSkillNames].filter(name => !selectedNames.has(name)),
|
|
234
|
+
)
|
|
235
|
+
for (const platform of skillPlatforms) {
|
|
236
|
+
const destRoot = resolveProjectSkillDir(projectDir, platform)
|
|
237
|
+
const existingNames = await scanExisting(path.dirname(destRoot), 'skills')
|
|
238
|
+
await removeManagedItems(
|
|
239
|
+
destRoot,
|
|
240
|
+
existingNames,
|
|
241
|
+
toRemove,
|
|
242
|
+
path.relative(projectDir, destRoot),
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 交互最后一步:选 skill 同步位置,默认仅项目。
|
|
249
|
+
* 勾了全局 dest 后再问哪些 skill 额外拷到全局;不删用户 home 里其它 skill。
|
|
250
|
+
*
|
|
251
|
+
* @param {{ selectedSkills: Array<{ name: string }>, syncState: { skillScope?: object } }} args
|
|
252
|
+
* @returns {Promise<{ destinations: string[], globalSkills: string[] } | null>}
|
|
253
|
+
*/
|
|
254
|
+
async function promptSkillScope({ selectedSkills, syncState }) {
|
|
255
|
+
const previous = normalizeSkillScope(syncState.skillScope)
|
|
256
|
+
const destInitial = previous.destinations.length > 0
|
|
257
|
+
? previous.destinations
|
|
258
|
+
: [...DEFAULT_SKILL_DESTINATIONS]
|
|
259
|
+
|
|
260
|
+
console.log(chalk.gray('\nSkill 默认同步到项目目录。勾选全局目录可把部分 skill 额外拷到本机 home。'))
|
|
261
|
+
|
|
262
|
+
const destResult = await searchableMultiselect({
|
|
263
|
+
message: '选择 skill 同步位置(默认项目)',
|
|
264
|
+
options: listSkillDestOptions(),
|
|
265
|
+
initialValues: destInitial,
|
|
266
|
+
required: true,
|
|
267
|
+
maxItems: 8,
|
|
268
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
269
|
+
})
|
|
270
|
+
if (isCancel(destResult)) return null
|
|
271
|
+
|
|
272
|
+
const destinations = destResult
|
|
273
|
+
const globalDests = destinations.filter(id => id !== 'project')
|
|
274
|
+
if (globalDests.length === 0 || selectedSkills.length === 0) {
|
|
275
|
+
return { destinations, globalSkills: [] }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const selectedNames = selectedSkills.map(item => item.name)
|
|
279
|
+
const globalInitial = previous.globalSkills.filter(name => selectedNames.includes(name))
|
|
280
|
+
const globalResult = await searchableMultiselect({
|
|
281
|
+
message: '哪些 skill 额外同步到全局?',
|
|
282
|
+
options: selectedSkills.map(item => ({
|
|
283
|
+
value: item.name,
|
|
284
|
+
label: item.name,
|
|
285
|
+
description: item.description,
|
|
286
|
+
})),
|
|
287
|
+
initialValues: globalInitial,
|
|
288
|
+
required: false,
|
|
289
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
290
|
+
})
|
|
291
|
+
if (isCancel(globalResult)) return null
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
destinations,
|
|
295
|
+
globalSkills: globalResult,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
146
299
|
async function selectPlatforms(projectDir) {
|
|
147
300
|
const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
|
|
148
301
|
|
|
@@ -152,12 +305,14 @@ async function selectPlatforms(projectDir) {
|
|
|
152
305
|
console.log(chalk.gray(' Cursor → rules'))
|
|
153
306
|
console.log(chalk.gray(' OpenCode → rules / skills / commands\n'))
|
|
154
307
|
|
|
155
|
-
|
|
308
|
+
// 可搜索多选 + 底部说明区
|
|
309
|
+
const result = await searchableMultiselect({
|
|
156
310
|
message: '选择要同步的平台',
|
|
157
311
|
options: PLATFORM_OPTIONS,
|
|
158
312
|
initialValues,
|
|
159
313
|
required: true,
|
|
160
314
|
maxItems: 6,
|
|
315
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
161
316
|
})
|
|
162
317
|
if (isCancel(result)) return null
|
|
163
318
|
return result
|
|
@@ -194,6 +349,8 @@ async function loadSyncState(projectDir) {
|
|
|
194
349
|
return {
|
|
195
350
|
version: 1,
|
|
196
351
|
platforms: {},
|
|
352
|
+
skillScope: normalizeSkillScope(undefined),
|
|
353
|
+
syncMode: DEFAULT_SYNC_MODE,
|
|
197
354
|
}
|
|
198
355
|
}
|
|
199
356
|
|
|
@@ -201,6 +358,8 @@ async function loadSyncState(projectDir) {
|
|
|
201
358
|
return {
|
|
202
359
|
version: 1,
|
|
203
360
|
platforms: state.platforms || {},
|
|
361
|
+
skillScope: normalizeSkillScope(state.skillScope),
|
|
362
|
+
syncMode: normalizeSyncMode(state.syncMode),
|
|
204
363
|
}
|
|
205
364
|
}
|
|
206
365
|
|
|
@@ -310,11 +469,12 @@ async function promptStaleRemovals(staleEntries) {
|
|
|
310
469
|
console.log(chalk.gray(` - ${name}`))
|
|
311
470
|
}
|
|
312
471
|
|
|
313
|
-
const selected = await
|
|
472
|
+
const selected = await searchableMultiselect({
|
|
314
473
|
message: `选择要删除的 ${PLATFORM_LABELS[platform]} ${TYPE_LABELS[type]}`,
|
|
315
474
|
options: names.map(name => ({ value: name, label: name })),
|
|
316
475
|
initialValues: names,
|
|
317
476
|
required: false,
|
|
477
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
318
478
|
})
|
|
319
479
|
if (isCancel(selected)) return null
|
|
320
480
|
approvals[platform][type] = new Set(selected)
|
|
@@ -324,6 +484,19 @@ async function promptStaleRemovals(staleEntries) {
|
|
|
324
484
|
return approvals
|
|
325
485
|
}
|
|
326
486
|
|
|
487
|
+
/**
|
|
488
|
+
* 仅 Skills 模式:陈旧项提示只保留 skills,避免问 rules/commands/hooks。
|
|
489
|
+
*
|
|
490
|
+
* @param {Record<string, Record<string, string[]>>} staleEntries
|
|
491
|
+
*/
|
|
492
|
+
function retainSkillStaleEntries(staleEntries) {
|
|
493
|
+
const next = createEmptyRemovalState()
|
|
494
|
+
for (const [platform, types] of Object.entries(staleEntries)) {
|
|
495
|
+
if (types.skills?.length) next[platform] = { skills: types.skills }
|
|
496
|
+
}
|
|
497
|
+
return next
|
|
498
|
+
}
|
|
499
|
+
|
|
327
500
|
function getPreservedStaleNames(staleEntries, approvedRemovals, platform, type) {
|
|
328
501
|
const staleNames = staleEntries[platform]?.[type] || []
|
|
329
502
|
const approvedNames = approvedRemovals[platform]?.[type] || new Set()
|
|
@@ -552,6 +725,12 @@ export async function syncAll({
|
|
|
552
725
|
selected: selectedOverride,
|
|
553
726
|
selectedPlatforms: selectedPlatformsOverride,
|
|
554
727
|
staleRemovals: staleRemovalsOverride,
|
|
728
|
+
// 测试/脚本可注入;交互模式在平台选择之后再问
|
|
729
|
+
skillScope: skillScopeOverride,
|
|
730
|
+
// all = 完整同步;skills = 只写 skill,测试/脚本可注入
|
|
731
|
+
syncMode: syncModeOverride,
|
|
732
|
+
// 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
|
|
733
|
+
homeDir,
|
|
555
734
|
} = {}) {
|
|
556
735
|
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
557
736
|
if (!await fs.pathExists(sourceDir)) {
|
|
@@ -601,7 +780,20 @@ export async function syncAll({
|
|
|
601
780
|
console.log(chalk.gray('首次同步,进入交互式选择...\n'))
|
|
602
781
|
}
|
|
603
782
|
|
|
604
|
-
const
|
|
783
|
+
const skipInteractive = !!(selectedOverride || selectedPlatformsOverride)
|
|
784
|
+
const syncMode = syncModeOverride
|
|
785
|
+
? normalizeSyncMode(syncModeOverride)
|
|
786
|
+
: (skipInteractive
|
|
787
|
+
? normalizeSyncMode(syncState.syncMode)
|
|
788
|
+
: await promptSyncMode(normalizeSyncMode(syncState.syncMode)))
|
|
789
|
+
if (syncMode === null) {
|
|
790
|
+
console.log(chalk.yellow('\n已取消同步。'))
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
|
|
795
|
+
categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
|
|
796
|
+
})
|
|
605
797
|
if (selected === null) {
|
|
606
798
|
console.log(chalk.yellow('\n已取消同步。'))
|
|
607
799
|
return
|
|
@@ -613,7 +805,10 @@ export async function syncAll({
|
|
|
613
805
|
return
|
|
614
806
|
}
|
|
615
807
|
|
|
616
|
-
const
|
|
808
|
+
const staleEntriesRaw = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
|
|
809
|
+
const staleEntries = syncMode === SYNC_MODE_SKILLS
|
|
810
|
+
? retainSkillStaleEntries(staleEntriesRaw)
|
|
811
|
+
: staleEntriesRaw
|
|
617
812
|
const approvedStaleRemovals = staleRemovalsOverride
|
|
618
813
|
? normalizeRemovalInput(staleRemovalsOverride)
|
|
619
814
|
: await promptStaleRemovals(staleEntries)
|
|
@@ -622,79 +817,132 @@ export async function syncAll({
|
|
|
622
817
|
return
|
|
623
818
|
}
|
|
624
819
|
|
|
820
|
+
const skillScope = skillScopeOverride
|
|
821
|
+
? normalizeSkillScope(skillScopeOverride)
|
|
822
|
+
: (selectedOverride
|
|
823
|
+
? normalizeSkillScope(syncState.skillScope)
|
|
824
|
+
: await promptSkillScope({ selectedSkills: selected.skills || [], syncState }))
|
|
825
|
+
if (skillScope === null) {
|
|
826
|
+
console.log(chalk.yellow('\n已取消同步。'))
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const selectedSkillNames = new Set((selected.skills || []).map(item => item.name))
|
|
831
|
+
const skillsToWrite = scans.skills.filter(item => selectedSkillNames.has(item.name))
|
|
625
832
|
let syncedAnySupportedPlatform = false
|
|
626
833
|
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
834
|
+
if (syncMode === SYNC_MODE_SKILLS) {
|
|
835
|
+
const staleSkillNames = new Set()
|
|
836
|
+
for (const platform of selectedPlatforms) {
|
|
837
|
+
for (const name of approvedStaleRemovals[platform]?.skills || []) staleSkillNames.add(name)
|
|
838
|
+
}
|
|
839
|
+
await syncSkillsOnly({
|
|
632
840
|
projectDir,
|
|
633
|
-
|
|
634
|
-
|
|
841
|
+
selectedPlatforms,
|
|
842
|
+
skills: skillsToWrite,
|
|
843
|
+
templateSkillNames: new Set(scans.skills.map(item => item.name)),
|
|
844
|
+
staleSkillNames,
|
|
845
|
+
skillScope,
|
|
635
846
|
})
|
|
636
|
-
|
|
847
|
+
syncedAnySupportedPlatform = selectedPlatforms.some(platform => resolveProjectSkillDir(projectDir, platform))
|
|
848
|
+
|| skillScope.destinations.some(id => id !== 'project')
|
|
849
|
+
} else {
|
|
850
|
+
if (selectedPlatforms.includes('claude')) {
|
|
851
|
+
syncedAnySupportedPlatform = true
|
|
852
|
+
console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
|
|
853
|
+
await syncClaude({
|
|
854
|
+
sourceDir,
|
|
855
|
+
projectDir,
|
|
856
|
+
selected,
|
|
857
|
+
staleRemovals: approvedStaleRemovals.claude,
|
|
858
|
+
})
|
|
859
|
+
}
|
|
637
860
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
861
|
+
if (selectedPlatforms.includes('codex')) {
|
|
862
|
+
syncedAnySupportedPlatform = true
|
|
863
|
+
console.log(chalk.blue('\nSyncing Codex configuration...\n'))
|
|
864
|
+
await syncCodex({
|
|
865
|
+
sourceDir,
|
|
866
|
+
projectDir,
|
|
867
|
+
selected,
|
|
868
|
+
staleRemovals: approvedStaleRemovals.codex,
|
|
869
|
+
preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
|
|
870
|
+
})
|
|
871
|
+
}
|
|
649
872
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
873
|
+
if (selectedPlatforms.includes('cursor')) {
|
|
874
|
+
syncedAnySupportedPlatform = true
|
|
875
|
+
await syncCursor({
|
|
876
|
+
sourceDir,
|
|
877
|
+
projectDir,
|
|
878
|
+
selected,
|
|
879
|
+
staleRemovals: approvedStaleRemovals.cursor,
|
|
880
|
+
})
|
|
881
|
+
}
|
|
659
882
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
883
|
+
if (selectedPlatforms.includes('opencode')) {
|
|
884
|
+
syncedAnySupportedPlatform = true
|
|
885
|
+
await syncOpenCode({
|
|
886
|
+
sourceDir,
|
|
887
|
+
projectDir,
|
|
888
|
+
selected,
|
|
889
|
+
staleRemovals: approvedStaleRemovals.opencode,
|
|
890
|
+
preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
|
|
891
|
+
})
|
|
892
|
+
}
|
|
670
893
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
894
|
+
if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
|
|
895
|
+
await clearProjectAgentsMd(projectDir)
|
|
896
|
+
}
|
|
674
897
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
898
|
+
if (!selectedPlatforms.includes('claude')) {
|
|
899
|
+
await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
|
|
900
|
+
}
|
|
678
901
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
902
|
+
if (!selectedPlatforms.includes('cursor')) {
|
|
903
|
+
await cleanupCursorPlatform(projectDir, scans, baseline)
|
|
904
|
+
}
|
|
682
905
|
|
|
683
|
-
|
|
684
|
-
|
|
906
|
+
if (!selectedPlatforms.includes('codex')) {
|
|
907
|
+
await cleanupCodexPlatform(projectDir, scans, codexManagedHookNames)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!selectedPlatforms.includes('opencode')) {
|
|
911
|
+
await cleanupOpenCodePlatform(projectDir, scans)
|
|
912
|
+
}
|
|
685
913
|
}
|
|
686
914
|
|
|
687
|
-
|
|
688
|
-
|
|
915
|
+
const globalSkillNameSet = new Set(skillScope.globalSkills)
|
|
916
|
+
const globalSkills = scans.skills.filter(item => globalSkillNameSet.has(item.name))
|
|
917
|
+
if (globalSkills.length > 0 && skillScope.destinations.some(id => id !== 'project')) {
|
|
918
|
+
console.log(chalk.blue('\nSyncing selected skills to global directories...\n'))
|
|
919
|
+
await copySkillsToGlobalDirs({
|
|
920
|
+
skills: globalSkills,
|
|
921
|
+
destIds: skillScope.destinations,
|
|
922
|
+
homeDir,
|
|
923
|
+
})
|
|
689
924
|
}
|
|
690
925
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
syncState.platforms[platform]
|
|
694
|
-
|
|
695
|
-
syncState.platforms[platform] =
|
|
926
|
+
if (syncMode === SYNC_MODE_SKILLS) {
|
|
927
|
+
for (const platform of selectedPlatforms) {
|
|
928
|
+
const previous = syncState.platforms[platform] || createEmptyPlatformState()
|
|
929
|
+
const skillsOnlySelected = { skills: selected.skills || [], rules: [], commands: [], hooks: [] }
|
|
930
|
+
syncState.platforms[platform] = {
|
|
931
|
+
...previous,
|
|
932
|
+
skills: buildNextPlatformState(skillsOnlySelected, staleEntries, approvedStaleRemovals, platform).skills,
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
|
|
937
|
+
if (selectedPlatforms.includes(platform)) {
|
|
938
|
+
syncState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
|
|
939
|
+
} else {
|
|
940
|
+
syncState.platforms[platform] = createEmptyPlatformState()
|
|
941
|
+
}
|
|
696
942
|
}
|
|
697
943
|
}
|
|
944
|
+
syncState.skillScope = skillScope
|
|
945
|
+
syncState.syncMode = syncMode
|
|
698
946
|
await saveSyncState(projectDir, syncState)
|
|
699
947
|
|
|
700
948
|
if (!syncedAnySupportedPlatform) {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将已选 skill 额外拷到用户全局目录。
|
|
3
|
+
* 项目目录同步仍由各平台 sync 负责;本模块只处理 ~/.claude / ~/.grok 等 home 下目录。
|
|
4
|
+
* 只覆盖本次点名的 skill,不删除全局目录里其它个人 skill。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs-extra'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import { copyFilter, logAction } from './sync.js'
|
|
11
|
+
|
|
12
|
+
/** 项目目录:随所选平台落到 .claude/skills、.agents/skills 等 */
|
|
13
|
+
export const SKILL_DEST_PROJECT = 'project'
|
|
14
|
+
|
|
15
|
+
/** 默认同步位置:只写项目,不碰用户 home */
|
|
16
|
+
export const DEFAULT_SKILL_DESTINATIONS = [SKILL_DEST_PROJECT]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 全局 dest id → home 下相对路径。
|
|
20
|
+
* 路径跟各工具官方发现目录对齐;测试通过 homeDir 注入,避免写真实 $HOME。
|
|
21
|
+
*/
|
|
22
|
+
const GLOBAL_SKILL_DIR_SEGMENTS = {
|
|
23
|
+
claude: ['.claude', 'skills'],
|
|
24
|
+
grok: ['.grok', 'skills'],
|
|
25
|
+
agents: ['.agents', 'skills'],
|
|
26
|
+
cursor: ['.cursor', 'skills'],
|
|
27
|
+
opencode: ['.config', 'opencode', 'skills'],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const GLOBAL_SKILL_DEST_IDS = Object.keys(GLOBAL_SKILL_DIR_SEGMENTS)
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 项目内 skill 目录(按已选平台)。Cursor 无 skills,不列入。
|
|
34
|
+
* 仅 Skills 模式用这张表写项目,避免走完整 sync 误改 rules/commands/hooks。
|
|
35
|
+
*/
|
|
36
|
+
export const PROJECT_SKILL_DIR_BY_PLATFORM = {
|
|
37
|
+
claude: ['.claude', 'skills'],
|
|
38
|
+
codex: ['.agents', 'skills'],
|
|
39
|
+
opencode: ['.opencode', 'skills'],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 解析某平台在项目内的 skills 目录。不支持的平台返回 null。
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectDir
|
|
46
|
+
* @param {string} platform
|
|
47
|
+
*/
|
|
48
|
+
export function resolveProjectSkillDir(projectDir, platform) {
|
|
49
|
+
const segments = PROJECT_SKILL_DIR_BY_PLATFORM[platform]
|
|
50
|
+
if (!segments) return null
|
|
51
|
+
return path.join(projectDir, ...segments)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEST_LABELS = {
|
|
55
|
+
project: '项目目录',
|
|
56
|
+
claude: 'Claude Code 全局',
|
|
57
|
+
grok: 'Grok 全局',
|
|
58
|
+
agents: 'Codex / 跨运行时全局',
|
|
59
|
+
cursor: 'Cursor 全局',
|
|
60
|
+
opencode: 'OpenCode 全局',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 规范化交互/缓存里的 skill 同步范围。
|
|
65
|
+
* 未知 dest、空 skill 名丢弃;无 destinations 时回退到仅项目。
|
|
66
|
+
*
|
|
67
|
+
* @param {{ destinations?: string[], globalSkills?: string[] } | null | undefined} raw
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeSkillScope(raw) {
|
|
70
|
+
const allowed = new Set([SKILL_DEST_PROJECT, ...GLOBAL_SKILL_DEST_IDS])
|
|
71
|
+
const destinations = (raw?.destinations || [])
|
|
72
|
+
.filter(id => allowed.has(id))
|
|
73
|
+
const globalSkills = [...new Set((raw?.globalSkills || []).filter(name => typeof name === 'string' && name.trim()))]
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
destinations: destinations.length > 0 ? destinations : [...DEFAULT_SKILL_DESTINATIONS],
|
|
77
|
+
globalSkills,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 解析全局 dest 的绝对目录。project 返回 null(由平台 sync 写项目)。
|
|
83
|
+
*
|
|
84
|
+
* @param {string} destId
|
|
85
|
+
* @param {string} [homeDir]
|
|
86
|
+
*/
|
|
87
|
+
export function resolveGlobalSkillDir(destId, homeDir = os.homedir()) {
|
|
88
|
+
const segments = GLOBAL_SKILL_DIR_SEGMENTS[destId]
|
|
89
|
+
if (!segments) return null
|
|
90
|
+
return path.join(homeDir, ...segments)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 最后一步 UI 的位置选项。project 放第一项,默认勾选。
|
|
95
|
+
*
|
|
96
|
+
* @param {string} [homeDir]
|
|
97
|
+
* @returns {Array<{ value: string, label: string, description: string }>}
|
|
98
|
+
*/
|
|
99
|
+
export function listSkillDestOptions(homeDir = os.homedir()) {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
value: SKILL_DEST_PROJECT,
|
|
103
|
+
label: DEST_LABELS.project,
|
|
104
|
+
description: '默认。随上面所选平台写入项目 .claude/skills、.agents/skills 等',
|
|
105
|
+
},
|
|
106
|
+
...GLOBAL_SKILL_DEST_IDS.map(id => {
|
|
107
|
+
const dir = resolveGlobalSkillDir(id, homeDir)
|
|
108
|
+
return {
|
|
109
|
+
value: id,
|
|
110
|
+
label: DEST_LABELS[id],
|
|
111
|
+
description: dir,
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 把 skill 目录拷到已选全局 dest。destIds 含 project 时跳过(项目侧已同步)。
|
|
119
|
+
* 只写传入的 skills,不清理目标根目录里其它条目。
|
|
120
|
+
*
|
|
121
|
+
* @param {{ skills: Array<{ name: string, sourcePath: string }>, destIds: string[], homeDir?: string }} opts
|
|
122
|
+
* @returns {Promise<number>} 写入次数(每个 dest × 每个 skill)
|
|
123
|
+
*/
|
|
124
|
+
export async function copySkillsToGlobalDirs({ skills, destIds, homeDir = os.homedir() }) {
|
|
125
|
+
let count = 0
|
|
126
|
+
|
|
127
|
+
for (const destId of destIds) {
|
|
128
|
+
const destRoot = resolveGlobalSkillDir(destId, homeDir)
|
|
129
|
+
if (!destRoot) continue
|
|
130
|
+
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
const dest = path.join(destRoot, skill.name)
|
|
133
|
+
await fs.ensureDir(path.dirname(dest))
|
|
134
|
+
await fs.copy(skill.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
135
|
+
logAction(`${DEST_LABELS[destId] || destId}/${skill.name}`)
|
|
136
|
+
count++
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return count
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 仅 Skills 模式:把 skill 拷到项目内已选平台的 skills 目录。
|
|
145
|
+
* 不写 rules / commands / hooks。
|
|
146
|
+
*
|
|
147
|
+
* @param {{ projectDir: string, platforms: string[], skills: Array<{ name: string, sourcePath: string }> }} opts
|
|
148
|
+
* @returns {Promise<number>}
|
|
149
|
+
*/
|
|
150
|
+
export async function copySkillsToProjectPlatforms({ projectDir, platforms, skills }) {
|
|
151
|
+
let count = 0
|
|
152
|
+
|
|
153
|
+
for (const platform of platforms) {
|
|
154
|
+
const destRoot = resolveProjectSkillDir(projectDir, platform)
|
|
155
|
+
if (!destRoot) continue
|
|
156
|
+
|
|
157
|
+
for (const skill of skills) {
|
|
158
|
+
const dest = path.join(destRoot, skill.name)
|
|
159
|
+
await fs.ensureDir(path.dirname(dest))
|
|
160
|
+
await fs.copy(skill.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
161
|
+
logAction(`${path.relative(projectDir, destRoot)}/${skill.name}`)
|
|
162
|
+
count++
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return count
|
|
167
|
+
}
|