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.
- package/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/cli-help.js +28 -2
- package/src/prompt-style.js +82 -0
- package/src/searchable-select.js +12 -29
- package/src/sync-all.js +95 -583
- package/src/sync-codex.js +14 -7
- package/src/sync-opencode.js +8 -22
- package/src/sync.js +60 -149
- package/src/tab-multiselect.js +21 -64
- package/templates/ai-tools/CHANGELOG.md +195 -0
- package/templates/ai-tools/CLAUDE.md +39 -58
- package/templates/ai-tools/VERSION +1 -1
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +5 -4
- package/templates/ai-tools/rules/Android/dwy-android-core.md +147 -13
- package/templates/ai-tools/rules/Android/dwy-android-layering.md +142 -0
- package/templates/ai-tools/rules/Android/dwy-android-mvp.md +55 -0
- package/templates/ai-tools/rules/Android/dwy-android-support-only.md +2 -0
- package/templates/ai-tools/rules/Docker/dwy-docker.md +23 -1
- package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +2 -0
- package/templates/ai-tools/rules/Flutter/dwy-flutter-testing.md +1 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +12 -8
- package/templates/ai-tools/rules/Python/dwy-python-backend.md +4 -43
- package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +1 -1
- package/templates/ai-tools/rules/Python/dwy-python-layering.md +169 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +11 -49
- package/templates/ai-tools/rules/Vue/dwy-vue-layering.md +153 -0
- package/templates/ai-tools/rules/iOS/dwy-apple-layering.md +133 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +2 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-craft.md +83 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +27 -66
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +8 -219
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +27 -123
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prod-branch.md +31 -0
- package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-postgres.md +17 -10
- package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-redis.md +6 -3
- package/templates/ai-tools/skills/Docker/dwy-deploy-first/SKILL.md +14 -14
- package/templates/ai-tools/skills/Docker/dwy-deploy-first/references/chapter-01-mirrors.md +2 -2
- package/templates/ai-tools/skills/Docker/dwy-docker/SKILL.md +21 -13
- package/templates/ai-tools/skills/Docker/dwy-docker/references/ask-templates.md +13 -12
- package/templates/ai-tools/skills/Docker/dwy-docker/references/templates.md +3 -1
- package/templates/ai-tools/skills/Docker/dwy-docker/references/version-rules.md +10 -6
- package/templates/ai-tools/skills/Docker/dwy-docker/scripts/query_dockerhub.py +21 -6
- package/templates/ai-tools/skills/Docker/dwy-docker/scripts/scan_images.sh +2 -2
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/SKILL.md +44 -30
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/dockerfile-snippets.md +2 -0
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/mirror-providers.md +8 -8
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/apply_mirrors.py +14 -1
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/check_mirrors.py +4 -0
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/SKILL.md +8 -6
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/python-sdk.md +3 -3
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/stream-engine.md +2 -2
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/anti-patterns.md +3 -1
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/change-checklist.md +4 -4
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/lethal-violations.md +5 -5
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/perf-baselines.md +5 -5
- package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +59 -200
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/anticorruption.md +13 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/layering.md +5 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/response-envelope.md +20 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/timezone.md +7 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +15 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +43 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +103 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +49 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +13 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +62 -306
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +8 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +0 -31
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/overlay.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/reka.md +5 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v1/index.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/breaking.md +14 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +55 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/lookup.md +29 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +12 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +11 -14
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +1 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +1 -1
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +4 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +1 -1
- 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
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +45 -27
- 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
- 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
- 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
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +8 -0
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +7 -7
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +63 -59
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_base.sh +10 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_capacity.sh +21 -23
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_db.sh +9 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +20 -24
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_logs.sh +3 -3
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +4 -2
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +1 -1
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +1 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +7 -5
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +5 -3
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +140 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +113 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +16 -13
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
- package/src/sync-packs.js +0 -223
- package/src/sync-scene.js +0 -178
- 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 /
|
|
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
|
|
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', '
|
|
43
|
+
claude: ['skills', 'rules', 'hooks'],
|
|
52
44
|
codex: ['skills', 'rules', 'hooks'],
|
|
53
45
|
cursor: ['rules'],
|
|
54
|
-
opencode: ['skills', 'rules'
|
|
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
|
-
|
|
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
|
|
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/
|
|
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('
|
|
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`
|
|
822
|
-
*
|
|
395
|
+
* `dwy` / `dwy sync` 直接进入内容选择,再选位置和平台。
|
|
396
|
+
* action 仅供已有脚本调用;主交互不再显示动作菜单。
|
|
823
397
|
*
|
|
824
398
|
* @param {object} [opts]
|
|
825
399
|
*/
|
|
826
400
|
export async function runDwy(opts = {}) {
|
|
827
|
-
|
|
828
|
-
const action = opts.action
|
|
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
|
-
//
|
|
862
|
-
homeDir,
|
|
863
|
-
|
|
864
|
-
|
|
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:
|
|
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
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
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
|
|
966
|
-
|
|
967
|
-
:
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
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
|
|
1082
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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('未选择已接入平台,本次未执行同步。'))
|