create-dwy 0.22.0 → 0.24.1

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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # create-dwy
2
2
 
3
+ ## [0.24.1] - 2026-11-03
4
+
5
+ ### Patch Changes
6
+
7
+ - 包含 0.24.0 的 Skills / Hooks / Rules / 通用规范管理、显式移除和全局去重能力。
8
+ - 收敛配置管理集成测试的无断言日志,规避 Node 22 测试运行器在大量中文 stdout 下的 IPC 反序列化问题;保留全部断言和真实终端输出测试,不改变 CLI 行为。
9
+
10
+ ---
11
+
12
+ ## [0.24.0] - 2026-11-03
13
+
14
+ ### Minor Changes
15
+
16
+ - 全局提供 Skills、Hooks 和 AGENTS.md 通用规范,项目另外提供 Rules。全局首次默认启用通用规范;Hooks 与规范增加显式启用/更新和移除动作,未选择的类型保持原样,规范移除后不自动复选。
17
+ - 项目通用规范与 Rules 分别操作各自托管块,移除任一项保留其他内容。任一所选 CLI 已有有效全局基线时,项目禁止重复安装,共享项目文件的混合平台场景提示补齐缺失的全局入口。
18
+ - Hooks 按有效脚本及对应事件、matcher 和完整命令注册判断全局提供者,项目只补缺失项。全局启用后明确清理当前项目重复脚本和注册项;移除全局配置不自动回填项目。
19
+ - Hook 清理支持历史托管项,保留同名个人路径、组合命令和其他设置;配置目录及单文件链接在规划和执行时检查,防止项目操作穿透改写全局或外部目标。
20
+ - 补充配置独立启停、移除记忆、继承、取消及真实终端回归,旧测试统一注入临时用户目录。
21
+
22
+ ---
23
+
24
+ ## [0.23.0] - 2026-11-02
25
+
26
+ ### Minor Changes
27
+
28
+ - 同步改为「范围 → 平台 → 本次处理内容 → 逐项选择 → 清单确认」。默认只选 Skills;项目 Rules、全局通用规范和 Hooks 必须显式选择,未选择的类型及其记录保持原样。全局不提供 Cursor。
29
+ - 默认勾选只读取当前范围及所选平台。项目 Skills 全部适用平台已有有效全局入口时显示“全局已启用”并禁用重复勾选;部分平台覆盖时只为缺少入口的平台安装。
30
+ - 确认页和执行共用操作清单,列出共享副本、入口、旧项清理、配置和同步记录。项目去重只备份或移除项目入口,不改写全局提供者;用户主目录及其别名不能作为项目范围同步。
31
+ - `sync` / `migrate` 只准备缺失或损坏的共享副本,已有内容由 `refresh` / `upgrade` 刷新。产品流程的外部依赖补装也纳入确认清单,只拉取缺失内容并保留已有副本。
32
+ - 全局 Codex Hooks 遵循自定义 `CODEX_HOME`,命令路径正确引用空格和单引号;同步保留个人配置,补充平台兼容、取消无写入和真实终端交互回归。
33
+
34
+ ---
35
+
3
36
  ## [0.22.0] - 2026-11-02
4
37
 
