create-dwy 0.19.0 → 0.20.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 (117) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +1 -1
  3. package/src/cli-help.js +28 -2
  4. package/src/prompt-style.js +82 -0
  5. package/src/searchable-select.js +12 -29
  6. package/src/sync-all.js +95 -583
  7. package/src/sync-codex.js +14 -7
  8. package/src/sync-opencode.js +8 -22
  9. package/src/sync.js +60 -149
  10. package/src/tab-multiselect.js +21 -64
  11. package/templates/ai-tools/CHANGELOG.md +195 -0
  12. package/templates/ai-tools/CLAUDE.md +39 -58
  13. package/templates/ai-tools/VERSION +1 -1
  14. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +5 -4
  15. package/templates/ai-tools/rules/Android/dwy-android-core.md +147 -13
  16. package/templates/ai-tools/rules/Android/dwy-android-layering.md +142 -0
  17. package/templates/ai-tools/rules/Android/dwy-android-mvp.md +55 -0
  18. package/templates/ai-tools/rules/Android/dwy-android-support-only.md +2 -0
  19. package/templates/ai-tools/rules/Docker/dwy-docker.md +23 -1
  20. package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +2 -0
  21. package/templates/ai-tools/rules/Flutter/dwy-flutter-testing.md +1 -1
  22. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +12 -8
  23. package/templates/ai-tools/rules/Python/dwy-python-backend.md +4 -43
  24. package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +1 -1
  25. package/templates/ai-tools/rules/Python/dwy-python-layering.md +169 -0
  26. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +11 -49
  27. package/templates/ai-tools/rules/Vue/dwy-vue-layering.md +153 -0
  28. package/templates/ai-tools/rules/iOS/dwy-apple-layering.md +133 -0
  29. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +2 -0
  30. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-craft.md +83 -0
  31. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +27 -66
  32. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +8 -219
  33. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +27 -123
  34. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prod-branch.md +31 -0
  35. package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-postgres.md +17 -10
  36. package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-redis.md +6 -3
  37. package/templates/ai-tools/skills/Docker/dwy-deploy-first/SKILL.md +14 -14
  38. package/templates/ai-tools/skills/Docker/dwy-deploy-first/references/chapter-01-mirrors.md +2 -2
  39. package/templates/ai-tools/skills/Docker/dwy-docker/SKILL.md +21 -13
  40. package/templates/ai-tools/skills/Docker/dwy-docker/references/ask-templates.md +13 -12
  41. package/templates/ai-tools/skills/Docker/dwy-docker/references/templates.md +3 -1
  42. package/templates/ai-tools/skills/Docker/dwy-docker/references/version-rules.md +10 -6
  43. package/templates/ai-tools/skills/Docker/dwy-docker/scripts/query_dockerhub.py +21 -6
  44. package/templates/ai-tools/skills/Docker/dwy-docker/scripts/scan_images.sh +2 -2
  45. package/templates/ai-tools/skills/Docker/dwy-mirror-source/SKILL.md +44 -30
  46. package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/dockerfile-snippets.md +2 -0
  47. package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/mirror-providers.md +8 -8
  48. package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/apply_mirrors.py +14 -1
  49. package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/check_mirrors.py +4 -0
  50. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/SKILL.md +8 -6
  51. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/python-sdk.md +3 -3
  52. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/stream-engine.md +2 -2
  53. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/anti-patterns.md +3 -1
  54. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/change-checklist.md +4 -4
  55. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/lethal-violations.md +5 -5
  56. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/perf-baselines.md +5 -5
  57. package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +59 -200
  58. package/templates/ai-tools/skills/Python/dwy-eapi/shared/anticorruption.md +13 -0
  59. package/templates/ai-tools/skills/Python/dwy-eapi/shared/layering.md +5 -0
  60. package/templates/ai-tools/skills/Python/dwy-eapi/shared/response-envelope.md +20 -0
  61. package/templates/ai-tools/skills/Python/dwy-eapi/shared/timezone.md +7 -0
  62. package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +15 -0
  63. package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +43 -0
  64. package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +103 -0
  65. package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +49 -0
  66. package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +13 -0
  67. package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +62 -306
  68. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +8 -1
  69. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +0 -31
  70. package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +8 -0
  71. package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +6 -0
  72. package/templates/ai-tools/skills/Vue/dwy-eui/shared/overlay.md +6 -0
  73. package/templates/ai-tools/skills/Vue/dwy-eui/shared/reka.md +5 -0
  74. package/templates/ai-tools/skills/Vue/dwy-eui/v1/index.md +8 -0
  75. package/templates/ai-tools/skills/Vue/dwy-eui/v2/breaking.md +14 -0
  76. package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +55 -0
  77. package/templates/ai-tools/skills/Vue/dwy-eui/v2/lookup.md +29 -0
  78. package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +12 -0
  79. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +11 -14
  80. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +1 -1
  81. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +1 -1
  82. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +32 -15
  83. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/electron-release-workflow-template.yml +3 -1
  84. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/release-workflow-template.yml +8 -5
  85. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +1 -1
  86. 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 +1 -1
  87. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/aso.md +2 -0
  88. 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 +1 -1
  89. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +4 -3
  90. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +1 -1
  91. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +1 -1
  92. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +45 -27
  93. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +1 -1
  94. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +1 -1
  95. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +1 -1
  96. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +1 -1
  97. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +1 -1
  98. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +8 -0
  99. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +7 -7
  100. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +63 -59
  101. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_base.sh +10 -1
  102. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_capacity.sh +21 -23
  103. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_db.sh +9 -1
  104. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +20 -24
  105. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_logs.sh +3 -3
  106. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +4 -2
  107. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +1 -1
  108. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +1 -0
  109. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +7 -5
  110. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +5 -3
  111. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +140 -0
  112. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +113 -0
  113. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +16 -13
  114. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
  115. package/src/sync-packs.js +0 -223
  116. package/src/sync-scene.js +0 -178
  117. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
package/src/sync-all.js CHANGED
@@ -1,23 +1,19 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
+ import os from 'node:os'
3
4
  import { confirm, isCancel } from '@clack/prompts'
4
5
  import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
5
6
  import {
6
- DEFAULT_SKILL_DESTINATIONS,
7
7
  copySkillsToGlobalDirs,
8
8
  copySkillsToProjectPlatforms,
9
- listSkillDestOptions,
10
9
  normalizeSkillScope,
11
10
  resolveProjectSkillDir,
12
11
  } from './sync-global-skills.js'
