create-dwy 0.21.0 → 0.23.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 +24 -0
- package/bin/index.js +6 -3
- package/package.json +1 -1
- package/src/cli-help.js +29 -13
- package/src/self-upgrade.js +7 -0
- package/src/skill-store.js +197 -0
- package/src/skills-install.js +47 -11
- package/src/sync-all.js +216 -605
- package/src/sync-codex.js +48 -25
- package/src/sync-global-skills.js +64 -14
- package/src/sync-opencode.js +9 -5
- package/src/sync-plan.js +278 -0
- package/src/sync.js +33 -18
- package/src/tab-multiselect.js +7 -5
- package/templates/ai-tools/AGENTS.md +1 -1
- package/templates/ai-tools/CHANGELOG.md +6 -0
- package/templates/ai-tools/VERSION +1 -1
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/official/progr/sql/createExternalTable.md +2 -2
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/official/tools/superset.md +1 -1
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/official/tutorials/Migrate_data_from_InfluxDB_to_DolphinDB.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +18 -25
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +3 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +4 -4
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +6 -70
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +3 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +5 -5
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +4 -79
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -2
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/publish-config.md +4 -4
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/version-sync.md +7 -7
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +3 -7
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-development-review/SKILL.md +58 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-development-review/references/delegation-guide.md +46 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-development-review/references/review-guide.md +57 -0
package/src/sync-all.js
CHANGED
|
@@ -1,89 +1,43 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
2
|
+
import path from 'node:path'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import { confirm, isCancel } from '@clack/prompts'
|
|
5
5
|
import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
copySkillsToProjectPlatforms,
|
|
9
|
-
normalizeSkillScope,
|
|
10
|
-
resolveProjectSkillDir,
|
|
11
|
-
} from './sync-global-skills.js'
|
|
6
|
+
import { normalizeSkillScope, resolveProjectSkillDir, resolveGlobalSkillDir, copySkillsToProjectPlatforms, deduplicateProjectSkills } from './sync-global-skills.js'
|
|
7
|
+
import { ensureSharedSkills } from './skill-store.js'
|
|
12
8
|
import { chalk } from './utils.js'
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
resolveSourceDir,
|
|
18
|
-
scanExisting,
|
|
19
|
-
scanHooks,
|
|
20
|
-
scanRules,
|
|
21
|
-
scanSkills,
|
|
22
|
-
syncClaude,
|
|
23
|
-
} from './sync.js'
|
|
24
|
-
import {
|
|
25
|
-
parseManagedRuleNames,
|
|
26
|
-
removeLegacyBaseline,
|
|
27
|
-
scanExistingCodexHooks,
|
|
28
|
-
scanExistingCodexSkills,
|
|
29
|
-
syncCodex,
|
|
30
|
-
syncGlobalBaselineDoc,
|
|
31
|
-
syncProjectAgentsMd,
|
|
32
|
-
} from './sync-codex.js'
|
|
33
|
-
import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
|
|
34
|
-
import { syncOpenCode } from './sync-opencode.js'
|
|
35
|
-
import { ensureSkillsInstalled, installSkills } from './skills-install.js'
|
|
9
|
+
import { interactiveSelect, readBaselineDoc, resolveSourceDir, scanExisting, scanHooks, scanRules, scanSkills } from './sync.js'
|
|
10
|
+
import { parseManagedRuleNames, scanExistingCodexHooks, scanExistingCodexSkills } from './sync-codex.js'
|
|
11
|
+
import { scanExistingCursorRules } from './sync-cursor.js'
|
|
12
|
+
import { installSkills } from './skills-install.js'
|
|
36
13
|
import { selfUpgrade } from './self-upgrade.js'
|
|
14
|
+
import { buildSyncPlan, contentOptions, projectSkillChoices, PLATFORM_LABELS, PLATFORM_SYNC_TYPES } from './sync-plan.js'
|
|
37
15
|
|
|
38
16
|
const PLATFORM_OPTIONS = [
|
|
39
|
-
{ value: 'claude', label: 'Claude Code', description: '支持
|
|
40
|
-
{ value: 'codex', label: 'Codex', description: '支持
|
|
41
|
-
{ value: '
|
|
42
|
-
{ value: '
|
|
17
|
+
{ value: 'claude', label: 'Claude Code', description: '支持 Skills、项目 Rules、Hooks 和全局通用规范' },
|
|
18
|
+
{ value: 'codex', label: 'Codex', description: '支持 Skills、项目 Rules、Hooks 和全局通用规范' },
|
|
19
|
+
{ value: 'opencode', label: 'OpenCode', description: '支持 Skills、项目 Rules 和全局通用规范;不支持 Hooks' },
|
|
20
|
+
{ value: 'cursor', label: 'Cursor', description: '只支持项目 Rules' },
|
|
43
21
|
]
|
|
44
|
-
|
|
45
22
|
const SYNC_STATE_PATH = ['.dwy', 'sync-state.json']
|
|
46
|
-
|
|
47
|
-
claude: ['skills', 'rules', 'hooks'],
|
|
48
|
-
codex: ['skills', 'rules', 'hooks'],
|
|
49
|
-
cursor: ['rules'],
|
|
50
|
-
opencode: ['skills', 'rules'],
|
|
51
|
-
}
|
|
52
|
-
const PLATFORM_LABELS = {
|
|
53
|
-
claude: 'Claude Code',
|
|
54
|
-
codex: 'Codex',
|
|
55
|
-
cursor: 'Cursor',
|
|
56
|
-
opencode: 'OpenCode',
|
|
57
|
-
}
|
|
58
|
-
/** 完整同步:逐项选择 skills / rules,hooks 自动启用。 */
|
|
23
|
+
/** 按显式选中的内容同步;保留已有脚本的模式标识。 */
|
|
59
24
|
export const SYNC_MODE_ALL = 'all'
|
|
60
|
-
/**
|
|
25
|
+
/** 兼容已有脚本的仅 Skills 模式。 */
|
|
61
26
|
export const SYNC_MODE_SKILLS = 'skills'
|
|
62
27
|
export const DEFAULT_SYNC_MODE = SYNC_MODE_ALL
|
|
63
|
-
|
|
64
|
-
/** 同步项目配置(skills / rules / hooks) */
|
|
28
|
+
/** 交互式配置同步。 */
|
|
65
29
|
export const ACTION_SYNC = 'sync'
|
|
66
|
-
/**
|
|
30
|
+
/** 安装产品流程依赖的外部 Skills;仅供已有脚本调用。 */
|
|
67
31
|
export const ACTION_INSTALL_SKILLS = 'install-skills'
|
|
68
|
-
/**
|
|
32
|
+
/** 升级全局 CLI。 */
|
|
69
33
|
export const ACTION_UPGRADE = 'upgrade'
|
|
70
34
|
|
|
71
|
-
/**
|
|
72
|
-
* 规范化同步范围。未知值回退完整同步,避免误进仅 Skills 删不该删的东西。
|
|
73
|
-
*
|
|
74
|
-
* @param {unknown} raw
|
|
75
|
-
* @returns {'all' | 'skills'}
|
|
76
|
-
*/
|
|
35
|
+
/** 兼容已有脚本的模式值,不从模式推导交互中的隐式写入。 */
|
|
77
36
|
export function normalizeSyncMode(raw) {
|
|
78
37
|
return raw === SYNC_MODE_SKILLS ? SYNC_MODE_SKILLS : DEFAULT_SYNC_MODE
|
|
79
38
|
}
|
|
80
39
|
|
|
81
|
-
/**
|
|
82
|
-
* 规范化入口动作。未知值回退同步,避免误进强制重装。
|
|
83
|
-
*
|
|
84
|
-
* @param {unknown} raw
|
|
85
|
-
* @returns {'sync' | 'install-skills' | 'upgrade'}
|
|
86
|
-
*/
|
|
40
|
+
/** 兼容已有脚本的入口动作;未知动作进入正常交互。 */
|
|
87
41
|
export function normalizeAction(raw) {
|
|
88
42
|
if (raw === ACTION_INSTALL_SKILLS) return ACTION_INSTALL_SKILLS
|
|
89
43
|
if (raw === ACTION_UPGRADE) return ACTION_UPGRADE
|
|
@@ -94,599 +48,256 @@ function unionSets(...sets) {
|
|
|
94
48
|
return new Set(sets.flatMap(set => [...set]))
|
|
95
49
|
}
|
|
96
50
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
skills: new Set(),
|
|
104
|
-
rules: new Set(),
|
|
105
|
-
commands: new Set(),
|
|
106
|
-
hooks: new Set(),
|
|
51
|
+
/** 从点名的平台记录恢复选择;未传平台时保留历史调用约定。 */
|
|
52
|
+
export function buildSelectionDefaultsFromSyncState(existing, syncState, platforms = Object.keys(syncState?.platforms || {})) {
|
|
53
|
+
const result = { skills: new Set(), rules: new Set(), commands: new Set(), hooks: new Set() }
|
|
54
|
+
for (const platform of platforms) {
|
|
55
|
+
const state = syncState?.platforms?.[platform] || {}
|
|
56
|
+
for (const type of Object.keys(result)) for (const name of state[type] || []) result[type].add(name)
|
|
107
57
|
}
|
|
108
|
-
|
|
109
|
-
for (const platformState of Object.values(syncState?.platforms || {})) {
|
|
110
|
-
for (const type of ['skills', 'rules', 'commands', 'hooks']) {
|
|
111
|
-
for (const name of platformState?.[type] || []) {
|
|
112
|
-
stateDefaults[type].add(name)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return hasSetItems(stateDefaults) ? stateDefaults : existing
|
|
58
|
+
return Object.values(result).some(names => names.size) ? result : existing
|
|
118
59
|
}
|
|
119
60
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const agentsMdPath = path.join(projectDir, 'AGENTS.md')
|
|
123
|
-
const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
skills: unionSets(
|
|
127
|
-
await scanExisting(claudeDir, 'skills'),
|
|
128
|
-
await scanExistingCodexSkills(projectDir),
|
|
129
|
-
),
|
|
130
|
-
rules: unionSets(
|
|
131
|
-
await scanExisting(claudeDir, 'rules'),
|
|
132
|
-
parseManagedRuleNames(agentsMdContent),
|
|
133
|
-
),
|
|
134
|
-
hooks: unionSets(
|
|
135
|
-
await scanExisting(claudeDir, 'hooks'),
|
|
136
|
-
await scanExistingCodexHooks(projectDir),
|
|
137
|
-
),
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function buildPlatformDefaultsFromLocalState(projectDir) {
|
|
61
|
+
/** 从当前范围的实际入口推导平台默认值;不读取另一个范围的配置。 */
|
|
62
|
+
export async function buildPlatformDefaultsFromLocalState(projectDir, scope = 'project', codexHome = path.join(projectDir, '.codex')) {
|
|
142
63
|
const defaults = []
|
|
143
|
-
|
|
144
|
-
if (await fs.pathExists(path.join(projectDir, '.
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
await fs.pathExists(path.join(projectDir, '.codex'))
|
|
64
|
+
if (await fs.pathExists(path.join(projectDir, '.claude'))) defaults.push('claude')
|
|
65
|
+
if (await fs.pathExists(path.join(projectDir, '.codex'))
|
|
66
|
+
|| (scope === 'global' && await fs.pathExists(codexHome))
|
|
149
67
|
|| await fs.pathExists(path.join(projectDir, '.agents'))
|
|
150
|
-
|| await fs.pathExists(path.join(projectDir, 'AGENTS.md'))
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (await fs.pathExists(path.join(projectDir, '.cursor'))) {
|
|
155
|
-
defaults.push('cursor')
|
|
156
|
-
}
|
|
157
|
-
if (await fs.pathExists(path.join(projectDir, '.opencode'))) {
|
|
158
|
-
defaults.push('opencode')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return defaults.length > 0 ? defaults : ['claude', 'codex']
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
|
|
166
|
-
* 禁止调用完整平台 sync,否则空 rules/hooks 会删掉项目已有项。
|
|
167
|
-
*
|
|
168
|
-
* @param {{ projectDir: string, selectedPlatforms: string[], skills: Array<{ name: string, sourcePath: string }>, templateSkillNames: Set<string>, staleSkillNames: Set<string>, skillScope: { destinations: string[] } }} opts
|
|
169
|
-
*/
|
|
170
|
-
async function syncSkillsOnly({
|
|
171
|
-
projectDir,
|
|
172
|
-
selectedPlatforms,
|
|
173
|
-
skills,
|
|
174
|
-
templateSkillNames,
|
|
175
|
-
staleSkillNames,
|
|
176
|
-
skillScope,
|
|
177
|
-
}) {
|
|
178
|
-
const writeProject = skillScope.destinations.includes('project')
|
|
179
|
-
if (!writeProject) return
|
|
180
|
-
|
|
181
|
-
const skillPlatforms = selectedPlatforms.filter(platform => resolveProjectSkillDir(projectDir, platform))
|
|
182
|
-
if (skillPlatforms.length === 0) {
|
|
183
|
-
console.log(chalk.yellow('已选平台不支持项目内 skills,跳过项目写入。'))
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
console.log(chalk.blue('\nSyncing skills only (rules / commands / hooks untouched)...\n'))
|
|
188
|
-
await copySkillsToProjectPlatforms({
|
|
189
|
-
projectDir,
|
|
190
|
-
platforms: skillPlatforms,
|
|
191
|
-
skills,
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const selectedNames = new Set(skills.map(item => item.name))
|
|
195
|
-
const toRemove = new Set(
|
|
196
|
-
[...templateSkillNames, ...staleSkillNames].filter(name => !selectedNames.has(name)),
|
|
197
|
-
)
|
|
198
|
-
for (const platform of skillPlatforms) {
|
|
199
|
-
const destRoot = resolveProjectSkillDir(projectDir, platform)
|
|
200
|
-
const existingNames = await scanExisting(path.dirname(destRoot), 'skills')
|
|
201
|
-
await removeManagedItems(
|
|
202
|
-
destRoot,
|
|
203
|
-
existingNames,
|
|
204
|
-
toRemove,
|
|
205
|
-
path.relative(projectDir, destRoot),
|
|
206
|
-
)
|
|
207
|
-
}
|
|
68
|
+
|| (scope === 'project' && await fs.pathExists(path.join(projectDir, 'AGENTS.md')))) defaults.push('codex')
|
|
69
|
+
if (scope === 'project' && await fs.pathExists(path.join(projectDir, '.cursor'))) defaults.push('cursor')
|
|
70
|
+
if (await fs.pathExists(path.join(projectDir, ...(scope === 'global' ? ['.config', 'opencode'] : ['.opencode'])))) defaults.push('opencode')
|
|
71
|
+
return defaults.length ? defaults : ['claude', 'codex']
|
|
208
72
|
}
|
|
209
73
|
|
|
210
|
-
async function selectPlatforms(
|
|
211
|
-
const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
|
|
212
|
-
|
|
213
|
-
// 可搜索多选 + 底部说明区
|
|
74
|
+
async function selectPlatforms(root, scope, codexHome) {
|
|
214
75
|
const result = await searchableMultiselect({
|
|
215
76
|
message: '选择要同步的平台',
|
|
216
|
-
options: PLATFORM_OPTIONS
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
maxItems: 6,
|
|
220
|
-
placeholder: SEARCH_PLACEHOLDER,
|
|
77
|
+
options: PLATFORM_OPTIONS.filter(option => scope === 'project' || option.value !== 'cursor')
|
|
78
|
+
.map(option => ({ ...option, description: contentOptions(scope, [option.value]).map(item => item.label).join('、') })),
|
|
79
|
+
initialValues: await buildPlatformDefaultsFromLocalState(root, scope, codexHome),
|
|
80
|
+
required: true, maxItems: 6, placeholder: SEARCH_PLACEHOLDER,
|
|
221
81
|
})
|
|
222
|
-
|
|
223
|
-
return result
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function buildCursorRuleName(ruleName) {
|
|
227
|
-
return `dwy-${ruleName.replace(/\.md$/i, '')}.mdc`
|
|
82
|
+
return isCancel(result) ? null : result
|
|
228
83
|
}
|
|
229
84
|
|
|
230
85
|
function createEmptyPlatformState() {
|
|
231
|
-
return {
|
|
232
|
-
skills: [],
|
|
233
|
-
rules: [],
|
|
234
|
-
commands: [],
|
|
235
|
-
hooks: [],
|
|
236
|
-
}
|
|
86
|
+
return { skills: [], rules: [], commands: [], hooks: [] }
|
|
237
87
|
}
|
|
238
88
|
|
|
239
|
-
async function loadSyncState(
|
|
240
|
-
const
|
|
241
|
-
if (!await fs.pathExists(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
platforms: {},
|
|
245
|
-
skillScope: normalizeSkillScope(undefined),
|
|
246
|
-
syncMode: DEFAULT_SYNC_MODE,
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const state = await fs.readJson(statePath)
|
|
251
|
-
return {
|
|
252
|
-
version: 1,
|
|
253
|
-
platforms: state.platforms || {},
|
|
254
|
-
skillScope: normalizeSkillScope(state.skillScope),
|
|
255
|
-
syncMode: normalizeSyncMode(state.syncMode),
|
|
256
|
-
}
|
|
89
|
+
async function loadSyncState(root) {
|
|
90
|
+
const file = path.join(root, ...SYNC_STATE_PATH)
|
|
91
|
+
if (!await fs.pathExists(file)) return { version: 1, platforms: {}, skillScope: normalizeSkillScope(undefined), syncMode: DEFAULT_SYNC_MODE }
|
|
92
|
+
const state = await fs.readJson(file)
|
|
93
|
+
return { version: 1, platforms: state.platforms || {}, skillScope: normalizeSkillScope(state.skillScope), syncMode: normalizeSyncMode(state.syncMode) }
|
|
257
94
|
}
|
|
258
95
|
|
|
259
|
-
async function saveSyncState(
|
|
260
|
-
const
|
|
261
|
-
await fs.ensureDir(path.dirname(
|
|
262
|
-
await fs.writeJson(
|
|
96
|
+
async function saveSyncState(root, state) {
|
|
97
|
+
const file = path.join(root, ...SYNC_STATE_PATH)
|
|
98
|
+
await fs.ensureDir(path.dirname(file))
|
|
99
|
+
await fs.writeJson(file, state, { spaces: 2 })
|
|
263
100
|
}
|
|
264
101
|
|
|
265
|
-
function
|
|
266
|
-
return {
|
|
267
|
-
skills: new Set(scans.skills.map(item => item.name)),
|
|
268
|
-
rules: new Set(scans.rules.map(item => item.name)),
|
|
269
|
-
commands: new Set(scans.commands.map(item => item.name)),
|
|
270
|
-
hooks: new Set(scans.hooks.map(item => item.name)),
|
|
271
|
-
}
|
|
102
|
+
function buildCursorRuleName(name) {
|
|
103
|
+
return `dwy-${name.replace(/\.md$/i, '')}.mdc`
|
|
272
104
|
}
|
|
273
105
|
|
|
274
|
-
async function
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
106
|
+
async function managedNames(root, global, codexHome) {
|
|
107
|
+
const projectFile = path.join(root, 'AGENTS.md')
|
|
108
|
+
const projectRules = !global && await fs.pathExists(projectFile)
|
|
109
|
+
? parseManagedRuleNames(await fs.readFile(projectFile, 'utf8')) : new Set()
|
|
110
|
+
const claude = path.join(root, '.claude')
|
|
111
|
+
const opencode = path.join(root, ...(global ? ['.config', 'opencode'] : ['.opencode']))
|
|
112
|
+
const codexSkills = await scanExistingCodexSkills(root)
|
|
113
|
+
if (global && await fs.pathExists(path.join(codexHome, 'skills'))) {
|
|
114
|
+
for (const name of await fs.readdir(path.join(codexHome, 'skills'))) codexSkills.add(name)
|
|
115
|
+
}
|
|
280
116
|
return {
|
|
281
|
-
claude: {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
},
|
|
286
|
-
codex: {
|
|
287
|
-
skills: await scanExistingCodexSkills(projectDir),
|
|
288
|
-
rules: parseManagedRuleNames(agentsMdContent),
|
|
289
|
-
hooks: await scanExistingCodexHooks(projectDir),
|
|
290
|
-
},
|
|
291
|
-
cursor: {
|
|
292
|
-
rules: await scanExistingCursorRules(projectDir),
|
|
293
|
-
},
|
|
294
|
-
opencode: {
|
|
295
|
-
skills: await scanExisting(openCodeDir, 'skills'),
|
|
296
|
-
rules: global ? parseManagedRuleNames(await fs.pathExists(path.join(openCodeDir, 'AGENTS.md')) ? await fs.readFile(path.join(openCodeDir, 'AGENTS.md'), 'utf-8') : '') : parseManagedRuleNames(agentsMdContent),
|
|
297
|
-
},
|
|
117
|
+
claude: { skills: await scanExisting(claude, 'skills'), rules: unionSets(projectRules, await scanExisting(claude, 'rules')), hooks: await scanExisting(claude, 'hooks') },
|
|
118
|
+
codex: { skills: codexSkills, rules: projectRules, hooks: await scanExistingCodexHooks(root, global ? codexHome : path.join(root, '.codex')) },
|
|
119
|
+
opencode: { skills: await scanExisting(opencode, 'skills'), rules: projectRules, hooks: new Set() },
|
|
120
|
+
cursor: { rules: await scanExistingCursorRules(root), skills: new Set(), hooks: new Set() },
|
|
298
121
|
}
|
|
299
122
|
}
|
|
300
123
|
|
|
301
|
-
function
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
124
|
+
function staleEntries(scans, state, existing, platforms) {
|
|
125
|
+
const current = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type, new Set(scans[type].map(item => item.name))]))
|
|
126
|
+
const result = {}
|
|
127
|
+
for (const platform of platforms) {
|
|
128
|
+
result[platform] = {}
|
|
129
|
+
for (const type of PLATFORM_SYNC_TYPES[platform]) {
|
|
130
|
+
result[platform][type] = (state.platforms[platform]?.[type] || []).filter(name =>
|
|
131
|
+
!current[type].has(name) && existing[platform][type].has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
|
|
307
132
|
}
|
|
308
133
|
}
|
|
309
|
-
return
|
|
134
|
+
return result
|
|
310
135
|
}
|
|
311
136
|
|
|
312
|
-
function
|
|
313
|
-
return Object.fromEntries(
|
|
314
|
-
Object.
|
|
315
|
-
)
|
|
137
|
+
function normalizeRemovals(raw) {
|
|
138
|
+
return Object.fromEntries(Object.entries(raw).map(([platform, types]) => [platform,
|
|
139
|
+
Object.fromEntries(Object.entries(types).map(([type, names]) => [type, new Set(names)]))]))
|
|
316
140
|
}
|
|
317
141
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
for (const type of PLATFORM_SYNC_TYPES[platform] || []) {
|
|
328
|
-
const previousNames = previous[type] || []
|
|
329
|
-
const currentNames = currentTemplateNames[type] || new Set()
|
|
330
|
-
const managedNames = platformManaged[type] || new Set()
|
|
331
|
-
const staleNames = []
|
|
332
|
-
|
|
333
|
-
for (const name of previousNames) {
|
|
334
|
-
if (currentNames.has(name)) continue
|
|
335
|
-
if (type === 'rules' && platform === 'cursor') {
|
|
336
|
-
if (!managedNames.has(buildCursorRuleName(name))) continue
|
|
337
|
-
} else if (!managedNames.has(name)) {
|
|
338
|
-
continue
|
|
339
|
-
}
|
|
340
|
-
staleNames.push(name)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (staleNames.length > 0) staleEntries[platform][type] = staleNames.sort((a, b) => a.localeCompare(b, 'zh'))
|
|
344
|
-
}
|
|
142
|
+
/** `dwy` 和 `dwy sync` 共用范围 → 平台 → 内容 → 逐项选择 → 清单确认。 */
|
|
143
|
+
export async function runDwy(opts = {}) {
|
|
144
|
+
console.log(chalk.bold.cyan('\ndwy') + chalk.gray(' · AI 开发工具\n'))
|
|
145
|
+
const action = normalizeAction(opts.action)
|
|
146
|
+
if (action === ACTION_INSTALL_SKILLS) { await installSkills(); return }
|
|
147
|
+
if (action === ACTION_UPGRADE) {
|
|
148
|
+
try { await selfUpgrade() } catch (error) { console.error(error.message); process.exitCode = 1 }
|
|
149
|
+
return
|
|
345
150
|
}
|
|
346
|
-
|
|
347
|
-
return staleEntries
|
|
151
|
+
return syncAll(opts)
|
|
348
152
|
}
|
|
349
153
|
|
|
350
154
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* @param {Record<string, Record<string, string[]>>} staleEntries
|
|
155
|
+
* 一键迁移当前项目已有的 CLI Skills,不进入选择流程、不补装其他平台、不修改 Rules / Hooks。
|
|
156
|
+
* 仅当前模板或历史同步记录中的名称视为托管;已移除的历史模板和整个根目录链接保留并报告。
|
|
354
157
|
*/
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
158
|
+
export async function migrateSkills({
|
|
159
|
+
sourceDir,
|
|
160
|
+
projectDir = process.cwd(),
|
|
161
|
+
homeDir = os.homedir(),
|
|
162
|
+
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'),
|
|
163
|
+
} = {}) {
|
|
164
|
+
const realProject = await fs.realpath(projectDir)
|
|
165
|
+
if (await fs.pathExists(homeDir) && realProject === await fs.realpath(homeDir)) {
|
|
166
|
+
throw new Error('当前目录是用户主目录,请使用全局同步或进入项目目录迁移')
|
|
167
|
+
}
|
|
168
|
+
const scans = await scanSkills(sourceDir || await resolveSourceDir())
|
|
169
|
+
const available = new Map(scans.map(skill => [skill.name, skill]))
|
|
170
|
+
const state = await loadSyncState(projectDir)
|
|
171
|
+
const existingByPlatform = {}
|
|
172
|
+
const neededNames = new Set()
|
|
173
|
+
const retained = []
|
|
174
|
+
for (const platform of ['claude', 'codex', 'opencode']) {
|
|
175
|
+
const root = resolveProjectSkillDir(projectDir, platform)
|
|
176
|
+
if (!await fs.pathExists(root)) continue
|
|
177
|
+
if (await fs.realpath(root) !== path.join(realProject, path.relative(projectDir, root))) {
|
|
178
|
+
console.log(chalk.yellow(`${root} 已通过根目录链接共享内容,保留原链接,不迁移其目标。`))
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const existing = await scanExisting(path.dirname(root), 'skills')
|
|
182
|
+
const historical = new Set(state.platforms[platform]?.skills || [])
|
|
183
|
+
const managed = [...existing].filter(name => available.has(name) || historical.has(name))
|
|
184
|
+
if (managed.length === 0) continue
|
|
185
|
+
existingByPlatform[platform] = managed
|
|
186
|
+
for (const name of managed) {
|
|
187
|
+
if (available.has(name)) neededNames.add(name)
|
|
188
|
+
else {
|
|
189
|
+
retained.push({ platform, name })
|
|
190
|
+
console.log(chalk.yellow(`${platform}/${name} 已不在当前模板中,保留原目录。`))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
359
193
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
for (const type of PLATFORM_SYNC_TYPES[platform] || []) {
|
|
373
|
-
const selectedNames = new Set((selected[type] || []).map(item => item.name))
|
|
374
|
-
for (const name of staleEntries[platform]?.[type] || []) {
|
|
375
|
-
const approvedNames = approvedRemovals[platform]?.[type] || new Set()
|
|
376
|
-
if (!approvedNames.has(name)) selectedNames.add(name)
|
|
194
|
+
const shared = await ensureSharedSkills({ skills: [...neededNames].map(name => available.get(name)), homeDir })
|
|
195
|
+
let linked = 0
|
|
196
|
+
let globalCount = 0
|
|
197
|
+
for (const [platform, names] of Object.entries(existingByPlatform)) {
|
|
198
|
+
const skills = shared.filter(skill => names.includes(skill.name))
|
|
199
|
+
const duplicates = await deduplicateProjectSkills({ projectDir, platform, skills, homeDir, codexHome })
|
|
200
|
+
const local = skills.filter(skill => !duplicates.has(skill.name))
|
|
201
|
+
linked += await copySkillsToProjectPlatforms({ projectDir, platforms: [platform], skills: local, homeDir })
|
|
202
|
+
globalCount += duplicates.size
|
|
203
|
+
state.platforms[platform] = {
|
|
204
|
+
...(state.platforms[platform] || createEmptyPlatformState()),
|
|
205
|
+
skills: names.filter(name => !duplicates.has(name)).sort(),
|
|
377
206
|
}
|
|
378
|
-
state[type] = [...selectedNames].sort((a, b) => a.localeCompare(b, 'zh'))
|
|
379
207
|
}
|
|
380
|
-
|
|
381
|
-
|
|
208
|
+
if (Object.keys(existingByPlatform).length) await saveSyncState(projectDir, state)
|
|
209
|
+
console.log(chalk.green(`迁移完成:${linked} 个项目链接,${globalCount} 个使用全局入口,${retained.length} 个历史条目保留。`))
|
|
210
|
+
return { linked, global: globalCount, retained }
|
|
382
211
|
}
|
|
383
212
|
|
|
384
|
-
async function removeManagedItems(targetDir, existingNames, managedNames, labelPrefix) {
|
|
385
|
-
let removedCount = 0
|
|
386
|
-
|
|
387
|
-
for (const name of existingNames) {
|
|
388
|
-
if (!managedNames.has(name)) continue
|
|
389
|
-
await fs.remove(path.join(targetDir, name))
|
|
390
|
-
logAction(`${labelPrefix}/${name}`, 'red', '×')
|
|
391
|
-
removedCount++
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return removedCount
|
|
395
|
-
}
|
|
396
213
|
|
|
397
214
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* @param {object} [opts]
|
|
215
|
+
* 只执行点名范围和内容;测试/脚本可注入选择,确认前只读。
|
|
216
|
+
* contentTypes 未传时,已有脚本按 selected 中明确提供的字段处理;主交互默认只选 Skills。
|
|
402
217
|
*/
|
|
403
|
-
export async function runDwy(opts = {}) {
|
|
404
|
-
console.log(chalk.bold.cyan('\ndwy') + chalk.gray(' · AI 开发工具\n'))
|
|
405
|
-
const action = normalizeAction(opts.action)
|
|
406
|
-
if (action === ACTION_INSTALL_SKILLS) {
|
|
407
|
-
await installSkills()
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
if (action === ACTION_UPGRADE) {
|
|
411
|
-
try {
|
|
412
|
-
await selfUpgrade()
|
|
413
|
-
} catch (error) {
|
|
414
|
-
console.error(error.message)
|
|
415
|
-
process.exitCode = 1
|
|
416
|
-
}
|
|
417
|
-
return
|
|
418
|
-
}
|
|
419
|
-
await syncAll(opts)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
218
|
export async function syncAll({
|
|
423
|
-
sourceDir: sourceDirOverride,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
staleRemovals: staleRemovalsOverride,
|
|
428
|
-
// 兼容已有脚本;主交互的位置只控制 Skills / Hooks。
|
|
429
|
-
skillScope: skillScopeOverride,
|
|
430
|
-
// all = 完整同步;skills = 只写 skill,测试/脚本可注入
|
|
431
|
-
syncMode: syncModeOverride,
|
|
432
|
-
// 全局配置根目录;测试必须注入临时用户目录。
|
|
433
|
-
homeDir = os.homedir(),
|
|
434
|
-
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'),
|
|
435
|
-
scope: scopeOverride,
|
|
436
|
-
confirmed,
|
|
219
|
+
sourceDir: sourceDirOverride, projectDir: projectDirOverride, selected: selectedOverride,
|
|
220
|
+
selectedPlatforms: platformsOverride, contentTypes: typesOverride, staleRemovals: removalsOverride,
|
|
221
|
+
skillScope: skillScopeOverride, syncMode: modeOverride, homeDir = os.homedir(),
|
|
222
|
+
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'), scope: scopeOverride, confirmed,
|
|
437
223
|
} = {}) {
|
|
438
224
|
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
439
|
-
if (!await fs.pathExists(sourceDir))
|
|
440
|
-
throw new Error('ai-tools templates not found in repo')
|
|
441
|
-
}
|
|
442
|
-
|
|
225
|
+
if (!await fs.pathExists(sourceDir)) throw new Error('ai-tools templates not found in repo')
|
|
443
226
|
const projectDir = projectDirOverride || process.cwd()
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
skills: await scanSkills(sourceDir),
|
|
447
|
-
rules: await scanRules(sourceDir),
|
|
448
|
-
commands: [],
|
|
449
|
-
hooks: await scanHooks(sourceDir),
|
|
450
|
-
}
|
|
451
|
-
const existing = await scanCombinedExisting(projectDir)
|
|
452
|
-
const syncState = await loadSyncState(projectDir)
|
|
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
|
-
})
|
|
462
|
-
if (selected === null) {
|
|
463
|
-
console.log(chalk.yellow('\n已取消同步。'))
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (syncMode === SYNC_MODE_ALL) selected.hooks = scans.hooks
|
|
468
|
-
|
|
469
|
-
const scopeResult = scopeOverride || (selectedOverride || selectedPlatformsOverride ? 'project' : await searchableSelect({
|
|
470
|
-
message: '选择同步位置(Skills / Hooks)',
|
|
227
|
+
const scope = scopeOverride || (selectedOverride || platformsOverride ? 'project' : await searchableSelect({
|
|
228
|
+
message: '选择同步范围',
|
|
471
229
|
options: [
|
|
472
|
-
{ value: 'project', label: '当前项目', description: 'Skills
|
|
473
|
-
{ value: 'global', label: '全局', description: 'Skills
|
|
474
|
-
],
|
|
475
|
-
initialValue: 'project',
|
|
230
|
+
{ value: 'project', label: '当前项目', description: '管理项目启用的 Skills、项目 Rules 和 Hooks;全局已有 Skills 自动去重' },
|
|
231
|
+
{ value: 'global', label: '全局', description: '管理用户级 Skills、通用规范和 Hooks;项目重复 Skill 入口会明确列出清理项' },
|
|
232
|
+
], initialValue: 'project',
|
|
476
233
|
}))
|
|
477
|
-
if (isCancel(
|
|
478
|
-
if (!['project', 'global'].includes(
|
|
479
|
-
const global =
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
]
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
234
|
+
if (isCancel(scope)) { console.log(chalk.yellow('已取消同步。')); return }
|
|
235
|
+
if (!['project', 'global'].includes(scope)) throw new Error('无效同步范围')
|
|
236
|
+
const global = scope === 'global', targetRoot = global ? homeDir : projectDir
|
|
237
|
+
const platforms = platformsOverride || await selectPlatforms(targetRoot, scope, codexHome)
|
|
238
|
+
if (platforms === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
239
|
+
if (platforms.length === 0 || platforms.some(platform => !PLATFORM_SYNC_TYPES[platform] || (global && platform === 'cursor'))) throw new Error('所选平台不支持此同步范围')
|
|
240
|
+
const options = contentOptions(scope, platforms)
|
|
241
|
+
const allowed = new Set(options.map(option => option.value))
|
|
242
|
+
let contentTypes = typesOverride
|
|
243
|
+
if (!contentTypes && modeOverride === SYNC_MODE_SKILLS) contentTypes = ['skills']
|
|
244
|
+
if (!contentTypes && selectedOverride) contentTypes = Object.keys(selectedOverride).filter(type => allowed.has(type))
|
|
245
|
+
if (!contentTypes) {
|
|
246
|
+
const result = await searchableMultiselect({
|
|
247
|
+
message: '选择本次处理内容(未勾选的类型保持原样)', options,
|
|
248
|
+
initialValues: allowed.has('skills') ? ['skills'] : [], required: true, maxItems: 5,
|
|
249
|
+
})
|
|
250
|
+
if (isCancel(result)) { console.log(chalk.yellow('已取消同步。')); return }
|
|
251
|
+
contentTypes = result
|
|
252
|
+
}
|
|
253
|
+
if (contentTypes.some(type => !allowed.has(type))) throw new Error('所选范围或平台不支持此同步内容')
|
|
254
|
+
if (contentTypes.length === 0) { console.log('未选择处理内容。'); return }
|
|
255
|
+
const scans = { skills: await scanSkills(sourceDir), rules: await scanRules(sourceDir), hooks: await scanHooks(sourceDir), commands: [] }
|
|
256
|
+
const projectState = await loadSyncState(projectDir), globalState = await loadSyncState(homeDir)
|
|
257
|
+
const targetState = global ? globalState : projectState
|
|
258
|
+
const existing = await managedNames(targetRoot, global, codexHome)
|
|
259
|
+
const current = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type,
|
|
260
|
+
unionSets(...platforms.map(platform => existing[platform][type]))]))
|
|
261
|
+
const defaults = buildSelectionDefaultsFromSyncState(current, targetState, platforms)
|
|
262
|
+
for (const type of ['skills', 'rules', 'hooks']) defaults[type] = unionSets(defaults[type], current[type])
|
|
263
|
+
const choices = !global && contentTypes.includes('skills')
|
|
264
|
+
? { ...scans, skills: await projectSkillChoices(scans.skills, platforms, { homeDir, codexHome }) } : scans
|
|
265
|
+
const selected = selectedOverride
|
|
266
|
+
? { skills: selectedOverride.skills || [], rules: selectedOverride.rules || [], hooks: [], commands: [] }
|
|
267
|
+
: await interactiveSelect(choices, defaults, { types: contentTypes })
|
|
268
|
+
if (selected === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
269
|
+
const stale = staleEntries(scans, targetState, existing, platforms)
|
|
270
|
+
const removals = normalizeRemovals(removalsOverride || stale)
|
|
271
|
+
const baseline = contentTypes.some(type => type === 'rules' || type === 'baseline') ? await readBaselineDoc(sourceDir) : ''
|
|
272
|
+
const plan = await buildSyncPlan({
|
|
273
|
+
scope, platforms, contentTypes, selected, scans, sourceDir, projectDir, homeDir, codexHome,
|
|
274
|
+
projectState, globalState, staleRemovals: removals, staleEntries: stale, baseline,
|
|
275
|
+
skillScope: normalizeSkillScope(skillScopeOverride),
|
|
276
|
+
})
|
|
277
|
+
const nextTarget = global ? plan.nextGlobal : plan.nextProject
|
|
278
|
+
nextTarget.contentTypes = contentTypes
|
|
279
|
+
nextTarget.syncMode = contentTypes.length === 1 && contentTypes[0] === 'skills' ? SYNC_MODE_SKILLS : SYNC_MODE_ALL
|
|
280
|
+
nextTarget.skillScope = normalizeSkillScope(skillScopeOverride)
|
|
281
|
+
const stateFile = path.join(targetRoot, ...SYNC_STATE_PATH)
|
|
282
|
+
plan.operations.push({ kind: 'state', label: '更新当前范围同步记录', paths: [stateFile], run: () => saveSyncState(targetRoot, nextTarget) })
|
|
283
|
+
if (global && plan.projectChanged) plan.operations.push({
|
|
284
|
+
kind: 'state', label: '更新项目去重记录', paths: [path.join(projectDir, ...SYNC_STATE_PATH)],
|
|
285
|
+
run: () => saveSyncState(projectDir, plan.nextProject),
|
|
286
|
+
})
|
|
287
|
+
console.log(`\n同步范围:${global ? '全局' : '当前项目'};平台:${platforms.map(p => PLATFORM_LABELS[p]).join('、')}`)
|
|
288
|
+
console.log(`处理内容:${contentTypes.map(type => options.find(option => option.value === type).label).join('、')}`)
|
|
289
|
+
if (contentTypes.includes('skills')) console.log(`Skills:${selected.skills.map(item => item.name).join('、') || '无项目安装选择;已有全局入口仅去重'}`)
|
|
290
|
+
if (contentTypes.includes('rules')) console.log(`Rules:${selected.rules.map(item => item.name).join('、') || '移除当前项目托管规则'}`)
|
|
291
|
+
if (contentTypes.includes('baseline')) console.log(`全局通用规范摘要:${baseline.split('\n').filter(line => /^#/.test(line)).slice(0,3).join(';')}`)
|
|
292
|
+
for (const operation of plan.operations) {
|
|
293
|
+
console.log(operation.label)
|
|
294
|
+
for (const dest of operation.paths) console.log(` → ${dest}`)
|
|
516
295
|
}
|
|
517
296
|
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
297
|
const answer = confirmed === false ? false : await confirm({ message: '确认同步?' })
|
|
546
298
|
if (isCancel(answer) || !answer) { console.log(chalk.yellow('已取消同步。')); return }
|
|
547
299
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const skillsToWrite = scans.skills.filter(item => selectedSkillNames.has(item.name))
|
|
552
|
-
let syncedAnySupportedPlatform = false
|
|
553
|
-
|
|
554
|
-
if (syncMode === SYNC_MODE_SKILLS) {
|
|
555
|
-
const staleSkillNames = new Set()
|
|
556
|
-
for (const platform of selectedPlatforms) {
|
|
557
|
-
for (const name of approvedStaleRemovals[platform]?.skills || []) staleSkillNames.add(name)
|
|
558
|
-
}
|
|
559
|
-
await syncSkillsOnly({
|
|
560
|
-
projectDir,
|
|
561
|
-
selectedPlatforms,
|
|
562
|
-
skills: skillsToWrite,
|
|
563
|
-
templateSkillNames: new Set(scans.skills.map(item => item.name)),
|
|
564
|
-
staleSkillNames,
|
|
565
|
-
skillScope,
|
|
566
|
-
})
|
|
567
|
-
syncedAnySupportedPlatform = selectedPlatforms.some(platform => resolveProjectSkillDir(projectDir, platform))
|
|
568
|
-
|| skillScope.destinations.some(id => id !== 'project')
|
|
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
|
-
}
|
|
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
|
-
}
|
|
583
|
-
syncedAnySupportedPlatform = true
|
|
584
|
-
console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
|
|
585
|
-
await syncClaude({
|
|
586
|
-
sourceDir,
|
|
587
|
-
projectDir: targetRoot,
|
|
588
|
-
global,
|
|
589
|
-
selected,
|
|
590
|
-
staleRemovals: approvedStaleRemovals.claude,
|
|
591
|
-
})
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (selectedPlatforms.includes('codex')) {
|
|
595
|
-
syncedAnySupportedPlatform = true
|
|
596
|
-
console.log(chalk.blue('\nSyncing Codex configuration...\n'))
|
|
597
|
-
await syncCodex({
|
|
598
|
-
sourceDir,
|
|
599
|
-
projectDir: targetRoot,
|
|
600
|
-
global,
|
|
601
|
-
selected,
|
|
602
|
-
staleRemovals: approvedStaleRemovals.codex,
|
|
603
|
-
})
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (selectedPlatforms.includes('cursor')) {
|
|
607
|
-
syncedAnySupportedPlatform = true
|
|
608
|
-
await syncCursor({
|
|
609
|
-
sourceDir,
|
|
610
|
-
projectDir,
|
|
611
|
-
selected,
|
|
612
|
-
staleRemovals: approvedStaleRemovals.cursor,
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (selectedPlatforms.includes('opencode')) {
|
|
617
|
-
syncedAnySupportedPlatform = true
|
|
618
|
-
await syncOpenCode({
|
|
619
|
-
sourceDir,
|
|
620
|
-
projectDir: targetRoot,
|
|
621
|
-
global,
|
|
622
|
-
selected,
|
|
623
|
-
staleRemovals: approvedStaleRemovals.opencode,
|
|
624
|
-
})
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const globalSkillNameSet = new Set(skillScope.globalSkills)
|
|
631
|
-
const globalSkills = scans.skills.filter(item => globalSkillNameSet.has(item.name))
|
|
632
|
-
if (globalSkills.length > 0 && skillScope.destinations.some(id => id !== 'project')) {
|
|
633
|
-
console.log(chalk.blue('\nSyncing selected skills to global directories...\n'))
|
|
634
|
-
await copySkillsToGlobalDirs({
|
|
635
|
-
skills: globalSkills,
|
|
636
|
-
destIds: skillScope.destinations,
|
|
637
|
-
homeDir,
|
|
638
|
-
})
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (syncMode === SYNC_MODE_SKILLS) {
|
|
642
|
-
for (const platform of selectedPlatforms) {
|
|
643
|
-
const previous = syncState.platforms[platform] || createEmptyPlatformState()
|
|
644
|
-
const skillsOnlySelected = { skills: selected.skills || [], rules: [], commands: [], hooks: [] }
|
|
645
|
-
targetState.platforms[platform] = {
|
|
646
|
-
...previous,
|
|
647
|
-
skills: buildNextPlatformState(skillsOnlySelected, staleEntries, approvedStaleRemovals, platform).skills,
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
} else {
|
|
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: [] }
|
|
659
|
-
} else {
|
|
660
|
-
targetState.platforms[platform] = next
|
|
661
|
-
}
|
|
662
|
-
if (platform !== 'cursor') {
|
|
663
|
-
globalState.platforms[platform] = { ...globalState.platforms[platform], rules: [] }
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
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
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (!syncedAnySupportedPlatform) {
|
|
690
|
-
console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))
|
|
691
|
-
}
|
|
300
|
+
for (const operation of plan.operations) await operation.run()
|
|
301
|
+
console.log(chalk.green('同步完成。'))
|
|
302
|
+
return plan.operations.map(({ kind, label, paths }) => ({ kind, label, paths }))
|
|
692
303
|
}
|