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.
Files changed (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/index.js +1 -1
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/sync-all.js +313 -65
  6. package/src/sync-global-skills.js +167 -0
  7. package/src/sync.js +59 -43
  8. package/templates/ai-tools/CHANGELOG.md +210 -0
  9. package/templates/ai-tools/VERSION +1 -0
  10. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  11. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
  13. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  14. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  15. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  17. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  18. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  19. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  20. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  21. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  22. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  23. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  32. 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
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  34. 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
  35. 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
  36. 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
  37. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  38. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  39. 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
  40. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  41. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  42. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  43. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  44. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  45. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  47. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  48. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  50. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  53. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  54. 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
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  56. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  57. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  58. 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
  59. 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
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  61. 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
  62. 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
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  64. 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
  65. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  66. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  67. 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
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  70. 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
  71. 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
  72. 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
  73. 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
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  76. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  79. 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
  80. 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
  81. 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
  82. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  84. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  85. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  86. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  87. 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
  88. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  89. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  90. 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
  91. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  92. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  93. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  94. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  95. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  96. 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
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  98. 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
  99. 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
  100. 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
  101. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  102. 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, multiselect } from '@clack/prompts'
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', hint: '支持 rules / skills / commands / hooks' },
31
- { value: 'codex', label: 'Codex', hint: '支持 rules / skills / hooks' },
32
- { value: 'cursor', label: 'Cursor', hint: '支持 rules' },
33
- { value: 'opencode', label: 'OpenCode', hint: '支持 rules / skills / commands' },
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
- const result = await multiselect({
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 multiselect({
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 selected = selectedOverride || await interactiveSelect(scans, selectionDefaults)
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 staleEntries = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
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 (selectedPlatforms.includes('claude')) {
628
- syncedAnySupportedPlatform = true
629
- console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
630
- await syncClaude({
631
- sourceDir,
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
- selected,
634
- staleRemovals: approvedStaleRemovals.claude,
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
- if (selectedPlatforms.includes('codex')) {
639
- syncedAnySupportedPlatform = true
640
- console.log(chalk.blue('\nSyncing Codex configuration...\n'))
641
- await syncCodex({
642
- sourceDir,
643
- projectDir,
644
- selected,
645
- staleRemovals: approvedStaleRemovals.codex,
646
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
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
- if (selectedPlatforms.includes('cursor')) {
651
- syncedAnySupportedPlatform = true
652
- await syncCursor({
653
- sourceDir,
654
- projectDir,
655
- selected,
656
- staleRemovals: approvedStaleRemovals.cursor,
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
- if (selectedPlatforms.includes('opencode')) {
661
- syncedAnySupportedPlatform = true
662
- await syncOpenCode({
663
- sourceDir,
664
- projectDir,
665
- selected,
666
- staleRemovals: approvedStaleRemovals.opencode,
667
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
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
- if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
672
- await clearProjectAgentsMd(projectDir)
673
- }
894
+ if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
895
+ await clearProjectAgentsMd(projectDir)
896
+ }
674
897
 
675
- if (!selectedPlatforms.includes('claude')) {
676
- await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
677
- }
898
+ if (!selectedPlatforms.includes('claude')) {
899
+ await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
900
+ }
678
901
 
679
- if (!selectedPlatforms.includes('cursor')) {
680
- await cleanupCursorPlatform(projectDir, scans, baseline)
681
- }
902
+ if (!selectedPlatforms.includes('cursor')) {
903
+ await cleanupCursorPlatform(projectDir, scans, baseline)
904
+ }
682
905
 
683
- if (!selectedPlatforms.includes('codex')) {
684
- await cleanupCodexPlatform(projectDir, scans, codexManagedHookNames)
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
- if (!selectedPlatforms.includes('opencode')) {
688
- await cleanupOpenCodePlatform(projectDir, scans)
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
- for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
692
- if (selectedPlatforms.includes(platform)) {
693
- syncState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
694
- } else {
695
- syncState.platforms[platform] = createEmptyPlatformState()
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
+ }