create-dwy 0.16.0 → 0.16.1

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 (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/index.js +0 -0
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/sync-all.js +11 -7
  6. package/src/sync.js +38 -32
  7. package/templates/ai-tools/CHANGELOG.md +76 -0
  8. package/templates/ai-tools/VERSION +1 -0
  9. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  10. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  11. package/templates/ai-tools/rules/Android/dwy-android-core.md +215 -4
  12. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  13. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  14. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  15. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  16. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  17. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  18. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +511 -0
  19. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +126 -0
  20. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +88 -0
  21. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +195 -0
  22. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +179 -0
  23. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +39 -0
  24. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +278 -0
  25. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +147 -0
  26. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +230 -0
  27. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +64 -0
  28. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +60 -0
  29. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +209 -0
  30. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +100 -0
  31. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +157 -0
  32. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +265 -0
  33. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +94 -0
  34. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +56 -0
  35. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +105 -0
  36. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  37. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  38. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  39. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  40. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  41. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  42. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 交互同步每一步选项支持关键字搜索(Skills / Rules / Commands / Hooks、平台选择、陈旧项删除、导航与汇总确认)。
8
+ - 选项说明改为列表下方固定「说明」公共区展示当前聚焦项完整描述,不再跟在选项行尾导致截断错乱。
9
+ - 新增 `src/searchable-select.js`(基于 `@clack/core` AutocompletePrompt 自定义 render);`@clack/prompts` 下限抬至 `^1.7.0`,显式依赖 `@clack/core`。
10
+
3
11
  ## 0.16.0
4
12
 
5
13
  ### Minor Changes
package/bin/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,8 @@
28
28
  "license": "MIT",
29
29
  "author": "dwydev",
30
30
  "dependencies": {
31
- "@clack/prompts": "^1.5.1",
31
+ "@clack/core": "^1.4.3",
32
+ "@clack/prompts": "^1.7.0",
32
33
  "handlebars": "^4.7.0",
33
34
  "fs-extra": "^11.0.0",
34
35
  "chalk": "^5.0.0"
@@ -0,0 +1,419 @@
1
+ /**
2
+ * 可搜索选择器:列表不展示 hint,底部固定「说明」公共区展示当前聚焦项描述。
3
+ *
4
+ * 基于 @clack/core AutocompletePrompt 自定义 render,绕过 prompts 默认「hint 跟在聚焦行后」行为。
5
+ */
6
+
7
+ import { AutocompletePrompt, getColumns, settings } from '@clack/core'
8
+ import { limitOptions } from '@clack/prompts'
9
+ import { styleText } from 'node:util'
10
+ import process from 'node:process'
11
+
12
+ /** 是否支持 unicode 符号(与 clack prompts 判定一致) */
13
+ function isUnicodeSupported() {
14
+ if (process.platform !== 'win32') {
15
+ return process.env.TERM !== 'linux'
16
+ }
17
+ return Boolean(process.env.CI)
18
+ || Boolean(process.env.WT_SESSION)
19
+ || Boolean(process.env.TERMINUS_SUBLIME)
20
+ || process.env.ConEmuTask === '{cmd::Cmder}'
21
+ || process.env.TERM_PROGRAM === 'Terminus-Sublime'
22
+ || process.env.TERM_PROGRAM === 'vscode'
23
+ || process.env.TERM === 'xterm-256color'
24
+ || process.env.TERM === 'alacritty'
25
+ || process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
26
+ }
27
+
28
+ const unicode = isUnicodeSupported()
29
+ const unicodeOr = (c, fallback) => (unicode ? c : fallback)
30
+
31
+ const S_STEP_ACTIVE = unicodeOr('◆', '*')
32
+ const S_STEP_CANCEL = unicodeOr('■', 'x')
33
+ const S_STEP_ERROR = unicodeOr('▲', 'x')
34
+ const S_STEP_SUBMIT = unicodeOr('◇', 'o')
35
+ const S_BAR = unicodeOr('│', '|')
36
+ const S_BAR_END = unicodeOr('└', '—')
37
+ const S_RADIO_ACTIVE = unicodeOr('●', '>')
38
+ const S_RADIO_INACTIVE = unicodeOr('○', ' ')
39
+ const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]')
40
+ const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]')
41
+
42
+ /** 底部说明区固定行数,避免列表高度随描述长短跳动 */
43
+ const DESC_PANEL_LINES = 3
44
+
45
+ /** 搜索框默认 placeholder */
46
+ export const SEARCH_PLACEHOLDER = '输入关键字筛选…'
47
+
48
+ /**
49
+ * 按状态返回步骤符号。
50
+ * @param {string} state
51
+ */
52
+ function symbol(state) {
53
+ switch (state) {
54
+ case 'cancel':
55
+ return styleText('red', S_STEP_CANCEL)
56
+ case 'error':
57
+ return styleText('yellow', S_STEP_ERROR)
58
+ case 'submit':
59
+ return styleText('green', S_STEP_SUBMIT)
60
+ default:
61
+ return styleText('cyan', S_STEP_ACTIVE)
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 默认过滤:匹配 label / value / description。
67
+ * @param {string} search
68
+ * @param {{ value: unknown, label?: string, description?: string, hint?: string }} opt
69
+ */
70
+ function defaultFilter(search, opt) {
71
+ if (!search) return true
72
+ const q = search.toLowerCase()
73
+ const label = (opt.label ?? String(opt.value ?? '')).toLowerCase()
74
+ const desc = (opt.description ?? opt.hint ?? '').toLowerCase()
75
+ const value = String(opt.value ?? '').toLowerCase()
76
+ return label.includes(q) || desc.includes(q) || value.includes(q)
77
+ }
78
+
79
+ /**
80
+ * 取选项展示文案。
81
+ * @param {{ label?: string, value?: unknown }} opt
82
+ */
83
+ function optionLabel(opt) {
84
+ return opt.label ?? String(opt.value ?? '')
85
+ }
86
+
87
+ /**
88
+ * 按终端宽度硬换行(中日文按字符计,足够 CLI 展示)。
89
+ * @param {string} text
90
+ * @param {number} width
91
+ * @returns {string[]}
92
+ */
93
+ function wrapHard(text, width) {
94
+ if (!text) return []
95
+ const w = Math.max(width, 8)
96
+ const lines = []
97
+ let row = ''
98
+ for (const ch of text) {
99
+ if (ch === '\n') {
100
+ lines.push(row)
101
+ row = ''
102
+ continue
103
+ }
104
+ if (row.length >= w) {
105
+ lines.push(row)
106
+ row = ch
107
+ } else {
108
+ row += ch
109
+ }
110
+ }
111
+ if (row) lines.push(row)
112
+ return lines
113
+ }
114
+
115
+ /**
116
+ * 构建底部固定高度说明区。
117
+ * 无描述时用占位,保证行数恒定。
118
+ *
119
+ * @param {string | undefined} description 当前聚焦项完整描述
120
+ * @param {string} barPrefix 左侧竖线前缀(含颜色)
121
+ * @param {number} contentWidth 可写字符宽
122
+ * @returns {string[]}
123
+ */
124
+ function buildDescPanel(description, barPrefix, contentWidth) {
125
+ const raw = (description && description.trim()) ? description.trim() : '(无说明)'
126
+ const wrapped = wrapHard(raw, contentWidth)
127
+ // 固定 DESC_PANEL_LINES 行:超出截断末行加省略,不足补空
128
+ const lines = []
129
+ for (let i = 0; i < DESC_PANEL_LINES; i++) {
130
+ let part = wrapped[i] ?? ''
131
+ if (i === DESC_PANEL_LINES - 1 && wrapped.length > DESC_PANEL_LINES) {
132
+ const ellipsis = '…'
133
+ part = (wrapped[i] ?? '').slice(0, Math.max(contentWidth - ellipsis.length, 0)) + ellipsis
134
+ }
135
+ if (i === 0) {
136
+ lines.push(`${barPrefix}${styleText('dim', '说明:')}${styleText('dim', part)}`)
137
+ } else {
138
+ // 续行缩进对齐「说明:」四字宽
139
+ lines.push(`${barPrefix}${styleText('dim', ' ')}${styleText('dim', part)}`)
140
+ }
141
+ }
142
+ return lines
143
+ }
144
+
145
+ /**
146
+ * 从 options 中取 focusedValue 对应项的 description。
147
+ * @param {Array<{ value: unknown, description?: string, hint?: string }>} options
148
+ * @param {unknown} focusedValue
149
+ */
150
+ function resolveDescription(options, focusedValue) {
151
+ if (focusedValue === undefined) return undefined
152
+ const hit = options.find(o => o.value === focusedValue)
153
+ return hit?.description ?? hit?.hint
154
+ }
155
+
156
+ /**
157
+ * 可搜索多选:列表无行内 hint,底部公共区展示当前项说明。
158
+ *
159
+ * @param {object} opts
160
+ * @param {string} opts.message
161
+ * @param {Array<{ value: unknown, label?: string, description?: string, disabled?: boolean }>} opts.options
162
+ * @param {unknown[]} [opts.initialValues]
163
+ * @param {boolean} [opts.required]
164
+ * @param {number} [opts.maxItems]
165
+ * @param {string} [opts.placeholder]
166
+ * @param {(search: string, opt: object) => boolean} [opts.filter]
167
+ * @returns {Promise<unknown[] | symbol>}
168
+ */
169
+ export function searchableMultiselect(opts) {
170
+ const withGuide = opts.withGuide ?? settings.withGuide
171
+ const placeholder = opts.placeholder ?? SEARCH_PLACEHOLDER
172
+ const filter = opts.filter ?? defaultFilter
173
+
174
+ const prompt = new AutocompletePrompt({
175
+ options: opts.options,
176
+ multiple: true,
177
+ placeholder,
178
+ filter,
179
+ initialValue: opts.initialValues,
180
+ signal: opts.signal,
181
+ input: opts.input,
182
+ output: opts.output,
183
+ validate: () => {
184
+ if (opts.required && prompt.selectedValues.length === 0) {
185
+ return '请至少选择一项'
186
+ }
187
+ },
188
+ render() {
189
+ const msgLine = `${withGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} ${opts.message}\n`
190
+ const userInput = this.userInput
191
+ const isPlaceholder = userInput === '' && placeholder !== undefined
192
+ const searchDisplay = this.isNavigating || isPlaceholder
193
+ ? styleText('dim', isPlaceholder ? placeholder : userInput)
194
+ : this.userInputWithCursor
195
+ const matchHint = this.filteredOptions.length !== this.options.length
196
+ ? styleText('dim', ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`)
197
+ : ''
198
+
199
+ if (this.state === 'submit') {
200
+ return `${msgLine}${withGuide ? `${styleText('gray', S_BAR)} ` : ''}${styleText(
201
+ 'dim',
202
+ `已选 ${this.selectedValues.length} 项`,
203
+ )}`
204
+ }
205
+ if (this.state === 'cancel') {
206
+ return `${msgLine}${withGuide ? `${styleText('gray', S_BAR)} ` : ''}${styleText(
207
+ ['strikethrough', 'dim'],
208
+ userInput,
209
+ )}`
210
+ }
211
+
212
+ const barColor = this.state === 'error' ? 'yellow' : 'cyan'
213
+ const bar = withGuide ? `${styleText(barColor, S_BAR)} ` : ''
214
+ const barEnd = withGuide ? styleText(barColor, S_BAR_END) : ''
215
+ const noMatch = this.filteredOptions.length === 0 && userInput
216
+ ? [`${bar}${styleText('yellow', '无匹配项')}`]
217
+ : []
218
+ const errLines = this.state === 'error'
219
+ ? [`${bar}${styleText('yellow', this.error)}`]
220
+ : []
221
+
222
+ const header = [
223
+ ...`${msgLine}${withGuide ? styleText(barColor, S_BAR) : ''}`.split('\n'),
224
+ `${bar}${styleText('dim', 'Search:')} ${searchDisplay}${matchHint}`,
225
+ ...noMatch,
226
+ ...errLines,
227
+ ]
228
+
229
+ // 底部:说明区 + 快捷键(说明区行数固定,计入 rowPadding)
230
+ const cols = getColumns(opts.output ?? process.stdout)
231
+ const contentWidth = Math.max(cols - (withGuide ? 10 : 6), 20)
232
+ const descPanel = buildDescPanel(
233
+ resolveDescription(this.filteredOptions, this.focusedValue),
234
+ bar,
235
+ contentWidth,
236
+ )
237
+ const instructions = [
238
+ `${styleText('dim', '↑/↓')} to navigate`,
239
+ `${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
240
+ `${styleText('dim', 'Enter:')} confirm`,
241
+ `${styleText('dim', 'Type:')} to search`,
242
+ ]
243
+ const footer = [
244
+ `${bar}${styleText('dim', S_BAR_H())}`,
245
+ ...descPanel,
246
+ `${bar}${instructions.join(' · ')}`,
247
+ barEnd,
248
+ ].filter(Boolean)
249
+
250
+ const listLines = this.filteredOptions.length === 0
251
+ ? []
252
+ : limitOptions({
253
+ cursor: this.cursor,
254
+ options: this.filteredOptions,
255
+ maxItems: opts.maxItems,
256
+ output: opts.output,
257
+ rowPadding: header.length + footer.length,
258
+ style: (opt, active) => {
259
+ const selected = this.selectedValues.includes(opt.value)
260
+ const label = optionLabel(opt)
261
+ const box = selected
262
+ ? styleText('green', S_CHECKBOX_SELECTED)
263
+ : styleText('dim', S_CHECKBOX_INACTIVE)
264
+ if (opt.disabled) {
265
+ return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`
266
+ }
267
+ // 聚焦也不拼 hint,描述只在底部公共区
268
+ if (active) {
269
+ return `${box} ${label}`
270
+ }
271
+ return `${box} ${styleText('dim', label)}`
272
+ },
273
+ })
274
+
275
+ return [
276
+ ...header,
277
+ ...listLines.map(line => `${bar}${line}`),
278
+ ...footer,
279
+ ].join('\n')
280
+ },
281
+ })
282
+
283
+ return prompt.prompt()
284
+ }
285
+
286
+ /** 横线字符(与 clack 一致) */
287
+ function S_BAR_H() {
288
+ return unicodeOr('─', '-')
289
+ }
290
+
291
+ /**
292
+ * 可搜索单选:列表无行内 hint,底部公共区展示当前项说明。
293
+ *
294
+ * @param {object} opts
295
+ * @param {string} opts.message
296
+ * @param {Array<{ value: unknown, label?: string, description?: string, disabled?: boolean }>} opts.options
297
+ * @param {unknown} [opts.initialValue]
298
+ * @param {number} [opts.maxItems]
299
+ * @param {string} [opts.placeholder]
300
+ * @param {(search: string, opt: object) => boolean} [opts.filter]
301
+ * @returns {Promise<unknown | symbol>}
302
+ */
303
+ export function searchableSelect(opts) {
304
+ const withGuide = opts.withGuide ?? settings.withGuide
305
+ const placeholder = opts.placeholder ?? SEARCH_PLACEHOLDER
306
+ const filter = opts.filter ?? defaultFilter
307
+
308
+ const prompt = new AutocompletePrompt({
309
+ options: opts.options,
310
+ multiple: false,
311
+ placeholder,
312
+ filter,
313
+ initialValue: opts.initialValue !== undefined ? [opts.initialValue] : undefined,
314
+ signal: opts.signal,
315
+ input: opts.input,
316
+ output: opts.output,
317
+ validate: opts.validate,
318
+ render() {
319
+ const headerBase = withGuide
320
+ ? [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`]
321
+ : [`${symbol(this.state)} ${opts.message}`]
322
+ const userInput = this.userInput
323
+ const isPlaceholder = userInput === '' && placeholder !== undefined
324
+ let searchDisplay = ''
325
+ if (this.isNavigating || isPlaceholder) {
326
+ const d = isPlaceholder ? placeholder : userInput
327
+ searchDisplay = d !== '' ? ` ${styleText('dim', d)}` : ''
328
+ } else {
329
+ searchDisplay = ` ${this.userInputWithCursor}`
330
+ }
331
+ const matchHint = this.filteredOptions.length !== this.options.length
332
+ ? styleText(
333
+ 'dim',
334
+ ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`,
335
+ )
336
+ : ''
337
+
338
+ if (this.state === 'submit') {
339
+ const selected = this.filteredOptions.find(o => o.value === this.focusedValue)
340
+ ?? opts.options.find(o => o.value === this.focusedValue)
341
+ const text = selected ? optionLabel(selected) : ''
342
+ const body = text ? ` ${styleText('dim', text)}` : ''
343
+ const bar = withGuide ? styleText('gray', S_BAR) : ''
344
+ return `${headerBase.join('\n')}\n${bar}${body}`
345
+ }
346
+ if (this.state === 'cancel') {
347
+ const body = userInput ? ` ${styleText(['strikethrough', 'dim'], userInput)}` : ''
348
+ const bar = withGuide ? styleText('gray', S_BAR) : ''
349
+ return `${headerBase.join('\n')}\n${bar}${body}`
350
+ }
351
+
352
+ const barColor = this.state === 'error' ? 'yellow' : 'cyan'
353
+ const bar = withGuide ? `${styleText(barColor, S_BAR)} ` : ''
354
+ const barEnd = withGuide ? styleText(barColor, S_BAR_END) : ''
355
+ const noMatch = this.filteredOptions.length === 0 && userInput
356
+ ? [`${bar}${styleText('yellow', '无匹配项')}`]
357
+ : []
358
+ const errLines = this.state === 'error'
359
+ ? [`${bar}${styleText('yellow', this.error)}`]
360
+ : []
361
+
362
+ const header = [...headerBase]
363
+ if (withGuide) header.push(`${bar.trimEnd()}`)
364
+ header.push(
365
+ `${bar}${styleText('dim', 'Search:')}${searchDisplay}${matchHint}`,
366
+ ...noMatch,
367
+ ...errLines,
368
+ )
369
+
370
+ const cols = getColumns(opts.output ?? process.stdout)
371
+ const contentWidth = Math.max(cols - (withGuide ? 10 : 6), 20)
372
+ const descPanel = buildDescPanel(
373
+ resolveDescription(this.filteredOptions, this.focusedValue),
374
+ bar,
375
+ contentWidth,
376
+ )
377
+ const instructions = [
378
+ `${styleText('dim', '↑/↓')} to select`,
379
+ `${styleText('dim', 'Enter:')} confirm`,
380
+ `${styleText('dim', 'Type:')} to search`,
381
+ ]
382
+ const footer = [
383
+ `${bar}${styleText('dim', S_BAR_H())}`,
384
+ ...descPanel,
385
+ `${bar}${instructions.join(' · ')}`,
386
+ barEnd,
387
+ ].filter(Boolean)
388
+
389
+ const listLines = this.filteredOptions.length === 0
390
+ ? []
391
+ : limitOptions({
392
+ cursor: this.cursor,
393
+ options: this.filteredOptions,
394
+ maxItems: opts.maxItems,
395
+ output: opts.output,
396
+ columnPadding: withGuide ? 3 : 0,
397
+ rowPadding: header.length + footer.length,
398
+ style: (opt, active) => {
399
+ const label = optionLabel(opt)
400
+ if (opt.disabled) {
401
+ return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`
402
+ }
403
+ if (active) {
404
+ return `${styleText('green', S_RADIO_ACTIVE)} ${label}`
405
+ }
406
+ return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`
407
+ },
408
+ })
409
+
410
+ return [
411
+ ...header,
412
+ ...listLines.map(line => `${bar}${line}`),
413
+ ...footer,
414
+ ].join('\n')
415
+ },
416
+ })
417
+
418
+ return prompt.prompt()
419
+ }
package/src/sync-all.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { confirm, isCancel, multiselect } from '@clack/prompts'
3
+ import { confirm, isCancel } from '@clack/prompts'
4
+ import { SEARCH_PLACEHOLDER, searchableMultiselect } from './searchable-select.js'
4
5
  import { chalk } from './utils.js'
5
6
  import {
6
7
  interactiveSelect,
@@ -27,10 +28,10 @@ import { syncOpenCode } from './sync-opencode.js'
27
28
  import { ensureSkillsInstalled } from './skills-install.js'
28
29
 
29
30
  const PLATFORM_OPTIONS = [
30
- { value: 'claude', label: 'Claude Code', hint: '支持 rules / skills / commands / hooks' },
31
- { value: 'codex', label: 'Codex', hint: '支持 rules / skills / hooks' },
32
- { value: 'cursor', label: 'Cursor', hint: '支持 rules' },
33
- { value: 'opencode', label: 'OpenCode', hint: '支持 rules / skills / commands' },
31
+ { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / commands / hooks' },
32
+ { value: 'codex', label: 'Codex', description: '支持 rules / skills / hooks' },
33
+ { value: 'cursor', label: 'Cursor', description: '支持 rules' },
34
+ { value: 'opencode', label: 'OpenCode', description: '支持 rules / skills / commands' },
34
35
  ]
35
36
 
36
37
  const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
@@ -152,12 +153,14 @@ async function selectPlatforms(projectDir) {
152
153
  console.log(chalk.gray(' Cursor → rules'))
153
154
  console.log(chalk.gray(' OpenCode → rules / skills / commands\n'))
154
155
 
155
- const result = await multiselect({
156
+ // 可搜索多选 + 底部说明区
157
+ const result = await searchableMultiselect({
156
158
  message: '选择要同步的平台',
157
159
  options: PLATFORM_OPTIONS,
158
160
  initialValues,
159
161
  required: true,
160
162
  maxItems: 6,
163
+ placeholder: SEARCH_PLACEHOLDER,
161
164
  })
162
165
  if (isCancel(result)) return null
163
166
  return result
@@ -310,11 +313,12 @@ async function promptStaleRemovals(staleEntries) {
310
313
  console.log(chalk.gray(` - ${name}`))
311
314
  }
312
315
 
313
- const selected = await multiselect({
316
+ const selected = await searchableMultiselect({
314
317
  message: `选择要删除的 ${PLATFORM_LABELS[platform]} ${TYPE_LABELS[type]}`,
315
318
  options: names.map(name => ({ value: name, label: name })),
316
319
  initialValues: names,
317
320
  required: false,
321
+ placeholder: SEARCH_PLACEHOLDER,
318
322
  })
319
323
  if (isCancel(selected)) return null
320
324
  approvals[platform][type] = new Set(selected)
package/src/sync.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { groupMultiselect, isCancel, select } from '@clack/prompts'
3
+ import { isCancel } from '@clack/prompts'
4
+ import {
5
+ SEARCH_PLACEHOLDER,
6
+ searchableMultiselect,
7
+ searchableSelect,
8
+ } from './searchable-select.js'
4
9
  import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } from './utils.js'
5
10
 
6
11
  const CATEGORIES = [
@@ -208,44 +213,42 @@ export async function scanExisting(projectTargetDir, typePlural) {
208
213
  return new Set(entries.filter(name => name !== '.gitkeep' && name !== '.DS_Store'))
209
214
  }
210
215
 
211
- function buildGroupedOptions(items) {
212
- const groups = new Map()
213
- for (const item of items) {
214
- const category = item.category || '其他'
215
- const options = groups.get(category) || []
216
- const hint = item.description
217
- ? item.description.length > 72
218
- ? `${item.description.slice(0, 72)}...`
219
- : item.description
220
- : undefined
221
-
222
- options.push({
223
- value: item.name,
224
- label: hint ? `${item.name}(${hint})` : item.name,
216
+ /**
217
+ * 将扫描项转为可搜索扁平 options。
218
+ * label 带分类前缀便于筛选;完整 description 进底部公共说明区(不截断塞 hint)。
219
+ *
220
+ * @param {Array<{ name: string, category?: string, description?: string }>} items
221
+ * @returns {Array<{ value: string, label: string, description?: string }>}
222
+ */
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
+ }
225
239
  })
226
- groups.set(category, options)
227
- }
228
-
229
- return Object.fromEntries(
230
- [...groups.entries()]
231
- .sort(([a], [b]) => a.localeCompare(b, 'zh'))
232
- .map(([category, options]) => [
233
- category,
234
- options.sort((a, b) => a.label.localeCompare(b.label, 'zh')),
235
- ]),
236
- )
237
240
  }
238
241
 
239
242
  async function promptSelection(items, label, defaultNames) {
240
243
  if (items.length === 0) return []
241
- const selectedNames = await groupMultiselect({
244
+ // 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
245
+ const selectedNames = await searchableMultiselect({
242
246
  message: `选择 ${label}`,
243
- options: buildGroupedOptions(items),
247
+ options: buildSearchableOptions(items),
244
248
  initialValues: [...defaultNames],
245
249
  maxItems: 15,
246
250
  required: false,
247
- groupSpacing: 1,
248
- selectableGroups: true,
251
+ placeholder: SEARCH_PLACEHOLDER,
249
252
  })
250
253
  if (isCancel(selectedNames)) return null
251
254
  const selectedSet = new Set(selectedNames)
@@ -273,7 +276,8 @@ export async function interactiveSelect(scans, existing) {
273
276
 
274
277
  const nextLabel = CATEGORIES[i + 1].label
275
278
  const prevLabel = i > 0 ? CATEGORIES[i - 1].label : null
276
- const nav = await select({
279
+ // 导航步可搜索单选(选项少,交互与多选一致)
280
+ const nav = await searchableSelect({
277
281
  message: `${label} 已选 ${sel[key].length} 项。下一步:`,
278
282
  options: [
279
283
  { label: `继续选 ${nextLabel}`, value: 'next' },
@@ -281,6 +285,7 @@ export async function interactiveSelect(scans, existing) {
281
285
  { label: '取消同步', value: 'cancel' },
282
286
  ],
283
287
  initialValue: 'next',
288
+ placeholder: SEARCH_PLACEHOLDER,
284
289
  })
285
290
  if (isCancel(nav) || nav === 'cancel') return null
286
291
  i = nav === 'back' ? i - 1 : i + 1
@@ -292,7 +297,7 @@ export async function interactiveSelect(scans, existing) {
292
297
  for (const { key, label } of CATEGORIES) {
293
298
  console.log(chalk.gray(` ${label.padEnd(10)} ${sel[key].length} 项`))
294
299
  }
295
- const final = await select({
300
+ const final = await searchableSelect({
296
301
  message: '确认提交还是重选?',
297
302
  options: [
298
303
  { label: '确认提交', value: 'confirm' },
@@ -300,6 +305,7 @@ export async function interactiveSelect(scans, existing) {
300
305
  { label: '取消同步', value: 'cancel' },
301
306
  ],
302
307
  initialValue: 'confirm',
308
+ placeholder: SEARCH_PLACEHOLDER,
303
309
  })
304
310
  if (isCancel(final) || final === 'cancel') return null
305
311
  if (final === 'confirm') return sel