5
38
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.22.0",
3
+ "version": "0.24.1",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli-help.js CHANGED
@@ -14,7 +14,7 @@ export function buildHelpText(version) {
14
14
  `dwy ${version}`,
15
15
  '',
16
16
  '用法:',
17
- ' dwy 全局更新通用基线,项目注入所选 Rules;Skills / Hooks 可选位置',
17
+ ' dwy 选择范围、平台和处理内容,预览清单后同步',
18
18
  ' dwy sync 同上',
19
19
  ' dwy upgrade 升级到 npm 最新正式版,并刷新共享 Skills',
20
20
  ' dwy refresh 用当前 CLI 模板刷新已安装的共享 Skills',
@@ -23,20 +23,24 @@ export function buildHelpText(version) {
23
23
  ' dwy --version 显示版本',
24
24
  '',
25
25
  '同步步骤:',
26
- ' 1. 选择 Skills,再选择 Rules;按分类逐项勾选。',
27
- ' 2. 选择 Skills / Hooks 写入当前项目或全局,再选择平台。',
28
- ' 3. 查看更新、删除项,确认后才写入;取消不写文件。',
29
- ' 通用基线默认写入所选 CLI 的全局入口;所选 Rules 写入项目根 AGENTS.md。',
30
- ' Hooks 按平台自动同步,无需选择;Commands 不参与同步。',
26
+ ' 1. 选择同步范围:当前项目或全局。',
27
+ ' 2. 选择该范围支持的平台;全局不提供 Cursor。',
28
+ ' 3. 选择本次处理内容;项目默认只选 Skills,全局首次默认 Skills 和通用规范。',
29
+ ' 项目:Skills、Hooks、Rules、AGENTS.md 通用规范;全局:Skills、Hooks、AGENTS.md 通用规范。',
30
+ ' 内容按平台能力过滤;未选择的类型保持原样。',
31
+ ' 4. 逐项选择 Skills / Rules;Hooks 和通用规范分别选择启用/更新或移除。',
32
+ ' 5. 查看写入、备份和清理清单,确认后执行;取消不写文件。',
33
+ ' Commands 不参与同步。',
31
34
  '',
32
35
  '交互按键:',
33
36
  ' Skills / Rules:←/→ 切分类,↑/↓ 移动,Space 勾选,a 全选/清空当前分类。',
34
- ' 位置 / 平台:↑/↓ 移动;平台用 Space 勾选,可输入关键字筛选。',
37
+ ' 范围 / 平台 / 内容:↑/↓ 移动;多选用 Space 勾选,可输入关键字筛选。',
35
38
  ' Enter 确认当前步骤,Esc 或 Ctrl+C 取消。',
36
39
  '',
37
40
  '常见用法:',
38
- ' 同步当前项目:cd your-project 后执行 dwy,在位置步骤选择「当前项目」。',
39
- ' 同步全局 Skills / Hooks:在项目目录执行 dwy,在位置步骤选择「全局」。',
41
+ ' 同步当前项目:cd your-project 后执行 dwy,选择「当前项目」及本次处理内容。',
42
+ ' 同步全局:执行 dwy,选择「全局」及 Skills、Hooks 或 AGENTS.md 通用规范。',
43
+ ' 移除配置:选择对应范围和 Hooks / 通用规范,再选择「移除」并确认。',
40
44
  ' 更新或调整配置:再次执行 dwy sync,检查勾选项,再确认同步。',
41
45
  ' 更新 CLI:dwy upgrade(全局安装时可用;更新后执行 dwy --version 查看版本)。',
42
46
  ' 迁移老项目:cd your-project 后执行 dwy migrate,无需重新勾选。',
@@ -49,9 +53,21 @@ export function buildHelpText(version) {
49
53
  ' OpenCode:全局 ~/.config/opencode/AGENTS.md;项目根 AGENTS.md;当前不支持 Hooks。',
50
54
  ' Cursor:仅项目 Rules;全局规则在 Cursor 设置中管理。',
51
55
  '',
52
- '同步仅处理所选平台;基线固定全局、Rules 固定项目,位置选择只影响 Skills / Hooks。',
53
- '取消已勾选的托管项会在确认后删除,保留手写内容。',
56
+ '同步仅处理当前范围、所选平台和内容;未选择的类型及其记录保持原样。',
57
+ '用户主目录不能作为项目范围同步,请选择全局或进入项目目录。',
58
+ '取消已有勾选会在确认后移除当前范围的托管入口;不勾选整个类型则不处理。',
54
59
  'Skills 副本集中在 ~/.dwy/skills;项目和全局使用目录链接,同平台全局入口优先。',
60
+ '项目 Skills 全部适用平台均有全局入口时显示「全局已启用」,不能重复勾选。',
61
+ '部分平台已有全局入口时仍可选,只为缺少全局入口的平台安装项目链接。',
62
+ 'Hooks 与 Skills 独立;整组管理,项目仅安装缺少有效全局注册的 Hooks。',
63
+ '全局 Hook 的脚本与对应事件、matcher、完整命令注册均有效时,项目禁止重复安装。',
64
+ '任一所选 CLI 已有 dwy 全局通用规范时,项目只能继承并清理重复基线;其他 CLI 需补齐全局规范。',
65
+ '手写 AGENTS.md 本身不算全局已安装;通用规范与 Rules 共用文件但独立管理。',
66
+ '移除通用规范仅删除基线块,保留 Rules 和手写内容;移除后不会因默认值自动重新启用。',
67
+ '全局启用会明确列出当前项目重复项清理;全局移除不会自动向项目补装。',
68
+ '项目同步只清理项目重复入口,不改全局提供者;真实目录先备份。',
69
+ 'sync / migrate 仅准备缺失或损坏副本,不刷新已有共享内容;refresh / upgrade 才刷新。',
70
+ '产品流程缺失的外部依赖也列入清单,确认后联网补齐,已有内容保留。',
55
71
  '旧真实目录备份到同步根的 .dwy/skill-backups;个人 Skills、Rules、Hooks 不参与 migrate。',
56
72
  '旧项目首次需执行 dwy migrate;迁移后无需逐项目刷新 Skills。',
57
73
  'Rules / Hooks 的模板更新仍需执行 dwy sync;已有会话缓存的 Skill 可能需要重新加载。',
@@ -20,6 +20,21 @@ async function readInstalledNames(homeDir) {
20
20
  return names
21
21
  }
22
22
 
23
+ /** 仅为缺失或损坏的 Skill 准备共享副本;已有有效内容只登记,不随入口同步刷新。 */
24
+ export async function ensureSharedSkills({ skills, homeDir = os.homedir() }) {
25
+ const sources = []
26
+ for (const skill of skills) {
27
+ validateName(skill.name)
28
+ const cached = path.join(homeDir, '.dwy', 'skills', skill.name)
29
+ const stat = await fs.stat(path.join(cached, 'SKILL.md')).catch(error => {
30
+ if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code)) return null
31
+ throw error
32
+ })
33
+ sources.push(stat?.isFile() ? { ...skill, sourcePath: cached } : skill)
34
+ }
35
+ return prepareSharedSkills({ skills: sources, homeDir })
36
+ }
37
+
23
38
  /**
24
39
  * 将包内 Skills 准备到稳定用户目录,返回指向共享副本的条目。
25
40
  * 同一用户的刷新互斥;完整副本准备失败时不替换旧内容。外部 Skills 缓存不纳入本模块清单。
@@ -71,24 +71,54 @@ async function shallowCloneTag(repoUrl, tag, destDir) {
71
71
  await runGit(['clone', '--depth', '1', '--branch', tag, repoUrl, destDir])
72
72
  }
73
73
 
74
- // 安装/更新全部外部 skill 到 ~/.dwy/skills/。再次运行即覆盖式更新。
75
- export async function installSkills() {
76
- const targetDir = getGlobalSkillsDir()
77
- await fs.ensureDir(targetDir)
74
+ async function validExternalSkill(dest) {
75
+ const stat = await fs.stat(path.join(dest, 'SKILL.md')).catch(error => {
76
+ if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code)) return null
77
+ throw error
78
+ })
79
+ return Boolean(stat?.isFile())
80
+ }
78
81
 
79
- const tmpRoot = path.join(os.tmpdir(), `dwy-skills-${process.pid}`)
80
- await fs.ensureDir(tmpRoot)
82
+ async function missingExternalSources(targetDir) {
83
+ const sources = []
84
+ for (const [name, source] of Object.entries(SKILL_SOURCES)) {
85
+ const skills = {}
86
+ for (const [skill, repoPath] of Object.entries(source.skills)) {
87
+ if (!await validExternalSkill(path.join(targetDir, skill))) skills[skill] = repoPath
88
+ }
89
+ if (Object.keys(skills).length) sources.push([name, { ...source, skills }])
90
+ }
91
+ return sources
92
+ }
81
93
 
82
- const versions = {}
94
+ /** 只读列出产品流程缺失的外部依赖;确认后再联网补齐,已有副本不刷新。 */
95
+ export async function planMissingExternalSkills(targetDir) {
96
+ const sources = await missingExternalSources(targetDir)
97
+ const paths = sources.flatMap(([, source]) => Object.keys(source.skills).map(name => path.join(targetDir, name)))
98
+ for (const [, source] of sources) {
99
+ const license = path.join(targetDir, source.licenseDest)
100
+ if (!await fs.pathExists(license)) paths.push(license)
101
+ }
102
+ if (sources.length) paths.push(path.join(targetDir, 'VERSIONS.json'))
103
+ return { paths, run: () => installSkillSources(sources, targetDir, true) }
104
+ }
105
+
106
+ async function installSkillSources(sources, targetDir, missingOnly) {
107
+ if (sources.length === 0) return
108
+ const versionsFile = path.join(targetDir, 'VERSIONS.json')
109
+ const previous = missingOnly && await fs.pathExists(versionsFile) ? await fs.readJson(versionsFile) : {}
110
+ const versions = { ...previous.versions }
111
+ await fs.ensureDir(targetDir)
112
+ const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'dwy-skills-'))
83
113
  try {
84
- for (const [sourceName, src] of Object.entries(SKILL_SOURCES)) {
114
+ for (const [sourceName, src] of sources) {
85
115
  console.log(chalk.gray(`拉取 ${sourceName} @ ${src.tag} ...`))
86
116
  const repoDir = path.join(tmpRoot, sourceName)
87
117
  await shallowCloneTag(src.repo, src.tag, repoDir)
88
118
 
89
119
  // 搬 LICENSE(MIT 要求保留原版权声明)
90
120
  const licenseSrc = path.join(repoDir, src.licenseFile)
91
- if (await fs.pathExists(licenseSrc)) {
121
+ if (await fs.pathExists(licenseSrc) && (!missingOnly || !await fs.pathExists(path.join(targetDir, src.licenseDest)))) {
92
122
  await fs.copy(licenseSrc, path.join(targetDir, src.licenseDest), { overwrite: true })
93
123
  }
94
124
 
@@ -96,6 +126,7 @@ export async function installSkills() {
96
126
  for (const [skillName, repoPath] of Object.entries(src.skills)) {
97
127
  const from = path.join(repoDir, repoPath)
98
128
  const to = path.join(targetDir, skillName)
129
+ if (missingOnly && await validExternalSkill(to)) continue
99
130
  if (!await fs.pathExists(from)) {
100
131
  throw new Error(`${sourceName} @ ${src.tag} 内未找到 ${repoPath}(仓库结构可能变了)`)
101
132
  }
@@ -109,8 +140,8 @@ export async function installSkills() {
109
140
  // 写版本清单 + 清单指纹,供查看与后续更新比对。
110
141
  // manifest_hash 变化(cli 加了 skill / 改了 tag)→ 下次 ensureSkillsInstalled 触发重装。
111
142
  await fs.writeJson(
112
- path.join(targetDir, 'VERSIONS.json'),
113
- { manifest_hash: computeManifestHash(), versions },
143
+ versionsFile,
144
+ { manifest_hash: missingOnly ? previous.manifest_hash : computeManifestHash(), versions },
114
145
  { spaces: 2 },
115
146
  )
116
147
  console.log(chalk.green(`\n✓ 外部 skill 已安装到 ${targetDir}`))
@@ -119,6 +150,11 @@ export async function installSkills() {
119
150
  }
120
151
  }
121
152
 
153
+ /** 兼容显式安装调用:更新固定清单中的全部外部 Skills。 */
154
+ export async function installSkills() {
155
+ return installSkillSources(Object.entries(SKILL_SOURCES), getGlobalSkillsDir(), false)
156
+ }
157
+
122
158
  // 全局 skill 是否已就绪(VERSIONS.json 存在)。
123
159
  export async function skillsInstalled() {
124
160
  return fs.pathExists(path.join(getGlobalSkillsDir(), 'VERSIONS.json'))