create-dwy 0.19.1 → 0.21.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 (113) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/cli-help.js +31 -2
  4. package/src/sync-all.js +168 -577
  5. package/src/sync-codex.js +42 -28
  6. package/src/sync-cursor.js +0 -25
  7. package/src/sync-global-skills.js +2 -2
  8. package/src/sync-opencode.js +7 -33
  9. package/src/sync.js +59 -226
  10. package/src/tab-multiselect.js +15 -53
  11. package/src/utils.js +0 -1
  12. package/templates/ai-tools/AGENTS.md +223 -0
  13. package/templates/ai-tools/CHANGELOG.md +195 -0
  14. package/templates/ai-tools/VERSION +1 -1
  15. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +5 -4
  16. package/templates/ai-tools/rules/Android/dwy-android-core.md +147 -13
  17. package/templates/ai-tools/rules/Android/dwy-android-layering.md +142 -0
  18. package/templates/ai-tools/rules/Android/dwy-android-mvp.md +55 -0
  19. package/templates/ai-tools/rules/Android/dwy-android-support-only.md +2 -0
  20. package/templates/ai-tools/rules/Docker/dwy-docker.md +23 -1
  21. package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +2 -0
  22. package/templates/ai-tools/rules/Flutter/dwy-flutter-testing.md +1 -1
  23. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +12 -8
  24. package/templates/ai-tools/rules/Python/dwy-python-backend.md +4 -43
  25. package/templates/ai-tools/rules/Python/dwy-python-layering.md +169 -0
  26. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +11 -49
  27. package/templates/ai-tools/rules/Vue/dwy-vue-layering.md +153 -0
  28. package/templates/ai-tools/rules/iOS/dwy-apple-layering.md +133 -0
  29. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +2 -0
  30. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-craft.md +83 -0
  31. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +27 -66
  32. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +8 -219
  33. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +27 -123
  34. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prod-branch.md +31 -0
  35. package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-postgres.md +17 -10
  36. package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-redis.md +6 -3
  37. package/templates/ai-tools/skills/Docker/dwy-deploy-first/SKILL.md +14 -14
  38. package/templates/ai-tools/skills/Docker/dwy-deploy-first/references/chapter-01-mirrors.md +2 -2
  39. package/templates/ai-tools/skills/Docker/dwy-docker/SKILL.md +21 -13
  40. package/templates/ai-tools/skills/Docker/dwy-docker/references/ask-templates.md +13 -12
  41. package/templates/ai-tools/skills/Docker/dwy-docker/references/templates.md +3 -1
  42. package/templates/ai-tools/skills/Docker/dwy-docker/references/version-rules.md +10 -6
  43. package/templates/ai-tools/skills/Docker/dwy-docker/scripts/query_dockerhub.py +21 -6
  44. package/templates/ai-tools/skills/Docker/dwy-docker/scripts/scan_images.sh +2 -2
  45. package/templates/ai-tools/skills/Docker/dwy-mirror-source/SKILL.md +44 -30
  46. package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/dockerfile-snippets.md +2 -0
  47. package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/mirror-providers.md +8 -8
  48. package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/apply_mirrors.py +14 -1
  49. package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/check_mirrors.py +4 -0
  50. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/SKILL.md +8 -6
  51. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/python-sdk.md +3 -3
  52. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/stream-engine.md +2 -2
  53. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/anti-patterns.md +3 -1
  54. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/change-checklist.md +4 -4
  55. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/lethal-violations.md +5 -5
  56. package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/perf-baselines.md +5 -5
  57. package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +2 -2
  58. package/templates/ai-tools/skills/Python/dwy-eapi/references/tasks-integration-guide.md +4 -4
  59. package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +3 -0
  60. package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +28 -3
  61. package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +2 -2
  62. package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +12 -2
  63. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +1 -1
  64. package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +12 -0
  65. package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +2 -0
  66. package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +10 -0
  67. package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +5 -0
  68. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +11 -14
  69. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +1 -1
  70. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +1 -1
  71. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +1 -1
  72. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +32 -15
  73. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/electron-release-workflow-template.yml +3 -1
  74. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/release-workflow-template.yml +8 -5
  75. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +1 -1
  76. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +1 -1
  77. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/aso.md +2 -0
  78. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +1 -1
  79. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +4 -3
  80. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +1 -1
  81. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +1 -1
  82. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +45 -27
  83. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +1 -1
  84. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +1 -1
  85. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +1 -1
  86. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +1 -1
  87. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +1 -1
  88. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +8 -0
  89. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +7 -7
  90. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +63 -59
  91. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_base.sh +10 -1
  92. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_capacity.sh +21 -23
  93. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_db.sh +9 -1
  94. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +20 -24
  95. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_logs.sh +3 -3
  96. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +4 -2
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +1 -1
  98. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +1 -0
  99. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +7 -5
  100. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +5 -3
  101. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +140 -0
  102. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +113 -0
  103. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +17 -14
  104. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
  105. package/templates/project/fullstack-monorepo/{CLAUDE.md.hbs → AGENTS.md.hbs} +1 -1
  106. package/src/sync-packs.js +0 -223
  107. package/src/sync-scene.js +0 -178
  108. package/templates/ai-tools/CLAUDE.md +0 -210
  109. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
  110. /package/templates/project/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  111. /package/templates/project/fullstack-monorepo/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  112. /package/templates/project/fullstack-monorepo/frontend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
  113. /package/templates/project/web/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