13
12
  import { chalk } from './utils.js'
14
13
  import {
15
14
  interactiveSelect,
16
- loadHookManifests,
17
15
  logAction,
18
- readBaselineDoc,
19
16
  resolveSourceDir,
20
- scanCommands,
21
17
  scanExisting,
22
18
  scanHooks,
23
19
  scanRules,
@@ -25,7 +21,6 @@ import {
25
21
  syncClaude,
26
22
  } from './sync.js'
27
23
  import {
28
- clearProjectAgentsMd,
29
24
  parseManagedRuleNames,
30
25
  scanExistingCodexHooks,
31
26
  scanExistingCodexSkills,
@@ -35,23 +30,20 @@ import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
35
30
  import { syncOpenCode } from './sync-opencode.js'
36
31
  import { ensureSkillsInstalled, installSkills } from './skills-install.js'
37
32
  import { selfUpgrade } from './self-upgrade.js'
38
- import { normalizePacks } from './sync-packs.js'
39
- import { interactiveSelectByPacks } from './sync-scene.js'
40
33
 
41
34
  const PLATFORM_OPTIONS = [
42
- { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / commands / hooks' },
35
+ { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / hooks' },
43
36
  { value: 'codex', label: 'Codex', description: '支持 rules / skills / hooks' },
44
37
  { value: 'cursor', label: 'Cursor', description: '支持 rules' },
45
- { value: 'opencode', label: 'OpenCode', description: '支持 rules / skills / commands' },
38
+ { value: 'opencode', label: 'OpenCode', description: '支持 rules / skills' },
46
39
  ]
47
40
 
48
- const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
49
41
  const SYNC_STATE_PATH = ['.dwy', 'sync-state.json']
50
42
  const PLATFORM_SYNC_TYPES = {
51
- claude: ['skills', 'rules', 'commands', 'hooks'],
43
+ claude: ['skills', 'rules', 'hooks'],
52
44
  codex: ['skills', 'rules', 'hooks'],
53
45
  cursor: ['rules'],
54
- opencode: ['skills', 'rules', 'commands'],
46
+ opencode: ['skills', 'rules'],
55
47
  }
56
48
  const PLATFORM_LABELS = {
57
49
  claude: 'Claude Code',
@@ -59,25 +51,12 @@ const PLATFORM_LABELS = {
59
51
  cursor: 'Cursor',
60
52
  opencode: 'OpenCode',
61
53
  }
62
- const TYPE_LABELS = {
63
- skills: 'skills',
64
- rules: 'rules',
65
- commands: 'commands',
66
- hooks: 'hooks',
67
- }
68
-
69
- /** 完整同步:skills / rules / commands / hooks 都走平台写入 */
54
+ /** 完整同步:逐项选择 skills / rules,hooks 自动启用。 */
70
55
  export const SYNC_MODE_ALL = 'all'
71
56
  /** 仅 Skills:不写、不删 rules / commands / hooks */
72
57
  export const SYNC_MODE_SKILLS = 'skills'
73
58
  export const DEFAULT_SYNC_MODE = SYNC_MODE_ALL
74
59
 
75
- /** 按条目勾选 Skills / Rules / Hooks */
76
- export const SELECTION_STYLE_ITEMS = 'items'
77
- /** 按技术栈包 + 场景包勾选 */
78
- export const SELECTION_STYLE_PACKS = 'packs'
79
- export const DEFAULT_SELECTION_STYLE = SELECTION_STYLE_PACKS
80
-
81
60
  /** 同步项目配置(skills / rules / hooks) */
82
61
  export const ACTION_SYNC = 'sync'
83
62
  /** 强制刷新 ~/.dwy/skills 外部 skill */
@@ -85,8 +64,6 @@ export const ACTION_INSTALL_SKILLS = 'install-skills'
85
64
  /** 把全局 create-dwy 升到 npm latest */
86
65
  export const ACTION_UPGRADE = 'upgrade'
87
66
 
88
- const SKILL_ONLY_CATEGORIES = [{ key: 'skills', label: 'Skills' }]
89
-
90
67
  /**
91
68
  * 规范化同步范围。未知值回退完整同步,避免误进仅 Skills 删不该删的东西。
92
69
  *
@@ -97,16 +74,6 @@ export function normalizeSyncMode(raw) {
97
74
  return raw === SYNC_MODE_SKILLS ? SYNC_MODE_SKILLS : DEFAULT_SYNC_MODE
98
75
  }
99
76
 
100
- /**
101
- * 规范化勾选方式。未知值回退按包,避免新用户落到 80+ 条逐项勾选。
102
- *
103
- * @param {unknown} raw
104
- * @returns {'items' | 'packs'}
105
- */
106
- export function normalizeSelectionStyle(raw) {
107
- return raw === SELECTION_STYLE_ITEMS ? SELECTION_STYLE_ITEMS : DEFAULT_SELECTION_STYLE
108
- }
109
-
110
77
  /**
111
78
  * 规范化入口动作。未知值回退同步,避免误进强制重装。
112
79
  *
@@ -124,7 +91,7 @@ function unionSets(...sets) {
124
91
  }
125
92
 
126
93
  function hasSetItems(setsByType) {
127
- return setsByType.skills.size + setsByType.rules.size + setsByType.commands.size + setsByType.hooks.size > 0
94
+ return Object.values(setsByType).some(items => items.size > 0)
128
95
  }
129
96
 
130
97
  export function buildSelectionDefaultsFromSyncState(existing, syncState) {
@@ -160,7 +127,6 @@ async function scanCombinedExisting(projectDir) {
160
127
  await scanExisting(claudeDir, 'rules'),
161
128
  parseManagedRuleNames(agentsMdContent),
162
129
  ),
163
- commands: await scanExisting(claudeDir, 'commands'),
164
130
  hooks: unionSets(
165
131
  await scanExisting(claudeDir, 'hooks'),
166
132
  await scanExistingCodexHooks(projectDir),
@@ -168,23 +134,6 @@ async function scanCombinedExisting(projectDir) {
168
134
  }
169
135
  }
170
136
 
171
- async function shouldInitialize(projectDir, existing) {
172
- const hasAnyExisting = hasSetItems(existing)
173
- if (hasAnyExisting) return true
174
-
175
- const hasConfigDir = await fs.pathExists(path.join(projectDir, '.claude'))
176
- || await fs.pathExists(path.join(projectDir, '.codex'))
177
- || await fs.pathExists(path.join(projectDir, '.agents'))
178
-
179
- if (hasConfigDir) return true
180
-
181
- const ok = await confirm({
182
- message: `当前目录 ${chalk.yellow(projectDir)} 尚无 Claude Code/Codex 配置,是否在此初始化?`,
183
- initialValue: false,
184
- })
185
- return !isCancel(ok) && ok
186
- }
187
-
188
137
  export async function buildPlatformDefaultsFromLocalState(projectDir) {
189
138
  const defaults = []
190
139
 
@@ -208,93 +157,9 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
208
157
  return defaults.length > 0 ? defaults : ['claude', 'codex']
209
158
  }
210
159
 
211
- /**
212
- * dwy 入口第一问:同步全局 skills,或同步项目配置。
213
- * 自升级只走 `dwy upgrade`,不出现在这张菜单。
214
- *
215
- * @returns {Promise<'sync' | 'install-skills' | null>}
216
- */
217
- async function promptAction() {
218
- const result = await searchableSelect({
219
- message: '选择要做的事',
220
- options: [
221
- {
222
- value: ACTION_INSTALL_SKILLS,
223
- label: '同步全局 skills',
224
- description: '强制重装 ~/.dwy/skills(pm-skills / superpowers)',
225
- },
226
- {
227
- value: ACTION_SYNC,
228
- label: '同步项目配置',
229
- description: '按场景和技术栈,或一条条勾选,写入当前项目',
230
- },
231
- ],
232
- initialValue: ACTION_SYNC,
233
- placeholder: SEARCH_PLACEHOLDER,
234
- })
235
- if (isCancel(result)) return null
236
- return normalizeAction(result)
237
- }
238
-
239
- /**
240
- * 开头选同步范围。仅 Skills 时后面不再问 rules / commands / hooks。
241
- *
242
- * @param {'all' | 'skills'} initialValue
243
- * @returns {Promise<'all' | 'skills' | null>}
244
- */
245
- async function promptSyncMode(initialValue) {
246
- const result = await searchableSelect({
247
- message: '选择同步范围',
248
- options: [
249
- {
250
- value: SYNC_MODE_ALL,
251
- label: '完整同步',
252
- description: '同步 Skills / Rules / Commands / Hooks',
253
- },
254
- {
255
- value: SYNC_MODE_SKILLS,
256
- label: '仅 Skills',
257
- description: '只处理 skill,不改动已有 Rules / Commands / Hooks',
258
- },
259
- ],
260
- initialValue,
261
- placeholder: SEARCH_PLACEHOLDER,
262
- })
263
- if (isCancel(result)) return null
264
- return normalizeSyncMode(result)
265
- }
266
-
267
- /**
268
- * 选勾选方式。按包是默认推荐;按条目保留原来的逐项勾选。
269
- *
270
- * @param {'items' | 'packs'} initialValue
271
- * @returns {Promise<'items' | 'packs' | null>}
272
- */
273
- async function promptSelectionStyle(initialValue) {
274
- const result = await searchableSelect({
275
- message: '项目配置怎么勾',
276
- options: [
277
- {
278
- value: SELECTION_STYLE_PACKS,
279
- label: '按场景和技术栈(推荐)',
280
- description: '先选栈和场景包,再展示已勾子条目,可取消',
281
- },
282
- {
283
- value: SELECTION_STYLE_ITEMS,
284
- label: '一个一个选',
285
- description: '一条条勾选 Skills / Rules / Hooks',
286
- },
287
- ],
288
- initialValue,
289
- placeholder: SEARCH_PLACEHOLDER,
290
- })
291
- if (isCancel(result)) return null
292
- return normalizeSelectionStyle(result)
293
- }
294
-
295
160
  /**
296
161
  * 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
297
- * 禁止调用完整平台 sync,否则空 rules/commands 会删掉项目已有项。
162
+ * 禁止调用完整平台 sync,否则空 rules/hooks 会删掉项目已有项。
298
163
  *
299
164
  * @param {{ projectDir: string, selectedPlatforms: string[], skills: Array<{ name: string, sourcePath: string }>, templateSkillNames: Set<string>, staleSkillNames: Set<string>, skillScope: { destinations: string[] } }} opts
300
165
  */
@@ -338,72 +203,15 @@ async function syncSkillsOnly({
338
203
  }
339
204
  }
340
205
 
341
- /**
342
- * 交互最后一步:选 skill 同步位置,默认仅项目。
343
- * 勾了全局 dest 后再问哪些 skill 额外拷到全局;不删用户 home 里其它 skill。
344
- *
345
- * @param {{ selectedSkills: Array<{ name: string }>, syncState: { skillScope?: object } }} args
346
- * @returns {Promise<{ destinations: string[], globalSkills: string[] } | null>}
347
- */
348
- async function promptSkillScope({ selectedSkills, syncState }) {
349
- const previous = normalizeSkillScope(syncState.skillScope)
350
- const destInitial = previous.destinations.length > 0
351
- ? previous.destinations
352
- : [...DEFAULT_SKILL_DESTINATIONS]
353
-
354
- console.log(chalk.gray('\nSkill 默认同步到项目目录。勾选全局目录可把部分 skill 额外拷到本机 home。'))
355
-
356
- const destResult = await searchableMultiselect({
357
- message: '选择 skill 同步位置(默认项目)',
358
- options: listSkillDestOptions(),
359
- initialValues: destInitial,
360
- required: true,
361
- maxItems: 8,
362
- placeholder: SEARCH_PLACEHOLDER,
363
- })
364
- if (isCancel(destResult)) return null
365
-
366
- const destinations = destResult
367
- const globalDests = destinations.filter(id => id !== 'project')
368
- if (globalDests.length === 0 || selectedSkills.length === 0) {
369
- return { destinations, globalSkills: [] }
370
- }
371
-
372
- const selectedNames = selectedSkills.map(item => item.name)
373
- const globalInitial = previous.globalSkills.filter(name => selectedNames.includes(name))
374
- const globalResult = await searchableMultiselect({
375
- message: '哪些 skill 额外同步到全局?',
376
- options: selectedSkills.map(item => ({
377
- value: item.name,
378
- label: item.name,
379
- description: item.description,
380
- })),
381
- initialValues: globalInitial,
382
- required: false,
383
- placeholder: SEARCH_PLACEHOLDER,
384
- })
385
- if (isCancel(globalResult)) return null
386
-
387
- return {
388
- destinations,
389
- globalSkills: globalResult,
390
- }
391
- }
392
-
393
- async function selectPlatforms(projectDir) {
206
+ async function selectPlatforms(projectDir, global) {
394
207
  const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
395
208
 
396
- console.log(chalk.gray('\n平台能力预览:'))
397
- console.log(chalk.gray(' Claude Code → rules / skills / commands / hooks'))
398
- console.log(chalk.gray(' Codex → rules / skills / hooks'))
399
- console.log(chalk.gray(' Cursor → rules'))
400
- console.log(chalk.gray(' OpenCode → rules / skills / commands\n'))
401
-
209
+ if (global) console.log(chalk.gray('Cursor 全局规则通过应用设置管理,本次不提供文件同步。'))
402
210
  // 可搜索多选 + 底部说明区
403
211
  const result = await searchableMultiselect({
404
212
  message: '选择要同步的平台',
405
- options: PLATFORM_OPTIONS,
406
- initialValues,
213
+ options: PLATFORM_OPTIONS.filter(option => !global || option.value !== 'cursor'),
214
+ initialValues: initialValues.filter(value => !global || value !== 'cursor'),
407
215
  required: true,
408
216
  maxItems: 6,
409
217
  placeholder: SEARCH_PLACEHOLDER,
@@ -416,18 +224,6 @@ function buildCursorRuleName(ruleName) {
416
224
  return `dwy-${ruleName.replace(/\.md$/i, '')}.mdc`
417
225
  }
418
226
 
419
- function buildCursorBaselineContent(baseline) {
420
- return [
421
- '---',
422
- `description: ${JSON.stringify('[dwy] Global baseline')}`,
423
- 'alwaysApply: true',
424
- '---',
425
- '',
426
- baseline.trim(),
427
- '',
428
- ].join('\n')
429
- }
430
-
431
227
  function createEmptyPlatformState() {
432
228
  return {
433
229
  skills: [],
@@ -445,8 +241,6 @@ async function loadSyncState(projectDir) {
445
241
  platforms: {},
446
242
  skillScope: normalizeSkillScope(undefined),
447
243
  syncMode: DEFAULT_SYNC_MODE,
448
- selectionStyle: DEFAULT_SELECTION_STYLE,
449
- packs: normalizePacks(undefined),
450
244
  }
451
245
  }
452
246
 
@@ -456,8 +250,6 @@ async function loadSyncState(projectDir) {
456
250
  platforms: state.platforms || {},
457
251
  skillScope: normalizeSkillScope(state.skillScope),
458
252
  syncMode: normalizeSyncMode(state.syncMode),
459
- selectionStyle: normalizeSelectionStyle(state.selectionStyle),
460
- packs: normalizePacks(state.packs),
461
253
  }
462
254
  }
463
255
 
@@ -476,17 +268,16 @@ function getCurrentTemplateNames(scans) {
476
268
  }
477
269
  }
478
270
 
479
- async function collectCurrentManagedNames(projectDir) {
271
+ async function collectCurrentManagedNames(projectDir, global = false) {
480
272
  const claudeDir = path.join(projectDir, '.claude')
481
- const openCodeDir = path.join(projectDir, '.opencode')
482
- const agentsMdPath = path.join(projectDir, 'AGENTS.md')
273
+ const openCodeDir = global ? path.join(projectDir, '.config', 'opencode') : path.join(projectDir, '.opencode')
274
+ const agentsMdPath = global ? path.join(projectDir, '.codex', 'AGENTS.md') : path.join(projectDir, 'AGENTS.md')
483
275
  const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
484
276
 
485
277
  return {
486
278
  claude: {
487
279
  skills: await scanExisting(claudeDir, 'skills'),
488
280
  rules: await scanExisting(claudeDir, 'rules'),
489
- commands: await scanExisting(claudeDir, 'commands'),
490
281
  hooks: await scanExisting(claudeDir, 'hooks'),
491
282
  },
492
283
  codex: {
@@ -499,8 +290,7 @@ async function collectCurrentManagedNames(projectDir) {
499
290
  },
500
291
  opencode: {
501
292
  skills: await scanExisting(openCodeDir, 'skills'),
502
- rules: parseManagedRuleNames(agentsMdContent),
503
- commands: await scanExisting(openCodeDir, 'commands'),
293
+ rules: global ? parseManagedRuleNames(await fs.pathExists(path.join(openCodeDir, 'AGENTS.md')) ? await fs.readFile(path.join(openCodeDir, 'AGENTS.md'), 'utf-8') : '') : parseManagedRuleNames(agentsMdContent),
504
294
  },
505
295
  }
506
296
  }
@@ -522,9 +312,9 @@ function createEmptyRemovalState() {
522
312
  )
523
313
  }
524
314
 
525
- async function collectStaleEntries(projectDir, scans, syncState, selectedPlatforms) {
315
+ async function collectStaleEntries(projectDir, scans, syncState, selectedPlatforms, global = false) {
526
316
  const currentTemplateNames = getCurrentTemplateNames(scans)
527
- const currentManaged = await collectCurrentManagedNames(projectDir)
317
+ const currentManaged = await collectCurrentManagedNames(projectDir, global)
528
318
  const staleEntries = createEmptyRemovalState()
529
319
 
530
320
  for (const platform of selectedPlatforms) {
@@ -554,34 +344,6 @@ async function collectStaleEntries(projectDir, scans, syncState, selectedPlatfor
554
344
  return staleEntries
555
345
  }
556
346
 
557
- async function promptStaleRemovals(staleEntries) {
558
- const approvals = createEmptyRemovalState()
559
-
560
- for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
561
- for (const type of PLATFORM_SYNC_TYPES[platform]) {
562
- const names = staleEntries[platform]?.[type] || []
563
- if (names.length === 0) continue
564
-
565
- console.log(chalk.yellow(`\n检测到 ${PLATFORM_LABELS[platform]} 已同步但模板仓库已移除的 ${TYPE_LABELS[type]}:`))
566
- for (const name of names) {
567
- console.log(chalk.gray(` - ${name}`))
568
- }
569
-
570
- const selected = await searchableMultiselect({
571
- message: `选择要删除的 ${PLATFORM_LABELS[platform]} ${TYPE_LABELS[type]}`,
572
- options: names.map(name => ({ value: name, label: name })),
573
- initialValues: names,
574
- required: false,
575
- placeholder: SEARCH_PLACEHOLDER,
576
- })
577
- if (isCancel(selected)) return null
578
- approvals[platform][type] = new Set(selected)
579
- }
580
- }
581
-
582
- return approvals
583
- }
584
-
585
347
  /**
586
348
  * 仅 Skills 模式:陈旧项提示只保留 skills,避免问 rules/commands/hooks。
587
349
  *
@@ -629,209 +391,15 @@ async function removeManagedItems(targetDir, existingNames, managedNames, labelP
629
391
  return removedCount
630
392
  }
631
393
 
632
- async function removeManagedFileIfExact(filePath, managedContent, label) {
633
- if (!managedContent || !await fs.pathExists(filePath)) return 0
634
- const current = await fs.readFile(filePath, 'utf-8')
635
- if (current !== managedContent) return 0
636
- await fs.remove(filePath)
637
- logAction(label, 'red', '×')
638
- return 1
639
- }
640
-
641
- function isManagedClaudeHookCommand(command, managedHookNames) {
642
- if (typeof command !== 'string') return false
643
- const match = command.match(/\.claude\/hooks\/([^\s'"]+)/)
644
- return !!(match && managedHookNames.has(match[1]))
645
- }
646
-
647
- function isManagedCodexHookCommand(command, managedHookNames) {
648
- if (typeof command !== 'string') return false
649
- const match = command.match(/\.codex\/hooks\/([^\s'"]+)/)
650
- return !!(match && managedHookNames.has(match[1]))
651
- }
652
-
653
- function pruneManagedClaudeHooks(existingHooks, managedHookNames) {
654
- const merged = {}
655
-
656
- for (const [event, configs] of Object.entries(existingHooks || {})) {
657
- const kept = []
658
- for (const cfg of configs || []) {
659
- if (isManagedClaudeHookCommand(cfg.command, managedHookNames)) continue
660
- kept.push(cfg)
661
- }
662
- if (kept.length > 0) merged[event] = kept
663
- }
664
-
665
- return Object.keys(merged).length > 0 ? merged : undefined
666
- }
667
-
668
- function pruneManagedCodexHooks(existingHooks, managedHookNames) {
669
- const merged = {}
670
-
671
- for (const [event, configs] of Object.entries(existingHooks || {})) {
672
- const keptConfigs = []
673
- for (const cfg of configs || []) {
674
- const keptHooks = (cfg.hooks || []).filter(hook => !isManagedCodexHookCommand(hook.command, managedHookNames))
675
- if (keptHooks.length > 0) keptConfigs.push({ ...cfg, hooks: keptHooks })
676
- }
677
- if (keptConfigs.length > 0) merged[event] = keptConfigs
678
- }
679
-
680
- return Object.keys(merged).length > 0 ? merged : undefined
681
- }
682
-
683
- async function cleanupClaudeSettings(projectDir, managedHookNames) {
684
- const settingsPath = path.join(projectDir, '.claude', 'settings.json')
685
- if (!await fs.pathExists(settingsPath)) return 0
686
-
687
- const existing = await fs.readJson(settingsPath)
688
- if (!existing.hooks) return 0
689
-
690
- const nextHooks = pruneManagedClaudeHooks(existing.hooks, managedHookNames)
691
- if (!nextHooks) {
692
- const { hooks, ...rest } = existing
693
- if (Object.keys(rest).length === Object.keys(existing).length) return 0
694
- if (Object.keys(rest).length === 0) {
695
- await fs.remove(settingsPath)
696
- logAction('.claude/settings.json — removed', 'red', '×')
697
- return 1
698
- }
699
- await fs.writeJson(settingsPath, rest, { spaces: 2 })
700
- logAction('.claude/settings.json — cleaned', 'red', '×')
701
- return 1
702
- }
703
-
704
- const next = { ...existing, hooks: nextHooks }
705
- if (JSON.stringify(next) === JSON.stringify(existing)) return 0
706
- await fs.writeJson(settingsPath, next, { spaces: 2 })
707
- logAction('.claude/settings.json — cleaned', 'red', '×')
708
- return 1
709
- }
710
-
711
- async function cleanupCodexHooksJson(projectDir, managedHookNames) {
712
- const hooksPath = path.join(projectDir, '.codex', 'hooks.json')
713
- if (!await fs.pathExists(hooksPath)) return 0
714
-
715
- const existing = await fs.readJson(hooksPath)
716
- if (!existing.hooks) return 0
717
-
718
- const nextHooks = pruneManagedCodexHooks(existing.hooks, managedHookNames)
719
- if (!nextHooks) {
720
- const { hooks, ...rest } = existing
721
- if (Object.keys(rest).length === Object.keys(existing).length) return 0
722
- if (Object.keys(rest).length === 0) {
723
- await fs.remove(hooksPath)
724
- logAction('.codex/hooks.json — removed', 'red', '×')
725
- return 1
726
- }
727
- await fs.writeJson(hooksPath, rest, { spaces: 2 })
728
- logAction('.codex/hooks.json — cleaned', 'red', '×')
729
- return 1
730
- }
731
-
732
- const next = { ...existing, hooks: nextHooks }
733
- if (JSON.stringify(next) === JSON.stringify(existing)) return 0
734
- await fs.writeJson(hooksPath, next, { spaces: 2 })
735
- logAction('.codex/hooks.json — cleaned', 'red', '×')
736
- return 1
737
- }
738
-
739
- async function cleanupClaudePlatform(projectDir, scans, managedHookNames, baseline) {
740
- const claudeDir = path.join(projectDir, '.claude')
741
- const existing = {
742
- skills: await scanExisting(claudeDir, 'skills'),
743
- rules: await scanExisting(claudeDir, 'rules'),
744
- commands: await scanExisting(claudeDir, 'commands'),
745
- hooks: await scanExisting(claudeDir, 'hooks'),
746
- }
747
-
748
- let removedCount = 0
749
- removedCount += await removeManagedItems(path.join(claudeDir, 'skills'), existing.skills, new Set(scans.skills.map(item => item.name)), '.claude/skills')
750
- removedCount += await removeManagedItems(path.join(claudeDir, 'rules'), existing.rules, new Set(scans.rules.map(item => item.name)), '.claude/rules')
751
- removedCount += await removeManagedItems(path.join(claudeDir, 'commands'), existing.commands, new Set(scans.commands.map(item => item.name)), '.claude/commands')
752
- removedCount += await removeManagedItems(path.join(claudeDir, 'hooks'), existing.hooks, new Set(scans.hooks.map(item => item.name)), '.claude/hooks')
753
- removedCount += await cleanupClaudeSettings(projectDir, managedHookNames)
754
- removedCount += await removeManagedFileIfExact(
755
- path.join(claudeDir, 'CLAUDE.md'),
756
- baseline,
757
- '.claude/CLAUDE.md',
758
- )
759
- return removedCount
760
- }
761
-
762
- async function cleanupCursorPlatform(projectDir, scans, baseline) {
763
- const rulesDir = path.join(projectDir, '.cursor', 'rules')
764
- if (!await fs.pathExists(rulesDir)) return 0
765
-
766
- const entries = await fs.readdir(rulesDir, { withFileTypes: true })
767
- const existingManagedNames = new Set(
768
- entries
769
- .filter(entry => entry.isFile())
770
- .map(entry => entry.name)
771
- .filter(name => name === CURSOR_BASELINE_RULE_NAME || scans.rules.some(rule => buildCursorRuleName(rule.name) === name)),
772
- )
773
-
774
- let removedCount = 0
775
- for (const name of existingManagedNames) {
776
- if (name === CURSOR_BASELINE_RULE_NAME) continue
777
- await fs.remove(path.join(rulesDir, name))
778
- logAction(`.cursor/rules/${name}`, 'red', '×')
779
- removedCount++
780
- }
781
- removedCount += await removeManagedFileIfExact(
782
- path.join(rulesDir, CURSOR_BASELINE_RULE_NAME),
783
- baseline ? buildCursorBaselineContent(baseline) : '',
784
- `.cursor/rules/${CURSOR_BASELINE_RULE_NAME}`,
785
- )
786
- return removedCount
787
- }
788
-
789
- async function cleanupCodexPlatform(projectDir, scans, managedHookNames) {
790
- let removedCount = 0
791
- removedCount += await removeManagedItems(
792
- path.join(projectDir, '.agents', 'skills'),
793
- await scanExistingCodexSkills(projectDir),
794
- new Set(scans.skills.map(item => item.name)),
795
- '.agents/skills',
796
- )
797
- removedCount += await removeManagedItems(
798
- path.join(projectDir, '.codex', 'hooks'),
799
- await scanExistingCodexHooks(projectDir),
800
- new Set(scans.hooks.map(item => item.name)),
801
- '.codex/hooks',
802
- )
803
- removedCount += await cleanupCodexHooksJson(projectDir, managedHookNames)
804
- return removedCount
805
- }
806
-
807
- async function cleanupOpenCodePlatform(projectDir, scans) {
808
- const openCodeDir = path.join(projectDir, '.opencode')
809
- const existing = {
810
- skills: await scanExisting(openCodeDir, 'skills'),
811
- commands: await scanExisting(openCodeDir, 'commands'),
812
- }
813
-
814
- let removedCount = 0
815
- removedCount += await removeManagedItems(path.join(openCodeDir, 'skills'), existing.skills, new Set(scans.skills.map(item => item.name)), '.opencode/skills')
816
- removedCount += await removeManagedItems(path.join(openCodeDir, 'commands'), existing.commands, new Set(scans.commands.map(item => item.name)), '.opencode/commands')
817
- return removedCount
818
- }
819
-
820
394
  /**
821
- * `dwy` / `dwy sync` 入口。先选动作再分支:同步全局 skills / 同步项目配置。
822
- * 自升级只走 `dwy upgrade`。测试注入 selected / action 时跳过动作菜单。
395
+ * `dwy` / `dwy sync` 直接进入内容选择,再选位置和平台。
396
+ * action 仅供已有脚本调用;主交互不再显示动作菜单。
823
397
  *
824
398
  * @param {object} [opts]
825
399
  */
826
400
  export async function runDwy(opts = {}) {
827
- const skipMenu = !!(opts.selected || opts.selectedPlatforms || opts.action)
828
- const action = opts.action != null
829
- ? normalizeAction(opts.action)
830
- : (skipMenu ? ACTION_SYNC : await promptAction())
831
- if (action === null) {
832
- console.log(chalk.yellow('\n已取消。'))
833
- return
834
- }
401
+ console.log(chalk.bold.cyan('\ndwy') + chalk.gray(' · AI 开发工具\n'))
402
+ const action = normalizeAction(opts.action)
835
403
  if (action === ACTION_INSTALL_SKILLS) {
836
404
  await installSkills()
837
405
  return
@@ -854,14 +422,14 @@ export async function syncAll({
854
422
  selected: selectedOverride,
855
423
  selectedPlatforms: selectedPlatformsOverride,
856
424
  staleRemovals: staleRemovalsOverride,
857
- // 测试/脚本可注入;交互模式在平台选择之后再问
425
+ // 兼容已有脚本;主交互只选择整套配置的位置。
858
426
  skillScope: skillScopeOverride,
859
427
  // all = 完整同步;skills = 只写 skill,测试/脚本可注入
860
428
  syncMode: syncModeOverride,
861
- // 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
862
- homeDir,
863
- // 测试注入勾选方式;交互模式在同步范围之后再问
864
- selectionStyle: selectionStyleOverride,
429
+ // 全局配置根目录;测试必须注入临时用户目录。
430
+ homeDir = os.homedir(),
431
+ scope: scopeOverride,
432
+ confirmed,
865
433
  } = {}) {
866
434
  const sourceDir = sourceDirOverride || await resolveSourceDir()
867
435
  if (!await fs.pathExists(sourceDir)) {
@@ -870,107 +438,74 @@ export async function syncAll({
870
438
 
871
439
  const projectDir = projectDirOverride || process.cwd()
872
440
 
873
- // 开头无条件自检全局外部 skill:未装 / 清单变更(cli 加了 skill 或改了 tag)则自动更新,
874
- // 已装且清单一致则零开销跳过。失败不阻塞 sync(离线/网络故障,下次再来)。
875
- // 这样用户跑一次同步就把 skill 更新到位;强制重装走入口「刷新全局外部 skill」。
876
- console.log(chalk.gray('检查全局外部 skill 更新...'))
877
- await ensureSkillsInstalled()
878
-
879
- console.log(chalk.blue('\nScanning available Claude Code/Codex configuration...\n'))
880
441
  const scans = {
881
442
  skills: await scanSkills(sourceDir),
882
443
  rules: await scanRules(sourceDir),
883
- commands: await scanCommands(sourceDir),
444
+ commands: [],
884
445
  hooks: await scanHooks(sourceDir),
885
446
  }
886
- const baseline = await readBaselineDoc(sourceDir)
887
- const hookManifests = await loadHookManifests(sourceDir)
888
- const claudeManagedHookNames = new Set(
889
- hookManifests
890
- .filter(manifest => !manifest.platforms || manifest.platforms.includes('claude'))
891
- .map(manifest => manifest.name),
892
- )
893
- const codexManagedHookNames = new Set(
894
- hookManifests
895
- .filter(manifest => !manifest.platforms || manifest.platforms.includes('codex'))
896
- .map(manifest => manifest.name),
897
- )
898
- console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
899
-
900
447
  const existing = await scanCombinedExisting(projectDir)
901
448
  const syncState = await loadSyncState(projectDir)
902
- const selectionDefaults = buildSelectionDefaultsFromSyncState(existing, syncState)
903
- const skipInitPrompt = !!(selectedOverride || selectedPlatformsOverride)
904
- if (!skipInitPrompt && !await shouldInitialize(projectDir, existing)) {
905
- console.log(chalk.yellow('\n已取消同步。'))
906
- return
907
- }
908
-
909
- const hasAnyExisting = hasSetItems(existing)
910
- if (!hasAnyExisting && !selectedOverride) {
911
- console.log(chalk.gray('首次同步,进入交互式选择...\n'))
912
- }
913
-
914
- const skipInteractive = !!(selectedOverride || selectedPlatformsOverride)
915
- const syncMode = syncModeOverride
916
- ? normalizeSyncMode(syncModeOverride)
917
- : (skipInteractive
918
- ? normalizeSyncMode(syncState.syncMode)
919
- : await promptSyncMode(normalizeSyncMode(syncState.syncMode)))
920
- if (syncMode === null) {
921
- console.log(chalk.yellow('\n已取消同步。'))
922
- return
923
- }
924
-
925
- const selectionStyle = selectionStyleOverride
926
- ? normalizeSelectionStyle(selectionStyleOverride)
927
- : (skipInteractive
928
- ? normalizeSelectionStyle(syncState.selectionStyle)
929
- : await promptSelectionStyle(normalizeSelectionStyle(syncState.selectionStyle)))
930
- if (selectionStyle === null) {
931
- console.log(chalk.yellow('\n已取消同步。'))
932
- return
933
- }
934
-
935
- const selected = selectedOverride || (
936
- selectionStyle === SELECTION_STYLE_PACKS
937
- ? await interactiveSelectByPacks(scans, syncState, { skillsOnly: syncMode === SYNC_MODE_SKILLS })
938
- : await interactiveSelect(scans, selectionDefaults, {
939
- categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
940
- })
941
- )
449
+ const globalState = await loadSyncState(homeDir)
450
+ const projectDefaults = buildSelectionDefaultsFromSyncState(existing, syncState)
451
+ const globalDefaults = buildSelectionDefaultsFromSyncState({ skills: new Set(), rules: new Set(), hooks: new Set() }, globalState)
452
+ const selectionDefaults = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type, unionSets(projectDefaults[type], globalDefaults[type])]))
453
+ const syncMode = normalizeSyncMode(syncModeOverride)
454
+ const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
455
+ skillsOnly: syncMode === SYNC_MODE_SKILLS,
456
+ })
942
457
  if (selected === null) {
943
458
  console.log(chalk.yellow('\n已取消同步。'))
944
459
  return
945
460
  }
946
461
 
947
- const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(projectDir)
948
- if (selectedPlatforms === null) {
949
- console.log(chalk.yellow('\n已取消同步。'))
950
- return
951
- }
952
-
953
- const staleEntriesRaw = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
954
- const staleEntries = syncMode === SYNC_MODE_SKILLS
955
- ? retainSkillStaleEntries(staleEntriesRaw)
956
- : staleEntriesRaw
957
- const approvedStaleRemovals = staleRemovalsOverride
958
- ? normalizeRemovalInput(staleRemovalsOverride)
959
- : await promptStaleRemovals(staleEntries)
960
- if (approvedStaleRemovals === null) {
961
- console.log(chalk.yellow('\n已取消同步。'))
962
- return
963
- }
462
+ if (syncMode === SYNC_MODE_ALL) selected.hooks = scans.hooks
964
463
 
965
- const skillScope = skillScopeOverride
966
- ? normalizeSkillScope(skillScopeOverride)
967
- : (selectedOverride
968
- ? normalizeSkillScope(syncState.skillScope)
969
- : await promptSkillScope({ selectedSkills: selected.skills || [], syncState }))
970
- if (skillScope === null) {
971
- console.log(chalk.yellow('\n已取消同步。'))
972
- return
464
+ const scopeResult = scopeOverride || (selectedOverride || selectedPlatformsOverride ? 'project' : await searchableSelect({
465
+ message: '选择同步位置',
466
+ options: [
467
+ { value: 'project', label: '当前项目', description: '仅当前项目生效' },
468
+ { value: 'global', label: '全局', description: '所有项目生效' },
469
+ ],
470
+ initialValue: 'project',
471
+ }))
472
+ if (isCancel(scopeResult)) { console.log(chalk.yellow('已取消同步。')); return }
473
+ if (!['project', 'global'].includes(scopeResult)) throw new Error('无效同步位置')
474
+ const global = scopeResult === 'global'
475
+ const targetRoot = global ? homeDir : projectDir
476
+ const targetState = global ? await loadSyncState(homeDir) : syncState
477
+ const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(targetRoot, global)
478
+ if (selectedPlatforms === null) { console.log(chalk.yellow('已取消同步。')); return }
479
+ if (selectedPlatforms.some(platform => !PLATFORM_SYNC_TYPES[platform] || (global && platform === 'cursor'))) {
480
+ throw new Error('所选平台不支持此同步位置')
481
+ }
482
+ const staleEntriesRaw = await collectStaleEntries(targetRoot, scans, targetState, selectedPlatforms, global)
483
+ const staleEntries = syncMode === SYNC_MODE_SKILLS ? retainSkillStaleEntries(staleEntriesRaw) : staleEntriesRaw
484
+ const approvedStaleRemovals = normalizeRemovalInput(staleRemovalsOverride || staleEntries)
485
+ const skillScope = normalizeSkillScope(skillScopeOverride)
486
+ if ((!selectedOverride && confirmed !== true) || confirmed === false) {
487
+ const currentManaged = await collectCurrentManagedNames(targetRoot, global)
488
+ const templateNames = getCurrentTemplateNames(scans)
489
+ console.log(`\nSkills:${selected.skills.map(item => item.name).join('、') || '未选择'}`)
490
+ console.log(`Rules:${selected.rules.map(item => item.name).join('、') || '未选择'}`)
491
+ console.log(`位置:${global ? '全局' : '当前项目'};平台:${selectedPlatforms.map(platform => PLATFORM_LABELS[platform]).join('、')}`)
492
+ console.log('Hooks 按支持的平台自动启用。')
493
+ for (const platform of selectedPlatforms) {
494
+ for (const type of PLATFORM_SYNC_TYPES[platform]) {
495
+ const existingNames = currentManaged[platform]?.[type] || new Set()
496
+ const previous = [...new Set([...templateNames[type], ...(targetState.platforms[platform]?.[type] || [])])]
497
+ .filter(name => existingNames.has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
498
+ const names = new Set((selected[type] || []).map(item => item.name))
499
+ const removed = previous.filter(name => !names.has(name))
500
+ const changed = [...names].filter(name => existingNames.has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
501
+ if (changed.length) console.log(`更新 ${PLATFORM_LABELS[platform]} ${type}:${changed.join('、')}`)
502
+ if (removed.length) console.log(`删除 ${PLATFORM_LABELS[platform]} ${type}:${removed.join('、')}`)
503
+ }
504
+ }
505
+ const answer = confirmed === false ? false : await confirm({ message: '确认同步?' })
506
+ if (isCancel(answer) || !answer) { console.log(chalk.yellow('已取消同步。')); return }
973
507
  }
508
+ if (!selectedOverride && selected.skills.some(item => item.category === '产品0到1')) await ensureSkillsInstalled()
974
509
 
975
510
  const selectedSkillNames = new Set((selected.skills || []).map(item => item.name))
976
511
  const skillsToWrite = scans.skills.filter(item => selectedSkillNames.has(item.name))
@@ -997,7 +532,8 @@ export async function syncAll({
997
532
  console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
998
533
  await syncClaude({
999
534
  sourceDir,
1000
- projectDir,
535
+ projectDir: targetRoot,
536
+ global,
1001
537
  selected,
1002
538
  staleRemovals: approvedStaleRemovals.claude,
1003
539
  })
@@ -1008,7 +544,8 @@ export async function syncAll({
1008
544
  console.log(chalk.blue('\nSyncing Codex configuration...\n'))
1009
545
  await syncCodex({
1010
546
  sourceDir,
1011
- projectDir,
547
+ projectDir: targetRoot,
548
+ global,
1012
549
  selected,
1013
550
  staleRemovals: approvedStaleRemovals.codex,
1014
551
  preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
@@ -1019,7 +556,8 @@ export async function syncAll({
1019
556
  syncedAnySupportedPlatform = true
1020
557
  await syncCursor({
1021
558
  sourceDir,
1022
- projectDir,
559
+ projectDir: targetRoot,
560
+ global,
1023
561
  selected,
1024
562
  staleRemovals: approvedStaleRemovals.cursor,
1025
563
  })
@@ -1029,32 +567,15 @@ export async function syncAll({
1029
567
  syncedAnySupportedPlatform = true
1030
568
  await syncOpenCode({
1031
569
  sourceDir,
1032
- projectDir,
570
+ projectDir: targetRoot,
571
+ global,
1033
572
  selected,
1034
573
  staleRemovals: approvedStaleRemovals.opencode,
1035
574
  preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
1036
575
  })
1037
576
  }
1038
577
 
1039
- if (!selectedPlatforms.includes('codex') && !selectedPlatforms.includes('opencode')) {
1040
- await clearProjectAgentsMd(projectDir)
1041
- }
1042
-
1043
- if (!selectedPlatforms.includes('claude')) {
1044
- await cleanupClaudePlatform(projectDir, scans, claudeManagedHookNames, baseline)
1045
- }
1046
578
 
1047
- if (!selectedPlatforms.includes('cursor')) {
1048
- await cleanupCursorPlatform(projectDir, scans, baseline)
1049
- }
1050
-
1051
- if (!selectedPlatforms.includes('codex')) {
1052
- await cleanupCodexPlatform(projectDir, scans, codexManagedHookNames)
1053
- }
1054
-
1055
- if (!selectedPlatforms.includes('opencode')) {
1056
- await cleanupOpenCodePlatform(projectDir, scans)
1057
- }
1058
579
  }
1059
580
 
1060
581
  const globalSkillNameSet = new Set(skillScope.globalSkills)
@@ -1072,28 +593,19 @@ export async function syncAll({
1072
593
  for (const platform of selectedPlatforms) {
1073
594
  const previous = syncState.platforms[platform] || createEmptyPlatformState()
1074
595
  const skillsOnlySelected = { skills: selected.skills || [], rules: [], commands: [], hooks: [] }
1075
- syncState.platforms[platform] = {
596
+ targetState.platforms[platform] = {
1076
597
  ...previous,
1077
598
  skills: buildNextPlatformState(skillsOnlySelected, staleEntries, approvedStaleRemovals, platform).skills,
1078
599
  }
1079
600
  }
1080
601
  } else {
1081
- for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
1082
- if (selectedPlatforms.includes(platform)) {
1083
- syncState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
1084
- } else {
1085
- syncState.platforms[platform] = createEmptyPlatformState()
1086
- }
602
+ for (const platform of selectedPlatforms) {
603
+ targetState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
1087
604
  }
1088
605
  }
1089
- syncState.skillScope = skillScope
1090
- syncState.syncMode = syncMode
1091
- syncState.selectionStyle = selectionStyle
1092
- // 按条目同步不覆盖上次按包选择,方便下次再走按包
1093
- if (selectionStyle === SELECTION_STYLE_PACKS) {
1094
- syncState.packs = normalizePacks(selected.packs)
1095
- }
1096
- await saveSyncState(projectDir, syncState)
606
+ targetState.skillScope = skillScope
607
+ targetState.syncMode = syncMode
608
+ await saveSyncState(targetRoot, targetState)
1097
609
 
1098
610
  if (!syncedAnySupportedPlatform) {
1099
611
  console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))