create-dwy 0.23.0 → 0.25.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 +33 -0
- package/package.json +1 -1
- package/src/cli-help.js +33 -16
- package/src/skill-store.js +4 -4
- package/src/sync-all.js +111 -41
- package/src/sync-codex.js +36 -21
- package/src/sync-cursor.js +2 -1
- package/src/sync-inheritance.js +119 -0
- package/src/sync-opencode.js +1 -1
- package/src/sync-plan.js +137 -50
- package/src/sync.js +20 -23
- package/src/template-source.js +218 -0
- package/templates/ai-tools/AGENTS.md +4 -4
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-scope-guard/SKILL.md +91 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# create-dwy
|
|
2
2
|
|
|
3
|
+
## [0.25.0] - 2026-11-06
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 同步流程简化为「范围 → Skills → 项目 Rules → 通用规范 → Hooks → CLI → 预览确认」。规范和 Hooks 默认启用;Codex 默认选中,并恢复当前范围已同步的其他 CLI。项目已有有效全局规范或完整 Hooks 时跳过对应选择,不重复安装。
|
|
8
|
+
- 模板从公开源码仓库 master 分支运行时获取,新增 Skills 无需再发布 CLI。使用增量 Git 对象缓存和验证后的模板快照;离线保留有效缓存,无缓存才回退随包模板。首次下载显示进度,异常退出后的锁可自动恢复。
|
|
9
|
+
- 确认后刷新当前范围所需的共享 Skills,项目补其他 CLI 入口时保留有效全局同名共享内容;执行后报告实际完成信息。保留旧脚本的显式同步范围、个人配置及取消前目标不写入的约束。
|
|
10
|
+
- 新增版本克制与想法收录 Skill,支持新增功能分析、阶段收敛、持久化想法清单和新版本规划提醒。
|
|
11
|
+
- 修复无同步记录的旧 Cursor Rules 默认选择恢复,补充全局继承、内容保护、增量更新、离线及多进程锁回归,并更新同步帮助与使用说明。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [0.24.1] - 2026-11-03
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 包含 0.24.0 的 Skills / Hooks / Rules / 通用规范管理、显式移除和全局去重能力。
|
|
20
|
+
- 收敛配置管理集成测试的无断言日志,规避 Node 22 测试运行器在大量中文 stdout 下的 IPC 反序列化问题;保留全部断言和真实终端输出测试,不改变 CLI 行为。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.24.0] - 2026-11-03
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- 全局提供 Skills、Hooks 和 AGENTS.md 通用规范,项目另外提供 Rules。全局首次默认启用通用规范;Hooks 与规范增加显式启用/更新和移除动作,未选择的类型保持原样,规范移除后不自动复选。
|
|
29
|
+
- 项目通用规范与 Rules 分别操作各自托管块,移除任一项保留其他内容。任一所选 CLI 已有有效全局基线时,项目禁止重复安装,共享项目文件的混合平台场景提示补齐缺失的全局入口。
|
|
30
|
+
- Hooks 按有效脚本及对应事件、matcher 和完整命令注册判断全局提供者,项目只补缺失项。全局启用后明确清理当前项目重复脚本和注册项;移除全局配置不自动回填项目。
|
|
31
|
+
- Hook 清理支持历史托管项,保留同名个人路径、组合命令和其他设置;配置目录及单文件链接在规划和执行时检查,防止项目操作穿透改写全局或外部目标。
|
|
32
|
+
- 补充配置独立启停、移除记忆、继承、取消及真实终端回归,旧测试统一注入临时用户目录。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
3
36
|
## [0.23.0] - 2026-11-02
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/package.json
CHANGED
package/src/cli-help.js
CHANGED
|
@@ -14,36 +14,39 @@ export function buildHelpText(version) {
|
|
|
14
14
|
`dwy ${version}`,
|
|
15
15
|
'',
|
|
16
16
|
'用法:',
|
|
17
|
-
' dwy
|
|
17
|
+
' dwy 选择范围、配置和 CLI,预览清单后同步',
|
|
18
18
|
' dwy sync 同上',
|
|
19
19
|
' dwy upgrade 升级到 npm 最新正式版,并刷新共享 Skills',
|
|
20
|
-
' dwy refresh
|
|
20
|
+
' dwy refresh 用最新模板刷新已安装的共享 Skills,无需升级 CLI',
|
|
21
21
|
' dwy migrate 一键迁移老项目已有的托管 Skills,原目录先备份',
|
|
22
22
|
' dwy --help 显示本说明',
|
|
23
23
|
' dwy --version 显示版本',
|
|
24
24
|
'',
|
|
25
25
|
'同步步骤:',
|
|
26
26
|
' 1. 选择同步范围:当前项目或全局。',
|
|
27
|
-
' 2.
|
|
28
|
-
' 3.
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
27
|
+
' 2. 选择 Skills。',
|
|
28
|
+
' 3. 仅项目范围选择 Rules;全局跳过。',
|
|
29
|
+
' 4. 选择 AGENTS.md 通用规范,默认启用。',
|
|
30
|
+
' 5. 选择 Hooks,默认启用。',
|
|
31
|
+
' 项目已有有效全局规范或全部所需全局 Hooks 时,跳过对应操作页。',
|
|
32
|
+
' 6. 最后选择 CLI;首次只选 Codex,后续恢复当前范围同步过的 CLI 与有效旧托管入口。',
|
|
33
|
+
' 7. 按最终 CLI 重新核算覆盖,预览实际写入、迁移、备份和清理清单。',
|
|
34
|
+
' 8. 确认后执行,显示实际完成信息;取消不改同步目标。',
|
|
33
35
|
' Commands 不参与同步。',
|
|
34
36
|
'',
|
|
35
37
|
'交互按键:',
|
|
36
38
|
' Skills / Rules:←/→ 切分类,↑/↓ 移动,Space 勾选,a 全选/清空当前分类。',
|
|
37
|
-
' 范围 /
|
|
39
|
+
' 范围 / 通用规范 / Hooks / CLI:↑/↓ 移动;CLI 用 Space 勾选,可输入关键字筛选。',
|
|
38
40
|
' Enter 确认当前步骤,Esc 或 Ctrl+C 取消。',
|
|
39
41
|
'',
|
|
40
42
|
'常见用法:',
|
|
41
|
-
' 同步当前项目:cd your-project 后执行 dwy
|
|
42
|
-
' 同步全局:执行 dwy
|
|
43
|
+
' 同步当前项目:cd your-project 后执行 dwy,选择「当前项目」。',
|
|
44
|
+
' 同步全局:执行 dwy,选择「全局」。',
|
|
45
|
+
' 移除配置:在当前范围取消 Skills / Rules 勾选,或关闭 Hooks / 通用规范,再确认。',
|
|
43
46
|
' 更新或调整配置:再次执行 dwy sync,检查勾选项,再确认同步。',
|
|
44
47
|
' 更新 CLI:dwy upgrade(全局安装时可用;更新后执行 dwy --version 查看版本)。',
|
|
45
48
|
' 迁移老项目:cd your-project 后执行 dwy migrate,无需重新勾选。',
|
|
46
|
-
'
|
|
49
|
+
' 只更新 Skills:执行 dwy refresh,所有已迁移的链接同时生效。',
|
|
47
50
|
' 首次安装:npm install -g create-dwy@latest',
|
|
48
51
|
'',
|
|
49
52
|
'平台支持:',
|
|
@@ -52,18 +55,32 @@ export function buildHelpText(version) {
|
|
|
52
55
|
' OpenCode:全局 ~/.config/opencode/AGENTS.md;项目根 AGENTS.md;当前不支持 Hooks。',
|
|
53
56
|
' Cursor:仅项目 Rules;全局规则在 Cursor 设置中管理。',
|
|
54
57
|
'',
|
|
55
|
-
'
|
|
58
|
+
'同步按当前范围和最终 CLI 执行;项目 AGENTS.md 由多个 CLI 共享。',
|
|
56
59
|
'用户主目录不能作为项目范围同步,请选择全局或进入项目目录。',
|
|
57
|
-
'
|
|
60
|
+
'关闭选项只移除当前范围托管内容;项目操作不移除全局配置。',
|
|
58
61
|
'Skills 副本集中在 ~/.dwy/skills;项目和全局使用目录链接,同平台全局入口优先。',
|
|
59
62
|
'项目 Skills 全部适用平台均有全局入口时显示「全局已启用」,不能重复勾选。',
|
|
60
63
|
'部分平台已有全局入口时仍可选,只为缺少全局入口的平台安装项目链接。',
|
|
61
|
-
'
|
|
62
|
-
'
|
|
64
|
+
'Hooks 与 Skills 独立;整组管理,项目仅安装缺少有效全局注册的 Hooks。',
|
|
65
|
+
'全局 Hook 的脚本与对应事件、matcher、完整命令注册均有效时,项目禁止重复安装。',
|
|
66
|
+
'任一适用 CLI 已有 dwy 全局通用规范时,项目继承并清理重复基线;最终 CLI 中缺失的全局规范需补齐。',
|
|
67
|
+
'手写 AGENTS.md 本身不算全局已安装;通用规范与 Rules 共用文件但独立管理。',
|
|
68
|
+
'移除通用规范仅删除基线块;项目 Rules 独立管理,保留手写内容。',
|
|
69
|
+
'全局启用会明确列出当前项目重复项清理;全局移除不会自动向项目补装。',
|
|
70
|
+
'项目同步只清理项目重复入口,不改写有效全局提供者;真实目录先备份。',
|
|
71
|
+
'确认前仅可更新模板缓存;确认后才改项目/全局配置,并刷新所选共享 Skills。',
|
|
72
|
+
'由有效全局入口提供的 Skills 不借项目同步改写;全部已安装副本用 dwy refresh 更新。',
|
|
73
|
+
'sync / migrate 脚本接口仍须显式指定范围。',
|
|
63
74
|
'产品流程缺失的外部依赖也列入清单,确认后联网补齐,已有内容保留。',
|
|
64
75
|
'旧真实目录备份到同步根的 .dwy/skill-backups;个人 Skills、Rules、Hooks 不参与 migrate。',
|
|
65
76
|
'旧项目首次需执行 dwy migrate;迁移后无需逐项目刷新 Skills。',
|
|
66
77
|
'Rules / Hooks 的模板更新仍需执行 dwy sync;已有会话缓存的 Skill 可能需要重新加载。',
|
|
67
78
|
'',
|
|
79
|
+
'模板更新:',
|
|
80
|
+
' 固定公开仓库 https://github.com/danweiyuancircle/dwy-ai-experience.git 的 master 分支 dwy-cli/templates/ai-tools。',
|
|
81
|
+
' DWY_TEMPLATE_SOURCE_DIR 开发覆盖优先;远端模板缓存在 ~/.dwy/template-source。',
|
|
82
|
+
' 未变更只检查 commit;离线保留有效缓存,无有效缓存时回退包内模板并提示。',
|
|
83
|
+
' 新增 Skills 只需更新模板分发分支,无需发布 CLI;dwy upgrade 更新程序。',
|
|
84
|
+
'',
|
|
68
85
|
].join('\n')
|
|
69
86
|
}
|
package/src/skill-store.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function ensureSharedSkills({ skills, homeDir = os.homedir() }) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* 将当前模板 Skills 准备到稳定用户目录,返回指向共享副本的条目。
|
|
40
40
|
* 同一用户的刷新互斥;完整副本准备失败时不替换旧内容。外部 Skills 缓存不纳入本模块清单。
|
|
41
41
|
*/
|
|
42
42
|
export async function prepareSharedSkills({ skills, homeDir = os.homedir() }) {
|
|
@@ -183,15 +183,15 @@ export async function linkSkill({ skill, dest, backupRoot }) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/**
|
|
186
|
+
/** 从最新可用模板刷新已有共享副本;模板移除的条目保留,避免其他项目坏链。 */
|
|
187
187
|
export async function refreshSharedSkills({ sourceDir, homeDir = os.homedir() } = {}) {
|
|
188
188
|
const names = await readInstalledNames(homeDir)
|
|
189
|
-
const scans = await scanSkills(sourceDir || await resolveSourceDir())
|
|
189
|
+
const scans = await scanSkills(sourceDir || await resolveSourceDir({ homeDir }))
|
|
190
190
|
const available = new Map(scans.map(skill => [skill.name, skill]))
|
|
191
191
|
const updated = names.filter(name => available.has(name))
|
|
192
192
|
const retained = names.filter(name => !available.has(name))
|
|
193
193
|
await prepareSharedSkills({ skills: updated.map(name => available.get(name)), homeDir })
|
|
194
|
-
for (const name of retained) console.log(chalk.yellow(
|
|
194
|
+
for (const name of retained) console.log(chalk.yellow(`当前模板已移除 ${name},保留共享旧副本以免其他项目链接失效。`))
|
|
195
195
|
console.log(chalk.green(`已刷新 ${updated.length} 个共享 Skills,保留 ${retained.length} 个旧副本。`))
|
|
196
196
|
return { updated, retained }
|
|
197
197
|
}
|
package/src/sync-all.js
CHANGED
|
@@ -6,12 +6,13 @@ import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './s
|
|
|
6
6
|
import { normalizeSkillScope, resolveProjectSkillDir, resolveGlobalSkillDir, copySkillsToProjectPlatforms, deduplicateProjectSkills } from './sync-global-skills.js'
|
|
7
7
|
import { ensureSharedSkills } from './skill-store.js'
|
|
8
8
|
import { chalk } from './utils.js'
|
|
9
|
-
import { interactiveSelect, readBaselineDoc, resolveSourceDir, scanExisting, scanHooks, scanRules, scanSkills } from './sync.js'
|
|
10
|
-
import { parseManagedRuleNames, scanExistingCodexHooks, scanExistingCodexSkills } from './sync-codex.js'
|
|
9
|
+
import { interactiveSelect, loadHookManifests, readBaselineDoc, resolveSourceDir, scanExisting, scanHooks, scanRules, scanSkills } from './sync.js'
|
|
10
|
+
import { hasManagedBaseline, parseManagedRuleNames, scanExistingCodexHooks, scanExistingCodexSkills } from './sync-codex.js'
|
|
11
11
|
import { scanExistingCursorRules } from './sync-cursor.js'
|
|
12
12
|
import { installSkills } from './skills-install.js'
|
|
13
13
|
import { selfUpgrade } from './self-upgrade.js'
|
|
14
14
|
import { buildSyncPlan, contentOptions, projectSkillChoices, PLATFORM_LABELS, PLATFORM_SYNC_TYPES } from './sync-plan.js'
|
|
15
|
+
import { assertConfigPath, baselineProviders, globalBaselinePath, globalHookNames, hasManagedHookConfig } from './sync-inheritance.js'
|
|
15
16
|
|
|
16
17
|
const PLATFORM_OPTIONS = [
|
|
17
18
|
{ value: 'claude', label: 'Claude Code', description: '支持 Skills、项目 Rules、Hooks 和全局通用规范' },
|
|
@@ -58,25 +59,44 @@ export function buildSelectionDefaultsFromSyncState(existing, syncState, platfor
|
|
|
58
59
|
return Object.values(result).some(names => names.size) ? result : existing
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
+
/** Codex 默认选中;恢复当前范围的同步记录和有效旧托管入口,空目录不算已同步。 */
|
|
62
63
|
export async function buildPlatformDefaultsFromLocalState(projectDir, scope = 'project', codexHome = path.join(projectDir, '.codex')) {
|
|
63
|
-
const defaults = []
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
const defaults = ['codex']
|
|
65
|
+
const state = await loadSyncState(projectDir)
|
|
66
|
+
for (const platform of ['claude', 'opencode', ...(scope === 'project' ? ['cursor'] : [])]) {
|
|
67
|
+
const root = scope === 'global' ? resolveGlobalSkillDir(platform, projectDir)
|
|
68
|
+
: resolveProjectSkillDir(projectDir, platform)
|
|
69
|
+
let managed = Boolean(state.platforms[platform])
|
|
70
|
+
if (root && await fs.pathExists(root)) {
|
|
71
|
+
for (const name of await fs.readdir(root)) {
|
|
72
|
+
if (name.startsWith('dwy-') && await fs.pathExists(path.join(root, name, 'SKILL.md'))) managed = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (platform === 'cursor') {
|
|
76
|
+
if ([...await scanExistingCursorRules(projectDir)].some(name => name.startsWith('dwy-') || name === '00-dwy-global.mdc')) managed = true
|
|
77
|
+
} else {
|
|
78
|
+
const baseline = scope === 'global' ? globalBaselinePath(platform, projectDir, codexHome)
|
|
79
|
+
: path.join(projectDir, platform === 'claude' ? '.claude' : '.opencode', 'AGENTS.md')
|
|
80
|
+
if (await hasManagedBaseline(baseline)) managed = true
|
|
81
|
+
if (platform === 'claude') {
|
|
82
|
+
const configRoot = path.join(projectDir, '.claude')
|
|
83
|
+
if (await hasManagedBaseline(path.join(configRoot, 'CLAUDE.md'))
|
|
84
|
+
|| await hasManagedHookConfig(platform, configRoot, await scanExisting(configRoot, 'hooks'))) managed = true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (managed) defaults.push(platform)
|
|
88
|
+
}
|
|
89
|
+
return defaults
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
async function selectPlatforms(root, scope,
|
|
92
|
+
async function selectPlatforms(root, scope, initialValues) {
|
|
93
|
+
const state = await loadSyncState(root)
|
|
75
94
|
const result = await searchableMultiselect({
|
|
76
95
|
message: '选择要同步的平台',
|
|
77
96
|
options: PLATFORM_OPTIONS.filter(option => scope === 'project' || option.value !== 'cursor')
|
|
78
|
-
.map(option => ({ ...option, description:
|
|
79
|
-
|
|
97
|
+
.map(option => ({ ...option, description: [state.platforms[option.value] ? '已有同步记录,将核对并修复本地配置' : '',
|
|
98
|
+
contentOptions(scope, [option.value]).map(item => item.label).join('、')].filter(Boolean).join(';') })),
|
|
99
|
+
initialValues,
|
|
80
100
|
required: true, maxItems: 6, placeholder: SEARCH_PLACEHOLDER,
|
|
81
101
|
})
|
|
82
102
|
return isCancel(result) ? null : result
|
|
@@ -126,7 +146,7 @@ function staleEntries(scans, state, existing, platforms) {
|
|
|
126
146
|
const result = {}
|
|
127
147
|
for (const platform of platforms) {
|
|
128
148
|
result[platform] = {}
|
|
129
|
-
for (const type of PLATFORM_SYNC_TYPES[platform]) {
|
|
149
|
+
for (const type of PLATFORM_SYNC_TYPES[platform].filter(type => current[type])) {
|
|
130
150
|
result[platform][type] = (state.platforms[platform]?.[type] || []).filter(name =>
|
|
131
151
|
!current[type].has(name) && existing[platform][type].has(platform === 'cursor' && type === 'rules' ? buildCursorRuleName(name) : name))
|
|
132
152
|
}
|
|
@@ -139,7 +159,7 @@ function normalizeRemovals(raw) {
|
|
|
139
159
|
Object.fromEntries(Object.entries(types).map(([type, names]) => [type, new Set(names)]))]))
|
|
140
160
|
}
|
|
141
161
|
|
|
142
|
-
/** `dwy` 和 `dwy sync` 共用范围 →
|
|
162
|
+
/** `dwy` 和 `dwy sync` 共用范围 → Skills → 项目 Rules → 规范 → Hooks → CLI → 确认。 */
|
|
143
163
|
export async function runDwy(opts = {}) {
|
|
144
164
|
console.log(chalk.bold.cyan('\ndwy') + chalk.gray(' · AI 开发工具\n'))
|
|
145
165
|
const action = normalizeAction(opts.action)
|
|
@@ -165,7 +185,7 @@ export async function migrateSkills({
|
|
|
165
185
|
if (await fs.pathExists(homeDir) && realProject === await fs.realpath(homeDir)) {
|
|
166
186
|
throw new Error('当前目录是用户主目录,请使用全局同步或进入项目目录迁移')
|
|
167
187
|
}
|
|
168
|
-
const scans = await scanSkills(sourceDir || await resolveSourceDir())
|
|
188
|
+
const scans = await scanSkills(sourceDir || await resolveSourceDir({ homeDir }))
|
|
169
189
|
const available = new Map(scans.map(skill => [skill.name, skill]))
|
|
170
190
|
const state = await loadSyncState(projectDir)
|
|
171
191
|
const existingByPlatform = {}
|
|
@@ -213,65 +233,107 @@ export async function migrateSkills({
|
|
|
213
233
|
|
|
214
234
|
/**
|
|
215
235
|
* 只执行点名范围和内容;测试/脚本可注入选择,确认前只读。
|
|
216
|
-
*
|
|
236
|
+
* 主交互按内容顺序收集选择,CLI 后选,执行前重新核算继承;显式脚本仅处理点名的类型。
|
|
217
237
|
*/
|
|
218
238
|
export async function syncAll({
|
|
219
239
|
sourceDir: sourceDirOverride, projectDir: projectDirOverride, selected: selectedOverride,
|
|
220
240
|
selectedPlatforms: platformsOverride, contentTypes: typesOverride, staleRemovals: removalsOverride,
|
|
221
241
|
skillScope: skillScopeOverride, syncMode: modeOverride, homeDir = os.homedir(),
|
|
222
242
|
codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex'), scope: scopeOverride, confirmed,
|
|
243
|
+
contentActions: actionsOverride,
|
|
223
244
|
} = {}) {
|
|
224
|
-
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
245
|
+
const sourceDir = sourceDirOverride || await resolveSourceDir({ homeDir })
|
|
225
246
|
if (!await fs.pathExists(sourceDir)) throw new Error('ai-tools templates not found in repo')
|
|
226
247
|
const projectDir = projectDirOverride || process.cwd()
|
|
227
248
|
const scope = scopeOverride || (selectedOverride || platformsOverride ? 'project' : await searchableSelect({
|
|
228
249
|
message: '选择同步范围',
|
|
229
250
|
options: [
|
|
230
|
-
{ value: 'project', label: '当前项目', description: '
|
|
231
|
-
{ value: 'global', label: '全局', description: '
|
|
251
|
+
{ value: 'project', label: '当前项目', description: '管理 Skills、Hooks、Rules 和通用规范;继承有效全局配置' },
|
|
252
|
+
{ value: 'global', label: '全局', description: '管理 Skills、Hooks 和通用规范;清理当前项目重复入口' },
|
|
232
253
|
], initialValue: 'project',
|
|
233
254
|
}))
|
|
234
255
|
if (isCancel(scope)) { console.log(chalk.yellow('已取消同步。')); return }
|
|
235
256
|
if (!['project', 'global'].includes(scope)) throw new Error('无效同步范围')
|
|
236
257
|
const global = scope === 'global', targetRoot = global ? homeDir : projectDir
|
|
237
|
-
|
|
238
|
-
if (platforms === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
258
|
+
let platforms = platformsOverride || await buildPlatformDefaultsFromLocalState(targetRoot, scope, codexHome)
|
|
239
259
|
if (platforms.length === 0 || platforms.some(platform => !PLATFORM_SYNC_TYPES[platform] || (global && platform === 'cursor'))) throw new Error('所选平台不支持此同步范围')
|
|
240
|
-
const
|
|
241
|
-
const
|
|
260
|
+
const projectState = await loadSyncState(projectDir), globalState = await loadSyncState(homeDir)
|
|
261
|
+
const targetState = global ? globalState : projectState
|
|
262
|
+
const scans = { skills: await scanSkills(sourceDir), rules: await scanRules(sourceDir), hooks: await scanHooks(sourceDir), commands: [] }
|
|
263
|
+
const mainFlow = !typesOverride && !selectedOverride && modeOverride !== SYNC_MODE_SKILLS
|
|
264
|
+
const availablePlatforms = PLATFORM_OPTIONS.filter(option => !global || option.value !== 'cursor').map(option => option.value)
|
|
265
|
+
const allowed = new Set(contentOptions(scope, mainFlow ? availablePlatforms : platforms).map(option => option.value))
|
|
242
266
|
let contentTypes = typesOverride
|
|
243
267
|
if (!contentTypes && modeOverride === SYNC_MODE_SKILLS) contentTypes = ['skills']
|
|
244
268
|
if (!contentTypes && selectedOverride) contentTypes = Object.keys(selectedOverride).filter(type => allowed.has(type))
|
|
245
269
|
if (!contentTypes) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
if (isCancel(result)) { console.log(chalk.yellow('已取消同步。')); return }
|
|
251
|
-
contentTypes = result
|
|
270
|
+
contentTypes = [...allowed]
|
|
271
|
+
if (!await fs.pathExists(path.join(sourceDir, 'AGENTS.md'))) contentTypes = contentTypes.filter(type => type !== 'baseline')
|
|
272
|
+
if (!scans.hooks.length) contentTypes = contentTypes.filter(type => type !== 'hooks')
|
|
252
273
|
}
|
|
253
274
|
if (contentTypes.some(type => !allowed.has(type))) throw new Error('所选范围或平台不支持此同步内容')
|
|
254
275
|
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
276
|
const existing = await managedNames(targetRoot, global, codexHome)
|
|
277
|
+
const cursorRules = new Set(scans.rules.filter(rule => existing.cursor.rules.has(buildCursorRuleName(rule.name))).map(rule => rule.name))
|
|
259
278
|
const current = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type,
|
|
260
|
-
unionSets(...platforms.map(platform => existing[platform][type]))]))
|
|
279
|
+
unionSets(...platforms.map(platform => platform === 'cursor' && type === 'rules' ? cursorRules : existing[platform][type]))]))
|
|
261
280
|
const defaults = buildSelectionDefaultsFromSyncState(current, targetState, platforms)
|
|
262
281
|
for (const type of ['skills', 'rules', 'hooks']) defaults[type] = unionSets(defaults[type], current[type])
|
|
263
|
-
|
|
282
|
+
// CLI 尚未选择时允许勾选所有 Skills;最终计划按实际 CLI 去重,避免提前禁用其他 CLI 所需条目。
|
|
283
|
+
const choices = platformsOverride && !global && contentTypes.includes('skills')
|
|
264
284
|
? { ...scans, skills: await projectSkillChoices(scans.skills, platforms, { homeDir, codexHome }) } : scans
|
|
265
285
|
const selected = selectedOverride
|
|
266
286
|
? { skills: selectedOverride.skills || [], rules: selectedOverride.rules || [], hooks: [], commands: [] }
|
|
267
287
|
: await interactiveSelect(choices, defaults, { types: contentTypes })
|
|
268
288
|
if (selected === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
289
|
+
const contentActions = { ...actionsOverride }
|
|
290
|
+
if (!selectedOverride) {
|
|
291
|
+
for (const type of ['baseline', 'hooks'].filter(type => contentTypes.includes(type))) {
|
|
292
|
+
if (contentActions[type]) continue
|
|
293
|
+
const inherited = type === 'baseline' && !global ? await baselineProviders(platforms, { homeDir, codexHome }) : []
|
|
294
|
+
const name = type === 'hooks' ? 'Hooks' : 'AGENTS.md 通用规范'
|
|
295
|
+
let inheritedHooks = false
|
|
296
|
+
if (type === 'hooks' && !global) {
|
|
297
|
+
const manifests = await loadHookManifests(sourceDir)
|
|
298
|
+
let total = 0, covered = 0
|
|
299
|
+
for (const platform of platforms.filter(p => PLATFORM_SYNC_TYPES[p].includes('hooks'))) {
|
|
300
|
+
const available = scans.hooks.filter(hook => manifests.some(manifest => manifest.name === hook.name && (!manifest.platforms || manifest.platforms.includes(platform))))
|
|
301
|
+
const provided = await globalHookNames({ platform, sourceDir, homeDir, codexHome })
|
|
302
|
+
total += available.length
|
|
303
|
+
covered += available.filter(hook => provided.has(hook.name)).length
|
|
304
|
+
}
|
|
305
|
+
inheritedHooks = total > 0 && covered === total
|
|
306
|
+
}
|
|
307
|
+
if (inherited.length || inheritedHooks) {
|
|
308
|
+
contentActions[type] = 'sync'
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
const action = await searchableSelect({
|
|
312
|
+
message: `选择 ${name} 操作`, initialValue: 'sync',
|
|
313
|
+
options: [
|
|
314
|
+
{ value: 'sync', label: '启用 / 更新',
|
|
315
|
+
description: type === 'hooks' && !global ? '只为缺少有效全局注册的 Hook 安装项目配置,其他仅去重' : '执行清单会列出实际路径及清理项' },
|
|
316
|
+
{ value: 'remove', label: `移除当前范围的${name}`, description: '只移除 dwy 托管内容,保留个人配置和其他类型' },
|
|
317
|
+
],
|
|
318
|
+
})
|
|
319
|
+
if (isCancel(action)) { console.log(chalk.yellow('已取消同步。')); return }
|
|
320
|
+
contentActions[type] = action
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!platformsOverride) {
|
|
324
|
+
platforms = await selectPlatforms(targetRoot, scope, platforms)
|
|
325
|
+
if (platforms === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
326
|
+
}
|
|
327
|
+
const options = contentOptions(scope, platforms)
|
|
328
|
+
if (mainFlow) contentTypes = contentTypes.filter(type => options.some(option => option.value === type))
|
|
269
329
|
const stale = staleEntries(scans, targetState, existing, platforms)
|
|
270
330
|
const removals = normalizeRemovals(removalsOverride || stale)
|
|
271
331
|
const baseline = contentTypes.some(type => type === 'rules' || type === 'baseline') ? await readBaselineDoc(sourceDir) : ''
|
|
272
332
|
const plan = await buildSyncPlan({
|
|
273
333
|
scope, platforms, contentTypes, selected, scans, sourceDir, projectDir, homeDir, codexHome,
|
|
274
334
|
projectState, globalState, staleRemovals: removals, staleEntries: stale, baseline,
|
|
335
|
+
contentActions,
|
|
336
|
+
refreshSkills: mainFlow,
|
|
275
337
|
skillScope: normalizeSkillScope(skillScopeOverride),
|
|
276
338
|
})
|
|
277
339
|
const nextTarget = global ? plan.nextGlobal : plan.nextProject
|
|
@@ -279,16 +341,21 @@ export async function syncAll({
|
|
|
279
341
|
nextTarget.syncMode = contentTypes.length === 1 && contentTypes[0] === 'skills' ? SYNC_MODE_SKILLS : SYNC_MODE_ALL
|
|
280
342
|
nextTarget.skillScope = normalizeSkillScope(skillScopeOverride)
|
|
281
343
|
const stateFile = path.join(targetRoot, ...SYNC_STATE_PATH)
|
|
282
|
-
|
|
344
|
+
await assertConfigPath(stateFile, targetRoot)
|
|
345
|
+
plan.operations.push({ kind: 'state', label: '更新当前范围同步记录', paths: [stateFile], run: async () => {
|
|
346
|
+
await assertConfigPath(stateFile, targetRoot)
|
|
347
|
+
await saveSyncState(targetRoot, nextTarget)
|
|
348
|
+
} })
|
|
283
349
|
if (global && plan.projectChanged) plan.operations.push({
|
|
284
350
|
kind: 'state', label: '更新项目去重记录', paths: [path.join(projectDir, ...SYNC_STATE_PATH)],
|
|
285
|
-
run: () => saveSyncState(projectDir, plan.nextProject),
|
|
351
|
+
run: async () => { await assertConfigPath(path.join(projectDir, ...SYNC_STATE_PATH), projectDir); await saveSyncState(projectDir, plan.nextProject) },
|
|
286
352
|
})
|
|
353
|
+
if (global && plan.projectChanged) await assertConfigPath(path.join(projectDir, ...SYNC_STATE_PATH), projectDir)
|
|
287
354
|
console.log(`\n同步范围:${global ? '全局' : '当前项目'};平台:${platforms.map(p => PLATFORM_LABELS[p]).join('、')}`)
|
|
288
355
|
console.log(`处理内容:${contentTypes.map(type => options.find(option => option.value === type).label).join('、')}`)
|
|
289
356
|
if (contentTypes.includes('skills')) console.log(`Skills:${selected.skills.map(item => item.name).join('、') || '无项目安装选择;已有全局入口仅去重'}`)
|
|
290
357
|
if (contentTypes.includes('rules')) console.log(`Rules:${selected.rules.map(item => item.name).join('、') || '移除当前项目托管规则'}`)
|
|
291
|
-
if (contentTypes.includes('baseline')) console.log(
|
|
358
|
+
if (contentTypes.includes('baseline')) console.log(`通用规范:${contentActions.baseline === 'remove' ? '显式移除' : baseline.split('\n').filter(line => /^#/.test(line)).slice(0,3).join(';')}`)
|
|
292
359
|
for (const operation of plan.operations) {
|
|
293
360
|
console.log(operation.label)
|
|
294
361
|
for (const dest of operation.paths) console.log(` → ${dest}`)
|
|
@@ -297,7 +364,10 @@ export async function syncAll({
|
|
|
297
364
|
const answer = confirmed === false ? false : await confirm({ message: '确认同步?' })
|
|
298
365
|
if (isCancel(answer) || !answer) { console.log(chalk.yellow('已取消同步。')); return }
|
|
299
366
|
}
|
|
300
|
-
for (const operation of plan.operations)
|
|
367
|
+
for (const operation of plan.operations) {
|
|
368
|
+
await operation.run()
|
|
369
|
+
if (!['state', 'inherit'].includes(operation.kind)) console.log(chalk.green(`已完成:${operation.label}`))
|
|
370
|
+
}
|
|
301
371
|
console.log(chalk.green('同步完成。'))
|
|
302
372
|
return plan.operations.map(({ kind, label, paths }) => ({ kind, label, paths }))
|
|
303
373
|
}
|
package/src/sync-codex.js
CHANGED
|
@@ -111,19 +111,30 @@ function upsertBaselineBlock(content, baselineBody) {
|
|
|
111
111
|
return base ? `${block}\n\n${base}` : `${block}\n`
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/**
|
|
115
|
-
export async function
|
|
116
|
-
if (!
|
|
114
|
+
/** 判断文件是否含有有效的 dwy 通用规范;个人文件不作为继承依据。 */
|
|
115
|
+
export async function hasManagedBaseline(dest) {
|
|
116
|
+
if (!await fs.pathExists(dest)) return false
|
|
117
|
+
const match = (await fs.readFile(dest, 'utf8')).match(BASELINE_RE)
|
|
118
|
+
return Boolean(match && match[0].replace(/<!--[\s\S]*?-->/g, '').trim())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 独立更新通用规范,空内容移除基线块;保留 Rules 和个人内容。 */
|
|
122
|
+
export async function syncBaselineDoc(baseline, dest) {
|
|
117
123
|
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
118
|
-
const previous = current.trim() === baseline.trim() ? '' : current
|
|
119
|
-
const next =
|
|
124
|
+
const previous = baseline.trim() && current.trim() === baseline.trim() ? '' : current
|
|
125
|
+
const next = upsertBaselineBlock(previous, baseline)
|
|
120
126
|
if (next === current) return 0
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
if (next.trim()) {
|
|
128
|
+
await fs.ensureDir(path.dirname(dest))
|
|
129
|
+
await fs.writeFile(dest, next)
|
|
130
|
+
} else await fs.remove(dest)
|
|
131
|
+
logAction(`baseline → ${dest}`)
|
|
124
132
|
return 1
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
/** 兼容旧调用方;全局基线更新同样保留其他托管块。 */
|
|
136
|
+
export const syncGlobalBaselineDoc = syncBaselineDoc
|
|
137
|
+
|
|
127
138
|
/** 迁移旧入口中的基线;无托管标记时只移除与当前模板完全一致的文件。 */
|
|
128
139
|
export async function removeLegacyBaseline(baseline, dest) {
|
|
129
140
|
if (!await fs.pathExists(dest)) return
|
|
@@ -153,7 +164,7 @@ export async function syncProjectAgentsMd(projectDir, selectedRules, {
|
|
|
153
164
|
}
|
|
154
165
|
const blockBody = sections.join('\n\n---\n\n')
|
|
155
166
|
|
|
156
|
-
const next = upsertManagedBlock(
|
|
167
|
+
const next = upsertManagedBlock(current, blockBody)
|
|
157
168
|
if (next === current) return 0
|
|
158
169
|
await fs.ensureDir(path.dirname(dest))
|
|
159
170
|
await fs.writeFile(dest, next)
|
|
@@ -253,7 +264,8 @@ function globalHookCommand(configDir, name) {
|
|
|
253
264
|
return `bash '${script.replace(/'/g, "'\\''")}'`
|
|
254
265
|
}
|
|
255
266
|
|
|
256
|
-
|
|
267
|
+
/** 生成 Codex Hook 注册,继承检测和写入共用相同命令格式。 */
|
|
268
|
+
export function buildCodexHooksJson(manifests, selectedHookNames, global, configDir) {
|
|
257
269
|
const hooksJson = { hooks: {} }
|
|
258
270
|
const grouped = new Map()
|
|
259
271
|
|
|
@@ -284,14 +296,19 @@ function buildCodexHooksJson(manifests, selectedHookNames, global, configDir) {
|
|
|
284
296
|
return Object.keys(hooksJson.hooks).length > 0 ? hooksJson : {}
|
|
285
297
|
}
|
|
286
298
|
|
|
287
|
-
|
|
299
|
+
/** 只识别完整生成命令及已知历史格式,保留同名个人路径。 */
|
|
300
|
+
export function isManagedCodexHookCommand(command, managedHookNames, configDir) {
|
|
288
301
|
if (typeof command !== 'string') return false
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
return [...managedHookNames].some(name => [
|
|
303
|
+
globalHookCommand(configDir, name),
|
|
304
|
+
`bash "$(git rev-parse --show-toplevel)/.codex/hooks/${name}"`,
|
|
305
|
+
`bash "$HOME/.codex/hooks/${name}"`,
|
|
306
|
+
`bash ~/.codex/hooks/${name}`,
|
|
307
|
+
].includes(command))
|
|
292
308
|
}
|
|
293
309
|
|
|
294
|
-
|
|
310
|
+
/** 只替换指定托管注册,保留其他命令及配置字段。 */
|
|
311
|
+
export function mergeCodexHooks(existingHooks, generatedHooks, managedHookNames, configDir) {
|
|
295
312
|
const merged = {}
|
|
296
313
|
const sourceEvents = new Set([
|
|
297
314
|
...Object.keys(existingHooks || {}),
|
|
@@ -313,14 +330,12 @@ function mergeCodexHooks(existingHooks, generatedHooks, managedHookNames, config
|
|
|
313
330
|
return Object.keys(merged).length > 0 ? merged : undefined
|
|
314
331
|
}
|
|
315
332
|
|
|
316
|
-
async function syncCodexHooksJson(sourceDir, configDir, selectedHookNames, global) {
|
|
333
|
+
async function syncCodexHooksJson(sourceDir, configDir, selectedHookNames, global, staleHookNames) {
|
|
317
334
|
const dest = path.join(configDir, 'hooks.json')
|
|
318
335
|
const manifests = await loadHookManifests(sourceDir)
|
|
319
336
|
const generated = buildCodexHooksJson(manifests, selectedHookNames, global, configDir)
|
|
320
337
|
const managedHookNames = new Set(
|
|
321
|
-
manifests
|
|
322
|
-
.filter(manifest => !manifest.platforms || manifest.platforms.includes('codex'))
|
|
323
|
-
.map(manifest => manifest.name),
|
|
338
|
+
[...manifests.map(manifest => manifest.name), ...staleHookNames],
|
|
324
339
|
)
|
|
325
340
|
|
|
326
341
|
let nextJson
|
|
@@ -362,7 +377,7 @@ export async function syncCodex({
|
|
|
362
377
|
syncTypes = ['skills', 'hooks'],
|
|
363
378
|
staleRemovals = {},
|
|
364
379
|
} = {}) {
|
|
365
|
-
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
380
|
+
const sourceDir = sourceDirOverride || await resolveSourceDir({ homeDir })
|
|
366
381
|
if (!await fs.pathExists(sourceDir)) {
|
|
367
382
|
throw new Error('ai-tools templates not found in repo')
|
|
368
383
|
}
|
|
@@ -402,7 +417,7 @@ export async function syncCodex({
|
|
|
402
417
|
}
|
|
403
418
|
if (syncTypes.includes('hooks')) {
|
|
404
419
|
syncedCount += await copyCodexHooks(syncedSelection.hooks, configDir)
|
|
405
|
-
syncedCount += await syncCodexHooksJson(sourceDir, configDir, selectedHookNames, global)
|
|
420
|
+
syncedCount += await syncCodexHooksJson(sourceDir, configDir, selectedHookNames, global, staleRemovals.hooks || [])
|
|
406
421
|
}
|
|
407
422
|
|
|
408
423
|
const skillTemplateNames = new Set([
|
package/src/sync-cursor.js
CHANGED
|
@@ -108,10 +108,11 @@ export async function scanExistingCursorRules(projectDir) {
|
|
|
108
108
|
export async function syncCursor({
|
|
109
109
|
sourceDir: sourceDirOverride,
|
|
110
110
|
projectDir,
|
|
111
|
+
homeDir,
|
|
111
112
|
selected,
|
|
112
113
|
staleRemovals = {},
|
|
113
114
|
} = {}) {
|
|
114
|
-
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
115
|
+
const sourceDir = sourceDirOverride || await resolveSourceDir({ homeDir })
|
|
115
116
|
if (!await fs.pathExists(sourceDir)) {
|
|
116
117
|
throw new Error('ai-tools templates not found in repo')
|
|
117
118
|
}
|