create-dwy 0.14.4 → 0.14.6

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,21 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.14.6
4
+
5
+ ### Patch Changes
6
+
7
+ - `dwy` 交互同步时,默认勾选优先读取项目目录下 `/.dwy/sync-state.json` 的上次托管选择结果,减少每次重新勾选 `rules`、`skills`、`commands`、`hooks`。
8
+ - 新增 iOS 适配开发规则 `dwy-ios-layout-adaptation.md`,统一原型基准为 `393×852 pt`,补充 `375 / 393 / 430 pt` 验证矩阵与 Safe Area 约束。
9
+ - 补充 iOS 与 Android 应用图标安全边距规则:iOS 图标源稿使用 `1024x1024` PNG,Android 启动图标源稿使用 `mipmap-xxxhdpi` `192x192`,主体默认预留 `10%` 安全边距。
10
+
11
+ ## 0.14.5
12
+
13
+ ### Patch Changes
14
+
15
+ - `dwy` 同步模板改回运行时从仓库缓存拉取,默认更新 `~/.dwy/cache/dwy` 后再读取模板,不再依赖 CLI 包内静态模板。
16
+ - 新增项目级同步状态文件 `/.dwy/sync-state.json`,记录各平台上次由 `dwy` 托管的 skills / rules / commands / hooks。
17
+ - 当模板仓库移除已同步项时,再次执行 `dwy` 会罗列缺失项并让用户选择是否删除;未确认删除的旧项会继续保留。
18
+
3
19
  ## 0.14.4
4
20
 
5
21
  ### Patch Changes
@@ -7,6 +23,7 @@
7
23
  - 将 iOS 规则 `dwy-swift-style.md` 重命名为 `dwy-swift-core.md`,并重构为精简版规范。
8
24
  - 新增规则级配置收口约束,要求可配置项统一在 `AppConfig` 管理。
9
25
  - 加强国际化规则:默认要求中文/英文支持,统一 `localizationBundle + loc() + LocalizationManager + AppLanguage` 实施要点。
26
+ - 模板同步改为仅使用 CLI 内置 `dwy-cli/templates/ai-tools`,不再读取外部缓存目录或按 `origin` 拉取远端模板仓库。
10
27
 
11
28
  ## 0.14.3
12
29
 
@@ -35,9 +52,9 @@
35
52
  ### Minor Changes
36
53
 
37
54
  - CLI 重构为单一交互式入口,移除旧的命令式 `create` / `sync` / `claude` / `codex` 流程。
38
- - 模板源统一收敛到 `/Users/chances/WebstormProjects/dwy-shared/claude-cli/templates/ai-tools`,随 CLI 包一起发布,不再运行时拉取远端模板仓库。
55
+ - 模板源统一收敛到 `/Users/chances/WebstormProjects/dwy-shared/dwy-cli/templates/ai-tools`,随 CLI 包一起发布,不再运行时拉取远端模板仓库。
39
56
  - 新增多平台适配层:支持 `Claude Code`、`Codex`、`Cursor`、`OpenCode` 的 rules / skills / commands / hooks 同步。
40
- - 基准规范文件 `/Users/chances/WebstormProjects/dwy-shared/claude-cli/templates/ai-tools/CLAUDE.md` 现在会按平台分别同步到 `.claude/CLAUDE.md`、根 `AGENTS.md`、`.cursor/rules/00-dwy-global.mdc`。
57
+ - 基准规范文件 `/Users/chances/WebstormProjects/dwy-shared/dwy-cli/templates/ai-tools/CLAUDE.md` 现在会按平台分别同步到 `.claude/CLAUDE.md`、根 `AGENTS.md`、`.cursor/rules/00-dwy-global.mdc`。
41
58
  - 平台未选中时会清理 dwy 托管内容,同时保留用户自定义文件、用户自定义 hook 配置和 `AGENTS.md` 非托管内容。
42
59
 
43
60
  ## 0.13.8
package/bin/index.js CHANGED
@@ -7,6 +7,32 @@ import { syncAll } from '../src/sync-all.js'
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url))
9
9
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
10
+ const argv = process.argv.slice(2)
10
11
 
11
- console.log(`dwy ${pkg.version}`)
12
- await syncAll()
12
+ function printHelp() {
13
+ const lines = [
14
+ `dwy ${pkg.version}`,
15
+ '',
16
+ 'Usage:',
17
+ ' dwy 运行交互式同步',
18
+ ' dwy sync 运行交互式同步',
19
+ ' dwy --help 显示本说明',
20
+ ' dwy -h 显示本说明',
21
+ ' dwy --version 显示当前版本',
22
+ ' dwy -V 显示当前版本',
23
+ '',
24
+ ]
25
+ console.log(lines.join('\n'))
26
+ }
27
+
28
+ if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
29
+ printHelp()
30
+ } else if (argv.includes('--version') || argv.includes('-V')) {
31
+ console.log(pkg.version)
32
+ } else if (argv.length === 0 || argv[0] === 'sync') {
33
+ await syncAll()
34
+ } else {
35
+ console.error(`未知命令: ${argv.join(' ')}`)
36
+ printHelp()
37
+ process.exitCode = 1
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync-all.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  scanExistingCodexSkills,
23
23
  syncCodex,
24
24
  } from './sync-codex.js'
25
- import { syncCursor } from './sync-cursor.js'
25
+ import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
26
26
  import { syncOpenCode } from './sync-opencode.js'
27
27
 
