create-dwy 0.16.1 → 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 (104) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/index.js +1 -1
  3. package/package.json +1 -1
  4. package/src/sync-all.js +303 -59
  5. package/src/sync-global-skills.js +167 -0
  6. package/src/sync.js +21 -11
  7. package/templates/ai-tools/CHANGELOG.md +134 -0
  8. package/templates/ai-tools/VERSION +1 -1
  9. package/templates/ai-tools/rules/Android/dwy-android-core.md +65 -1
  10. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  11. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  12. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  13. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  21. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  22. 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
  23. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  24. 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
  25. 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
  26. 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
  27. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  29. 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
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  31. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  32. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  33. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  34. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  35. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  36. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  37. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  38. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  39. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  40. 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
  41. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  42. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  43. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  44. 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
  45. 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
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  47. 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
  48. 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
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  50. 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
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  53. 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
  54. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  56. 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
  57. 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
  58. 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
  59. 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
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  61. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  62. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  64. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  65. 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
  66. 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
  67. 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
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  70. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  71. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  72. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  73. 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
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  76. 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
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  79. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  80. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  81. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  82. 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
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  84. 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
  85. 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
  86. 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
  87. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +0 -511
  88. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +0 -126
  89. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +0 -88
  90. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +0 -195
  91. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +0 -179
  92. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +0 -39
  93. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +0 -278
  94. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +0 -147
  95. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +0 -230
  96. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +0 -64
  97. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +0 -60
  98. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +0 -209
  99. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +0 -100
  100. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +0 -157
  101. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +0 -265
  102. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +0 -94
  103. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +0 -56
  104. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +0 -105
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `dwy` 交互同步开头增加「同步范围」:完整同步(默认)或仅 Skills。仅 Skills 不写、不删 Rules / Commands / Hooks。
8
+ - 最后增加「Skill 同步位置」:默认仅项目目录;可另选 Claude / Grok / Codex / Cursor / OpenCode 全局目录,并勾选要额外拷贝的 skill。只覆盖点名 skill,不删用户 home 里其它个人 skill。选择写入 `/.dwy/sync-state.json` 的 `skillScope` / `syncMode`。
9
+
10
+ ---
11
+
3
12
  ## 0.16.1
4
13
 
5
14
  ### Patch Changes
