create-dwy 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.18.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 按包勾选改用 `@clack/prompts` 的 `groupMultiselect`:Space 勾父级=全选子项,子项可单独取消。去掉「下一步再取消子项」那一屏。
8
+
9
+ ---
10
+
11
+ ## 0.18.3
12
+
13
+ ### Patch Changes
14
+
15
+ - 技术栈/场景包下的子条目随父包显示为已勾选,不再画成删除线空框。逐条取消仍在下一步预勾清单里。
16
+
17
+ ---
18
+
3
19
  ## 0.18.2
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -263,7 +263,7 @@ export function searchableMultiselect(opts) {
263
263
  ? styleText('green', S_CHECKBOX_SELECTED)
264
264
  : styleText('dim', S_CHECKBOX_INACTIVE)
265
265
  if (opt.disabled) {
266
- return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`
266
+ return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'dim'], label)}`
267
267
  }
268
268
  // 聚焦也不拼 hint,描述只在底部公共区
269
269
  if (active) {
package/src/sync-scene.js CHANGED
@@ -1,99 +1,118 @@
1
1
  /**
2
- * 按包勾选交互:技术栈包 → 场景包(包下挂子条目)→ 预勾子条目可取消。
2
+ * 按包勾选交互:技术栈包 → 场景包。
3
+ * 用 @clack/prompts groupMultiselect:勾父级=全选子项,子项可单独取消。
3
4
  * 由 dwy / dwy sync 在「按场景和技术栈」模式下调用。
4
5
  */
5
6
 
6
- import { isCancel } from '@clack/prompts'
7
- import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
7
+ import { groupMultiselect, isCancel } from '@clack/prompts'
8
+ import { SEARCH_PLACEHOLDER, searchableSelect } from './searchable-select.js'
8
9
  import {
9
10
  SCENE_PACKS,
10
11
  STACK_PACKS,
11
- expandPackSelection,
12
12
  listPackItems,
13
13
  normalizePacks,
14
- packLabelForItem,
15
14
  } from './sync-packs.js'
16
15
  import { chalk } from './utils.js'
17
16
 
17
+ /** 子 value 三段分隔:packId / type / name。packId 不含冒号。 */
18
+ const PACK_CHILD_RE = /^([^:]+):(skills|rules|hooks):(.+)$/
19
+
20
+ /**
21
+ * 子条目在 groupMultiselect 里的稳定 value。
22
+ * 带 packId:同一 skill 可同时出现在 Vue 与 Python,取消一边不能误伤另一边。
23
+ *
24
+ * @param {string} packId
25
+ * @param {'skills' | 'rules' | 'hooks'} type
26
+ * @param {string} name
27
+ */
28
+ export function packChildKey(packId, type, name) {
29
+ return `${packId}:${type}:${name}`
30
+ }
31
+
18
32
  /**
19
- * 包名 + 其下子条目。子条目 disabled,只展示不参与勾选;勾的是整包。
33
+ * 解析 packChildKey。对不上的丢掉,避免脏 initialValues 撑进结果。
34
+ *
35
+ * @param {unknown} key
36
+ * @returns {{ packId: string, type: 'skills' | 'rules' | 'hooks', name: string } | null}
37
+ */
38
+ export function parsePackChildKey(key) {
39
+ const match = String(key).match(PACK_CHILD_RE)
40
+ if (!match) return null
41
+ return { packId: match[1], type: match[2], name: match[3] }
42
+ }
43
+
44
+ /**
45
+ * groupMultiselect 的 options:key 是分组标题(包名),value 是可单独勾的子项。
46
+ * 空包跳过——clack 空组 `every()` 恒 true,会显示成已全选。
20
47
  *
21
48
  * @param {object[]} packs
22
49
  * @param {object} scans
23
50
  * @param {{ skillsOnly?: boolean }} [opts]
51
+ * @returns {Record<string, { value: string, label: string, hint?: string }[]>}
24
52
  */
25
- export function packOptionsWithChildren(packs, scans, { skillsOnly = false } = {}) {
26
- const rows = []
53
+ export function packGroupOptions(packs, scans, { skillsOnly = false } = {}) {
54
+ const groups = {}
27
55
  for (const pack of packs) {
28
56
  const items = listPackItems(pack, scans, { skillsOnly })
29
- const names = items.map(item => item.name)
30
- rows.push({
31
- value: pack.id,
32
- label: `${pack.label}(${items.length})`,
33
- description: names.length > 0 ? names.join('、') : pack.description,
34
- })
35
- for (const item of items) {
36
- rows.push({
37
- value: `hint:${pack.id}:${item.type}:${item.name}`,
38
- label: ` ${item.name}`,
39
- description: item.description || pack.label,
40
- disabled: true,
41
- })
42
- }
57
+ if (items.length === 0) continue
58
+ groups[`${pack.label}(${items.length})`] = items.map(item => ({
59
+ value: packChildKey(pack.id, item.type, item.name),
60
+ label: item.name,
61
+ hint: item.description || undefined,
62
+ }))
43
63
  }
44
- return rows
45
- }
46
-
47
- /**
48
- * 子条目在多选里的稳定 key。类型前缀避免 skill/rule 重名撞车。
49
- *
50
- * @param {'skills' | 'rules' | 'hooks'} type
51
- * @param {string} name
52
- */
53
- export function packItemKey(type, name) {
54
- return `${type}:${name}`
64
+ return groups
55
65
  }
56
66
 
57
67
  /**
58
- * 把包展开结果打成一条可搜索列表,按包名分组,便于对照取消。
68
+ * 已选包 → 其下全部子 value,给 groupMultiselect initialValues。
69
+ * 父级勾选态由 clack 根据「子是否全在 value 里」推导,不要传包名。
59
70
  *
60
- * @param {{ skills: object[], rules: object[], hooks: object[] }} selected
61
- * @param {string[]} packIds
71
+ * @param {object[]} packs
72
+ * @param {object} scans
73
+ * @param {string[]} selectedPackIds
74
+ * @param {{ skillsOnly?: boolean }} [opts]
62
75
  */
63
- export function buildPackItemOptions(selected, packIds = []) {
64
- const rows = []
65
- for (const type of ['skills', 'rules', 'hooks']) {
66
- for (const item of selected[type] || []) {
67
- const packLabel = packLabelForItem(item, type, packIds)
68
- rows.push({
69
- value: packItemKey(type, item.name),
70
- label: `${packLabel} / ${item.name}`,
71
- description: item.description,
72
- })
76
+ export function initialChildValues(packs, scans, selectedPackIds, { skillsOnly = false } = {}) {
77
+ const selected = new Set(selectedPackIds)
78
+ const values = []
79
+ for (const pack of packs) {
80
+ if (!selected.has(pack.id)) continue
81
+ for (const item of listPackItems(pack, scans, { skillsOnly })) {
82
+ values.push(packChildKey(pack.id, item.type, item.name))
73
83
  }
74
84
  }
75
- rows.sort((a, b) => a.label.localeCompare(b.label, 'zh'))
76
- return rows
85
+ return values
77
86
  }
78
87
 
79
88
  /**
80
- * 按用户勾选的 key 从展开结果里筛子条目。未勾的丢掉。
89
+ * 把勾上的子 value 还原成条目 + 所属包。
90
+ * 只认扫描结果里真实存在的名字,防止 key 伪造。
81
91
  *
82
- * @param {{ skills: object[], rules: object[], hooks: object[] }} expanded
83
92
  * @param {string[]} keys
93
+ * @param {object} scans
94
+ * @param {{ skillsOnly?: boolean }} [opts]
84
95
  */
85
- export function splitPackItemKeys(expanded, keys) {
86
- const set = new Set(keys)
96
+ export function itemsFromChildKeys(keys, scans, { skillsOnly = false } = {}) {
97
+ const parsed = keys.map(parsePackChildKey).filter(Boolean)
98
+ const packIds = [...new Set(parsed.map(item => item.packId))]
99
+ const names = { skills: new Set(), rules: new Set(), hooks: new Set() }
100
+ for (const item of parsed) names[item.type].add(item.name)
101
+ const pick = (type) => (scans[type] || []).filter(item => names[type].has(item.name))
102
+ if (skillsOnly) {
103
+ return { packIds, skills: pick('skills'), rules: [], commands: [], hooks: [] }
104
+ }
87
105
  return {
88
- skills: expanded.skills.filter(item => set.has(packItemKey('skills', item.name))),
89
- rules: expanded.rules.filter(item => set.has(packItemKey('rules', item.name))),
90
- hooks: expanded.hooks.filter(item => set.has(packItemKey('hooks', item.name))),
106
+ packIds,
107
+ skills: pick('skills'),
108
+ rules: pick('rules'),
91
109
  commands: [],
110
+ hooks: pick('hooks'),
92
111
  }
93
112
  }
94
113
 
95
114
  /**
96
- * 打印包展开条数,随后立刻列出预勾子条目。
115
+ * 打印本次按包勾选的包名与条数。
97
116
  *
98
117
  * @param {string[]} stacks
99
118
  * @param {string[]} scenes
@@ -109,7 +128,7 @@ function printPackSummary(stacks, scenes, selected) {
109
128
  }
110
129
 
111
130
  /**
112
- * 按包选择条目。选包时列出包下子条目;确认后默认全勾,用户可取消。
131
+ * 按包选择条目。Space 勾父级=该包全部子项;光标在子项上可单独取消。
113
132
  * 返回 { skills, rules, commands, hooks, packs } 或 null(取消)。
114
133
  *
115
134
  * @param {object} scans
@@ -118,24 +137,31 @@ function printPackSummary(stacks, scenes, selected) {
118
137
  */
119
138
  export async function interactiveSelectByPacks(scans, syncState, { skillsOnly = false } = {}) {
120
139
  const previous = normalizePacks(syncState?.packs)
121
- let stacks = previous.stacks.length > 0 ? previous.stacks : ['common']
140
+ const packOpts = { skillsOnly }
141
+ const defaultStacks = previous.stacks.length > 0 ? previous.stacks : ['common']
142
+ let stacks = defaultStacks
122
143
  let scenes = previous.scenes
144
+ // 返回重选时保留已取消的子项,不要用包 id 重新全勾
145
+ let stackKeys = initialChildValues(STACK_PACKS, scans, defaultStacks, packOpts)
146
+ let sceneKeys = initialChildValues(SCENE_PACKS, scans, scenes, packOpts)
123
147
  let step = 'stacks'
124
- const packOpts = { skillsOnly }
125
148
 
126
149
  while (true) {
127
150
  if (step === 'stacks') {
128
- const result = await searchableMultiselect({
129
- message: '选择技术栈(通用默认勾选;包下列出子条目,下一步可逐条取消)',
130
- options: packOptionsWithChildren(STACK_PACKS, scans, packOpts),
131
- initialValues: stacks,
151
+ const options = packGroupOptions(STACK_PACKS, scans, packOpts)
152
+ const result = await groupMultiselect({
153
+ message: '选择技术栈(Space 勾整包,也可单独取消某个子项)',
154
+ options,
155
+ initialValues: stackKeys,
132
156
  required: true,
133
157
  maxItems: 15,
134
- placeholder: SEARCH_PLACEHOLDER,
135
- descLines: 8,
158
+ selectableGroups: true,
136
159
  })
137
160
  if (isCancel(result)) return null
138
- stacks = result.filter(id => !String(id).startsWith('hint:'))
161
+ stackKeys = result
162
+ stacks = STACK_PACKS.map(pack => pack.id).filter(id =>
163
+ stackKeys.some(key => parsePackChildKey(key)?.packId === id),
164
+ )
139
165
 
140
166
  const nav = await searchableSelect({
141
167
  message: `已选 ${stacks.length} 个技术栈。下一步:`,
@@ -152,41 +178,33 @@ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly =
152
178
  continue
153
179
  }
154
180
 
155
- if (step === 'scenes') {
156
- const result = await searchableMultiselect({
157
- message: '选择场景包(可全不选;包下列出子条目,下一步可逐条取消)',
158
- options: packOptionsWithChildren(SCENE_PACKS, scans, packOpts),
159
- initialValues: scenes,
181
+ const sceneOptions = packGroupOptions(SCENE_PACKS, scans, packOpts)
182
+ if (Object.keys(sceneOptions).length > 0) {
183
+ const result = await groupMultiselect({
184
+ message: '选择场景包(可全不选;Space 勾整包,也可单独取消某个子项)',
185
+ options: sceneOptions,
186
+ initialValues: sceneKeys,
160
187
  required: false,
161
188
  maxItems: 15,
162
- placeholder: SEARCH_PLACEHOLDER,
163
- descLines: 8,
189
+ selectableGroups: true,
164
190
  })
165
191
  if (isCancel(result)) return null
166
- scenes = result.filter(id => !String(id).startsWith('hint:'))
167
- step = 'items'
168
- continue
169
- }
170
-
171
- const expanded = expandPackSelection(scans, { stacks, scenes, skillsOnly })
172
- printPackSummary(stacks, scenes, expanded)
173
- const packIds = [...stacks, ...scenes]
174
- const options = buildPackItemOptions(expanded, packIds)
175
- if (options.length === 0) {
176
- return { skills: [], rules: [], commands: [], hooks: [], packs: { stacks, scenes } }
192
+ sceneKeys = result
193
+ scenes = SCENE_PACKS.map(pack => pack.id).filter(id =>
194
+ sceneKeys.some(key => parsePackChildKey(key)?.packId === id),
195
+ )
196
+ } else {
197
+ sceneKeys = []
198
+ scenes = []
177
199
  }
178
200
 
179
- const keys = await searchableMultiselect({
180
- message: '已按包预勾子条目,取消不需要的即可',
181
- options,
182
- initialValues: options.map(option => option.value),
183
- required: false,
184
- maxItems: 15,
185
- placeholder: SEARCH_PLACEHOLDER,
186
- })
187
- if (isCancel(keys)) return null
201
+ const selected = itemsFromChildKeys([...stackKeys, ...sceneKeys], scans, packOpts)
202
+ printPackSummary(stacks, scenes, selected)
188
203
  return {
189
- ...splitPackItemKeys(expanded, keys),
204
+ skills: selected.skills,
205
+ rules: selected.rules,
206
+ commands: selected.commands,
207
+ hooks: selected.hooks,
190
208
  packs: { stacks, scenes },
191
209
  }
192
210
  }