create-dwy 0.6.6 → 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.
- package/bin/index.js +23 -11
- package/package.json +1 -1
- package/src/sync.js +81 -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
|
|
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('
|
|
25
|
-
.description('
|
|
26
|
-
.option('--no-cache', '忽略已选缓存,重新交互式选择(已选项默认勾选)')
|
|
27
|
-
.action((opts) => syncClaude(opts))
|
|
28
|
+
const claudeCmd = program
|
|
29
|
+
.command('claude')
|
|
30
|
+
.description('Claude Code 配置管理')
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
.command('sync
|
|
31
|
-
.description('
|
|
32
|
-
.
|
|
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
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,
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
console.log(chalk.green(' ✓ settings.json — merged'))
|
|
168
|
+
action = 'merged'
|
|
193
169
|
} else {
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* 全局 ~/.claude/
|
|
214
|
+
* 入口:`dwy claude sync [target]`
|
|
215
|
+
* target=undefined 全量同步:项目 .claude/ + 全局 CLAUDE.md
|
|
216
|
+
* target='md' 仅同步 CLAUDE.md → 全局 ~/.claude/
|
|
230
217
|
*/
|
|
231
|
-
export async function syncClaude(
|
|
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
|
-
|
|
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,17 +274,18 @@ export async function syncClaude(opts = {}) {
|
|
|
274
274
|
|
|
275
275
|
let selectedSkills, selectedRules, selectedCommands, selectedHooks
|
|
276
276
|
|
|
277
|
-
if (
|
|
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
|
-
|
|
283
|
-
|
|
281
|
+
// hooks 与 settings.json 引用配套:未装过则装全部模板,已装过则按已选覆盖
|
|
282
|
+
selectedHooks = existingHooks.size > 0
|
|
283
|
+
? hooks.filter(h => existingHooks.has(h.name))
|
|
284
|
+
: hooks
|
|
285
|
+
console.log(chalk.gray(`使用已选覆盖同步(如需调整请加 -i / --reselect):`))
|
|
284
286
|
console.log(chalk.gray(` ${selectedSkills.length} skills, ${selectedRules.length} rules, ${selectedCommands.length} commands, ${selectedHooks.length} hooks\n`))
|
|
285
287
|
} else {
|
|
286
|
-
|
|
287
|
-
if (useCache && !hasAnyExisting) {
|
|
288
|
+
if (!reselect && !hasAnyExisting) {
|
|
288
289
|
console.log(chalk.gray('首次同步,进入交互式选择...\n'))
|
|
289
290
|
}
|
|
290
291
|
selectedSkills = await promptSelection(skills, 'Skills', existingSkills)
|
|
@@ -293,66 +294,22 @@ export async function syncClaude(opts = {}) {
|
|
|
293
294
|
selectedHooks = await promptSelection(hooks, 'Hooks', existingHooks)
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
|
|
297
|
-
if (totalSelected === 0) {
|
|
298
|
-
console.log(chalk.yellow('\nNo skills/rules/commands/hooks selected.'))
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
|
|
297
|
+
console.log(chalk.blue(`\n${dryRun ? '[dry-run] ' : ''}Syncing to ${projectTargetDir}...\n`))
|
|
302
298
|
|
|
303
|
-
await fs.ensureDir(projectTargetDir)
|
|
299
|
+
if (!dryRun) await fs.ensureDir(projectTargetDir)
|
|
304
300
|
|
|
305
301
|
let syncedCount = 0
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
syncedCount += await syncSettings(sourceDir, projectTargetDir)
|
|
312
|
-
|
|
313
|
-
// Hooks 同步到项目级 .claude/hooks/(settings.json 用 $CLAUDE_PROJECT_DIR 引用)
|
|
314
|
-
if (selectedHooks.length > 0) {
|
|
315
|
-
const hooksTargetDir = path.join(projectTargetDir, 'hooks')
|
|
316
|
-
await fs.ensureDir(hooksTargetDir)
|
|
317
|
-
for (const hook of selectedHooks) {
|
|
318
|
-
const dest = path.join(hooksTargetDir, hook.name)
|
|
319
|
-
await fs.copy(hook.sourcePath, dest, { overwrite: true })
|
|
320
|
-
await fs.chmod(dest, 0o755)
|
|
321
|
-
syncedCount++
|
|
322
|
-
console.log(chalk.green(` ✓ hooks/${hook.name}`))
|
|
323
|
-
}
|
|
324
|
-
}
|
|
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)
|
|
325
307
|
|
|
326
|
-
|
|
327
|
-
await
|
|
328
|
-
syncedCount += await syncClaudeMd(sourceDir, globalTargetDir)
|
|
308
|
+
if (!dryRun) await fs.ensureDir(globalTargetDir)
|
|
309
|
+
syncedCount += await syncClaudeMd(sourceDir, globalTargetDir, dryRun)
|
|
329
310
|
|
|
330
|
-
console.log(chalk.blue(`\
|
|
311
|
+
console.log(chalk.blue(`\n${dryRun ? 'Dry-run complete.' : 'Done!'} ${syncedCount} items.`))
|
|
331
312
|
console.log(chalk.gray(` Project config → ${projectTargetDir}`))
|
|
332
313
|
console.log(chalk.gray(` Hooks → ${path.join(projectTargetDir, 'hooks')}`))
|
|
333
314
|
console.log(chalk.gray(` CLAUDE.md → ${globalTargetDir}`))
|
|
334
315
|
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* 单独同步 CLAUDE.md 到全局 ~/.claude/
|
|
338
|
-
*/
|
|
339
|
-
export async function syncProjectClaudeMd() {
|
|
340
|
-
const sourceDir = await resolveSourceDir()
|
|
341
|
-
const targetDir = path.join(os.homedir(), '.claude')
|
|
342
|
-
|
|
343
|
-
if (!await fs.pathExists(sourceDir)) {
|
|
344
|
-
console.error(chalk.red('Error: claude-global templates not found in repo'))
|
|
345
|
-
process.exit(1)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
await fs.ensureDir(targetDir)
|
|
349
|
-
const synced = await syncClaudeMd(sourceDir, targetDir)
|
|
350
|
-
|
|
351
|
-
if (synced) {
|
|
352
|
-
console.log(chalk.blue(`\nDone! CLAUDE.md synced to ${targetDir}`))
|
|
353
|
-
} else {
|
|
354
|
-
console.log(chalk.yellow('\nCLAUDE.md not found in templates, nothing to sync.'))
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|