create-dwy 0.19.1 → 0.21.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 (113) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/cli-help.js +31 -2
  4. package/src/sync-all.js +168 -577
  5. package/src/sync-codex.js +42 -28
  6. package/src/sync-cursor.js +0 -25
  7. package/src/sync-global-skills.js +2 -2
  8. package/src/sync-opencode.js +7 -33
  9. package/src/sync.js +59 -226
  10. package/src/tab-multiselect.js +15 -53
  11. package/src/utils.js +0 -1
  12. package/templates/ai-tools/AGENTS.md +223 -0
  13. package/templates/ai-tools/CHANGELOG.md +195 -0
  14. package/templates/ai-tools/VERSION +1 -1
  15. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +5 -4
  16. package/templates/ai-tools/rules/Android/dwy-android-core.md +147 -13
  17. package/templates/ai-tools/rules/Android/dwy-android-layering.md +142 -0
  18. package/templates/ai-tools/rules/Android/dwy-android-mvp.md +55 -0
  19. package/templates/ai-tools/rules/Android/dwy-android-support-only.md +2 -0
  20. package/templates/ai-tools/rules/Docker/dwy-docker.md +23 -1
  21. package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +2 -0
  22. package/templates/ai-tools/rules/Flutter/dwy-flutter-testing.md +1 -1
  23. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +12 -8
  24. package/templates/ai-tools/rules/Python/dwy-python-backend.md +4 -43
  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 +2 -2
  58. package/templates/ai-tools/skills/Python/dwy-eapi/references/tasks-integration-guide.md +4 -4
  59. package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +3 -0
  60. package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +28 -3
  61. package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +2 -2
  62. package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +12 -2
  63. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +1 -1
  64. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +12 -0
  65. package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +2 -0
  66. package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +10 -0
  67. package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +5 -0
  68. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +11 -14
  69. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +1 -1
  70. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +1 -1
  71. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +1 -1
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +4 -3
  80. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +1 -1
  81. 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
  82. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +45 -27
  83. 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
  84. 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
  85. 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
  86. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +1 -1
  87. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +1 -1
  88. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +8 -0
  89. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +7 -7
  90. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +63 -59
  91. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_base.sh +10 -1
  92. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_capacity.sh +21 -23
  93. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_db.sh +9 -1
  94. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +20 -24
  95. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_logs.sh +3 -3
  96. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +4 -2
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +1 -1
  98. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +1 -0
  99. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +7 -5
  100. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +5 -3
  101. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +140 -0
  102. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +113 -0
  103. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +17 -14
  104. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
  105. package/templates/project/fullstack-monorepo/{CLAUDE.md.hbs → AGENTS.md.hbs} +1 -1
  106. package/src/sync-packs.js +0 -223
  107. package/src/sync-scene.js +0 -178
  108. package/templates/ai-tools/CLAUDE.md +0 -210
  109. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
  110. /package/templates/project/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  111. /package/templates/project/fullstack-monorepo/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  112. /package/templates/project/fullstack-monorepo/frontend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  113. /package/templates/project/web/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
package/src/sync-all.js CHANGED
@@ -1,23 +1,20 @@
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
16
  readBaselineDoc,
19
17
  resolveSourceDir,
20
- scanCommands,
21
18
  scanExisting,
22
19
  scanHooks,
23
20
  scanRules,
@@ -25,33 +22,32 @@ import {
25
22
  syncClaude,
26
23
  } from './sync.js'
27
24
  import {
28
- clearProjectAgentsMd,
29
25
  parseManagedRuleNames,
26
+ removeLegacyBaseline,
30
27
  scanExistingCodexHooks,
31
28
  scanExistingCodexSkills,
32
29
  syncCodex,
30
+ syncGlobalBaselineDoc,
31
+ syncProjectAgentsMd,
33
32
  } from './sync-codex.js'
34
33
  import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
35
34
  import { syncOpenCode } from './sync-opencode.js'
36
35
  import { ensureSkillsInstalled, installSkills } from './skills-install.js'
37
36
  import { selfUpgrade } from './self-upgrade.js'
38
- import { normalizePacks } from './sync-packs.js'
39
- import { interactiveSelectByPacks } from './sync-scene.js'
40
37
 
