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.
- package/CHANGELOG.md +8 -0
- package/bin/index.js +0 -0
- package/package.json +3 -2
- package/src/searchable-select.js +419 -0
- package/src/sync-all.js +11 -7
- package/src/sync.js +38 -32
- package/templates/ai-tools/CHANGELOG.md +76 -0
- package/templates/ai-tools/VERSION +1 -0
- package/templates/ai-tools/hook-manifests/hooks.json +7 -0
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
- package/templates/ai-tools/rules/Android/dwy-android-core.md +215 -4
- package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +511 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +126 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +88 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +195 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +179 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +147 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +230 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +100 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +265 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +94 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
- 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.
|
|
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/
|
|
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
|
|
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',
|
|
31
|
-
{ value: 'codex', label: 'Codex',
|
|
32
|
-
{ value: 'cursor', label: 'Cursor',
|
|
33
|
-
{ value: 'opencode', label: 'OpenCode',
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
244
|
+
// 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
|
|
245
|
+
const selectedNames = await searchableMultiselect({
|
|
242
246
|
message: `选择 ${label}`,
|
|
243
|
-
options:
|
|
247
|
+
options: buildSearchableOptions(items),
|
|
244
248
|
initialValues: [...defaultNames],
|
|
245
249
|
maxItems: 15,
|
|
246
250
|
required: false,
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
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
|