package/src/sync.js CHANGED
@@ -1,18 +1,12 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { isCancel } from '@clack/prompts'
4
- import {
5
- SEARCH_PLACEHOLDER,
6
- searchableMultiselect,
7
- searchableSelect,
8
- } from './searchable-select.js'
9
- import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } from './utils.js'
4
+ import { tabMultiselect } from './tab-multiselect.js'
5
+ import { chalk, PACKAGE_ROOT } from './utils.js'
10
6
 
11
7
  const CATEGORIES = [
12
8
  { key: 'skills', label: 'Skills' },
13
9
  { key: 'rules', label: 'Rules' },
14
- { key: 'commands', label: 'Commands' },
15
- { key: 'hooks', label: 'Hooks' },
16
10
  ]
17
11
 
18
12
  const COPY_EXCLUDE_PATTERNS = [
@@ -26,14 +20,9 @@ const COPY_EXCLUDE_PATTERNS = [
26
20
  /\.pyc$/,
27
21
  ]
28
22
 
29
- const TEMPLATE_SOURCE_CANDIDATES = [
30
- ['dwy-cli', 'templates', 'ai-tools'],
31
- ['claude-cli', 'templates', 'ai-tools'],
32
- ['templates', 'ai-tools'],
33
- ]
34
-
35
- export function copyFilter(src) {
36
- return !COPY_EXCLUDE_PATTERNS.some(re => re.test(src))
23
+ /** 只过滤模板内部缓存,避免 npm 安装路径中的 node_modules 导致整份模板被跳过。 */
24
+ export function copyFilter(sourceRoot) {
25
+ return src => !COPY_EXCLUDE_PATTERNS.some(re => re.test(path.relative(sourceRoot, src)))
37
26
  }
38
27
 
39
28
  export function extractDescription(content) {
@@ -147,27 +136,6 @@ export async function scanRules(sourceDir) {
147
136
  return rules.sort((a, b) => a.name.localeCompare(b.name))
148
137
  }
149
138
 
150
- export async function scanCommands(sourceDir) {
151
- const commandsDir = path.join(sourceDir, 'commands')
152
- if (!await fs.pathExists(commandsDir)) return []
153
-
154
- const entries = await fs.readdir(commandsDir, { withFileTypes: true })
155
- const commands = await Promise.all(entries
156
- .filter(e => e.name !== '.gitkeep' && e.name !== '.DS_Store')
157
- .map(async e => ({
158
- name: e.name,
159
- description: await readEntryDescription(
160
- path.join(commandsDir, e.name),
161
- e.isDirectory() ? '命令目录' : '命令文件',
162
- ),
163
- category: e.isDirectory() ? '目录命令' : '文件命令',
164
- sourcePath: path.join(commandsDir, e.name),
165
- type: 'command',
166
- })))
167
-
168
- return commands.sort((a, b) => a.name.localeCompare(b.name))
169
- }
170
-
171
139
  export async function scanHooks(sourceDir) {
172
140
  const hooksDir = path.join(sourceDir, 'hooks')
173
141
  if (!await fs.pathExists(hooksDir)) return []
@@ -201,7 +169,7 @@ export async function loadHookManifests(sourceDir) {
201
169
  }
202
170
 
203
171
  export async function readBaselineDoc(sourceDir) {
204
- const baselinePath = path.join(sourceDir, 'CLAUDE.md')
172
+ const baselinePath = path.join(sourceDir, 'AGENTS.md')
205
173
  if (!await fs.pathExists(baselinePath)) return ''
206
174
  return fs.readFile(baselinePath, 'utf-8')
207
175
  }
@@ -214,183 +182,69 @@ export async function scanExisting(projectTargetDir, typePlural) {
214
182
  }
215
183
 
216
184
  /**
217
- * 将扫描项转为可搜索扁平 options。
218
- * label 带分类前缀便于筛选;完整 description 进底部公共说明区(不截断塞 hint)。
185
+ * 按模板实际分类生成 Tab,每条独立勾选,不做包展开或跨分类关联。
219
186
  *
220
187
  * @param {Array<{ name: string, category?: string, description?: string }>} items
221
- * @returns {Array<{ value: string, label: string, description?: string }>}
188
+ * @param {'skills' | 'rules'} type
222
189
  */
223
- function buildSearchableOptions(items) {
224
- return items
225
- .slice()
226
- .sort((a, b) => {
227
- const catCmp = (a.category || '其他').localeCompare(b.category || '其他', 'zh')
228
- if (catCmp !== 0) return catCmp
229
- return a.name.localeCompare(b.name, 'zh')
230
- })
231
- .map(item => {
232
- const category = item.category || '其他'
233
- return {
234
- value: item.name,
235
- label: `${category} / ${item.name}`,
236
- // 完整描述给底部公共区;搜索也匹配 description
237
- description: item.description || undefined,
238
- }
190
+ export function buildCategoryTabs(items, type) {
191
+ const groups = new Map()
192
+ for (const item of items) {
193
+ const category = item.category || '其他'
194
+ if (!groups.has(category)) groups.set(category, [])
195
+ groups.get(category).push({
196
+ value: item.name,
197
+ label: item.name,
198
+ description: item.description,
199
+ type,
239
200
  })
201
+ }
202
+ return [...groups.entries()]
203
+ .sort(([a], [b]) => a.localeCompare(b, 'zh'))
204
+ .map(([category, options]) => ({
205
+ id: category,
206
+ label: category,
207
+ options: options.sort((a, b) => a.label.localeCompare(b.label, 'zh')),
208
+ }))
240
209
  }
241
210
 
242
211
  /**
243
- * 可搜索多选一层。items 为空直接返回 [],不弹窗。
244
- * 取消返回 null,调用方必须中止整次同步。
245
- *
246
- * @param {Array<{ name: string, category?: string, description?: string }>} items
247
- * @param {string} label
248
- * @param {Iterable<string>} defaultNames
249
- */
250
- export async function promptSelection(items, label, defaultNames) {
251
- if (items.length === 0) return []
252
- // 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
253
- const selectedNames = await searchableMultiselect({
254
- message: `选择 ${label}`,
255
- options: buildSearchableOptions(items),
256
- initialValues: [...defaultNames],
257
- maxItems: 15,
258
- required: false,
259
- placeholder: SEARCH_PLACEHOLDER,
260
- })
261
- if (isCancel(selectedNames)) return null
262
- const selectedSet = new Set(selectedNames)
263
- return items.filter(item => selectedSet.has(item.name))
264
- }
265
-
266
- /**
267
- * 交互式选择:每步可回退到上一步,最后总览支持任意类别重选。
268
- * categories 可缩成仅 Skills,未出现的类别返回空数组(调用方不得据此删除已有项)。
269
- * 返回 { skills, rules, commands, hooks } 或 null(取消)。
212
+ * 顺序选择 Skills、Rules;Enter 进入下一类,不再提供回退或重选菜单。
213
+ * 完整同步默认启用全部模板 Hooks;仅 Skills 模式不得写入其他配置。
214
+ * 取消返回 null,调用方必须终止同步。
270
215
  *
271
216
  * @param {object} scans
272
- * @param {object} existing
273
- * @param {{ categories?: Array<{ key: string, label: string }> }} [opts]
217
+ * @param {{ skills: Iterable<string>, rules: Iterable<string> }} existing
218
+ * @param {{ skillsOnly?: boolean }} [opts]
274
219
  */
275
- export async function interactiveSelect(scans, existing, { categories = CATEGORIES } = {}) {
276
- const sel = {
277
- skills: [],
278
- rules: [],
279
- commands: [],
280
- hooks: [],
281
- }
282
-
283
- // 第一轮:顺序遍历,每步给导航
284
- let i = 0
285
- while (i < categories.length) {
286
- const { key, label } = categories[i]
287
- const defaultNames = sel[key]?.length
288
- ? new Set(sel[key].map(x => x.name))
289
- : existing[key]
290
- sel[key] = await promptSelection(scans[key], label, defaultNames)
291
- if (sel[key] === null) return null
292
-
293
- if (i === categories.length - 1) break
294
-
295
- const nextLabel = categories[i + 1].label
296
- const prevLabel = i > 0 ? categories[i - 1].label : null
297
- // 导航步可搜索单选(选项少,交互与多选一致)
298
- const nav = await searchableSelect({
299
- message: `${label} 已选 ${sel[key].length} 项。下一步:`,
300
- options: [
301
- { label: `继续选 ${nextLabel}`, value: 'next' },
302
- ...(prevLabel ? [{ label: `返回重选 ${prevLabel}`, value: 'back' }] : []),
303
- { label: '取消同步', value: 'cancel' },
304
- ],
305
- initialValue: 'next',
306
- placeholder: SEARCH_PLACEHOLDER,
307
- })
308
- if (isCancel(nav) || nav === 'cancel') return null
309
- i = nav === 'back' ? i - 1 : i + 1
310
- }
311
-
312
- // 第二轮:总览 + 任意重选
313
- while (true) {
314
- console.log(chalk.gray('\n选择汇总:'))
315
- for (const { key, label } of categories) {
316
- console.log(chalk.gray(` ${label.padEnd(10)} ${sel[key].length} 项`))
317
- }
318
- const final = await searchableSelect({
319
- message: '确认提交还是重选?',
320
- options: [
321
- { label: '确认提交', value: 'confirm' },
322
- ...categories.map(c => ({ label: `重选 ${c.label}`, value: c.key })),
323
- { label: '取消同步', value: 'cancel' },
324
- ],
325
- initialValue: 'confirm',
326
- placeholder: SEARCH_PLACEHOLDER,
220
+ export async function interactiveSelect(scans, existing, { skillsOnly = false } = {}) {
221
+ const selected = { skills: [], rules: [], commands: [], hooks: skillsOnly ? [] : scans.hooks }
222
+ for (const { key, label } of skillsOnly ? CATEGORIES.slice(0, 1) : CATEGORIES) {
223
+ const tabs = buildCategoryTabs(scans[key], key)
224
+ const availableNames = new Set(scans[key].map(item => item.name))
225
+ const names = await tabMultiselect({
226
+ message: `选择 ${label}`,
227
+ tabs,
228
+ initialValues: [...existing[key]].filter(name => availableNames.has(name)),
229
+ required: false,
230
+ maxItems: 15,
327
231
  })
328
- if (isCancel(final) || final === 'cancel') return null
329
- if (final === 'confirm') return sel
330
- const cat = categories.find(c => c.key === final)
331
- sel[final] = await promptSelection(scans[final], cat.label, new Set(sel[final].map(x => x.name)))
332
- if (sel[final] === null) return null
232
+ if (isCancel(names)) return null
233
+ const selectedNames = new Set(names)
234
+ selected[key] = scans[key].filter(item => selectedNames.has(item.name))
333
235
  }
236
+ return selected
334
237
  }
335
238
 
336
239
  export function logAction(label, color = 'green', prefix = '✓') {
337
240
  console.log(chalk[color](` ${prefix} ${label}`))
338
241
  }
339
242
 
340
- function getTemplateRepoCacheDir() {
341
- return path.join(getDwyHomeDir(), 'cache', 'dwy')
342
- }
343
-
344
- async function resolveTemplateDirFromRepo(repoDir) {
345
- for (const segments of TEMPLATE_SOURCE_CANDIDATES) {
346
- const candidate = path.join(repoDir, ...segments)
347
- if (await fs.pathExists(candidate)) return candidate
348
- }
349
-
350
- throw new Error(`ai-tools templates not found in repo: ${repoDir}`)
351
- }
352
-
353
- async function readGitOriginUrl(repoDir) {
354
- try {
355
- return await runGit(['-C', repoDir, 'remote', 'get-url', 'origin'])
356
- } catch {
357
- return ''
358
- }
359
- }
360
-
361
- async function ensureTemplateRepoCache() {
362
- const repoDir = getTemplateRepoCacheDir()
363
- const repoRoot = path.dirname(repoDir)
364
- await fs.ensureDir(repoRoot)
365
-
366
- const hasGitDir = await fs.pathExists(path.join(repoDir, '.git'))
367
- if (hasGitDir) {
368
- const originUrl = await readGitOriginUrl(repoDir)
369
- if (!originUrl || originUrl !== DEFAULT_TEMPLATE_REPO_URL) {
370
- console.log(chalk.gray('缓存模板仓库 origin 已变更,重建缓存...'))
371
- await fs.remove(repoDir)
372
- }
373
- } else if (await fs.pathExists(repoDir)) {
374
- console.log(chalk.gray('缓存目录不是 git 仓库,重建缓存...'))
375
- await fs.remove(repoDir)
376
- }
377
-
378
- if (!await fs.pathExists(path.join(repoDir, '.git'))) {
379
- console.log(chalk.gray('拉取模板仓库...'))
380
- await runGit(['clone', DEFAULT_TEMPLATE_REPO_URL, repoDir])
381
- return repoDir
382
- }
383
-
384
- console.log(chalk.gray('更新模板仓库...'))
385
- await runGit(['-C', repoDir, 'pull', '--ff-only'])
386
- return repoDir
387
- }
388
-
389
243
  async function copyItems(items, targetDir) {
390
244
  for (const item of items) {
391
245
  const dest = path.join(targetDir, item.type + 's', item.name)
392
246
  await fs.ensureDir(path.dirname(dest))
393
- await fs.copy(item.sourcePath, dest, { overwrite: true, filter: copyFilter })
247
+ await fs.copy(item.sourcePath, dest, { overwrite: true, filter: copyFilter(item.sourcePath) })
394
248
  logAction(`${item.type}s/${item.name}`)
395
249
  }
396
250
  return items.length
@@ -406,7 +260,7 @@ async function copyHooks(items, targetDir) {
406
260
  continue
407
261
  }
408
262
  const dest = path.join(hooksTargetDir, hook.name)
409
- await fs.copy(hook.sourcePath, dest, { overwrite: true, filter: copyFilter })
263
+ await fs.copy(hook.sourcePath, dest, { overwrite: true, filter: copyFilter(hook.sourcePath) })
410
264
  if (!await fs.pathExists(dest)) {
411
265
  logAction(`hooks/${hook.name}`, 'yellow', '!')
412
266
  continue
@@ -431,7 +285,7 @@ async function removeUnselected(typePlural, existingNames, selectedNames, templa
431
285
  return count
432
286
  }
433
287
 
434
- function buildClaudeHooksSettings(manifests, selectedHookNames) {
288
+ function buildClaudeHooksSettings(manifests, selectedHookNames, global) {
435
289
  const grouped = new Map()
436
290
 
437
291
  for (const manifest of manifests) {
@@ -444,7 +298,7 @@ function buildClaudeHooksSettings(manifests, selectedHookNames) {
444
298
  const hooks = grouped.get(key) || []
445
299
  hooks.push({
446
300
  type: 'command',
447
- command: `bash $CLAUDE_PROJECT_DIR/.claude/hooks/${manifest.name}`,
301
+ command: `bash "${global ? '$HOME' : '$CLAUDE_PROJECT_DIR'}/.claude/hooks/${manifest.name}"`,
448
302
  })
449
303
  grouped.set(key, hooks)
450
304
  }
@@ -489,10 +343,10 @@ function mergeClaudeHooks(existingHooks, generatedHooks, managedHookNames) {
489
343
  return Object.keys(merged).length > 0 ? merged : undefined
490
344
  }
491
345
 
492
- async function syncSettings(sourceDir, targetDir, selectedHookNames) {
346
+ async function syncSettings(sourceDir, targetDir, selectedHookNames, global) {
493
347
  const settingsDest = path.join(targetDir, 'settings.json')
494
348
  const manifests = await loadHookManifests(sourceDir)
495
- const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames)
349
+ const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames, global)
496
350
  const managedHookNames = new Set(
497
351
  manifests
498
352
  .filter(manifest => !manifest.platforms || manifest.platforms.includes('claude'))
@@ -527,19 +381,7 @@ async function syncSettings(sourceDir, targetDir, selectedHookNames) {
527
381
  return 1
528
382
  }
529
383
 
530
- async function syncClaudeBaselineDoc(sourceDir, targetDir) {
531
- const baseline = await readBaselineDoc(sourceDir)
532
- if (!baseline) return 0
533
-
534
- const dest = path.join(targetDir, 'CLAUDE.md')
535
- const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
536
- if (current === baseline) return 0
537
-
538
- await fs.writeFile(dest, baseline)
539
- logAction('.claude/CLAUDE.md')
540
- return 1
541
- }
542
-
384
+ /** 默认读取随包模板;仅开发者显式设置环境变量时改用外部目录,不要求仓库权限。 */
543
385
  export async function resolveSourceDir() {
544
386
  const explicitSourceDir = process.env.DWY_TEMPLATE_SOURCE_DIR
545
387
  if (explicitSourceDir) {
@@ -549,13 +391,14 @@ export async function resolveSourceDir() {
549
391
  return explicitSourceDir
550
392
  }
551
393
 
552
- const repoDir = await ensureTemplateRepoCache()
553
- return resolveTemplateDirFromRepo(repoDir)
394
+ return path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
554
395
  }
555
396
 
397
+ /** 同步 Claude Skills 与 Hooks;基线和项目 Rules 由总同步入口分别处理。 */
556
398
  export async function syncClaude({
557
399
  sourceDir: sourceDirOverride,
558
400
  projectDir,
401
+ global = false,
559
402
  selected,
560
403
  staleRemovals = {},
561
404
  } = {}) {
@@ -571,7 +414,6 @@ export async function syncClaude({
571
414
  const scans = {
572
415
  skills: await scanSkills(sourceDir),
573
416
  rules: await scanRules(sourceDir),
574
- commands: await scanCommands(sourceDir),
575
417
  hooks: await scanHooks(sourceDir),
576
418
  }
577
419
  const availableHooks = []
@@ -588,27 +430,23 @@ export async function syncClaude({
588
430
  logAction(`hooks skipped: ${missing}`, 'yellow', '!')
589
431
  }
590
432
  scans.hooks = availableHooks
591
- console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
433
+ console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.hooks.length} hooks\n`))
592
434
 
593
435
  const existing = {
594
436
  skills: await scanExisting(projectTargetDir, 'skills'),
595
437
  rules: await scanExisting(projectTargetDir, 'rules'),
596
- commands: await scanExisting(projectTargetDir, 'commands'),
597
438
  hooks: await scanExisting(projectTargetDir, 'hooks'),
598
439
  }
599
440
 
600
441
  const selectedNames = key => new Set((selected[key] || []).map(item => item.name))
601
442
  const syncedSelection = {
602
443
  skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
603
- rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
604
- commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
605
444
  hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
606
445
  }
607
446
 
608
447
  const selectedHookNames = new Set(syncedSelection.hooks.map(h => h.name))
609
448
  const approvedStaleSkills = staleRemovals.skills || new Set()
610
449
  const approvedStaleRules = staleRemovals.rules || new Set()
611
- const approvedStaleCommands = staleRemovals.commands || new Set()
612
450
  const approvedStaleHooks = staleRemovals.hooks || new Set()
613
451
 
614
452
  console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
@@ -617,20 +455,15 @@ export async function syncClaude({
617
455
  let syncedCount = 0
618
456
  let removedCount = 0
619
457
 
620
- syncedCount += await syncClaudeBaselineDoc(sourceDir, projectTargetDir)
621
458
  syncedCount += await copyItems(syncedSelection.skills, projectTargetDir)
622
- syncedCount += await copyItems(syncedSelection.rules, projectTargetDir)
623
- syncedCount += await copyItems(syncedSelection.commands, projectTargetDir)
624
- syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames)
459
+ syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames, global)
625
460
  syncedCount += await copyHooks(syncedSelection.hooks, projectTargetDir)
626
461
 
627
462
  const skillTemplateNames = new Set([...scans.skills.map(s => s.name), ...approvedStaleSkills])
628
463
  const ruleTemplateNames = new Set([...scans.rules.map(r => r.name), ...approvedStaleRules])
629
- const commandTemplateNames = new Set([...scans.commands.map(c => c.name), ...approvedStaleCommands])
630
464
  const hookTemplateNames = new Set([...scans.hooks.map(h => h.name), ...approvedStaleHooks])
631
465
  removedCount += await removeUnselected('skills', existing.skills, new Set(syncedSelection.skills.map(s => s.name)), skillTemplateNames, projectTargetDir)
632
- removedCount += await removeUnselected('rules', existing.rules, new Set(syncedSelection.rules.map(r => r.name)), ruleTemplateNames, projectTargetDir)
633
- removedCount += await removeUnselected('commands', existing.commands, new Set(syncedSelection.commands.map(c => c.name)), commandTemplateNames, projectTargetDir)
466
+ removedCount += await removeUnselected('rules', existing.rules, new Set(), ruleTemplateNames, projectTargetDir)
634
467
  removedCount += await removeUnselected('hooks', existing.hooks, selectedHookNames, hookTemplateNames, projectTargetDir)
635
468
 
636
469
  console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
@@ -4,7 +4,7 @@
4
4
  * 基于 @clack/core Prompt 自绘,不引入新 UI 库。
5
5
  */
6
6
 
7
- import { Prompt, getColumns, settings } from '@clack/core'
7
+ import { Prompt, getColumns, settings, wrapTextWithPrefix } from '@clack/core'
8
8
  import { limitOptions } from '@clack/prompts'
9
9
  import { styleText } from 'node:util'
10
10
  import process from 'node:process'
@@ -39,39 +39,8 @@ const S_BAR_H = () => unicodeOr('─', '-')
39
39
  const S_TAB_MORE = unicodeOr('‹', '<')
40
40
  const S_TAB_MORE_RIGHT = unicodeOr('›', '>')
41
41
 
42
- /**
43
- * skill / rule / hook 的终端标记。
44
- * 不用 emoji:几何符号 + 固定宽度英文,无 unicode 时退回 S/R/H。
45
- */
46
- const ITEM_TYPE_META = {
47
- skills: { icon: unicodeOr('◈', 'S'), word: 'skill', color: 'cyan', title: '技能' },
48
- rules: { icon: unicodeOr('☰', 'R'), word: 'rule', color: 'yellow', title: '规则' },
49
- hooks: { icon: unicodeOr('⤷', 'H'), word: 'hook', color: 'magenta', title: '钩子' },
50
- }
51
-
52
- /**
53
- * 取类型展示元数据。未知类型当 skill,避免列表画出空白标记。
54
- *
55
- * @param {string | undefined} type
56
- */
57
- export function itemTypeMeta(type) {
58
- return ITEM_TYPE_META[type] || ITEM_TYPE_META.skills
59
- }
60
-
61
- /**
62
- * 行首类型前缀:图标 + 对齐后的 skill/rule/hook。
63
- *
64
- * @param {string | undefined} type
65
- */
66
- export function formatItemTypePrefix(type) {
67
- const meta = itemTypeMeta(type)
68
- return `${styleText(meta.color, meta.icon)} ${styleText(meta.color, meta.word.padEnd(5, ' '))}`
69
- }
70
-
71
42
  /** Tab 之间空格数。过密会糊成一行,过疏又浪费宽度。 */
72
43
  const TAB_GAP = 2
73
- /** 省略角标占位。‹/› 按 1 列计。 */
74
- const ELLIPSIS_W = 1
75
44
  /**
76
45
  * 技术栈 / 场景分组之间插入 ` | ` 比普通间隙多出来的宽度。
77
46
  * 普通间隙 TAB_GAP=2;再加 `|` 和一侧间隙 → 3。
@@ -151,35 +120,31 @@ export function tabWindow(segments, activeIndex, maxWidth, groups) {
151
120
  const n = segments.length
152
121
  if (n === 0) return { start: 0, end: 0, showLeft: false, showRight: false }
153
122
  const idx = Math.min(Math.max(activeIndex, 0), n - 1)
154
- const widths = segments.map(segment => segment.length)
155
-
156
- const used = (start, end) => {
157
- let w = 0
158
- for (let i = start; i < end; i++) {
159
- if (i > start) w += TAB_GAP
160
- w += widths[i]
161
- }
162
- w += groupSepWidth(groups, start, end)
163
- if (start > 0) w += ELLIPSIS_W + TAB_GAP
164
- if (end < n) w += TAB_GAP + ELLIPSIS_W
165
- return w
123
+ const fits = (start, end) => {
124
+ const gap = ' '.repeat(TAB_GAP)
125
+ let text = segments.slice(start, end).join(gap)
126
+ if (start > 0) text = `${S_TAB_MORE}${gap}${text}`
127
+ if (end < n) text += `${gap}${S_TAB_MORE_RIGHT}`
128
+ text += ' '.repeat(groupSepWidth(groups, start, end))
129
+ // 复用 clack 的终端宽度处理,中文分类不能按字符串 length 计算。
130
+ return !wrapTextWithPrefix({ columns: maxWidth }, text, '').includes('\n')
166
131
  }
167
132
 
168
133
  let start = idx
169
134
  let end = idx + 1
170
135
  // 当前段已经超宽:不再扩张,避免把邻 Tab 挤没
171
- if (used(start, end) > maxWidth) {
136
+ if (!fits(start, end)) {
172
137
  return { start, end, showLeft: start > 0, showRight: end < n }
173
138
  }
174
139
 
175
140
  let grew = true
176
141
  while (grew) {
177
142
  grew = false
178
- if (end < n && used(start, end + 1) <= maxWidth) {
143
+ if (end < n && fits(start, end + 1)) {
179
144
  end += 1
180
145
  grew = true
181
146
  }
182
- if (start > 0 && used(start - 1, end) <= maxWidth) {
147
+ if (start > 0 && fits(start - 1, end)) {
183
148
  start -= 1
184
149
  grew = true
185
150
  }
@@ -330,7 +295,7 @@ function renderTabBar(segments, activeIndex, win, tabs, selectedValues) {
330
295
 
331
296
  /**
332
297
  * 打开顶部 Tab + 下方多选。
333
- * tabs 为空时不进交互,直接返回 [](调用方应先过滤空包)。
298
+ * tabs 为空时不进交互,直接返回 [](调用方应先过滤空分类)。
334
299
  *
335
300
  * @param {object} opts
336
301
  * @param {string} opts.message
@@ -380,13 +345,11 @@ export function tabMultiselect(opts) {
380
345
  const contentWidth = Math.max(cols - (withGuide ? 10 : 6), 20)
381
346
  const segments = tabBarSegments(this.tabs, selected)
382
347
  const groups = this.tabs.map(tab => tab.group)
383
- const win = tabWindow(segments, this.tabIndex, contentWidth, groups)
348
+ const win = tabWindow(segments, this.tabIndex, contentWidth - 2, groups)
384
349
  const tabLine = renderTabBar(segments, this.tabIndex, win, this.tabs, selected)
385
350
 
386
351
  const focused = this.currentOptions[this.cursor]
387
- const focusedType = itemTypeMeta(focused?.type)
388
- const descRaw = (focused?.description || '').trim() || '(无说明)'
389
- const desc = `${focusedType.title} · ${descRaw}`
352
+ const desc = (focused?.description || '').trim() || '(无说明)'
390
353
  const instructions = [
391
354
  `${styleText('dim', '←/→')} 切 Tab`,
392
355
  `${styleText('dim', '↑/↓')} 移动`,
@@ -421,7 +384,6 @@ export function tabMultiselect(opts) {
421
384
  label: opt.label ?? String(opt.value ?? ''),
422
385
  selected: selected.includes(opt.value),
423
386
  active,
424
- prefix: formatItemTypePrefix(opt.type),
425
387
  }),
426
388
  })
427
389
 
package/src/utils.js CHANGED
@@ -11,7 +11,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
11
  const execFileAsync = promisify(execFile)
12
12
 
13
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
14
 
16
15
  export function getDwyHomeDir() {
17
16
  return process.env.DWY_HOME || path.join(os.homedir(), '.dwy')