28
28
  const PLATFORM_OPTIONS = [
@@ -33,11 +33,53 @@ const PLATFORM_OPTIONS = [
33
33
  ]
34
34
 
35
35
  const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
36
+ const SYNC_STATE_PATH = ['.dwy', 'sync-state.json']
37
+ const PLATFORM_SYNC_TYPES = {
38
+ claude: ['skills', 'rules', 'commands', 'hooks'],
39
+ codex: ['skills', 'rules', 'hooks'],
40
+ cursor: ['rules'],
41
+ opencode: ['skills', 'rules', 'commands'],
42
+ }
43
+ const PLATFORM_LABELS = {
44
+ claude: 'Claude Code',
45
+ codex: 'Codex',
46
+ cursor: 'Cursor',
47
+ opencode: 'OpenCode',
48
+ }
49
+ const TYPE_LABELS = {
50
+ skills: 'skills',
51
+ rules: 'rules',
52
+ commands: 'commands',
53
+ hooks: 'hooks',
54
+ }
36
55
 
37
56
  function unionSets(...sets) {
38
57
  return new Set(sets.flatMap(set => [...set]))
39
58
  }
40
59
 
60
+ function hasSetItems(setsByType) {
61
+ return setsByType.skills.size + setsByType.rules.size + setsByType.commands.size + setsByType.hooks.size > 0
62
+ }
63
+
64
+ export function buildSelectionDefaultsFromSyncState(existing, syncState) {
65
+ const stateDefaults = {
66
+ skills: new Set(),
67
+ rules: new Set(),
68
+ commands: new Set(),
69
+ hooks: new Set(),
70
+ }
71
+
72
+ for (const platformState of Object.values(syncState?.platforms || {})) {
73
+ for (const type of ['skills', 'rules', 'commands', 'hooks']) {
74
+ for (const name of platformState?.[type] || []) {
75
+ stateDefaults[type].add(name)
76
+ }
77
+ }
78
+ }
79
+
80
+ return hasSetItems(stateDefaults) ? stateDefaults : existing
81
+ }
82
+
41
83
  async function scanCombinedExisting(projectDir) {
42
84
  const claudeDir = path.join(projectDir, '.claude')
43
85
  const agentsMdPath = path.join(projectDir, 'AGENTS.md')
@@ -61,7 +103,7 @@ async function scanCombinedExisting(projectDir) {
61
103
  }
62
104
 
63
105
  async function shouldInitialize(projectDir, existing) {
64
- const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
106
+ const hasAnyExisting = hasSetItems(existing)
65
107
  if (hasAnyExisting) return true
66
108
 
67
109
  const hasConfigDir = await fs.pathExists(path.join(projectDir, '.claude'))
@@ -111,6 +153,172 @@ function buildCursorBaselineContent(baseline) {
111
153
  ].join('\n')
112
154
  }
113
155
 
156
+ function createEmptyPlatformState() {
157
+ return {
158
+ skills: [],
159
+ rules: [],
160
+ commands: [],
161
+ hooks: [],
162
+ }
163
+ }
164
+
165
+ async function loadSyncState(projectDir) {
166
+ const statePath = path.join(projectDir, ...SYNC_STATE_PATH)
167
+ if (!await fs.pathExists(statePath)) {
168
+ return {
169
+ version: 1,
170
+ platforms: {},
171
+ }
172
+ }
173
+
174
+ const state = await fs.readJson(statePath)
175
+ return {
176
+ version: 1,
177
+ platforms: state.platforms || {},
178
+ }
179
+ }
180
+
181
+ async function saveSyncState(projectDir, state) {
182
+ const statePath = path.join(projectDir, ...SYNC_STATE_PATH)
183
+ await fs.ensureDir(path.dirname(statePath))
184
+ await fs.writeJson(statePath, state, { spaces: 2 })
185
+ }
186
+
187
+ function getCurrentTemplateNames(scans) {
188
+ return {
189
+ skills: new Set(scans.skills.map(item => item.name)),
190
+ rules: new Set(scans.rules.map(item => item.name)),
191
+ commands: new Set(scans.commands.map(item => item.name)),
192
+ hooks: new Set(scans.hooks.map(item => item.name)),
193
+ }
194
+ }
195
+
196
+ async function collectCurrentManagedNames(projectDir) {
197
+ const claudeDir = path.join(projectDir, '.claude')
198
+ const openCodeDir = path.join(projectDir, '.opencode')
199
+ const agentsMdPath = path.join(projectDir, 'AGENTS.md')
200
+ const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
201
+
202
+ return {
203
+ claude: {
204
+ skills: await scanExisting(claudeDir, 'skills'),
205
+ rules: await scanExisting(claudeDir, 'rules'),
206
+ commands: await scanExisting(claudeDir, 'commands'),
207
+ hooks: await scanExisting(claudeDir, 'hooks'),
208
+ },
209
+ codex: {
210
+ skills: await scanExistingCodexSkills(projectDir),
211
+ rules: parseManagedRuleNames(agentsMdContent),
212
+ hooks: await scanExistingCodexHooks(projectDir),
213
+ },
214
+ cursor: {
215
+ rules: await scanExistingCursorRules(projectDir),
216
+ },
217
+ opencode: {
218
+ skills: await scanExisting(openCodeDir, 'skills'),
219
+ rules: parseManagedRuleNames(agentsMdContent),
220
+ commands: await scanExisting(openCodeDir, 'commands'),
221
+ },
222
+ }
223
+ }
224
+
225
+ function normalizeRemovalInput(removals = {}) {
226
+ const normalized = {}
227
+ for (const [platform, types] of Object.entries(removals)) {
228
+ normalized[platform] = {}
229
+ for (const [type, names] of Object.entries(types || {})) {
230
+ normalized[platform][type] = new Set(Array.isArray(names) ? names : [...names])
231
+ }
232
+ }
233
+ return normalized
234
+ }
235
+
236
+ function createEmptyRemovalState() {
237
+ return Object.fromEntries(
238
+ Object.keys(PLATFORM_SYNC_TYPES).map(platform => [platform, {}]),
239
+ )
240
+ }
241
+
242
+ async function collectStaleEntries(projectDir, scans, syncState, selectedPlatforms) {
243
+ const currentTemplateNames = getCurrentTemplateNames(scans)
244
+ const currentManaged = await collectCurrentManagedNames(projectDir)
245
+ const staleEntries = createEmptyRemovalState()
246
+
247
+ for (const platform of selectedPlatforms) {
248
+ const previous = syncState.platforms?.[platform] || {}
249
+ const platformManaged = currentManaged[platform] || {}
250
+
251
+ for (const type of PLATFORM_SYNC_TYPES[platform] || []) {
252
+ const previousNames = previous[type] || []
253
+ const currentNames = currentTemplateNames[type] || new Set()
254
+ const managedNames = platformManaged[type] || new Set()
255
+ const staleNames = []
256
+
257
+ for (const name of previousNames) {
258
+ if (currentNames.has(name)) continue
259
+ if (type === 'rules' && platform === 'cursor') {
260
+ if (!managedNames.has(buildCursorRuleName(name))) continue
261
+ } else if (!managedNames.has(name)) {
262
+ continue
263
+ }
264
+ staleNames.push(name)
265
+ }
266
+
267
+ if (staleNames.length > 0) staleEntries[platform][type] = staleNames.sort((a, b) => a.localeCompare(b, 'zh'))
268
+ }
269
+ }
270
+
271
+ return staleEntries
272
+ }
273
+
274
+ async function promptStaleRemovals(staleEntries) {
275
+ const approvals = createEmptyRemovalState()
276
+
277
+ for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
278
+ for (const type of PLATFORM_SYNC_TYPES[platform]) {
279
+ const names = staleEntries[platform]?.[type] || []
280
+ if (names.length === 0) continue
281
+
282
+ console.log(chalk.yellow(`\n检测到 ${PLATFORM_LABELS[platform]} 已同步但模板仓库已移除的 ${TYPE_LABELS[type]}:`))
283
+ for (const name of names) {
284
+ console.log(chalk.gray(` - ${name}`))
285
+ }
286
+
287
+ const selected = await multiselect({
288
+ message: `选择要删除的 ${PLATFORM_LABELS[platform]} ${TYPE_LABELS[type]}`,
289
+ options: names.map(name => ({ value: name, label: name })),
290
+ initialValues: names,
291
+ required: false,
292
+ })
293
+ if (isCancel(selected)) return null
294
+ approvals[platform][type] = new Set(selected)
295
+ }
296
+ }
297
+
298
+ return approvals
299
+ }
300
+
301
+ function getPreservedStaleNames(staleEntries, approvedRemovals, platform, type) {
302
+ const staleNames = staleEntries[platform]?.[type] || []
303
+ const approvedNames = approvedRemovals[platform]?.[type] || new Set()
304
+ return new Set(staleNames.filter(name => !approvedNames.has(name)))
305
+ }
306
+
307
+ function buildNextPlatformState(selected, staleEntries, approvedRemovals, platform) {
308
+ const state = createEmptyPlatformState()
309
+
310
+ for (const type of PLATFORM_SYNC_TYPES[platform] || []) {
311
+ const selectedNames = new Set((selected[type] || []).map(item => item.name))
312
+ for (const name of staleEntries[platform]?.[type] || []) {
313
+ const approvedNames = approvedRemovals[platform]?.[type] || new Set()
314
+ if (!approvedNames.has(name)) selectedNames.add(name)
315
+ }
316
+ state[type] = [...selectedNames].sort((a, b) => a.localeCompare(b, 'zh'))
317
+ }
318
+
319
+ return state
320
+ }
321
+
114
322
  async function removeManagedItems(targetDir, existingNames, managedNames, labelPrefix) {
115
323
  let removedCount = 0
116
324
 
@@ -267,7 +475,6 @@ async function cleanupCursorPlatform(projectDir, scans, baseline) {
267
475
  )
268
476
 
269
477
  let removedCount = 0
270
- const managedRuleNames = new Set(scans.rules.map(rule => buildCursorRuleName(rule.name)))
271
478
  for (const name of existingManagedNames) {
272
479
  if (name === CURSOR_BASELINE_RULE_NAME) continue
273
480
  await fs.remove(path.join(rulesDir, name))
@@ -316,6 +523,9 @@ async function cleanupOpenCodePlatform(projectDir, scans) {
316
523
  export async function syncAll({
317
524
  sourceDir: sourceDirOverride,
318
525
  projectDir: projectDirOverride,
526
+ selected: selectedOverride,
527
+ selectedPlatforms: selectedPlatformsOverride,
528
+ staleRemovals: staleRemovalsOverride,
319
529
  } = {}) {
320
530
  const sourceDir = sourceDirOverride || await resolveSourceDir()
321
531
  if (!await fs.pathExists(sourceDir)) {
@@ -346,28 +556,40 @@ export async function syncAll({
346
556
  console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
347
557
 
348
558
  const existing = await scanCombinedExisting(projectDir)
349
- if (!await shouldInitialize(projectDir, existing)) {
559
+ const syncState = await loadSyncState(projectDir)
560
+ const selectionDefaults = buildSelectionDefaultsFromSyncState(existing, syncState)
561
+ const skipInitPrompt = !!(selectedOverride || selectedPlatformsOverride)
562
+ if (!skipInitPrompt && !await shouldInitialize(projectDir, existing)) {
350
563
  console.log(chalk.yellow('\n已取消同步。'))
351
564
  return
352
565
  }
353
566
 
354
- const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
355
- if (!hasAnyExisting) {
567
+ const hasAnyExisting = hasSetItems(existing)
568
+ if (!hasAnyExisting && !selectedOverride) {
356
569
  console.log(chalk.gray('首次同步,进入交互式选择...\n'))
357
570
  }
358
571
 
359
- const selected = await interactiveSelect(scans, existing)
572
+ const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults)
360
573
  if (selected === null) {
361
574
  console.log(chalk.yellow('\n已取消同步。'))
362
575
  return
363
576
  }
364
577
 
365
- const selectedPlatforms = await selectPlatforms()
578
+ const selectedPlatforms = selectedPlatformsOverride || await selectPlatforms()
366
579
  if (selectedPlatforms === null) {
367
580
  console.log(chalk.yellow('\n已取消同步。'))
368
581
  return
369
582
  }
370
583
 
584
+ const staleEntries = await collectStaleEntries(projectDir, scans, syncState, selectedPlatforms)
585
+ const approvedStaleRemovals = staleRemovalsOverride
586
+ ? normalizeRemovalInput(staleRemovalsOverride)
587
+ : await promptStaleRemovals(staleEntries)
588
+ if (approvedStaleRemovals === null) {
589
+ console.log(chalk.yellow('\n已取消同步。'))
590
+ return
591
+ }
592
+
371
593
  let syncedAnySupportedPlatform = false
372
594
 
373
595
  if (selectedPlatforms.includes('claude')) {
@@ -377,6 +599,7 @@ export async function syncAll({
377
599
  sourceDir,
378
600
  projectDir,
379
601
  selected,
602
+ staleRemovals: approvedStaleRemovals.claude,
380
603
  })
381
604
  }
382
605
 
@@ -387,6 +610,8 @@ export async function syncAll({
387
610
  sourceDir,
388
611
  projectDir,
389
612
  selected,
613
+ staleRemovals: approvedStaleRemovals.codex,
614
+ preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'codex', 'rules'),
390
615
  })
391
616
  }
392
617
 
@@ -396,6 +621,7 @@ export async function syncAll({
396
621
  sourceDir,
397
622
  projectDir,
398
623
  selected,
624
+ staleRemovals: approvedStaleRemovals.cursor,
399
625
  })
400
626
  }
401
627
 
@@ -405,6 +631,8 @@ export async function syncAll({
405
631
  sourceDir,
406
632
  projectDir,
407
633
  selected,
634
+ staleRemovals: approvedStaleRemovals.opencode,
635
+ preserveMissingRules: getPreservedStaleNames(staleEntries, approvedStaleRemovals, 'opencode', 'rules'),
408
636
  })
409
637
  }
410
638
 
@@ -428,6 +656,15 @@ export async function syncAll({
428
656
  await cleanupOpenCodePlatform(projectDir, scans)
429
657
  }
430
658
 
659
+ for (const platform of Object.keys(PLATFORM_SYNC_TYPES)) {
660
+ if (selectedPlatforms.includes(platform)) {
661
+ syncState.platforms[platform] = buildNextPlatformState(selected, staleEntries, approvedStaleRemovals, platform)
662
+ } else {
663
+ syncState.platforms[platform] = createEmptyPlatformState()
664
+ }
665
+ }
666
+ await saveSyncState(projectDir, syncState)
667
+
431
668
  if (!syncedAnySupportedPlatform) {
432
669
  console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))
433
670
  }
package/src/sync-codex.js CHANGED
@@ -60,6 +60,32 @@ export function parseManagedRuleNames(content) {
60
60
  return names
61
61
  }
62
62
 
63
+ export function parseManagedRuleSections(content) {
64
+ const block = content.match(BLOCK_RE)
65
+ if (!block) return new Map()
66
+
67
+ const sections = new Map()
68
+ const blockContent = block[0]
69
+ const markerRe = /<!-- dwy-rule:\s*(.+?)\s*-->/g
70
+ const markers = []
71
+ let match
72
+ while ((match = markerRe.exec(blockContent))) {
73
+ markers.push({
74
+ name: match[1],
75
+ index: match.index,
76
+ })
77
+ }
78
+
79
+ const blockEndIndex = blockContent.lastIndexOf(BLOCK_END)
80
+ for (let i = 0; i < markers.length; i++) {
81
+ const start = markers[i].index
82
+ const end = i + 1 < markers.length ? markers[i + 1].index : blockEndIndex
83
+ sections.set(markers[i].name, blockContent.slice(start, end).trim())
84
+ }
85
+
86
+ return sections
87
+ }
88
+
63
89
  function upsertManagedBlock(content, blockBody) {
64
90
  if (!blockBody.trim()) {
65
91
  if (!BLOCK_RE.test(content)) return content
@@ -86,12 +112,21 @@ function upsertBaselineBlock(content, baselineBody) {
86
112
  return base ? `${block}\n\n${base}` : `${block}\n`
87
113
  }
88
114
 
89
- export async function syncProjectAgentsMd(sourceDir, projectDir, selectedRules) {
115
+ export async function syncProjectAgentsMd(sourceDir, projectDir, selectedRules, {
116
+ preservedRuleNames = new Set(),
117
+ } = {}) {
90
118
  const dest = path.join(projectDir, 'AGENTS.md')
91
119
  const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
92
120
  const baselineBody = await readBaselineDoc(sourceDir)
121
+ const selectedRuleNames = new Set(selectedRules.map(rule => rule.name))
122
+ const existingSections = parseManagedRuleSections(current)
93
123
  const sections = []
94
124
  for (const rule of selectedRules) sections.push(await ruleToSection(rule))
125
+ for (const ruleName of preservedRuleNames) {
126
+ if (selectedRuleNames.has(ruleName)) continue
127
+ const existingSection = existingSections.get(ruleName)
128
+ if (existingSection) sections.push(existingSection)
129
+ }
95
130
  const blockBody = sections.join('\n\n---\n\n')
96
131
 
97
132
  const withBaseline = upsertBaselineBlock(current, baselineBody)
@@ -172,8 +207,16 @@ async function copyCodexHooks(hooks, proj) {
172
207
  const dir = path.join(proj, '.codex', 'hooks')
173
208
  await fs.ensureDir(dir)
174
209
  for (const h of hooks) {
210
+ if (!await fs.pathExists(h.sourcePath)) {
211
+ logAction(`.codex/hooks/${h.name}`, 'yellow', '!')
212
+ continue
213
+ }
175
214
  const dest = path.join(dir, h.name)
176
215
  await fs.copy(h.sourcePath, dest, { overwrite: true, filter: copyFilter })
216
+ if (!await fs.pathExists(dest)) {
217
+ logAction(`.codex/hooks/${h.name}`, 'yellow', '!')
218
+ continue
219
+ }
177
220
  await fs.chmod(dest, 0o755)
178
221
  logAction(`.codex/hooks/${h.name}`)
179
222
  }
@@ -278,6 +321,8 @@ export async function syncCodex({
278
321
  sourceDir: sourceDirOverride,
279
322
  projectDir,
280
323
  selected,
324
+ staleRemovals = {},
325
+ preserveMissingRules = new Set(),
281
326
  } = {}) {
282
327
  const sourceDir = sourceDirOverride || await resolveSourceDir()
283
328
  if (!await fs.pathExists(sourceDir)) {
@@ -316,12 +361,20 @@ export async function syncCodex({
316
361
  let removedCount = 0
317
362
 
318
363
  syncedCount += await copySkillsFlat(syncedSelection.skills, proj)
319
- syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules)
364
+ syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules, {
365
+ preservedRuleNames: preserveMissingRules,
366
+ })
320
367
  syncedCount += await copyCodexHooks(syncedSelection.hooks, proj)
321
368
  syncedCount += await syncCodexHooksJson(sourceDir, proj, selectedHookNames)
322
369
 
323
- const skillTemplateNames = new Set(scans.skills.map(s => s.name))
324
- const hookTemplateNames = new Set(scans.hooks.map(h => h.name))
370
+ const skillTemplateNames = new Set([
371
+ ...scans.skills.map(s => s.name),
372
+ ...(staleRemovals.skills || new Set()),
373
+ ])
374
+ const hookTemplateNames = new Set([
375
+ ...scans.hooks.map(h => h.name),
376
+ ...(staleRemovals.hooks || new Set()),
377
+ ])
325
378
  removedCount += await removeUnselectedFlat(['.agents', 'skills'], existing.skills, new Set(syncedSelection.skills.map(s => s.name)), skillTemplateNames, proj)
326
379
  removedCount += await removeUnselectedFlat(['.codex', 'hooks'], existing.hooks, selectedHookNames, hookTemplateNames, proj)
327
380
 
@@ -101,12 +101,13 @@ async function syncCursorBaselineRule(sourceDir, projectDir) {
101
101
  return 1
102
102
  }
103
103
 
104
- async function removeUnselectedCursorRules(existingNames, selectedNames, projectDir) {
104
+ async function removeUnselectedCursorRules(existingNames, selectedNames, managedNames, projectDir) {
105
105
  const rulesDir = path.join(projectDir, '.cursor', 'rules')
106
106
  let removedCount = 0
107
107
 
108
108
  for (const name of existingNames) {
109
109
  if (selectedNames.has(name)) continue
110
+ if (!managedNames.has(name)) continue
110
111
  await fs.remove(path.join(rulesDir, name))
111
112
  logAction(`.cursor/rules/${name}`, 'red', '×')
112
113
  removedCount++
@@ -115,7 +116,7 @@ async function removeUnselectedCursorRules(existingNames, selectedNames, project
115
116
  return removedCount
116
117
  }
117
118
 
118
- async function scanExistingCursorRules(projectDir) {
119
+ export async function scanExistingCursorRules(projectDir) {
119
120
  const rulesDir = path.join(projectDir, '.cursor', 'rules')
120
121
  if (!await fs.pathExists(rulesDir)) return new Set()
121
122
  const entries = await fs.readdir(rulesDir, { withFileTypes: true })
@@ -131,6 +132,7 @@ export async function syncCursor({
131
132
  sourceDir: sourceDirOverride,
132
133
  projectDir,
133
134
  selected,
135
+ staleRemovals = {},
134
136
  } = {}) {
135
137
  const sourceDir = sourceDirOverride || await resolveSourceDir()
136
138
  if (!await fs.pathExists(sourceDir)) {
@@ -148,6 +150,11 @@ export async function syncCursor({
148
150
  const syncedSelection = scans.rules.filter(rule => selectedRuleNames.has(rule.name))
149
151
  const existingRules = await scanExistingCursorRules(proj)
150
152
  const nextRuleNames = new Set(syncedSelection.map(rule => toCursorRuleName(rule.name)))
153
+ const managedRuleNames = new Set([
154
+ ...scans.rules.map(rule => toCursorRuleName(rule.name)),
155
+ ...[...(staleRemovals.rules || new Set())].map(ruleName => toCursorRuleName(ruleName)),
156
+ BASELINE_RULE_NAME,
157
+ ])
151
158
 
152
159
  let syncedCount = 0
153
160
  let removedCount = 0
@@ -155,7 +162,7 @@ export async function syncCursor({
155
162
  syncedCount += await syncCursorBaselineRule(sourceDir, proj)
156
163
  syncedCount += await syncCursorRules(syncedSelection, proj)
157
164
  nextRuleNames.add(BASELINE_RULE_NAME)
158
- removedCount += await removeUnselectedCursorRules(existingRules, nextRuleNames, proj)
165
+ removedCount += await removeUnselectedCursorRules(existingRules, nextRuleNames, managedRuleNames, proj)
159
166
 
160
167
  console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
161
168
  console.log(chalk.gray(` Rules → ${path.join(proj, '.cursor', 'rules')}`))
@@ -46,6 +46,8 @@ export async function syncOpenCode({
46
46
  sourceDir: sourceDirOverride,
47
47
  projectDir,
48
48
  selected,
49
+ staleRemovals = {},
50
+ preserveMissingRules = new Set(),
49
51
  } = {}) {
50
52
  const sourceDir = sourceDirOverride || await resolveSourceDir()
51
53
  if (!await fs.pathExists(sourceDir)) {
@@ -80,7 +82,9 @@ export async function syncOpenCode({
80
82
  let removedCount = 0
81
83
 
82
84
  const agentsPath = path.join(proj, 'AGENTS.md')
83
- syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules)
85
+ syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules, {
86
+ preservedRuleNames: preserveMissingRules,
87
+ })
84
88
 
85
89
  syncedCount += await copyItemsTo(openCodeDir, 'skills', syncedSelection.skills)
86
90
  syncedCount += await copyItemsTo(openCodeDir, 'commands', syncedSelection.commands)
@@ -89,14 +93,20 @@ export async function syncOpenCode({
89
93
  'skills',
90
94
  existing.skills,
91
95
  new Set(syncedSelection.skills.map(item => item.name)),
92
- new Set(scans.skills.map(item => item.name)),
96
+ new Set([
97
+ ...scans.skills.map(item => item.name),
98
+ ...(staleRemovals.skills || new Set()),
99
+ ]),
93
100
  proj,
94
101
  )
95
102
  removedCount += await removeUnselected(
96
103
  'commands',
97
104
  existing.commands,
98
105
  new Set(syncedSelection.commands.map(item => item.name)),
99
- new Set(scans.commands.map(item => item.name)),
106
+ new Set([
107
+ ...scans.commands.map(item => item.name),
108
+ ...(staleRemovals.commands || new Set()),
109
+ ]),
100
110
  proj,
101
111
  )
102
112
 
package/src/sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { groupMultiselect, isCancel, select } from '@clack/prompts'
4
- import { PACKAGE_ROOT, chalk } from './utils.js'
4
+ import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } from './utils.js'
5
5
 
6
6
  const CATEGORIES = [
7
7
  { key: 'skills', label: 'Skills' },
@@ -21,6 +21,12 @@ const COPY_EXCLUDE_PATTERNS = [
21
21
  /\.pyc$/,
22
22
  ]
23
23
 
24
+ const TEMPLATE_SOURCE_CANDIDATES = [
25
+ ['dwy-cli', 'templates', 'ai-tools'],
26
+ ['claude-cli', 'templates', 'ai-tools'],
27
+ ['templates', 'ai-tools'],
28
+ ]
29
+
24
30
  export function copyFilter(src) {
25
31
  return !COPY_EXCLUDE_PATTERNS.some(re => re.test(src))
26
32
  }
@@ -307,6 +313,55 @@ export function logAction(label, color = 'green', prefix = '✓') {
307
313
  console.log(chalk[color](` ${prefix} ${label}`))
308
314
  }
309
315
 
316
+ function getTemplateRepoCacheDir() {
317
+ return path.join(getDwyHomeDir(), 'cache', 'dwy')
318
+ }
319
+
320
+ async function resolveTemplateDirFromRepo(repoDir) {
321
+ for (const segments of TEMPLATE_SOURCE_CANDIDATES) {
322
+ const candidate = path.join(repoDir, ...segments)
323
+ if (await fs.pathExists(candidate)) return candidate
324
+ }
325
+
326
+ throw new Error(`ai-tools templates not found in repo: ${repoDir}`)
327
+ }
328
+
329
+ async function readGitOriginUrl(repoDir) {
330
+ try {
331
+ return await runGit(['-C', repoDir, 'remote', 'get-url', 'origin'])
332
+ } catch {
333
+ return ''
334
+ }
335
+ }
336
+
337
+ async function ensureTemplateRepoCache() {
338
+ const repoDir = getTemplateRepoCacheDir()
339
+ const repoRoot = path.dirname(repoDir)
340
+ await fs.ensureDir(repoRoot)
341
+
342
+ const hasGitDir = await fs.pathExists(path.join(repoDir, '.git'))
343
+ if (hasGitDir) {
344
+ const originUrl = await readGitOriginUrl(repoDir)
345
+ if (!originUrl || originUrl !== DEFAULT_TEMPLATE_REPO_URL) {
346
+ console.log(chalk.gray('缓存模板仓库 origin 已变更,重建缓存...'))
347
+ await fs.remove(repoDir)
348
+ }
349
+ } else if (await fs.pathExists(repoDir)) {
350
+ console.log(chalk.gray('缓存目录不是 git 仓库,重建缓存...'))
351
+ await fs.remove(repoDir)
352
+ }
353
+
354
+ if (!await fs.pathExists(path.join(repoDir, '.git'))) {
355
+ console.log(chalk.gray('拉取模板仓库...'))
356
+ await runGit(['clone', DEFAULT_TEMPLATE_REPO_URL, repoDir])
357
+ return repoDir
358
+ }
359
+
360
+ console.log(chalk.gray('更新模板仓库...'))
361
+ await runGit(['-C', repoDir, 'pull', '--ff-only'])
362
+ return repoDir
363
+ }
364
+
310
365
  async function copyItems(items, targetDir) {
311
366
  for (const item of items) {
312
367
  const dest = path.join(targetDir, item.type + 's', item.name)
@@ -322,8 +377,16 @@ async function copyHooks(items, targetDir) {
322
377
  const hooksTargetDir = path.join(targetDir, 'hooks')
323
378
  await fs.ensureDir(hooksTargetDir)
324
379
  for (const hook of items) {
380
+ if (!await fs.pathExists(hook.sourcePath)) {
381
+ logAction(`hooks/${hook.name}`, 'yellow', '!')
382
+ continue
383
+ }
325
384
  const dest = path.join(hooksTargetDir, hook.name)
326
385
  await fs.copy(hook.sourcePath, dest, { overwrite: true, filter: copyFilter })
386
+ if (!await fs.pathExists(dest)) {
387
+ logAction(`hooks/${hook.name}`, 'yellow', '!')
388
+ continue
389
+ }
327
390
  await fs.chmod(dest, 0o755)
328
391
  logAction(`hooks/${hook.name}`)
329
392
  }
@@ -454,17 +517,23 @@ async function syncClaudeBaselineDoc(sourceDir, targetDir) {
454
517
  }
455
518
 
456
519
  export async function resolveSourceDir() {
457
- const packagedTemplate = path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
458
- if (!await fs.pathExists(packagedTemplate)) {
459
- throw new Error(`packaged ai-tools templates not found: ${packagedTemplate}`)
520
+ const explicitSourceDir = process.env.DWY_TEMPLATE_SOURCE_DIR
521
+ if (explicitSourceDir) {
522
+ if (!await fs.pathExists(explicitSourceDir)) {
523
+ throw new Error(`DWY_TEMPLATE_SOURCE_DIR not found: ${explicitSourceDir}`)
524
+ }
525
+ return explicitSourceDir
460
526
  }
461
- return packagedTemplate
527
+
528
+ const repoDir = await ensureTemplateRepoCache()
529
+ return resolveTemplateDirFromRepo(repoDir)
462
530
  }
463
531
 
464
532
  export async function syncClaude({
465
533
  sourceDir: sourceDirOverride,
466
534
  projectDir,
467
535
  selected,
536
+ staleRemovals = {},
468
537
  } = {}) {
469
538
  const sourceDir = sourceDirOverride || await resolveSourceDir()
470
539
  if (!await fs.pathExists(sourceDir)) {
@@ -481,6 +550,20 @@ export async function syncClaude({
481
550
  commands: await scanCommands(sourceDir),
482
551
  hooks: await scanHooks(sourceDir),
483
552
  }
553
+ const availableHooks = []
554
+ for (const hook of scans.hooks) {
555
+ if (await fs.pathExists(hook.sourcePath)) {
556
+ availableHooks.push(hook)
557
+ }
558
+ }
559
+ if (availableHooks.length < scans.hooks.length) {
560
+ const missing = scans.hooks
561
+ .filter(h => !availableHooks.includes(h))
562
+ .map(h => h.name)
563
+ .join('、')
564
+ logAction(`hooks skipped: ${missing}`, 'yellow', '!')
565
+ }
566
+ scans.hooks = availableHooks
484
567
  console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
485
568
 
486
569
  const existing = {
@@ -499,6 +582,10 @@ export async function syncClaude({
499
582
  }
500
583
 
501
584
  const selectedHookNames = new Set(syncedSelection.hooks.map(h => h.name))
585
+ const approvedStaleSkills = staleRemovals.skills || new Set()
586
+ const approvedStaleRules = staleRemovals.rules || new Set()
587
+ const approvedStaleCommands = staleRemovals.commands || new Set()
588
+ const approvedStaleHooks = staleRemovals.hooks || new Set()
502
589
 
503
590
  console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
504
591
  await fs.ensureDir(projectTargetDir)
@@ -513,10 +600,10 @@ export async function syncClaude({
513
600
  syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames)
514
601
  syncedCount += await copyHooks(syncedSelection.hooks, projectTargetDir)
515
602
 
516
- const skillTemplateNames = new Set(scans.skills.map(s => s.name))
517
- const ruleTemplateNames = new Set(scans.rules.map(r => r.name))
518
- const commandTemplateNames = new Set(scans.commands.map(c => c.name))
519
- const hookTemplateNames = new Set(scans.hooks.map(h => h.name))
603
+ const skillTemplateNames = new Set([...scans.skills.map(s => s.name), ...approvedStaleSkills])
604
+ const ruleTemplateNames = new Set([...scans.rules.map(r => r.name), ...approvedStaleRules])
605
+ const commandTemplateNames = new Set([...scans.commands.map(c => c.name), ...approvedStaleCommands])
606
+ const hookTemplateNames = new Set([...scans.hooks.map(h => h.name), ...approvedStaleHooks])
520
607
  removedCount += await removeUnselected('skills', existing.skills, new Set(syncedSelection.skills.map(s => s.name)), skillTemplateNames, projectTargetDir)
521
608
  removedCount += await removeUnselected('rules', existing.rules, new Set(syncedSelection.rules.map(r => r.name)), ruleTemplateNames, projectTargetDir)
522
609
  removedCount += await removeUnselected('commands', existing.commands, new Set(syncedSelection.commands.map(c => c.name)), commandTemplateNames, projectTargetDir)
package/src/utils.js CHANGED
@@ -1,12 +1,32 @@
1
1
  import fs from 'fs-extra'
2
+ import os from 'node:os'
2
3
  import path from 'path'
3
4
  import Handlebars from 'handlebars'
4
5
  import chalk from 'chalk'
6
+ import { execFile } from 'node:child_process'
7
+ import { promisify } from 'node:util'
5
8
  import { fileURLToPath } from 'url'
6
9
 
7
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const execFileAsync = promisify(execFile)
8
12
 
9
13
  export const PACKAGE_ROOT = path.resolve(__dirname, '..')
14
+ export const DEFAULT_TEMPLATE_REPO_URL = process.env.DWY_REPO_URL || 'https://github.com/danweiyuancircle/dwy-ai-experience.git'
15
+
16
+ export function getDwyHomeDir() {
17
+ return process.env.DWY_HOME || path.join(os.homedir(), '.dwy')
18
+ }
19
+
20
+ export async function runGit(args, options = {}) {
21
+ try {
22
+ const { stdout } = await execFileAsync('git', args, options)
23
+ return stdout.trim()
24
+ } catch (error) {
25
+ const stderr = error.stderr?.trim()
26
+ const details = stderr ? `: ${stderr}` : ''
27
+ throw new Error(`git ${args.join(' ')} failed${details}`)
28
+ }
29
+ }
10
30
 
11
31
  export async function renderTemplate(templateDir, destDir, context) {
12
32
  const entries = await fs.readdir(templateDir, { withFileTypes: true })
@@ -97,6 +97,8 @@ res/drawable/
97
97
  #### mipmap / 9-patch / 模块前缀
98
98
 
99
99
  - **mipmap 只放启动图标**(`ic_launcher` 系列),其余图片一律放 `drawable` —— mipmap 会跨密度保留资源,普通图放 mipmap 白白增大包体
100
+ - 应用图标设计稿只提供 `mipmap-xxxhdpi` 目录下的 `192x192` 启动图标源图,其余密度版本交给系统或构建流程派生。
101
+ - 图标主体必须预留至少 `10%` 安全边距,避免启动器缩放、遮罩或厂商裁切时贴边缺角。
100
102
  - **9-patch 图保留 `.9` 后缀**:`bg_chat_bubble.9.png`
101
103
  - **模块前缀(可选)**:多模块 / 大型项目推荐资源加业务模块前缀,便于检索、避免跨模块重名 —— `user_activity_login.xml`、id `user_tv_name`;单模块 / 老项目可省略,保持本规范其余示例的无前缀写法
102
104
 
@@ -783,4 +785,4 @@ val result = calculate(x)
783
785
  | 21 | Gradle 脚本(Groovy / kts):锁版本 / `exclude` / 自定义 task / 变体 / 关键魔法值有 why 注释,无复述式、无注释掉的配置块 | ✓ |
784
786
 
785
787
 
786
- **不执行自检就提交代码 = 违规。**
788
+ **不执行自检就提交代码 = 违规。**
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: iOS 适配开发与原型基准规范(393pt 主基准、安全区、自适应布局)
3
+ paths:
4
+ - "**/*.swift"
5
+ ---
6
+
7
+ ## iOS 适配基线
8
+
9
+ - iOS 布局统一使用 `pt`,不要按 `px` 做分辨率换算。
10
+ - iOS 适配依赖 SwiftUI 自适应布局 + Safe Area,不为每个机型单独写一套尺寸。
11
+ - 原型图与开发都以同一套布局意图为准,原型不是按像素缩放给不同机型。
12
+
13
+ ## 原型基准尺寸
14
+
15
+ - 做 iOS 原型图时,统一基于 `393×852 pt` 制作,对齐 `iPhone 15 / iPhone 16` 主流尺寸。
16
+ - `393×852 pt` 是视觉基准,不是要求开发写死页面宽高。
17
+ - 切图资源按系统倍率处理,布局本身仍按 `pt` 思考。
18
+
19
+ ## 适配验证矩阵
20
+
21
+ - 小屏下限按 `375pt` 宽验证,不允许关键内容挤压、截断、横向溢出。
22
+ - 主基准按 `393pt` 宽设计与开发。
23
+ - 大屏上限按 `430pt` 宽验证,要求布局自然放宽,不靠写死间距撑版。
24
+
25
+ ## 布局规则
26
+
27
+ - 页面容器、列表、卡片宽度优先 `maxWidth: .infinity` 或等价自适应写法。
28
+ - 间距优先用 `Spacer`、stack、grid、`safeAreaInset` 等布局系统能力,不用绝对 `offset` 拼版。
29
+ - 禁止用 `UIScreen.main.bounds` 为页面主布局做硬编码换算,优先 `GeometryReader` 或容器相对布局。
30
+ - 具有固定视觉比例的实体控件可以使用固定 `pt` 尺寸,但外层容器必须自适应。
31
+
32
+ ## 安全区
33
+
34
+ - 前景内容默认在安全区内布局,不进入刘海、灵动岛、Home 指示条区域。
35
+ - 全屏背景、遮罩可用 `ignoresSafeArea()`,但不能让主要交互内容越过安全区。
36
+ - 禁止写死状态栏、刘海、底部 Home 条高度,统一依赖系统安全区。
37
+
38
+ ## 字体与可访问性
39
+
40
+ - 字号优先语义化管理,避免页面散落裸值。
41
+ - 关键文案默认兼容 Dynamic Type,系统字体放大后不能破版。
42
+
43
+ ## 开发验证
44
+
45
+ - 至少验证三档宽度:`375pt`、`393pt`、`430pt`。
46
+ - 检查内容是否被刘海、灵动岛、Home 指示条遮挡。
47
+ - 检查滚动页、底部固定栏、弹层在小屏和大屏下都无错位或裁切。
@@ -53,6 +53,8 @@ private let motionSpring = Animation.spring(response: 0.5, dampingFraction: 0.75
53
53
  ## 图标与文案
54
54
 
55
55
  - 图标优先 `Image(systemName:)`,无符号则用成熟 SVG。
56
+ - iOS 应用图标设计稿只提供一张 `1024x1024` 的 PNG 源图,由系统生成各尺寸适配版本。
57
+ - 图标主体必须预留至少 `10%` 安全边距,避免系统缩放、圆角遮罩或视觉放大时被切边。
56
58
  - 文案禁止 emoji,禁止硬编码,统一 `loc("key")`。
57
59
  - 禁止使用 emoji 作为功能说明、状态、提示。
58
60
 
@@ -40,7 +40,7 @@ skill 与 dwy-shared 仓库深度绑定但**不硬编码路径**。每次执行
40
40
  CONFIG="$HOME/.config/dwy-shared/repo.json"
41
41
  if [ -f "$CONFIG" ]; then
42
42
  REPO_PATH=$(jq -r .repo_path "$CONFIG" 2>/dev/null)
43
- if [ -n "$REPO_PATH" ] && [ -d "$REPO_PATH/.git" ] && [ -d "$REPO_PATH/claude-cli/templates/ai-tools" ]; then
43
+ if [ -n "$REPO_PATH" ] && [ -d "$REPO_PATH/.git" ] && [ -d "$REPO_PATH/dwy-cli/templates/ai-tools" ]; then
44
44
  echo "OK: $REPO_PATH"
45
45
  else
46
46
  echo "INVALID"
@@ -69,7 +69,7 @@ fi
69
69
  - `test -d "$PATH"` 路径存在
70
70
  - `test -d "$PATH/.git"` 是 git 仓库
71
71
  - `git -C "$PATH" remote -v | grep -q dwy-shared` remote 指向 dwy-shared
72
- - `test -d "$PATH/claude-cli/templates/ai-tools"` 含特征目录
72
+ - `test -d "$PATH/dwy-cli/templates/ai-tools"` 含特征目录
73
73
  4. 全部通过 → 写缓存:
74
74
 
75
75
  ```bash
@@ -114,7 +114,7 @@ fi
114
114
  ### 步骤 3:读现有分类 + 准备推荐上下文
115
115
 
116
116
  ```bash
117
- TYPE_DIR="$REPO_PATH/claude-cli/templates/ai-tools/<type>"
117
+ TYPE_DIR="$REPO_PATH/dwy-cli/templates/ai-tools/<type>"
118
118
  for cat in "$TYPE_DIR"/*/; do
119
119
  echo "=== $(basename "$cat") ==="
120
120
  # 读分类下第一个样例的 description 行(前 100 字)
@@ -141,7 +141,7 @@ done
141
141
  ### 步骤 6:执行文件操作
142
142
 
143
143
  ```bash
144
- TARGET_DIR="$REPO_PATH/claude-cli/templates/ai-tools/<type>/<category>"
144
+ TARGET_DIR="$REPO_PATH/dwy-cli/templates/ai-tools/<type>/<category>"
145
145
  mkdir -p "$TARGET_DIR"
146
146
 
147
147
  # 源在仓库外:cp
@@ -183,7 +183,7 @@ description: "调用 dwy create CLI 生成全栈 Vue 3 + FastAPI monorepo 骨架
183
183
  | 症状 | 原因 | 解决 |
184
184
  |------|------|------|
185
185
  | `dwy create` 命令找不到 | create-dwy 未安装或不在 PATH | `pnpm i -g create-dwy` 或在 dwy-shared 仓库 `pnpm link --global` |
186
- | `dwy create` 报模板找不到 | Gitee 缓存过期或 dwy-shared 未推送最新版 | 删 `~/.dwy/cache/` 重试,确认 Gitee 上 dwy-shared 含 `fullstack-monorepo` 模板 |
186
+ | `dwy create` 报模板找不到 | 当前版本 CLI 未包含对应模板,或用户本机 install 的版本落后 | 升级 `create-dwy` 到最新版本;确认 `dwy --version` 与线上版本一致后重试 |
187
187
  | `uv sync` 报 dwyeapi 找不到 | aliyun 镜像未同步 | pyproject 已声明 pypi-org 备 index,确保 uv >=0.4 |
188
188
  | `pnpm install` 报 catalog 找不到 | pnpm <10 不支持 catalog | 升级: `npm i -g pnpm@latest` |
189
189
  | backend health check 失败 | DATABASE_URL/SECRET_KEY 用了占位值导致启动报错 | 检查 .env 是否已 cp 自 .env.example;查 uvicorn stderr 输出 |