create-dwy 0.20.0 → 0.22.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 +18 -8
- package/src/self-upgrade.js +7 -0
- package/src/skill-store.js +182 -0
- package/src/sync-all.js +232 -51
- package/src/sync-codex.js +47 -30
- package/src/sync-cursor.js +0 -25
- package/src/sync-global-skills.js +65 -14
- package/src/sync-opencode.js +10 -18
- package/src/sync.js +23 -92
- package/src/utils.js +0 -1
- package/templates/ai-tools/{CLAUDE.md → AGENTS.md} +33 -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/Python/dwy-eapi/references/tasks-integration-guide.md +4 -4
- 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-stage-dev/SKILL.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/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +1 -1
- package/templates/project/fullstack-monorepo/{CLAUDE.md.hbs → AGENTS.md.hbs} +1 -1
- /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
|
@@ -8,11 +8,17 @@ import {
|
|
|
8
8
|
copySkillsToProjectPlatforms,
|
|
9
9
|
normalizeSkillScope,
|
|
10
10
|
resolveProjectSkillDir,
|
|
11
|
+
resolveGlobalSkillDir,
|
|
12
|
+
deduplicateProjectSkills,
|
|
13
|
+
findGlobalSkill,
|
|
14
|
+
scanGlobalSkills,
|
|
11
15
|
} from './sync-global-skills.js'
|
|
16
|
+
import { prepareSharedSkills, removeSkillEntry } from './skill-store.js'
|
|
12
17
|
import { chalk } from './utils.js'
|
|
13
18
|
import {
|
|
14
19
|
interactiveSelect,
|
|
15
20
|
logAction,
|
|
21
|
+
readBaselineDoc,
|
|
16
22
|
resolveSourceDir,
|
|
17
23
|
scanExisting,
|
|
18
24
|
scanHooks,
|
|
@@ -22,9 +28,12 @@ import {
|
|
|
22
28
|
} from './sync.js'
|
|
23
29
|
import {
|
|
24
30
|
parseManagedRuleNames,
|
|
31
|
+
removeLegacyBaseline,
|
|
25
32
|
scanExistingCodexHooks,
|
|
26
33
|
scanExistingCodexSkills,
|
|
27
34
|
syncCodex,
|
|
35
|
+
syncGlobalBaselineDoc,
|
|
36
|
+
syncProjectAgentsMd,
|
|
28
37
|
} from './sync-codex.js'
|
|
29
38
|
import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
|
|
30
39
|
import { syncOpenCode } from './sync-opencode.js'
|
|
@@ -113,7 +122,7 @@ export function buildSelectionDefaultsFromSyncState(existing, syncState) {
|
|
|
113
122
|
return hasSetItems(stateDefaults) ? stateDefaults : existing
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
async function scanCombinedExisting(projectDir) {
|
|
125
|
+
async function scanCombinedExisting(projectDir, global = false) {
|
|
117
126
|
const claudeDir = path.join(projectDir, '.claude')
|
|
118
127
|
const agentsMdPath = path.join(projectDir, 'AGENTS.md')
|
|
119
128
|
const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
|
|
@@ -122,6 +131,7 @@ async function scanCombinedExisting(projectDir) {
|
|
|
122
131
|
skills: unionSets(
|
|
123
132
|
await scanExisting(claudeDir, 'skills'),
|
|
124
133
|
await scanExistingCodexSkills(projectDir),
|
|
134
|
+
await scanExisting(path.join(projectDir, ...(global ? ['.config', 'opencode'] : ['.opencode'])), 'skills'),
|
|
125
135
|
),
|
|
126
136
|
rules: unionSets(
|
|
127
137
|
await scanExisting(claudeDir, 'rules'),
|
|
@@ -170,6 +180,9 @@ async function syncSkillsOnly({
|
|
|
170
180
|
templateSkillNames,
|
|
171
181
|
staleSkillNames,
|
|
172
182
|
skillScope,
|
|
183
|
+
homeDir,
|
|
184
|
+
global,
|
|
185
|
+
duplicateNames,
|
|
173
186
|
}) {
|
|
174
187
|
const writeProject = skillScope.destinations.includes('project')
|
|
175
188
|
if (!writeProject) return
|
|
@@ -181,37 +194,37 @@ async function syncSkillsOnly({
|
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
console.log(chalk.blue('\nSyncing skills only (rules / commands / hooks untouched)...\n'))
|
|
184
|
-
await copySkillsToProjectPlatforms({
|
|
185
|
-
projectDir,
|
|
186
|
-
platforms: skillPlatforms,
|
|
187
|
-
skills,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const selectedNames = new Set(skills.map(item => item.name))
|
|
191
|
-
const toRemove = new Set(
|
|
192
|
-
[...templateSkillNames, ...staleSkillNames].filter(name => !selectedNames.has(name)),
|
|
193
|
-
)
|
|
194
197
|
for (const platform of skillPlatforms) {
|
|
195
|
-
const
|
|
198
|
+
const platformSkills = skills.filter(skill => !duplicateNames[platform]?.has(skill.name))
|
|
199
|
+
if (global) {
|
|
200
|
+
await copySkillsToGlobalDirs({ skills: platformSkills, destIds: [platform === 'codex' ? 'agents' : platform], homeDir })
|
|
201
|
+
} else {
|
|
202
|
+
await copySkillsToProjectPlatforms({ projectDir, platforms: [platform], skills: platformSkills, homeDir })
|
|
203
|
+
}
|
|
204
|
+
const selectedNames = new Set(platformSkills.map(item => item.name))
|
|
205
|
+
const toRemove = new Set([...templateSkillNames, ...staleSkillNames].filter(name => !selectedNames.has(name)))
|
|
206
|
+
const destRoot = global
|
|
207
|
+
? resolveGlobalSkillDir(platform === 'codex' ? 'agents' : platform, homeDir)
|
|
208
|
+
: resolveProjectSkillDir(projectDir, platform)
|
|
196
209
|
const existingNames = await scanExisting(path.dirname(destRoot), 'skills')
|
|
197
210
|
await removeManagedItems(
|
|
198
211
|
destRoot,
|
|
199
212
|
existingNames,
|
|
200
213
|
toRemove,
|
|
201
214
|
path.relative(projectDir, destRoot),
|
|
215
|
+
path.join(global ? homeDir : projectDir, '.dwy', 'skill-backups'),
|
|
202
216
|
)
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
async function selectPlatforms(projectDir
|
|
220
|
+
async function selectPlatforms(projectDir) {
|
|
207
221
|
const initialValues = await buildPlatformDefaultsFromLocalState(projectDir)
|
|
208
222
|
|
|
209
|
-
if (global) console.log(chalk.gray('Cursor 全局规则通过应用设置管理,本次不提供文件同步。'))
|
|
210
223
|
// 可搜索多选 + 底部说明区
|
|
211
224
|
const result = await searchableMultiselect({
|
|
212
225
|
message: '选择要同步的平台',
|
|
213
|
-
options: PLATFORM_OPTIONS
|
|
214
|
-
initialValues
|
|
226
|
+
options: PLATFORM_OPTIONS,
|
|
227
|
+
initialValues,
|
|
215
228
|
required: true,
|
|
216
229
|
maxItems: 6,
|
|
217
230
|
placeholder: SEARCH_PLACEHOLDER,
|
|
@@ -277,7 +290,7 @@ async function collectCurrentManagedNames(projectDir, global = false) {
|
|
|
277
290
|
return {
|
|
278
291
|
claude: {
|
|
279
292
|
skills: await scanExisting(claudeDir, 'skills'),
|
|
280
|
-
rules: await scanExisting(claudeDir, 'rules'),
|
|
293
|
+
rules: unionSets(await scanExisting(claudeDir, 'rules'), parseManagedRuleNames(agentsMdContent)),
|
|
281
294
|
hooks: await scanExisting(claudeDir, 'hooks'),
|
|
282
295
|
},
|
|
283
296
|
codex: {
|
|
@@ -378,12 +391,12 @@ function buildNextPlatformState(selected, staleEntries, approvedRemovals, platfo
|
|
|
378
391
|
return state
|
|
379
392
|
}
|
|
380
393
|
|
|
381
|
-
async function removeManagedItems(targetDir, existingNames, managedNames, labelPrefix) {
|
|
394
|
+
async function removeManagedItems(targetDir, existingNames, managedNames, labelPrefix, backupRoot) {
|
|
382
395
|
let removedCount = 0
|
|
383
396
|
|
|
384
397
|
for (const name of existingNames) {
|
|
385
398
|
if (!managedNames.has(name)) continue
|
|
386
|
-
await
|
|
399
|
+
await removeSkillEntry({ dest: path.join(targetDir, name), backupRoot })
|
|
387
400
|
logAction(`${labelPrefix}/${name}`, 'red', '×')
|
|
388
401
|
removedCount++
|
|
389
402
|
}
|
|
@@ -416,18 +429,75 @@ export async function runDwy(opts = {}) {
|
|
|
416
429
|
await syncAll(opts)
|
|
417
430
|
}
|
|
418
431
|
|
|
432
|
+
/**
|
|
433
|
+
* 一键迁移当前项目已有的 CLI Skills,不进入选择流程、不补装其他平台、不修改 Rules / Hooks。
|
|
434
|
+
* 仅当前模板或历史同步记录中的名称视为托管;已移除的历史模板和整个根目录链接保留并报告。
|
|
435
|
+
*/
|
|
436
|
+
export async function migrateSkills({
|
|
437
|
+
sourceDir,
|
|
438
|
+
projectDir = process.cwd(),
|
|
439
|
+
homeDir = os.homedir(),
|
|
440
|
+
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'),
|
|
441
|
+
} = {}) {
|
|
442
|
+
const scans = await scanSkills(sourceDir || await resolveSourceDir())
|
|
443
|
+
const available = new Map(scans.map(skill => [skill.name, skill]))
|
|
444
|
+
const state = await loadSyncState(projectDir)
|
|
445
|
+
const existingByPlatform = {}
|
|
446
|
+
const neededNames = new Set()
|
|
447
|
+
const retained = []
|
|
448
|
+
const realProject = await fs.realpath(projectDir)
|
|
449
|
+
for (const platform of ['claude', 'codex', 'opencode']) {
|
|
450
|
+
const root = resolveProjectSkillDir(projectDir, platform)
|
|
451
|
+
if (!await fs.pathExists(root)) continue
|
|
452
|
+
if (await fs.realpath(root) !== path.join(realProject, path.relative(projectDir, root))) {
|
|
453
|
+
console.log(chalk.yellow(`${root} 已通过根目录链接共享内容,保留原链接,不迁移其目标。`))
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
const existing = await scanExisting(path.dirname(root), 'skills')
|
|
457
|
+
const historical = new Set(state.platforms[platform]?.skills || [])
|
|
458
|
+
const managed = [...existing].filter(name => available.has(name) || historical.has(name))
|
|
459
|
+
if (managed.length === 0) continue
|
|
460
|
+
existingByPlatform[platform] = managed
|
|
461
|
+
for (const name of managed) {
|
|
462
|
+
if (available.has(name)) neededNames.add(name)
|
|
463
|
+
else {
|
|
464
|
+
retained.push({ platform, name })
|
|
465
|
+
console.log(chalk.yellow(`${platform}/${name} 已不在当前模板中,保留原目录。`))
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const shared = await prepareSharedSkills({ skills: [...neededNames].map(name => available.get(name)), homeDir })
|
|
470
|
+
let linked = 0
|
|
471
|
+
let globalCount = 0
|
|
472
|
+
for (const [platform, names] of Object.entries(existingByPlatform)) {
|
|
473
|
+
const skills = shared.filter(skill => names.includes(skill.name))
|
|
474
|
+
const duplicates = await deduplicateProjectSkills({ projectDir, platform, skills, homeDir, codexHome })
|
|
475
|
+
const local = skills.filter(skill => !duplicates.has(skill.name))
|
|
476
|
+
linked += await copySkillsToProjectPlatforms({ projectDir, platforms: [platform], skills: local, homeDir })
|
|
477
|
+
globalCount += duplicates.size
|
|
478
|
+
state.platforms[platform] = {
|
|
479
|
+
...(state.platforms[platform] || createEmptyPlatformState()),
|
|
480
|
+
skills: names.filter(name => !duplicates.has(name)).sort(),
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (Object.keys(existingByPlatform).length) await saveSyncState(projectDir, state)
|
|
484
|
+
console.log(chalk.green(`迁移完成:${linked} 个项目链接,${globalCount} 个使用全局入口,${retained.length} 个历史条目保留。`))
|
|
485
|
+
return { linked, global: globalCount, retained }
|
|
486
|
+
}
|
|
487
|
+
|
|
419
488
|
export async function syncAll({
|
|
420
489
|
sourceDir: sourceDirOverride,
|
|
421
490
|
projectDir: projectDirOverride,
|
|
422
491
|
selected: selectedOverride,
|
|
423
492
|
selectedPlatforms: selectedPlatformsOverride,
|
|
424
493
|
staleRemovals: staleRemovalsOverride,
|
|
425
|
-
//
|
|
494
|
+
// 兼容已有脚本;主交互的位置只控制 Skills / Hooks。
|
|
426
495
|
skillScope: skillScopeOverride,
|
|
427
496
|
// all = 完整同步;skills = 只写 skill,测试/脚本可注入
|
|
428
497
|
syncMode: syncModeOverride,
|
|
429
498
|
// 全局配置根目录;测试必须注入临时用户目录。
|
|
430
499
|
homeDir = os.homedir(),
|
|
500
|
+
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'),
|
|
431
501
|
scope: scopeOverride,
|
|
432
502
|
confirmed,
|
|
433
503
|
} = {}) {
|
|
@@ -448,8 +518,12 @@ export async function syncAll({
|
|
|
448
518
|
const syncState = await loadSyncState(projectDir)
|
|
449
519
|
const globalState = await loadSyncState(homeDir)
|
|
450
520
|
const projectDefaults = buildSelectionDefaultsFromSyncState(existing, syncState)
|
|
451
|
-
const
|
|
521
|
+
const globalExisting = await scanCombinedExisting(homeDir, true)
|
|
522
|
+
globalExisting.skills = await scanGlobalSkills({ homeDir, codexHome })
|
|
523
|
+
const globalDefaults = buildSelectionDefaultsFromSyncState(globalExisting, globalState)
|
|
524
|
+
globalDefaults.skills = unionSets(globalDefaults.skills, globalExisting.skills)
|
|
452
525
|
const selectionDefaults = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type, unionSets(projectDefaults[type], globalDefaults[type])]))
|
|
526
|
+
selectionDefaults.rules = projectDefaults.rules
|
|
453
527
|
const syncMode = normalizeSyncMode(syncModeOverride)
|
|
454
528
|
const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
|
|
455
529
|
skillsOnly: syncMode === SYNC_MODE_SKILLS,
|
|
@@ -462,10 +536,10 @@ export async function syncAll({
|
|
|
462
536
|
if (syncMode === SYNC_MODE_ALL) selected.hooks = scans.hooks
|
|
463
537
|
|
|
464
538
|
const scopeResult = scopeOverride || (selectedOverride || selectedPlatformsOverride ? 'project' : await searchableSelect({
|
|
465
|
-
message: '
|
|
539
|
+
message: '选择同步位置(Skills / Hooks)',
|
|
466
540
|
options: [
|
|
467
|
-
{ value: 'project', label: '当前项目', description: '
|
|
468
|
-
{ value: 'global', label: '全局', description: '
|
|
541
|
+
{ value: 'project', label: '当前项目', description: 'Skills / Hooks 写入项目;基线固定全局,Rules 固定项目' },
|
|
542
|
+
{ value: 'global', label: '全局', description: 'Skills / Hooks 写入用户目录;基线固定全局,Rules 固定项目' },
|
|
469
543
|
],
|
|
470
544
|
initialValue: 'project',
|
|
471
545
|
}))
|
|
@@ -473,27 +547,63 @@ export async function syncAll({
|
|
|
473
547
|
if (!['project', 'global'].includes(scopeResult)) throw new Error('无效同步位置')
|
|
474
548
|
const global = scopeResult === 'global'
|
|
475
549
|
const targetRoot = global ? homeDir : projectDir
|
|
476
|
-
const targetState = global ?
|
|
477
|
-
const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(
|
|
550
|
+
const targetState = global ? globalState : syncState
|
|
551
|
+
const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms(projectDir)
|
|
478
552
|
if (selectedPlatforms === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
479
|
-
if (selectedPlatforms.some(platform => !PLATFORM_SYNC_TYPES[platform]
|
|
553
|
+
if (selectedPlatforms.some(platform => !PLATFORM_SYNC_TYPES[platform])) {
|
|
480
554
|
throw new Error('所选平台不支持此同步位置')
|
|
481
555
|
}
|
|
482
|
-
const
|
|
556
|
+
const projectStaleEntries = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
|
|
557
|
+
const staleEntriesRaw = global
|
|
558
|
+
? await collectStaleEntries(targetRoot, scans, targetState, selectedPlatforms, true)
|
|
559
|
+
: projectStaleEntries
|
|
560
|
+
for (const platform of selectedPlatforms) {
|
|
561
|
+
staleEntriesRaw[platform].rules = projectStaleEntries[platform].rules || []
|
|
562
|
+
}
|
|
483
563
|
const staleEntries = syncMode === SYNC_MODE_SKILLS ? retainSkillStaleEntries(staleEntriesRaw) : staleEntriesRaw
|
|
484
564
|
const approvedStaleRemovals = normalizeRemovalInput(staleRemovalsOverride || staleEntries)
|
|
485
565
|
const skillScope = normalizeSkillScope(skillScopeOverride)
|
|
566
|
+
const baseline = syncMode === SYNC_MODE_ALL ? await readBaselineDoc(sourceDir) : ''
|
|
567
|
+
const cliPlatforms = selectedPlatforms.filter(platform => platform !== 'cursor')
|
|
568
|
+
const baselinePaths = {
|
|
569
|
+
claude: path.join(homeDir, '.claude', 'CLAUDE.md'),
|
|
570
|
+
codex: path.join(codexHome, 'AGENTS.md'),
|
|
571
|
+
opencode: path.join(homeDir, '.config', 'opencode', 'AGENTS.md'),
|
|
572
|
+
}
|
|
573
|
+
const legacyClaudeRules = new Set()
|
|
574
|
+
if (syncMode === SYNC_MODE_ALL && selectedPlatforms.includes('claude')) {
|
|
575
|
+
for (const [root, previousNames] of [
|
|
576
|
+
[projectDir, approvedStaleRemovals.claude?.rules || []],
|
|
577
|
+
...(baseline.trim() ? [[homeDir, globalState.platforms.claude?.rules || []]] : []),
|
|
578
|
+
]) {
|
|
579
|
+
for (const name of new Set([...scans.rules.map(rule => rule.name), ...previousNames])) {
|
|
580
|
+
if (path.basename(name) !== name || !name.endsWith('.md')) continue
|
|
581
|
+
const dest = path.join(root, '.claude', 'rules', name)
|
|
582
|
+
if (await fs.pathExists(dest)) legacyClaudeRules.add(dest)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
486
586
|
if ((!selectedOverride && confirmed !== true) || confirmed === false) {
|
|
487
587
|
const currentManaged = await collectCurrentManagedNames(targetRoot, global)
|
|
588
|
+
const projectManaged = await collectCurrentManagedNames(projectDir)
|
|
589
|
+
for (const platform of selectedPlatforms) currentManaged[platform].rules = projectManaged[platform].rules
|
|
488
590
|
const templateNames = getCurrentTemplateNames(scans)
|
|
489
591
|
console.log(`\nSkills:${selected.skills.map(item => item.name).join('、') || '未选择'}`)
|
|
490
592
|
console.log(`Rules:${selected.rules.map(item => item.name).join('、') || '未选择'}`)
|
|
491
|
-
console.log(
|
|
593
|
+
console.log(`Skills / Hooks 位置:${global ? '全局' : '当前项目'};平台:${selectedPlatforms.map(platform => PLATFORM_LABELS[platform]).join('、')}`)
|
|
594
|
+
if (syncMode === SYNC_MODE_ALL) {
|
|
595
|
+
for (const platform of cliPlatforms) console.log(`全局基线 ${PLATFORM_LABELS[platform]} → ${baselinePaths[platform]}(替换基线并清除旧托管 Rules)`)
|
|
596
|
+
if (cliPlatforms.length) console.log(`项目 Rules → ${path.join(projectDir, 'AGENTS.md')}(保留手写内容,移除旧托管基线)`)
|
|
597
|
+
if (selectedPlatforms.includes('claude')) console.log('旧 Claude 基线入口仅清理 dwy 托管块或与当前模板完全一致的内容。')
|
|
598
|
+
for (const dest of legacyClaudeRules) console.log(`删除旧 Claude Rules → ${dest}`)
|
|
599
|
+
}
|
|
492
600
|
console.log('Hooks 按支持的平台自动启用。')
|
|
601
|
+
console.log('Skills 将迁为用户共享副本的目录链接;原真实目录先备份,同平台全局入口优先。')
|
|
493
602
|
for (const platform of selectedPlatforms) {
|
|
494
603
|
for (const type of PLATFORM_SYNC_TYPES[platform]) {
|
|
495
604
|
const existingNames = currentManaged[platform]?.[type] || new Set()
|
|
496
|
-
const
|
|
605
|
+
const previousState = type === 'rules' ? syncState : targetState
|
|
606
|
+
const previous = [...new Set([...templateNames[type], ...(previousState.platforms[platform]?.[type] || [])])]
|
|
497
607
|
.filter(name => existingNames.has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
|
|
498
608
|
const names = new Set((selected[type] || []).map(item => item.name))
|
|
499
609
|
const removed = previous.filter(name => !names.has(name))
|
|
@@ -508,7 +618,32 @@ export async function syncAll({
|
|
|
508
618
|
if (!selectedOverride && selected.skills.some(item => item.category === '产品0到1')) await ensureSkillsInstalled()
|
|
509
619
|
|
|
510
620
|
const selectedSkillNames = new Set((selected.skills || []).map(item => item.name))
|
|
511
|
-
const
|
|
621
|
+
const globalSkillNameSet = new Set(skillScope.globalSkills)
|
|
622
|
+
const neededNames = new Set(skillScope.destinations.some(id => id !== 'project') ? globalSkillNameSet : [])
|
|
623
|
+
if (selectedPlatforms.some(platform => resolveProjectSkillDir(projectDir, platform))) {
|
|
624
|
+
for (const name of selectedSkillNames) neededNames.add(name)
|
|
625
|
+
}
|
|
626
|
+
for (const platform of selectedPlatforms) {
|
|
627
|
+
for (const skill of scans.skills) {
|
|
628
|
+
if (await findGlobalSkill({ name: skill.name, platform, homeDir, codexHome })) neededNames.add(skill.name)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const sharedSkills = await prepareSharedSkills({ skills: scans.skills.filter(item => neededNames.has(item.name)), homeDir })
|
|
632
|
+
const skillsToWrite = sharedSkills.filter(item => selectedSkillNames.has(item.name))
|
|
633
|
+
const globalSkills = sharedSkills.filter(item => globalSkillNameSet.has(item.name))
|
|
634
|
+
if (globalSkills.length > 0 && skillScope.destinations.some(id => id !== 'project')) {
|
|
635
|
+
await copySkillsToGlobalDirs({ skills: globalSkills, destIds: skillScope.destinations, homeDir })
|
|
636
|
+
}
|
|
637
|
+
const duplicateNames = {}
|
|
638
|
+
if (!global) {
|
|
639
|
+
for (const platform of selectedPlatforms) {
|
|
640
|
+
duplicateNames[platform] = await deduplicateProjectSkills({ projectDir, platform, skills: sharedSkills, homeDir, codexHome })
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const selectionFor = platform => ({
|
|
644
|
+
...selected,
|
|
645
|
+
skills: selected.skills.filter(skill => !duplicateNames[platform]?.has(skill.name)),
|
|
646
|
+
})
|
|
512
647
|
let syncedAnySupportedPlatform = false
|
|
513
648
|
|
|
514
649
|
if (syncMode === SYNC_MODE_SKILLS) {
|
|
@@ -523,18 +658,35 @@ export async function syncAll({
|
|
|
523
658
|
templateSkillNames: new Set(scans.skills.map(item => item.name)),
|
|
524
659
|
staleSkillNames,
|
|
525
660
|
skillScope,
|
|
661
|
+
homeDir,
|
|
662
|
+
global,
|
|
663
|
+
duplicateNames,
|
|
526
664
|
})
|
|
527
665
|
syncedAnySupportedPlatform = selectedPlatforms.some(platform => resolveProjectSkillDir(projectDir, platform))
|
|
528
666
|
|| skillScope.destinations.some(id => id !== 'project')
|
|
529
667
|
} else {
|
|
668
|
+
for (const platform of cliPlatforms) await syncGlobalBaselineDoc(baseline, baselinePaths[platform])
|
|
669
|
+
if (cliPlatforms.length) {
|
|
670
|
+
const preservedRuleNames = unionSets(...cliPlatforms.map(platform => getPreservedStaleNames(staleEntries, approvedStaleRemovals, platform, 'rules')))
|
|
671
|
+
const ruleNames = new Set(selected.rules.map(rule => rule.name))
|
|
672
|
+
await syncProjectAgentsMd(projectDir, scans.rules.filter(rule => ruleNames.has(rule.name)), { preservedRuleNames })
|
|
673
|
+
}
|
|
530
674
|
if (selectedPlatforms.includes('claude')) {
|
|
675
|
+
await removeLegacyBaseline(baseline, path.join(homeDir, '.claude', 'AGENTS.md'))
|
|
676
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md']) await removeLegacyBaseline(baseline, path.join(projectDir, '.claude', name))
|
|
677
|
+
for (const dest of legacyClaudeRules) {
|
|
678
|
+
await fs.remove(dest)
|
|
679
|
+
logAction(`legacy rule → ${dest}`, 'red', '×')
|
|
680
|
+
}
|
|
531
681
|
syncedAnySupportedPlatform = true
|
|
532
682
|
console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
|
|
533
683
|
await syncClaude({
|
|
534
684
|
sourceDir,
|
|
535
685
|
projectDir: targetRoot,
|
|
536
686
|
global,
|
|
537
|
-
selected,
|
|
687
|
+
selected: selectionFor('claude'),
|
|
688
|
+
homeDir,
|
|
689
|
+
sharedSkills,
|
|
538
690
|
staleRemovals: approvedStaleRemovals.claude,
|
|
539
691
|
})
|
|
540
692
|
}
|
|
@@ -546,9 +698,10 @@ export async function syncAll({
|
|
|
546
698
|
sourceDir,
|
|
547
699
|
projectDir: targetRoot,
|
|
548
700
|
global,
|
|
549
|
-
selected,
|
|
701
|
+
selected: selectionFor('codex'),
|
|
702
|
+
homeDir,
|
|
703
|
+
sharedSkills,
|
|
550
704
|
staleRemovals: approvedStaleRemovals.codex,
|
|
551
|
-
preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
|
|
552
705
|
})
|
|
553
706
|
}
|
|
554
707
|
|
|
@@ -556,8 +709,7 @@ export async function syncAll({
|
|
|
556
709
|
syncedAnySupportedPlatform = true
|
|
557
710
|
await syncCursor({
|
|
558
711
|
sourceDir,
|
|
559
|
-
projectDir
|
|
560
|
-
global,
|
|
712
|
+
projectDir,
|
|
561
713
|
selected,
|
|
562
714
|
staleRemovals: approvedStaleRemovals.cursor,
|
|
563
715
|
})
|
|
@@ -569,30 +721,29 @@ export async function syncAll({
|
|
|
569
721
|
sourceDir,
|
|
570
722
|
projectDir: targetRoot,
|
|
571
723
|
global,
|
|
572
|
-
selected,
|
|
724
|
+
selected: selectionFor('opencode'),
|
|
725
|
+
homeDir,
|
|
726
|
+
sharedSkills,
|
|
573
727
|
staleRemovals: approvedStaleRemovals.opencode,
|
|
574
|
-
preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
|
|
575
728
|
})
|
|
576
729
|
}
|
|
577
730
|
|
|
578
731
|
|
|
579
732
|
}
|
|
580
733
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
homeDir,
|
|
589
|
-
})
|
|
734
|
+
if (global) {
|
|
735
|
+
for (const platform of selectedPlatforms) {
|
|
736
|
+
duplicateNames[platform] = await deduplicateProjectSkills({ projectDir, platform, skills: sharedSkills, homeDir, codexHome })
|
|
737
|
+
if (syncState.platforms[platform]) {
|
|
738
|
+
syncState.platforms[platform].skills = syncState.platforms[platform].skills.filter(name => !duplicateNames[platform].has(name))
|
|
739
|
+
}
|
|
740
|
+
}
|
|
590
741
|
}
|
|
591
742
|
|
|
592
743
|
if (syncMode === SYNC_MODE_SKILLS) {
|
|
593
744
|
for (const platform of selectedPlatforms) {
|
|
594
|
-
const previous =
|
|
595
|
-
const skillsOnlySelected = { skills: selected.skills
|
|
745
|
+
const previous = targetState.platforms[platform] || createEmptyPlatformState()
|
|
746
|
+
const skillsOnlySelected = { skills: (global ? selected : selectionFor(platform)).skills, rules: [], commands: [], hooks: [] }
|
|
596
747
|
targetState.platforms[platform] = {
|
|
597
748
|
...previous,
|
|
598
749
|
skills: buildNextPlatformState(skillsOnlySelected, staleEntries, approvedStaleRemovals, platform).skills,
|
|
@@ -600,12 +751,42 @@ export async function syncAll({
|
|
|
600
751
|
}
|
|
601
752
|
} else {
|
|
602
753
|
for (const platform of selectedPlatforms) {
|
|
603
|
-
|
|
754
|
+
const next = buildNextPlatformState(global ? selected : selectionFor(platform), staleEntries, approvedStaleRemovals, platform)
|
|
755
|
+
if (global) {
|
|
756
|
+
syncState.platforms[platform] = {
|
|
757
|
+
...(syncState.platforms[platform] || createEmptyPlatformState()),
|
|
758
|
+
rules: next.rules,
|
|
759
|
+
}
|
|
760
|
+
if (platform !== 'cursor') targetState.platforms[platform] = { ...next, rules: [] }
|
|
761
|
+
} else {
|
|
762
|
+
targetState.platforms[platform] = next
|
|
763
|
+
}
|
|
764
|
+
if (platform !== 'cursor') {
|
|
765
|
+
globalState.platforms[platform] = { ...globalState.platforms[platform], rules: [] }
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (syncMode === SYNC_MODE_ALL && cliPlatforms.length) {
|
|
770
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md')
|
|
771
|
+
const content = await fs.pathExists(agentsPath) ? await fs.readFile(agentsPath, 'utf-8') : ''
|
|
772
|
+
const rules = [...parseManagedRuleNames(content)].sort((a, b) => a.localeCompare(b, 'zh'))
|
|
773
|
+
// 三个 CLI 共用同一规则块,已有记录必须一起更新,避免切换平台后恢复已取消的规则。
|
|
774
|
+
for (const platform of Object.keys(baselinePaths)) {
|
|
775
|
+
if (syncState.platforms[platform]) syncState.platforms[platform].rules = rules
|
|
604
776
|
}
|
|
605
777
|
}
|
|
606
778
|
targetState.skillScope = skillScope
|
|
607
779
|
targetState.syncMode = syncMode
|
|
608
|
-
|
|
780
|
+
if (global) {
|
|
781
|
+
syncState.syncMode = syncMode
|
|
782
|
+
await saveSyncState(projectDir, syncState)
|
|
783
|
+
if (cliPlatforms.length) await saveSyncState(homeDir, targetState)
|
|
784
|
+
} else {
|
|
785
|
+
await saveSyncState(targetRoot, targetState)
|
|
786
|
+
if (syncMode === SYNC_MODE_ALL && cliPlatforms.length && baseline.trim()) {
|
|
787
|
+
await saveSyncState(homeDir, globalState)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
609
790
|
|
|
610
791
|
if (!syncedAnySupportedPlatform) {
|
|
611
792
|
console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))
|
package/src/sync-codex.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { chalk } from './utils.js'
|
|
4
|
+
import { linkSkill, prepareSharedSkills, removeSkillEntry } from './skill-store.js'
|
|
4
5
|
import {
|
|
5
6
|
extractDescription,
|
|
6
7
|
loadHookManifests,
|
|
7
|
-
readBaselineDoc,
|
|
8
8
|
scanSkills,
|
|
9
|
-
scanRules,
|
|
10
9
|
scanHooks,
|
|
11
10
|
resolveSourceDir,
|
|
12
11
|
copyFilter,
|
|
@@ -112,13 +111,37 @@ function upsertBaselineBlock(content, baselineBody) {
|
|
|
112
111
|
return base ? `${block}\n\n${base}` : `${block}\n`
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
/** 更新 CLI 全局基线,仅替换 dwy 托管块,并清除旧版写入的项目 Rules 托管块。 */
|
|
115
|
+
export async function syncGlobalBaselineDoc(baseline, dest) {
|
|
116
|
+
if (!baseline.trim()) return 0
|
|
117
|
+
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
118
|
+
const previous = current.trim() === baseline.trim() ? '' : current
|
|
119
|
+
const next = upsertManagedBlock(upsertBaselineBlock(previous, baseline), '')
|
|
120
|
+
if (next === current) return 0
|
|
121
|
+
await fs.ensureDir(path.dirname(dest))
|
|
122
|
+
await fs.writeFile(dest, next)
|
|
123
|
+
logAction(`global baseline → ${dest}`)
|
|
124
|
+
return 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 迁移旧入口中的基线;无托管标记时只移除与当前模板完全一致的文件。 */
|
|
128
|
+
export async function removeLegacyBaseline(baseline, dest) {
|
|
129
|
+
if (!await fs.pathExists(dest)) return
|
|
130
|
+
if (!baseline.trim()) return
|
|
131
|
+
const current = await fs.readFile(dest, 'utf-8')
|
|
132
|
+
const next = current.trim() === baseline.trim() ? '' : upsertBaselineBlock(current, '')
|
|
133
|
+
if (next === current) return
|
|
134
|
+
if (next.trim()) await fs.writeFile(dest, next)
|
|
135
|
+
else await fs.remove(dest)
|
|
136
|
+
logAction(`legacy baseline → ${dest}`, 'red', '×')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 三种 CLI 共用项目根 AGENTS.md;只写所选 Rules,保留托管块之外的项目内容。 */
|
|
140
|
+
export async function syncProjectAgentsMd(projectDir, selectedRules, {
|
|
116
141
|
preservedRuleNames = new Set(),
|
|
117
|
-
targetPath = path.join(projectDir, 'AGENTS.md'),
|
|
118
142
|
} = {}) {
|
|
119
|
-
const dest =
|
|
143
|
+
const dest = path.join(projectDir, 'AGENTS.md')
|
|
120
144
|
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
121
|
-
const baselineBody = await readBaselineDoc(sourceDir)
|
|
122
145
|
const selectedRuleNames = new Set(selectedRules.map(rule => rule.name))
|
|
123
146
|
const existingSections = parseManagedRuleSections(current)
|
|
124
147
|
const sections = []
|
|
@@ -130,15 +153,11 @@ export async function syncProjectAgentsMd(sourceDir, projectDir, selectedRules,
|
|
|
130
153
|
}
|
|
131
154
|
const blockBody = sections.join('\n\n---\n\n')
|
|
132
155
|
|
|
133
|
-
const
|
|
134
|
-
const next = upsertManagedBlock(withBaseline, blockBody)
|
|
156
|
+
const next = upsertManagedBlock(upsertBaselineBlock(current, ''), blockBody)
|
|
135
157
|
if (next === current) return 0
|
|
136
158
|
await fs.ensureDir(path.dirname(dest))
|
|
137
159
|
await fs.writeFile(dest, next)
|
|
138
|
-
|
|
139
|
-
if (baselineBody.trim()) parts.push('baseline')
|
|
140
|
-
parts.push(blockBody.trim() ? `${selectedRules.length} rules` : 'rules removed')
|
|
141
|
-
logAction(`AGENTS.md — ${parts.join(', ')}`)
|
|
160
|
+
logAction(`AGENTS.md — ${blockBody.trim() ? `${selectedRules.length} rules` : 'rules removed'}`)
|
|
142
161
|
return 1
|
|
143
162
|
}
|
|
144
163
|
|
|
@@ -168,15 +187,14 @@ export async function scanExistingCodexSkills(proj) {
|
|
|
168
187
|
const dir = path.join(proj, '.agents', 'skills')
|
|
169
188
|
if (!await fs.pathExists(dir)) return new Set()
|
|
170
189
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
171
|
-
return new Set(entries.filter(e => e.isDirectory()).map(e => e.name))
|
|
190
|
+
return new Set(entries.filter(e => e.isDirectory() || e.isSymbolicLink()).map(e => e.name))
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
async function copySkillsFlat(skills, proj) {
|
|
175
194
|
const root = path.join(proj, '.agents', 'skills')
|
|
176
195
|
for (const s of skills) {
|
|
177
196
|
const dest = path.join(root, s.name)
|
|
178
|
-
await
|
|
179
|
-
await fs.copy(s.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
197
|
+
await linkSkill({ skill: s, dest, backupRoot: path.join(proj, '.dwy', 'skill-backups') })
|
|
180
198
|
logAction(`.agents/skills/${s.name}`)
|
|
181
199
|
}
|
|
182
200
|
return skills.length
|
|
@@ -188,7 +206,12 @@ async function removeUnselectedFlat(subdir, existingNames, selectedNames, templa
|
|
|
188
206
|
if (selectedNames.has(name)) continue
|
|
189
207
|
// 项目独有(模板未提供过)的条目永不删除,保留用户自定义
|
|
190
208
|
if (!templateNames.has(name)) continue
|
|
191
|
-
|
|
209
|
+
const dest = path.join(proj, ...subdir, name)
|
|
210
|
+
if (subdir[1] === 'skills') {
|
|
211
|
+
await removeSkillEntry({ dest, backupRoot: path.join(proj, '.dwy', 'skill-backups') })
|
|
212
|
+
} else {
|
|
213
|
+
await fs.remove(dest)
|
|
214
|
+
}
|
|
192
215
|
logAction(`${subdir.join('/')}/${name}`, 'red', '×')
|
|
193
216
|
count++
|
|
194
217
|
}
|
|
@@ -214,7 +237,7 @@ async function copyCodexHooks(hooks, proj) {
|
|
|
214
237
|
continue
|
|
215
238
|
}
|
|
216
239
|
const dest = path.join(dir, h.name)
|
|
217
|
-
await fs.copy(h.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
240
|
+
await fs.copy(h.sourcePath, dest, { overwrite: true, filter: copyFilter(h.sourcePath) })
|
|
218
241
|
if (!await fs.pathExists(dest)) {
|
|
219
242
|
logAction(`.codex/hooks/${h.name}`, 'yellow', '!')
|
|
220
243
|
continue
|
|
@@ -327,8 +350,9 @@ export async function syncCodex({
|
|
|
327
350
|
projectDir,
|
|
328
351
|
global = false,
|
|
329
352
|
selected,
|
|
353
|
+
homeDir,
|
|
354
|
+
sharedSkills,
|
|
330
355
|
staleRemovals = {},
|
|
331
|
-
preserveMissingRules = new Set(),
|
|
332
356
|
} = {}) {
|
|
333
357
|
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
334
358
|
if (!await fs.pathExists(sourceDir)) {
|
|
@@ -341,22 +365,17 @@ export async function syncCodex({
|
|
|
341
365
|
|
|
342
366
|
const scans = {
|
|
343
367
|
skills: await scanSkills(sourceDir),
|
|
344
|
-
rules: await scanRules(sourceDir),
|
|
345
368
|
hooks: await scanHooks(sourceDir),
|
|
346
369
|
}
|
|
347
|
-
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.
|
|
370
|
+
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.hooks.length} hooks\n`))
|
|
348
371
|
|
|
349
|
-
const agentsMdPath = global ? path.join(proj, '.codex', 'AGENTS.md') : path.join(proj, 'AGENTS.md')
|
|
350
|
-
const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
|
|
351
372
|
const existing = {
|
|
352
373
|
skills: await scanExistingCodexSkills(proj),
|
|
353
|
-
rules: parseManagedRuleNames(agentsMdContent),
|
|
354
374
|
hooks: await scanExistingCodexHooks(proj),
|
|
355
375
|
}
|
|
356
376
|
const selectedNames = key => new Set((selected[key] || []).map(item => item.name))
|
|
357
377
|
const syncedSelection = {
|
|
358
378
|
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
359
|
-
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
360
379
|
hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
|
|
361
380
|
}
|
|
362
381
|
const selectedHookNames = new Set(syncedSelection.hooks.map(h => h.name))
|
|
@@ -366,11 +385,10 @@ export async function syncCodex({
|
|
|
366
385
|
let syncedCount = 0
|
|
367
386
|
let removedCount = 0
|
|
368
387
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
})
|
|
388
|
+
const skills = sharedSkills
|
|
389
|
+
? sharedSkills.filter(skill => selectedNames('skills').has(skill.name))
|
|
390
|
+
: await prepareSharedSkills({ skills: syncedSelection.skills, homeDir })
|
|
391
|
+
syncedCount += await copySkillsFlat(skills, proj)
|
|
374
392
|
syncedCount += await copyCodexHooks(syncedSelection.hooks, proj)
|
|
375
393
|
syncedCount += await syncCodexHooksJson(sourceDir, proj, selectedHookNames, global)
|
|
376
394
|
|
|
@@ -386,7 +404,6 @@ export async function syncCodex({
|
|
|
386
404
|
removedCount += await removeUnselectedFlat(['.codex', 'hooks'], existing.hooks, selectedHookNames, hookTemplateNames, proj)
|
|
387
405
|
|
|
388
406
|
console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
|
|
389
|
-
console.log(chalk.gray(` Rules → ${agentsMdPath}`))
|
|
390
407
|
console.log(chalk.gray(` Skills → ${path.join(proj, '.agents', 'skills')}`))
|
|
391
408
|
console.log(chalk.gray(` Hooks → ${path.join(proj, '.codex')}`))
|
|
392
409
|
}
|