create-dwy 0.16.0 → 0.17.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 (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/index.js +1 -1
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/sync-all.js +313 -65
  6. package/src/sync-global-skills.js +167 -0
  7. package/src/sync.js +59 -43
  8. package/templates/ai-tools/CHANGELOG.md +210 -0
  9. package/templates/ai-tools/VERSION +1 -0
  10. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  11. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
  13. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  14. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  15. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  17. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  18. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  19. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  20. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  21. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  22. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  23. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  24. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +65 -0
  25. 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 +43 -0
  26. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +17 -0
  27. 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 +16 -0
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +16 -0
  29. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/scripts/appstore_config.sh +140 -0
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  32. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +2 -1
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  34. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +2 -1
  35. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -1
  36. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +82 -0
  37. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  38. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  39. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +2 -2
  40. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  41. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  42. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  43. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  44. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  45. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  47. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  48. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  50. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  53. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  54. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/assets/cover-style-master-v1.png +0 -0
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  56. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  57. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  58. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates/topic-backlog.template.md +50 -0
  59. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates//351/200/211/351/242/230/346/261/240.template.md +16 -0
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  61. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates/channel-profile.template.md +52 -0
  62. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates//344/272/247/345/223/201/345/215/241.template.md +132 -0
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  64. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/build_facts_html.py +430 -0
  65. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  66. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  67. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/templates//344/272/213/345/256/236/350/265/204/346/226/231/345/272/223.template.md +55 -0
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  70. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//344/272/213/345/256/236.template.md +82 -0
  71. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//345/215/225/351/233/206/346/226/207/346/241/243.template.md +175 -0
  72. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//350/204/232/346/234/254.template.md +69 -0
  73. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//351/252/250/346/236/266-v1.1.md +174 -0
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  76. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  79. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/345/214/205.template.md +1 -0
  80. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/350/257/264/346/230/216.md +15 -0
  81. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//346/210/220/347/211/207/345/214/205.template.md +49 -0
  82. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  84. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  85. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  86. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  87. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow//346/211/247/350/241/214/346/270/205/345/215/225.template.md +60 -0
  88. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  89. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  90. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/knowledge-town-paper-film-v1.json +80 -0
  91. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  92. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  93. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  94. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  95. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  96. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/sticker-asset-production.md +30 -0
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  98. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/build_cue_timeline.py +75 -0
  99. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_cue_timeline.py +68 -0
  100. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_style_preset.py +22 -0
  101. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  102. 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,22 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `dwy` 交互同步开头增加「同步范围」:完整同步(默认)或仅 Skills。仅 Skills 不写、不删 Rules / Commands / Hooks。
8
+ - 最后增加「Skill 同步位置」:默认仅项目目录;可另选 Claude / Grok / Codex / Cursor / OpenCode 全局目录,并勾选要额外拷贝的 skill。只覆盖点名 skill,不删用户 home 里其它个人 skill。选择写入 `/.dwy/sync-state.json` 的 `skillScope` / `syncMode`。
9
+
10
+ ---
11
+
12
+ ## 0.16.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 交互同步每一步选项支持关键字搜索(Skills / Rules / Commands / Hooks、平台选择、陈旧项删除、导航与汇总确认)。
17
+ - 选项说明改为列表下方固定「说明」公共区展示当前聚焦项完整描述,不再跟在选项行尾导致截断错乱。
18
+ - 新增 `src/searchable-select.js`(基于 `@clack/core` AutocompletePrompt 自定义 render);`@clack/prompts` 下限抬至 `^1.7.0`,显式依赖 `@clack/core`。
19
+
3
20
  ## 0.16.0
4
21
 
5
22
  ### Minor Changes
package/bin/index.js CHANGED
@@ -15,7 +15,7 @@ function printHelp() {
15
15
  `dwy ${pkg.version}`,
16
16
  '',
17
17
  'Usage:',
18
- ' dwy 运行交互式同步(自动更新全局外部 skill)',
18
+ ' dwy 交互同步:先选完整/仅 Skills,末步可选全局目录',
19
19
  ' dwy --help 显示本说明',
20
20
  ' dwy -h 显示本说明',
21
21
  ' dwy --version 显示当前版本',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
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
+ }