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.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/cli-help.js +31 -2
- package/src/sync-all.js +168 -577
- package/src/sync-codex.js +42 -28
- package/src/sync-cursor.js +0 -25
- package/src/sync-global-skills.js +2 -2
- package/src/sync-opencode.js +7 -33
- package/src/sync.js +59 -226
- package/src/tab-multiselect.js +15 -53
- package/src/utils.js +0 -1
- package/templates/ai-tools/AGENTS.md +223 -0
- package/templates/ai-tools/CHANGELOG.md +195 -0
- 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-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 +2 -2
- package/templates/ai-tools/skills/Python/dwy-eapi/references/tasks-integration-guide.md +4 -4
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +3 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +28 -3
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +2 -2
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +12 -2
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +1 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +12 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +2 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +10 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +5 -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//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/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 +17 -14
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
- package/templates/project/fullstack-monorepo/{CLAUDE.md.hbs → AGENTS.md.hbs} +1 -1
- package/src/sync-packs.js +0 -223
- package/src/sync-scene.js +0 -178
- package/templates/ai-tools/CLAUDE.md +0 -210
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
- /package/templates/project/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /package/templates/project/fullstack-monorepo/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /package/templates/project/fullstack-monorepo/frontend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /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 /
|
|
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
|
|
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', '
|
|
47
|
+
claude: ['skills', 'rules', 'hooks'],
|
|
52
48
|
codex: ['skills', 'rules', 'hooks'],
|
|
53
49
|
cursor: ['rules'],
|
|
54
|
-
opencode: ['skills', 'rules'
|
|
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
|
-
|
|
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
|
|
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/
|
|
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`
|
|
822
|
-
*
|
|
398
|
+
* `dwy` / `dwy sync` 直接进入内容选择,再选位置和平台。
|
|
399
|
+
* action 仅供已有脚本调用;主交互不再显示动作菜单。
|
|
823
400
|
*
|
|
824
401
|
* @param {object} [opts]
|
|
825
402
|
*/
|
|
826
403
|
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
|
-
}
|
|
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
|
-
//
|
|
862
|
-
homeDir,
|
|
863
|
-
|
|
864
|
-
|
|
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:
|
|
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
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
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
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
:
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
console.log(
|
|
972
|
-
|
|
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
|
-
|
|
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
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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('未选择已接入平台,本次未执行同步。'))
|