41
38
  const PLATFORM_OPTIONS = [
42
- { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / commands / hooks' },
39
+ { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / hooks' },
43
40
  { value: 'codex', label: 'Codex', description: '支持 rules / skills / hooks' },
44
41
  { value: 'cursor', label: 'Cursor', description: '支持 rules' },
45
- { value: 'opencode', label: 'OpenCode', description: '支持 rules / skills / commands' },
42
+ { value: 'opencode', label: 'OpenCode', description: '支持 rules / skills' },
46
43
  ]
47
44
 
48
- const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
49
45
  const SYNC_STATE_PATH = ['.dwy', 'sync-state.json']
50
46
  const PLATFORM_SYNC_TYPES = {
51
- claude: ['skills', 'rules', 'commands', 'hooks'],
47
+ claude: ['skills', 'rules', 'hooks'],
52
48
  codex: ['skills', 'rules', 'hooks'],
53
49
  cursor: ['rules'],
54
- opencode: ['skills', 'rules', 'commands'],
50
+ opencode: ['skills', 'rules'],
55
51
  }
56
52
  const PLATFORM_LABELS = {
57
53
  claude: 'Claude Code',
@@ -59,25 +55,12 @@ const PLATFORM_LABELS = {
59
55
  cursor: 'Cursor',
60
56
  opencode: 'OpenCode',
61
57
  }
62
- const TYPE_LABELS = {
63
- skills: 'skills',
64
- rules: 'rules',
65
- commands: 'commands',
66
- hooks: 'hooks',
67
- }
68
-
69
- /** 完整同步:skills / rules / commands / hooks 都走平台写入 */
58
+ /** 完整同步:逐项选择 skills / rules,hooks 自动启用。 */
70
59
  export const SYNC_MODE_ALL = 'all'
71
60
  /** 仅 Skills:不写、不删 rules / commands / hooks */
72
61
  export const SYNC_MODE_SKILLS = 'skills'
73
62
  export const DEFAULT_SYNC_MODE = SYNC_MODE_ALL
74
63
 
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
64
  /** 同步项目配置(skills / rules / hooks) */
82
65
  export const ACTION_SYNC = 'sync'
83
66
  /** 强制刷新 ~/.dwy/skills 外部 skill */
@@ -85,8 +68,6 @@ export const ACTION_INSTALL_SKILLS = 'install-skills'
85
68
  /** 把全局 create-dwy 升到 npm latest */
86
69
  export const ACTION_UPGRADE = 'upgrade'
87
70
 
88
- const SKILL_ONLY_CATEGORIES = [{ key: 'skills', label: 'Skills' }]
89
-
90
71
  /**
91
72
  * 规范化同步范围。未知值回退完整同步,避免误进仅 Skills 删不该删的东西。
92
73
  *
@@ -97,16 +78,6 @@ export function normalizeSyncMode(raw) {
97
78
  return raw === SYNC_MODE_SKILLS ? SYNC_MODE_SKILLS : DEFAULT_SYNC_MODE
98
79
  }
99
80
 
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
81
  /**
111
82
  * 规范化入口动作。未知值回退同步,避免误进强制重装。
112
83
  *
@@ -124,7 +95,7 @@ function unionSets(...sets) {
124
95
  }
125
96
 
126
97
  function hasSetItems(setsByType) {
127
- return setsByType.skills.size + setsByType.rules.size + setsByType.commands.size + setsByType.hooks.size > 0
98
+ return Object.values(setsByType).some(items => items.size > 0)
128
99
  }
129
100
 
130
101
  export function buildSelectionDefaultsFromSyncState(existing, syncState) {
@@ -160,7 +131,6 @@ async function scanCombinedExisting(projectDir) {
160
131
  await scanExisting(claudeDir, 'rules'),
161
132
  parseManagedRuleNames(agentsMdContent),
162
133
  ),
163
- commands: await scanExisting(claudeDir, 'commands'),
164
134
  hooks: unionSets(
165
135
  await scanExisting(claudeDir, 'hooks'),
166
136
  await scanExistingCodexHooks(projectDir),
@@ -168,23 +138,6 @@ async function scanCombinedExisting(projectDir) {
168
138
  }
169
139
  }
170
140
 
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
141
  export async function buildPlatformDefaultsFromLocalState(projectDir) {
189
142
  const defaults = []
190
143
 
@@ -208,93 +161,9 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
208
161
  return defaults.length > 0 ? defaults : ['claude', 'codex']
209
162
  }
210
163
 
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
164
  /**
296
165
  * 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
297
- * 禁止调用完整平台 sync,否则空 rules/commands 会删掉项目已有项。
166
+ * 禁止调用完整平台 sync,否则空 rules/hooks 会删掉项目已有项。
298
167
  *
299
168
  * @param {{ projectDir: string, selectedPlatforms: string[], skills: Array<{ name: string, sourcePath: string }>, templateSkillNames: Set<string>, staleSkillNames: Set<string>, skillScope: { destinations: string[] } }} opts
300
169
  */
@@ -338,67 +207,9 @@ async function syncSkillsOnly({
338
207
  }
339
208
  }
340
209
 
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
210
  async function selectPlatforms(projectDir) {
394
211
  const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
395
212
 
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
-
402
213
  // 可搜索多选 + 底部说明区
403
214
  const result = await searchableMultiselect({
404
215
  message: '选择要同步的平台',
@@ -416,18 +227,6 @@ function buildCursorRuleName(ruleName) {
416
227
  return `dwy-${ruleName.replace(/\.md$/i, '')}.mdc`
417
228
  }
418
229
 
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
230
  function createEmptyPlatformState() {
432
231
  return {
433
232
  skills: [],
@@ -445,8 +244,6 @@ async function loadSyncState(projectDir) {
445
244
  platforms: {},
446
245
  skillScope: normalizeSkillScope(undefined),
447
246
  syncMode: DEFAULT_SYNC_MODE,
448
- selectionStyle: DEFAULT_SELECTION_STYLE,
449
- packs: normalizePacks(undefined),
450
247
  }
451
248
  }
452
249
 
@@ -456,8 +253,6 @@ async function loadSyncState(projectDir) {
456
253
  platforms: state.platforms || {},
457
254
  skillScope: normalizeSkillScope(state.skillScope),
458
255
  syncMode: normalizeSyncMode(state.syncMode),
459
- selectionStyle: normalizeSelectionStyle(state.selectionStyle),
460
- packs: normalizePacks(state.packs),
461
256
  }
462
257
  }
463
258
 
@@ -476,17 +271,16 @@ function getCurrentTemplateNames(scans) {
476
271
  }
477
272
  }
478
273
 
479
- async function collectCurrentManagedNames(projectDir) {
274
+ async function collectCurrentManagedNames(projectDir, global = false) {
480
275
  const claudeDir = path.join(projectDir, '.claude')
481
- const openCodeDir = path.join(projectDir, '.opencode')
482
- const agentsMdPath = path.join(projectDir, 'AGENTS.md')
276
+ const openCodeDir = global ? path.join(projectDir, '.config', 'opencode') : path.join(projectDir, '.opencode')
277
+ const agentsMdPath = global ? path.join(projectDir, '.codex', 'AGENTS.md') : path.join(projectDir, 'AGENTS.md')
483
278
  const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
484
279
 
485
280
  return {
486
281
  claude: {
487
282
  skills: await scanExisting(claudeDir, 'skills'),
488
- rules: await scanExisting(claudeDir, 'rules'),
489
- commands: await scanExisting(claudeDir, 'commands'),
283
+ rules: unionSets(await scanExisting(claudeDir, 'rules'), parseManagedRuleNames(agentsMdContent)),
490
284
  hooks: await scanExisting(claudeDir, 'hooks'),
491
285
  },
492
286
  codex: {
@@ -499,8 +293,7 @@ async function collectCurrentManagedNames(projectDir) {
499
293
  },
500
294
  opencode: {
501
295
  skills: await scanExisting(openCodeDir, 'skills'),
502
- rules: parseManagedRuleNames(agentsMdContent),
503
- commands: await scanExisting(openCodeDir, 'commands'),
296
+ rules: global ? parseManagedRuleNames(await fs.pathExists(path.join(openCodeDir, 'AGENTS.md')) ? await fs.readFile(path.join(openCodeDir, 'AGENTS.md'), 'utf-8') : '') : parseManagedRuleNames(agentsMdContent),
504
297
  },
505
298
  }
506
299
  }
@@ -522,9 +315,9 @@ function createEmptyRemovalState() {
522
315
  )
523
316
  }
524
317
 
525
- async function collectStaleEntries(projectDir, scans, syncState, selectedPlatforms) {
318
+ async function collectStaleEntries(projectDir, scans, syncState, selectedPlatforms, global = false) {
526
319
  const currentTemplateNames = getCurrentTemplateNames(scans)
527
- const currentManaged = await collectCurrentManagedNames(projectDir)
320
+ const currentManaged = await collectCurrentManagedNames(projectDir, global)
528
321
  const staleEntries = createEmptyRemovalState()
529
322
 
530
323
  for (const platform of selectedPlatforms) {
@@ -554,34 +347,6 @@ async function collectStaleEntries(projectDir, scans, syncState, selectedPlatfor
554
347
  return staleEntries
555
348
  }
556
349
 
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
350
  /**
586
351
  * 仅 Skills 模式:陈旧项提示只保留 skills,避免问 rules/commands/hooks。
587
352
  *
@@ -629,209 +394,15 @@ async function removeManagedItems(targetDir, existingNames, managedNames, labelP
629
394
  return removedCount
630
395
  }
631
396
 
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
397
  /**
821
- * `dwy` / `dwy sync` 入口。先选动作再分支:同步全局 skills / 同步项目配置。
822
- * 自升级只走 `dwy upgrade`。测试注入 selected / action 时跳过动作菜单。
398
+ * `dwy` / `dwy sync` 直接进入内容选择,再选位置和平台。
399
+ * action 仅供已有脚本调用;主交互不再显示动作菜单。
823
400
  *
824
401
  * @param {object} [opts]
825
402
  */
826
403
  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
- }
404
+ console.log(chalk.bold.cyan('\ndwy') + chalk.gray(' · AI 开发工具\n'))
405
+ const action = normalizeAction(opts.action)
835
406
  if (action === ACTION_INSTALL_SKILLS) {
836
407
  await installSkills()
837
408
  return
@@ -854,14 +425,15 @@ export async function syncAll({
854
425
  selected: selectedOverride,
855
426
  selectedPlatforms: selectedPlatformsOverride,
856
427
  staleRemovals: staleRemovalsOverride,
857
- // 测试/脚本可注入;交互模式在平台选择之后再问
428
+ // 兼容已有脚本;主交互的位置只控制 Skills / Hooks。
858
429
  skillScope: skillScopeOverride,
859
430
  // all = 完整同步;skills = 只写 skill,测试/脚本可注入
860
431
  syncMode: syncModeOverride,
861
- // 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
862
- homeDir,
863
- // 测试注入勾选方式;交互模式在同步范围之后再问
864
- selectionStyle: selectionStyleOverride,
432
+ // 全局配置根目录;测试必须注入临时用户目录。
433
+ homeDir = os.homedir(),
434
+ codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'),
435
+ scope: scopeOverride,
436
+ confirmed,
865
437
  } = {}) {
866
438
  const sourceDir = sourceDirOverride || await resolveSourceDir()
867
439
  if (!await fs.pathExists(sourceDir)) {
@@ -870,107 +442,110 @@ export async function syncAll({
870
442
 
871
443
  const projectDir = projectDirOverride || process.cwd()
872
444
 
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
445
  const scans = {
881
446
  skills: await scanSkills(sourceDir),
882
447
  rules: await scanRules(sourceDir),
883
- commands: await scanCommands(sourceDir),
448
+ commands: [],
884
449
  hooks: await scanHooks(sourceDir),
885
450
  }
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
451
  const existing = await scanCombinedExisting(projectDir)
901
452
  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
- )
453
+ const globalState = await loadSyncState(homeDir)
454
+ const projectDefaults = buildSelectionDefaultsFromSyncState(existing, syncState)
455
+ const globalDefaults = buildSelectionDefaultsFromSyncState({ skills: new Set(), rules: new Set(), hooks: new Set() }, globalState)
456
+ const selectionDefaults = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type, unionSets(projectDefaults[type], globalDefaults[type])]))
457
+ selectionDefaults.rules = projectDefaults.rules
458
+ const syncMode = normalizeSyncMode(syncModeOverride)
459
+ const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
460
+ skillsOnly: syncMode === SYNC_MODE_SKILLS,
461
+ })
942
462
  if (selected === null) {
943
463
  console.log(chalk.yellow('\n已取消同步。'))
944
464
  return
945
465
  }
946
466
 
947
- const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(projectDir)
948
- if (selectedPlatforms === null) {
949
- console.log(chalk.yellow('\n已取消同步。'))
950
- return
951
- }
467
+ if (syncMode === SYNC_MODE_ALL) selected.hooks = scans.hooks
952
468
 
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
469
+ const scopeResult = scopeOverride || (selectedOverride || selectedPlatformsOverride ? 'project' : await searchableSelect({
470
+ message: '选择同步位置(Skills / Hooks)',
471
+ options: [
472
+ { value: 'project', label: '当前项目', description: 'Skills / Hooks 写入项目;基线固定全局,Rules 固定项目' },
473
+ { value: 'global', label: '全局', description: 'Skills / Hooks 写入用户目录;基线固定全局,Rules 固定项目' },
474
+ ],
475
+ initialValue: 'project',
476
+ }))
477
+ if (isCancel(scopeResult)) { console.log(chalk.yellow('已取消同步。')); return }
478
+ if (!['project', 'global'].includes(scopeResult)) throw new Error('无效同步位置')
479
+ const global = scopeResult === 'global'
480
+ const targetRoot = global ? homeDir : projectDir
481
+ const targetState = global ? globalState : syncState
482
+ const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(projectDir)
483
+ if (selectedPlatforms === null) { console.log(chalk.yellow('已取消同步。')); return }
484
+ if (selectedPlatforms.some(platform => !PLATFORM_SYNC_TYPES[platform])) {
485
+ throw new Error('所选平台不支持此同步位置')
486
+ }
487
+ const projectStaleEntries = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
488
+ const staleEntriesRaw = global
489
+ ? await collectStaleEntries(targetRoot, scans, targetState, selectedPlatforms, true)
490
+ : projectStaleEntries
491
+ for (const platform of selectedPlatforms) {
492
+ staleEntriesRaw[platform].rules = projectStaleEntries[platform].rules || []
493
+ }
494
+ const staleEntries = syncMode === SYNC_MODE_SKILLS ? retainSkillStaleEntries(staleEntriesRaw) : staleEntriesRaw
495
+ const approvedStaleRemovals = normalizeRemovalInput(staleRemovalsOverride || staleEntries)
496
+ const skillScope = normalizeSkillScope(skillScopeOverride)
497
+ const baseline = syncMode === SYNC_MODE_ALL ? await readBaselineDoc(sourceDir) : ''
498
+ const cliPlatforms = selectedPlatforms.filter(platform => platform !== 'cursor')
499
+ const baselinePaths = {
500
+ claude: path.join(homeDir, '.claude', 'CLAUDE.md'),
501
+ codex: path.join(codexHome, 'AGENTS.md'),
502
+ opencode: path.join(homeDir, '.config', 'opencode', 'AGENTS.md'),
503
+ }
504
+ const legacyClaudeRules = new Set()
505
+ if (syncMode === SYNC_MODE_ALL && selectedPlatforms.includes('claude')) {
506
+ for (const [root, previousNames] of [
507
+ [projectDir, approvedStaleRemovals.claude?.rules || []],
508
+ ...(baseline.trim() ? [[homeDir, globalState.platforms.claude?.rules || []]] : []),
509
+ ]) {
510
+ for (const name of new Set([...scans.rules.map(rule => rule.name), ...previousNames])) {
511
+ if (path.basename(name) !== name || !name.endsWith('.md')) continue
512
+ const dest = path.join(root, '.claude', 'rules', name)
513
+ if (await fs.pathExists(dest)) legacyClaudeRules.add(dest)
514
+ }
515
+ }
963
516
  }
964
-
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
517
+ if ((!selectedOverride && confirmed !== true) || confirmed === false) {
518
+ const currentManaged = await collectCurrentManagedNames(targetRoot, global)
519
+ const projectManaged = await collectCurrentManagedNames(projectDir)
520
+ for (const platform of selectedPlatforms) currentManaged[platform].rules = projectManaged[platform].rules
521
+ const templateNames = getCurrentTemplateNames(scans)
522
+ console.log(`\nSkills:${selected.skills.map(item => item.name).join('、') || '未选择'}`)
523
+ console.log(`Rules:${selected.rules.map(item => item.name).join('、') || '未选择'}`)
524
+ console.log(`Skills / Hooks 位置:${global ? '全局' : '当前项目'};平台:${selectedPlatforms.map(platform => PLATFORM_LABELS[platform]).join('、')}`)
525
+ if (syncMode === SYNC_MODE_ALL) {
526
+ for (const platform of cliPlatforms) console.log(`全局基线 ${PLATFORM_LABELS[platform]} → ${baselinePaths[platform]}(替换基线并清除旧托管 Rules)`)
527
+ if (cliPlatforms.length) console.log(`项目 Rules → ${path.join(projectDir, 'AGENTS.md')}(保留手写内容,移除旧托管基线)`)
528
+ if (selectedPlatforms.includes('claude')) console.log('旧 Claude 基线入口仅清理 dwy 托管块或与当前模板完全一致的内容。')
529
+ for (const dest of legacyClaudeRules) console.log(`删除旧 Claude Rules → ${dest}`)
530
+ }
531
+ console.log('Hooks 按支持的平台自动启用。')
532
+ for (const platform of selectedPlatforms) {
533
+ for (const type of PLATFORM_SYNC_TYPES[platform]) {
534
+ const existingNames = currentManaged[platform]?.[type] || new Set()
535
+ const previousState = type === 'rules' ? syncState : targetState
536
+ const previous = [...new Set([...templateNames[type], ...(previousState.platforms[platform]?.[type] || [])])]
537
+ .filter(name => existingNames.has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
538
+ const names = new Set((selected[type] || []).map(item => item.name))
539
+ const removed = previous.filter(name => !names.has(name))
540
+ const changed = [...names].filter(name => existingNames.has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
541
+ if (changed.length) console.log(`更新 ${PLATFORM_LABELS[platform]} ${type}:${changed.join('、')}`)
542
+ if (removed.length) console.log(`删除 ${PLATFORM_LABELS[platform]} ${type}:${removed.join('、')}`)
543
+ }
544
+ }
545
+ const answer = confirmed === false ? false : await confirm({ message: '确认同步?' })
546
+ if (isCancel(answer) || !answer) { console.log(chalk.yellow('已取消同步。')); return }
973
547
  }
548
+ if (!selectedOverride && selected.skills.some(item => item.category === '产品0到1')) await ensureSkillsInstalled()
974
549
 
975
550
  const selectedSkillNames = new Set((selected.skills || []).map(item => item.name))
976
551
  const skillsToWrite = scans.skills.filter(item => selectedSkillNames.has(item.name))
@@ -992,12 +567,25 @@ export async function syncAll({
992
567
  syncedAnySupportedPlatform = selectedPlatforms.some(platform => resolveProjectSkillDir(projectDir, platform))
993
568
  || skillScope.destinations.some(id => id !== 'project')
994
569
  } else {
570
+ for (const platform of cliPlatforms) await syncGlobalBaselineDoc(baseline, baselinePaths[platform])
571
+ if (cliPlatforms.length) {
572
+ const preservedRuleNames = unionSets(...cliPlatforms.map(platform => getPreservedStaleNames(staleEntries, approvedStaleRemovals, platform, 'rules')))
573
+ const ruleNames = new Set(selected.rules.map(rule => rule.name))
574
+ await syncProjectAgentsMd(projectDir, scans.rules.filter(rule => ruleNames.has(rule.name)), { preservedRuleNames })
575
+ }
995
576
  if (selectedPlatforms.includes('claude')) {
577
+ await removeLegacyBaseline(baseline, path.join(homeDir, '.claude', 'AGENTS.md'))
578
+ for (const name of ['AGENTS.md', 'CLAUDE.md']) await removeLegacyBaseline(baseline, path.join(projectDir, '.claude', name))
579
+ for (const dest of legacyClaudeRules) {
580
+ await fs.remove(dest)
581
+ logAction(`legacy rule → ${dest}`, 'red', '×')
582
+ }
996
583
  syncedAnySupportedPlatform = true
997
584
  console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
998
585
  await syncClaude({
999
586
  sourceDir,
1000
- projectDir,
587
+ projectDir: targetRoot,
588
+ global,
1001
589
  selected,
1002
590
  staleRemovals: approvedStaleRemovals.claude,
1003
591
  })
@@ -1008,10 +596,10 @@ export async function syncAll({
1008
596
  console.log(chalk.blue('\nSyncing Codex configuration...\n'))
1009
597
  await syncCodex({
1010
598
  sourceDir,
1011
- projectDir,
599
+ projectDir: targetRoot,
600
+ global,
1012
601
  selected,
1013
602
  staleRemovals: approvedStaleRemovals.codex,
1014
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
1015
603
  })
1016
604
  }
1017
605
 
@@ -1029,32 +617,14 @@ export async function syncAll({
1029
617
  syncedAnySupportedPlatform = true
1030
618
  await syncOpenCode({
1031
619
  sourceDir,
1032
- projectDir,
620
+ projectDir: targetRoot,
621
+ global,
1033
622
  selected,
1034
623
  staleRemovals: approvedStaleRemovals.opencode,
1035
- preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
1036
624
  })
1037
625
  }
1038
626
 
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
627
 
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
628
  }
1059
629
 
1060
630
  const globalSkillNameSet = new Set(skillScope.globalSkills)
@@ -1072,28 +642,49 @@ export async function syncAll({
1072
642
  for (const platform of selectedPlatforms) {
1073
643
  const previous = syncState.platforms[platform] || createEmptyPlatformState()
1074
644
  const skillsOnlySelected = { skills: selected.skills || [], rules: [], commands: [], hooks: [] }
1075
- syncState.platforms[platform] = {
645
+ targetState.platforms[platform] = {
1076
646
  ...previous,
1077
647
  skills: buildNextPlatformState(skillsOnlySelected, staleEntries, approvedStaleRemovals, platform).skills,
1078
648
  }
1079
649
  }
1080
650
  } 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)
651
+ for (const platform of selectedPlatforms) {
652
+ const next = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
653
+ if (global) {
654
+ syncState.platforms[platform] = {
655
+ ...(syncState.platforms[platform] || createEmptyPlatformState()),
656
+ rules: next.rules,
657
+ }
658
+ if (platform !== 'cursor') targetState.platforms[platform] = { ...next, rules: [] }
1084
659
  } else {
1085
- syncState.platforms[platform] = createEmptyPlatformState()
660
+ targetState.platforms[platform] = next
661
+ }
662
+ if (platform !== 'cursor') {
663
+ globalState.platforms[platform] = { ...globalState.platforms[platform], rules: [] }
1086
664
  }
1087
665
  }
1088
666
  }
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)
667
+ if (syncMode === SYNC_MODE_ALL && cliPlatforms.length) {
668
+ const agentsPath = path.join(projectDir, 'AGENTS.md')
669
+ const content = await fs.pathExists(agentsPath) ? await fs.readFile(agentsPath, 'utf-8') : ''
670
+ const rules = [...parseManagedRuleNames(content)].sort((a, b) => a.localeCompare(b, 'zh'))
671
+ // 三个 CLI 共用同一规则块,已有记录必须一起更新,避免切换平台后恢复已取消的规则。
672
+ for (const platform of Object.keys(baselinePaths)) {
673
+ if (syncState.platforms[platform]) syncState.platforms[platform].rules = rules
674
+ }
675
+ }
676
+ targetState.skillScope = skillScope
677
+ targetState.syncMode = syncMode
678
+ if (syncMode === SYNC_MODE_ALL && global) {
679
+ syncState.syncMode = syncMode
680
+ await saveSyncState(projectDir, syncState)
681
+ if (cliPlatforms.length) await saveSyncState(homeDir, targetState)
682
+ } else {
683
+ await saveSyncState(targetRoot, targetState)
684
+ if (syncMode === SYNC_MODE_ALL && cliPlatforms.length && baseline.trim()) {
685
+ await saveSyncState(homeDir, globalState)
686
+ }
1095
687
  }
1096
- await saveSyncState(projectDir, syncState)
1097
688
 
1098
689
  if (!syncedAnySupportedPlatform) {
1099
690
  console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))