create-dwy 0.6.7 → 0.7.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.
Files changed (3) hide show
  1. package/bin/index.js +23 -11
  2. package/package.json +1 -1
  3. package/src/sync.js +78 -124
package/bin/index.js CHANGED
@@ -5,30 +5,42 @@ import { fileURLToPath } from 'url'
5
5
  import { dirname, resolve } from 'path'
6
6
  import { program } from 'commander'
7
7
  import { createProject } from '../src/create.js'
8
- import { syncClaude, syncProjectClaudeMd } from '../src/sync.js'
8
+ import { syncClaude } from '../src/sync.js'
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
11
11
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
12
12
 
13
13
  program
14
14
  .name('dwy')
15
- .description('项目脚手架与 Claude Code 配置同步工具')
15
+ .description('项目脚手架与 Claude Code 配置管理工具')
16
16
  .version(pkg.version)
17
17
 
18
18
  program
19
19
  .command('create [name]')
20
20
  .description('从模板创建新项目')
21
21
  .action(createProject)
22
+ .addHelpText('after', `
23
+ Examples:
24
+ $ dwy create
25
+ $ dwy create my-app
26
+ `)
22
27
 
23
- program
24
- .command('sync')
25
- .description('同步 Claude Code 配置到项目 .claude/(默认覆盖已选项;CLAUDE.md → 全局 ~/.claude/)')
26
- .option('--no-cache', '忽略已选缓存,重新交互式选择(已选项默认勾选)')
27
- .action((opts) => syncClaude(opts))
28
+ const claudeCmd = program
29
+ .command('claude')
30
+ .description('Claude Code 配置管理')
28
31
 
29
- program
30
- .command('sync-claude-md')
31
- .description('单独同步 CLAUDE.md 到全局 ~/.claude/')
32
- .action(syncProjectClaudeMd)
32
+ claudeCmd
33
+ .command('sync [target]')
34
+ .description('同步配置到项目 .claude/(target=md 时仅同步 CLAUDE.md 到 ~/.claude/)')
35
+ .option('-i, --reselect', '忽略已选缓存,重新交互式选择(已选项默认勾选)')
36
+ .option('--dry-run', '预演模式,不实际写入文件')
37
+ .action((target, opts) => syncClaude({ target, ...opts }))
38
+ .addHelpText('after', `
39
+ Examples:
40
+ $ dwy claude sync 基于已选缓存覆盖同步
41
+ $ dwy claude sync -i 重新交互式选择
42
+ $ dwy claude sync --dry-run 预演,不写入文件
43
+ $ dwy claude sync md 仅同步 CLAUDE.md 到 ~/.claude/
44
+ `)
33
45
 
34
46
  program.parse()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync.js CHANGED
@@ -2,11 +2,8 @@ import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import os from 'os'
4
4
  import inquirer from 'inquirer'
5
- import { ensureRepoCache, copyDir, chalk, PACKAGE_ROOT } from './utils.js'
5
+ import { ensureRepoCache, chalk } from './utils.js'
6
6
 
7
- /**
8
- * 从 YAML frontmatter 中提取 description 字段
9
- */
10
7
  function extractDescription(content) {
11
8
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/)
12
9
  if (!match) return ''
@@ -15,9 +12,6 @@ function extractDescription(content) {
15
12
  return descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : ''
16
13
  }
17
14
 
