create-dwy 0.22.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # create-dwy
2
2
 
3
+ ## [0.23.0] - 2026-11-02
4
+
5
+ ### Minor Changes
6
+
7
+ - 同步改为「范围 → 平台 → 本次处理内容 → 逐项选择 → 清单确认」。默认只选 Skills;项目 Rules、全局通用规范和 Hooks 必须显式选择,未选择的类型及其记录保持原样。全局不提供 Cursor。
8
+ - 默认勾选只读取当前范围及所选平台。项目 Skills 全部适用平台已有有效全局入口时显示“全局已启用”并禁用重复勾选;部分平台覆盖时只为缺少入口的平台安装。
9
+ - 确认页和执行共用操作清单,列出共享副本、入口、旧项清理、配置和同步记录。项目去重只备份或移除项目入口,不改写全局提供者;用户主目录及其别名不能作为项目范围同步。
10
+ - `sync` / `migrate` 只准备缺失或损坏的共享副本,已有内容由 `refresh` / `upgrade` 刷新。产品流程的外部依赖补装也纳入确认清单,只拉取缺失内容并保留已有副本。
11
+ - 全局 Codex Hooks 遵循自定义 `CODEX_HOME`,命令路径正确引用空格和单引号;同步保留个人配置,补充平台兼容、取消无写入和真实终端交互回归。
12
+
13
+ ---
14
+
3
15
  ## [0.22.0] - 2026-11-02
4
16
 
5
17
  ### 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.23.0",
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,23 @@ 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;未选择的类型保持原样。',
29
+ ' 项目:Skills、项目 Rules、Hooks;全局:Skills、全局通用规范、Hooks。',
30
+ ' 内容选项按平台能力过滤。Hooks 选中类型后按平台自动处理。',
31
+ ' 4. 逐项选择 Skills / 项目 Rules;全局通用规范显示摘要。',
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。',
40
43
  ' 更新或调整配置:再次执行 dwy sync,检查勾选项,再确认同步。',
41
44
  ' 更新 CLI:dwy upgrade(全局安装时可用;更新后执行 dwy --version 查看版本)。',
42
45
  ' 迁移老项目:cd your-project 后执行 dwy migrate,无需重新勾选。',
@@ -49,9 +52,15 @@ export function buildHelpText(version) {
49
52
  ' OpenCode:全局 ~/.config/opencode/AGENTS.md;项目根 AGENTS.md;当前不支持 Hooks。',
50
53
  ' Cursor:仅项目 Rules;全局规则在 Cursor 设置中管理。',
51
54
  '',
52
- '同步仅处理所选平台;基线固定全局、Rules 固定项目,位置选择只影响 Skills / Hooks。',
53
- '取消已勾选的托管项会在确认后删除,保留手写内容。',
55
+ '同步仅处理当前范围、所选平台和内容;未选择的类型及其记录保持原样。',
56
+ '用户主目录不能作为项目范围同步,请选择全局或进入项目目录。',
57
+ '取消已有勾选会在确认后移除当前范围的托管入口;不勾选整个类型则不处理。',
54
58
  'Skills 副本集中在 ~/.dwy/skills;项目和全局使用目录链接,同平台全局入口优先。',
59
+ '项目 Skills 全部适用平台均有全局入口时显示「全局已启用」,不能重复勾选。',
60
+ '部分平台已有全局入口时仍可选,只为缺少全局入口的平台安装项目链接。',
61
+ '项目同步只清理项目重复入口,不改全局提供者;真实目录先备份。',
62
+ 'sync / migrate 仅准备缺失或损坏副本,不刷新已有共享内容;refresh / upgrade 才刷新。',
63
+ '产品流程缺失的外部依赖也列入清单,确认后联网补齐,已有内容保留。',
55
64
  '旧真实目录备份到同步根的 .dwy/skill-backups;个人 Skills、Rules、Hooks 不参与 migrate。',
56
65
  '旧项目首次需执行 dwy migrate;迁移后无需逐项目刷新 Skills。',
57
66
  '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'))