package/bin/index.js CHANGED
@@ -15,7 +15,7 @@ function printHelp() {
15
15
  `dwy ${pkg.version}`,
16
16
  '',
17
17
  'Usage:',
18
- ' dwy 运行交互式同步(自动更新全局外部 skill)',
18
+ ' dwy 交互同步:先选完整/仅 Skills,末步可选全局目录',
19
19
  ' dwy --help 显示本说明',
20
20
  ' dwy -h 显示本说明',
21
21
  ' dwy --version 显示当前版本',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync-all.js CHANGED
@@ -1,7 +1,15 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { confirm, isCancel } from '@clack/prompts'
4
- import { SEARCH_PLACEHOLDER, searchableMultiselect } from './searchable-select.js'
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'
5
13
  import { chalk } from './utils.js'
6
14
  import {
7
15
  interactiveSelect,
@@ -55,6 +63,24 @@ const TYPE_LABELS = {
55
63
  hooks: 'hooks',
56
64
  }
57
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
+
58
84
  function unionSets(...sets) {
59
85
  return new Set(sets.flatMap(set => [...set]))
60
86
  }
@@ -144,6 +170,132 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
144
170
  return defaults.length > 0 ? defaults : ['claude', 'codex']
145
171
  }
146
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
+
147
299
  async function selectPlatforms(projectDir) {
148
300
  const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
149
301
 
@@ -197,6 +349,8 @@ async function loadSyncState(projectDir) {
197
349
  return {
198
350
  version: 1,
199
351
  platforms: {},
352
+ skillScope: normalizeSkillScope(undefined),
353
+ syncMode: DEFAULT_SYNC_MODE,
200
354
  }
201
355
  }
202
356
 
@@ -204,6 +358,8 @@ async function loadSyncState(projectDir) {
204
358
  return {
205
359
  version: 1,
206
360
  platforms: state.platforms || {},
361
+ skillScope: normalizeSkillScope(state.skillScope),
362
+ syncMode: normalizeSyncMode(state.syncMode),
207
363
  }
208
364
  }
209
365
 
@@ -328,6 +484,19 @@ async function promptStaleRemovals(staleEntries) {
328
484
  return approvals
329
485
  }
330
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
+
331
500
  function getPreservedStaleNames(staleEntries, approvedRemovals, platform, type) {
332
501
  const staleNames = staleEntries[platform]?.[type] || []
333
502
  const approvedNames = approvedRemovals[platform]?.[type] || new Set()
@@ -556,6 +725,12 @@ export async function syncAll({
556
725
  selected: selectedOverride,
557
726
  selectedPlatforms: selectedPlatformsOverride,
558
727
  staleRemovals: staleRemovalsOverride,
728
+ // 测试/脚本可注入;交互模式在平台选择之后再问
729
+ skillScope: skillScopeOverride,
730
+ // all = 完整同步;skills = 只写 skill,测试/脚本可注入
731
+ syncMode: syncModeOverride,
732
+ // 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
733
+ homeDir,
559
734
  } = {}) {
560
735
  const sourceDir = sourceDirOverride || await resolveSourceDir()
561
736
  if (!await fs.pathExists(sourceDir)) {
@@ -605,7 +780,20 @@ export async function syncAll({
605
780
  console.log(chalk.gray('首次同步,进入交互式选择...\n'))
606
781
  }
607
782
 
608
- 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
+ })
609
797
  if (selected === null) {
610
798
  console.log(chalk.yellow('\n已取消同步。'))
611
799
  return
@@ -617,7 +805,10 @@ export async function syncAll({
617
805
  return
618
806
  }
619
807
 
620
- 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
621
812
  const approvedStaleRemovals = staleRemovalsOverride
622
813
  ? normalizeRemovalInput(staleRemovalsOverride)
623
814
  : await promptStaleRemovals(staleEntries)
@@ -626,79 +817,132 @@ export async function syncAll({
626
817
  return
627
818
  }
628
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))
629
832
  let syncedAnySupportedPlatform = false
630
833
 
631
- if (selectedPlatforms.includes('claude')) {
632
- syncedAnySupportedPlatform = true
633
- console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
634
- await syncClaude({
635
- 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({
636
840
  projectDir,
637
- selected,
638
- staleRemovals: approvedStaleRemovals.claude,
841
+ selectedPlatforms,
842
+ skills: skillsToWrite,
843
+ templateSkillNames: new Set(scans.skills.map(item => item.name)),
844
+ staleSkillNames,
845
+ skillScope,
639
846
  })
640
- }
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
+ }
641
860
 
642
- if (selectedPlatforms.includes('codex')) {
643
- syncedAnySupportedPlatform = true
644
- console.log(chalk.blue('\nSyncing Codex configuration...\n'))
645
- await syncCodex({
646
- sourceDir,
647
- projectDir,
648
- selected,
649
- staleRemovals: approvedStaleRemovals.codex,
650
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
651
- })
652
- }
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
+ }
653
872
 
654
- if (selectedPlatforms.includes('cursor')) {
655
- syncedAnySupportedPlatform = true
656
- await syncCursor({
657
- sourceDir,
658
- projectDir,
659
- selected,
660
- staleRemovals: approvedStaleRemovals.cursor,
661
- })
662
- }
873
+ if (selectedPlatforms.includes('cursor')) {
874
+ syncedAnySupportedPlatform = true
875
+ await syncCursor({
876
+ sourceDir,
877
+ projectDir,
878
+ selected,
879
+ staleRemovals: approvedStaleRemovals.cursor,
880
+ })
881
+ }
663
882
 
664
- if (selectedPlatforms.includes('opencode')) {
665
- syncedAnySupportedPlatform = true
666
- await syncOpenCode({
667
- sourceDir,
668
- projectDir,
669
- selected,
670
- staleRemovals: approvedStaleRemovals.opencode,
671
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
672
- })
673
- }
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
+ }
674
893
 
675
- if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
676
- await clearProjectAgentsMd(projectDir)
677
- }
894
+ if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
895
+ await clearProjectAgentsMd(projectDir)
896
+ }
678
897
 
679
- if (!selectedPlatforms.includes('claude')) {
680
- await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
681
- }
898
+ if (!selectedPlatforms.includes('claude')) {
899
+ await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
900
+ }
682
901
 
683
- if (!selectedPlatforms.includes('cursor')) {
684
- await cleanupCursorPlatform(projectDir, scans, baseline)
685
- }
902
+ if (!selectedPlatforms.includes('cursor')) {
903
+ await cleanupCursorPlatform(projectDir, scans, baseline)
904
+ }
686
905
 
687
- if (!selectedPlatforms.includes('codex')) {
688
- 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
+ }
689
913
  }
690
914
 
691
- if (!selectedPlatforms.includes('opencode')) {
692
- 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
+ })
693
924
  }
694
925
 
695
- for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
696
- if (selectedPlatforms.includes(platform)) {
697
- syncState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
698
- } else {
699
- 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
+ }
700
942
  }
701
943
  }
944
+ syncState.skillScope = skillScope
945
+ syncState.syncMode = syncMode
702
946
  await saveSyncState(projectDir, syncState)
703
947
 
704
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
+ }