18
- /**
19
- * 扫描 skills 目录,返回 { name, description, sourcePath } 列表
20
- */
21
15
  async function scanSkills(sourceDir) {
22
16
  const skillsDir = path.join(sourceDir, 'skills')
23
17
  if (!await fs.pathExists(skillsDir)) return []
@@ -44,9 +38,6 @@ async function scanSkills(sourceDir) {
44
38
  return skills.sort((a, b) => a.name.localeCompare(b.name))
45
39
  }
46
40
 
47
- /**
48
- * 扫描 rules 目录,返回 { name, description, sourcePath } 列表
49
- */
50
41
  async function scanRules(sourceDir) {
51
42
  const rulesDir = path.join(sourceDir, 'rules')
52
43
  if (!await fs.pathExists(rulesDir)) return []
@@ -70,9 +61,6 @@ async function scanRules(sourceDir) {
70
61
  return rules.sort((a, b) => a.name.localeCompare(b.name))
71
62
  }
72
63
 
73
- /**
74
- * 扫描 commands 目录
75
- */
76
64
  async function scanCommands(sourceDir) {
77
65
  const commandsDir = path.join(sourceDir, 'commands')
78
66
  if (!await fs.pathExists(commandsDir)) return []
@@ -93,9 +81,6 @@ async function scanCommands(sourceDir) {
93
81
  return commands.sort((a, b) => a.name.localeCompare(b.name))
94
82
  }
95
83
 
96
- /**
97
- * 扫描 hooks 目录
98
- */
99
84
  async function scanHooks(sourceDir) {
100
85
  const hooksDir = path.join(sourceDir, 'hooks')
101
86
  if (!await fs.pathExists(hooksDir)) return []
@@ -116,9 +101,6 @@ async function scanHooks(sourceDir) {
116
101
  return hooks.sort((a, b) => a.name.localeCompare(b.name))
117
102
  }
118
103
 
119
- /**
120
- * 用 inquirer 做交互式多选
121
- */
122
104
  async function promptSelection(items, category, existingNames = new Set()) {
123
105
  if (items.length === 0) return []
124
106
 
@@ -140,9 +122,6 @@ async function promptSelection(items, category, existingNames = new Set()) {
140
122
  return selected
141
123
  }
142
124
 
143
- /**
144
- * 扫描项目已选过的项(.claude/<type>s/ 下的子项名),返回 Set
145
- */
146
125
  async function scanExisting(projectTargetDir, typePlural) {
147
126
  const dir = path.join(projectTargetDir, typePlural)
148
127
  if (!await fs.pathExists(dir)) return new Set()
@@ -150,36 +129,34 @@ async function scanExisting(projectTargetDir, typePlural) {
150
129
  return new Set(entries.filter(name => name !== '.gitkeep' && name !== '.DS_Store'))
151
130
  }
152
131
 
153
- /**
154
- * 同步选中的文件到目标目录
155
- */
156
- async function syncSelectedItems(items, targetDir) {
157
- let syncedCount = 0
132
+ function logAction(dryRun, label) {
133
+ console.log(chalk.green(` ${dryRun ? '[dry-run] ' : '✓ '}${label}`))
134
+ }
158
135
 
136
+ async function syncSelectedItems(items, targetDir, dryRun) {
159
137
  for (const item of items) {
160
138
  const dest = path.join(targetDir, item.type + 's', item.name)
161
- await fs.ensureDir(path.dirname(dest))
162
- await fs.copy(item.sourcePath, dest, { overwrite: true })
163
- syncedCount++
164
- console.log(chalk.green(` ✓ ${item.type}s/${item.name}`))
139
+ if (!dryRun) {
140
+ await fs.ensureDir(path.dirname(dest))
141
+ await fs.copy(item.sourcePath, dest, { overwrite: true })
142
+ }
143
+ logAction(dryRun, `${item.type}s/${item.name}`)
165
144
  }
166
-
167
- return syncedCount
145
+ return items.length
168
146
  }
169
147
 
170
- /**
171
- * 同步 settings.json(智能合并)
172
- */
173
- async function syncSettings(sourceDir, targetDir) {
148
+ async function syncSettings(sourceDir, targetDir, dryRun) {
174
149
  const settingsSrc = path.join(sourceDir, 'settings.json')
175
150
  if (!await fs.pathExists(settingsSrc)) return 0
176
151
 
177
152
  const settingsDest = path.join(targetDir, 'settings.json')
178
153
  const newSettings = await fs.readJson(settingsSrc)
179
154
 
155
+ let merged
156
+ let action
180
157
  if (await fs.pathExists(settingsDest)) {
181
158
  const existing = await fs.readJson(settingsDest)
182
- const merged = { ...existing }
159
+ merged = { ...existing }
183
160
  for (const [key, value] of Object.entries(newSettings)) {
184
161
  if (key in merged && typeof merged[key] === 'object' && !Array.isArray(merged[key])
185
162
  && typeof value === 'object' && !Array.isArray(value)) {
@@ -188,31 +165,41 @@ async function syncSettings(sourceDir, targetDir) {
188
165
  merged[key] = value
189
166
  }
190
167
  }
191
- await fs.writeJson(settingsDest, merged, { spaces: 2 })
192
- console.log(chalk.green(' ✓ settings.json — merged'))
168
+ action = 'merged'
193
169
  } else {
194
- await fs.writeJson(settingsDest, newSettings, { spaces: 2 })
195
- console.log(chalk.green(' ✓ settings.json — created'))
170
+ merged = newSettings
171
+ action = 'created'
196
172
  }
197
173
 
174
+ if (!dryRun) await fs.writeJson(settingsDest, merged, { spaces: 2 })
175
+ logAction(dryRun, `settings.json — ${action}`)
198
176
  return 1
199
177
  }
200
178
 
201
- /**
202
- * 同步 CLAUDE.md
203
- */
204
- async function syncClaudeMd(sourceDir, targetDir) {
179
+ async function syncClaudeMd(sourceDir, targetDir, dryRun) {
205
180
  const claudeMdSrc = path.join(sourceDir, 'CLAUDE.md')
206
181
  if (!await fs.pathExists(claudeMdSrc)) return 0
207
182
 
208
- await fs.copy(claudeMdSrc, path.join(targetDir, 'CLAUDE.md'), { overwrite: true })
209
- console.log(chalk.green(' ✓ CLAUDE.md'))
183
+ if (!dryRun) await fs.copy(claudeMdSrc, path.join(targetDir, 'CLAUDE.md'), { overwrite: true })
184
+ logAction(dryRun, 'CLAUDE.md')
210
185
  return 1
211
186
  }
212
187
 
213
- /**
214
- * 检测本地模板目录:如果当前是 dwy-shared monorepo,直接用本地模板
215
- */
188
+ async function syncHooks(items, targetDir, dryRun) {
189
+ if (items.length === 0) return 0
190
+ const hooksTargetDir = path.join(targetDir, 'hooks')
191
+ if (!dryRun) await fs.ensureDir(hooksTargetDir)
192
+ for (const hook of items) {
193
+ const dest = path.join(hooksTargetDir, hook.name)
194
+ if (!dryRun) {
195
+ await fs.copy(hook.sourcePath, dest, { overwrite: true })
196
+ await fs.chmod(dest, 0o755)
197
+ }
198
+ logAction(dryRun, `hooks/${hook.name}`)
199
+ }
200
+ return items.length
201
+ }
202
+
216
203
  async function resolveSourceDir() {
217
204
  const localTemplate = path.join(process.cwd(), 'claude-cli', 'templates', 'claude-global')
218
205
  if (await fs.pathExists(localTemplate)) {
@@ -224,24 +211,39 @@ async function resolveSourceDir() {
224
211
  }
225
212
 
226
213
  /**
227
- * 主同步函数:默认基于已选缓存覆盖同步,--no-cache 走交互式选择
228
- * 项目级 .claude/ 接收 skills/rules/commands/hooks/settings.json
229
- * 全局 ~/.claude/ 接收 CLAUDE.md
214
+ * 入口:`dwy claude sync [target]`
215
+ * target=undefined 全量同步:项目 .claude/ + 全局 CLAUDE.md
216
+ * target='md' 仅同步 CLAUDE.md → 全局 ~/.claude/
230
217
  */
231
- export async function syncClaude(opts = {}) {
232
- const useCache = opts.cache !== false
233
- console.log(chalk.blue('\nScanning available Claude configuration...\n'))
234
-
218
+ export async function syncClaude({ target, reselect = false, dryRun = false } = {}) {
235
219
  const sourceDir = await resolveSourceDir()
236
- const projectTargetDir = path.join(process.cwd(), '.claude')
237
- const globalTargetDir = path.join(os.homedir(), '.claude')
238
-
239
220
  if (!await fs.pathExists(sourceDir)) {
240
221
  console.error(chalk.red('Error: claude-global templates not found in repo'))
241
222
  process.exit(1)
242
223
  }
243
224
 
244
- // 如果当前目录没有 .claude,先确认是否在此创建
225
+ const globalTargetDir = path.join(os.homedir(), '.claude')
226
+
227
+ if (target === 'md') {
228
+ if (!dryRun) await fs.ensureDir(globalTargetDir)
229
+ console.log(chalk.blue(`\nSyncing CLAUDE.md to ${globalTargetDir}...\n`))
230
+ const synced = await syncClaudeMd(sourceDir, globalTargetDir, dryRun)
231
+ if (!synced) {
232
+ console.log(chalk.yellow('CLAUDE.md not found in templates, nothing to sync.'))
233
+ } else {
234
+ console.log(chalk.blue(`\n${dryRun ? 'Dry-run complete.' : 'Done.'}`))
235
+ }
236
+ return
237
+ }
238
+
239
+ if (target !== undefined) {
240
+ console.error(chalk.red(`Error: unknown sync target "${target}". Supported: (none) | md`))
241
+ process.exit(2)
242
+ }
243
+
244
+ console.log(chalk.blue('\nScanning available Claude configuration...\n'))
245
+ const projectTargetDir = path.join(process.cwd(), '.claude')
246
+
245
247
  if (!await fs.pathExists(projectTargetDir)) {
246
248
  const { shouldCreate } = await inquirer.prompt([
247
249
  {
@@ -257,7 +259,6 @@ export async function syncClaude(opts = {}) {
257
259
  }
258
260
  }
259
261
 
260
- // 扫描模板资源
261
262
  const skills = await scanSkills(sourceDir)
262
263
  const rules = await scanRules(sourceDir)
263
264
  const commands = await scanCommands(sourceDir)
@@ -265,7 +266,6 @@ export async function syncClaude(opts = {}) {
265
266
 
266
267
  console.log(chalk.yellow(`Found ${skills.length} skills, ${rules.length} rules, ${commands.length} commands, ${hooks.length} hooks\n`))
267
268
 
268
- // 扫描项目已选过的项
269
269
  const existingSkills = await scanExisting(projectTargetDir, 'skills')
270
270
  const existingRules = await scanExisting(projectTargetDir, 'rules')
271
271
  const existingCommands = await scanExisting(projectTargetDir, 'commands')
@@ -274,20 +274,18 @@ export async function syncClaude(opts = {}) {
274
274
 
275
275
  let selectedSkills, selectedRules, selectedCommands, selectedHooks
276
276
 
277
- if (useCache && hasAnyExisting) {
278
- // 缓存模式:模板 ∩ 已选 覆盖同步
277
+ if (!reselect && hasAnyExisting) {
279
278
  selectedSkills = skills.filter(s => existingSkills.has(s.name))
280
279
  selectedRules = rules.filter(r => existingRules.has(r.name))
281
280
  selectedCommands = commands.filter(c => existingCommands.has(c.name))
282
- // hooks 与 settings.json 引用配套:从未装过则装全部模板 hooks,已装过则按已选覆盖
281
+ // hooks 与 settings.json 引用配套:未装过则装全部模板,已装过则按已选覆盖
283
282
  selectedHooks = existingHooks.size > 0
284
283
  ? hooks.filter(h => existingHooks.has(h.name))
285
284
  : hooks
286
- console.log(chalk.gray(`使用已选缓存覆盖同步(如需调整请加 --no-cache):`))
285
+ console.log(chalk.gray(`使用已选覆盖同步(如需调整请加 -i / --reselect):`))
287
286
  console.log(chalk.gray(` ${selectedSkills.length} skills, ${selectedRules.length} rules, ${selectedCommands.length} commands, ${selectedHooks.length} hooks\n`))
288
287
  } else {
289
- // 交互模式:已选项默认勾选
290
- if (useCache && !hasAnyExisting) {
288
+ if (!reselect && !hasAnyExisting) {
291
289
  console.log(chalk.gray('首次同步,进入交互式选择...\n'))
292
290
  }
293
291
  selectedSkills = await promptSelection(skills, 'Skills', existingSkills)
@@ -296,66 +294,22 @@ export async function syncClaude(opts = {}) {
296
294
  selectedHooks = await promptSelection(hooks, 'Hooks', existingHooks)
297
295
  }
298
296
 
299
- const totalSelected = selectedSkills.length + selectedRules.length + selectedCommands.length + selectedHooks.length
300
- if (totalSelected === 0) {
301
- console.log(chalk.yellow('\nNo skills/rules/commands/hooks selected.'))
302
- }
303
-
304
- console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
297
+ console.log(chalk.blue(`\n${dryRun ? '[dry-run] ' : ''}Syncing to ${projectTargetDir}...\n`))
305
298
 
306
- await fs.ensureDir(projectTargetDir)
299
+ if (!dryRun) await fs.ensureDir(projectTargetDir)
307
300
 
308
301
  let syncedCount = 0
309
- if (totalSelected > 0) {
310
- syncedCount += await syncSelectedItems(selectedSkills, projectTargetDir)
311
- syncedCount += await syncSelectedItems(selectedRules, projectTargetDir)
312
- syncedCount += await syncSelectedItems(selectedCommands, projectTargetDir)
313
- }
314
- syncedCount += await syncSettings(sourceDir, projectTargetDir)
315
-
316
- // Hooks 同步到项目级 .claude/hooks/(settings.json 用 $CLAUDE_PROJECT_DIR 引用)
317
- if (selectedHooks.length > 0) {
318
- const hooksTargetDir = path.join(projectTargetDir, 'hooks')
319
- await fs.ensureDir(hooksTargetDir)
320
- for (const hook of selectedHooks) {
321
- const dest = path.join(hooksTargetDir, hook.name)
322
- await fs.copy(hook.sourcePath, dest, { overwrite: true })
323
- await fs.chmod(dest, 0o755)
324
- syncedCount++
325
- console.log(chalk.green(` ✓ hooks/${hook.name}`))
326
- }
327
- }
302
+ syncedCount += await syncSelectedItems(selectedSkills, projectTargetDir, dryRun)
303
+ syncedCount += await syncSelectedItems(selectedRules, projectTargetDir, dryRun)
304
+ syncedCount += await syncSelectedItems(selectedCommands, projectTargetDir, dryRun)
305
+ syncedCount += await syncSettings(sourceDir, projectTargetDir, dryRun)
306
+ syncedCount += await syncHooks(selectedHooks, projectTargetDir, dryRun)
328
307
 
329
- // CLAUDE.md 同步到全局
330
- await fs.ensureDir(globalTargetDir)
331
- syncedCount += await syncClaudeMd(sourceDir, globalTargetDir)
308
+ if (!dryRun) await fs.ensureDir(globalTargetDir)
309
+ syncedCount += await syncClaudeMd(sourceDir, globalTargetDir, dryRun)
332
310
 
333
- console.log(chalk.blue(`\nDone! Synced ${syncedCount} items.`))
311
+ console.log(chalk.blue(`\n${dryRun ? 'Dry-run complete.' : 'Done!'} ${syncedCount} items.`))
334
312
  console.log(chalk.gray(` Project config → ${projectTargetDir}`))
335
313
  console.log(chalk.gray(` Hooks → ${path.join(projectTargetDir, 'hooks')}`))
336
314
  console.log(chalk.gray(` CLAUDE.md → ${globalTargetDir}`))
337
315
  }
338
-
339
- /**
340
- * 单独同步 CLAUDE.md 到全局 ~/.claude/
341
- */
342
- export async function syncProjectClaudeMd() {
343
- const sourceDir = await resolveSourceDir()
344
- const targetDir = path.join(os.homedir(), '.claude')
345
-
346
- if (!await fs.pathExists(sourceDir)) {
347
- console.error(chalk.red('Error: claude-global templates not found in repo'))
348
- process.exit(1)
349
- }
350
-
351
- await fs.ensureDir(targetDir)
352
- const synced = await syncClaudeMd(sourceDir, targetDir)
353
-
354
- if (synced) {
355
- console.log(chalk.blue(`\nDone! CLAUDE.md synced to ${targetDir}`))
356
- } else {
357
- console.log(chalk.yellow('\nCLAUDE.md not found in templates, nothing to sync.'))
358
- }
359
- }
360
-
361
-