create-dwy 0.18.1 → 0.18.2

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,13 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.18.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 技术栈包与场景包都会在包名下列出子条目;选包后统一进入预勾清单,可逐条取消。
8
+
9
+ ---
10
+
3
11
  ## 0.18.1
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -121,14 +121,13 @@ function wrapHard(text, width) {
121
121
  * @param {number} contentWidth 可写字符宽
122
122
  * @returns {string[]}
123
123
  */
124
- function buildDescPanel(description, barPrefix, contentWidth) {
124
+ function buildDescPanel(description, barPrefix, contentWidth, lineCount = DESC_PANEL_LINES) {
125
125
  const raw = (description && description.trim()) ? description.trim() : '(无说明)'
126
126
  const wrapped = wrapHard(raw, contentWidth)
127
- // 固定 DESC_PANEL_LINES 行:超出截断末行加省略,不足补空
128
127
  const lines = []
129
- for (let i = 0; i < DESC_PANEL_LINES; i++) {
128
+ for (let i = 0; i < lineCount; i++) {
130
129
  let part = wrapped[i] ?? ''
131
- if (i === DESC_PANEL_LINES - 1 && wrapped.length > DESC_PANEL_LINES) {
130
+ if (i === lineCount - 1 && wrapped.length > lineCount) {
132
131
  const ellipsis = '…'
133
132
  part = (wrapped[i] ?? '').slice(0, Math.max(contentWidth - ellipsis.length, 0)) + ellipsis
134
133
  }
@@ -162,6 +161,7 @@ function resolveDescription(options, focusedValue) {
162
161
  * @param {unknown[]} [opts.initialValues]
163
162
  * @param {boolean} [opts.required]
164
163
  * @param {number} [opts.maxItems]
164
+ * @param {number} [opts.descLines] 底部说明区行数,包内子条目列表可加高
165
165
  * @param {string} [opts.placeholder]
166
166
  * @param {(search: string, opt: object) => boolean} [opts.filter]
167
167
  * @returns {Promise<unknown[] | symbol>}
@@ -233,6 +233,7 @@ export function searchableMultiselect(opts) {
233
233
  resolveDescription(this.filteredOptions, this.focusedValue),
234
234
  bar,
235
235
  contentWidth,
236
+ opts.descLines ?? DESC_PANEL_LINES,
236
237
  )
237
238
  const instructions = [
238
239
  `${styleText('dim', '↑/↓')} to navigate`,
package/src/sync-packs.js CHANGED
@@ -131,7 +131,7 @@ export function normalizePacks(raw) {
131
131
  * @param {object} pack
132
132
  * @param {'skills' | 'rules' | 'hooks'} type
133
133
  */
134
- function itemInPack(item, pack, type) {
134
+ export function itemInPack(item, pack, type) {
135
135
  if (type === 'skills') {
136
136
  if ((pack.skillNames || []).includes(item.name)) return true
137
137
  return (pack.skillCategories || []).includes(item.category)
@@ -163,6 +163,37 @@ function uniqueByName(items) {
163
163
  * @param {{ skills: object[], rules: object[], commands?: object[], hooks: object[] }} scans
164
164
  * @param {{ stacks?: string[], scenes?: string[], skillsOnly?: boolean }} selection
165
165
  */
166
+ /**
167
+ * 列出某包在当前扫描结果里的子条目,带 type,供界面展示「包下面有哪些」。
168
+ *
169
+ * @param {object} pack
170
+ * @param {{ skills?: object[], rules?: object[], hooks?: object[] }} scans
171
+ * @param {{ skillsOnly?: boolean }} [opts]
172
+ */
173
+ export function listPackItems(pack, scans, { skillsOnly = false } = {}) {
174
+ const types = skillsOnly ? ['skills'] : ['skills', 'rules', 'hooks']
175
+ const items = []
176
+ for (const type of types) {
177
+ for (const item of scans[type] || []) {
178
+ if (itemInPack(item, pack, type)) items.push({ ...item, type })
179
+ }
180
+ }
181
+ return items
182
+ }
183
+
184
+ /**
185
+ * 条目属于已选包时,用包的中文名做分组前缀。
186
+ *
187
+ * @param {{ name: string, category?: string }} item
188
+ * @param {'skills' | 'rules' | 'hooks'} type
189
+ * @param {string[]} packIds
190
+ */
191
+ export function packLabelForItem(item, type, packIds) {
192
+ const packs = [...STACK_PACKS, ...SCENE_PACKS]
193
+ const hit = packs.find(pack => packIds.includes(pack.id) && itemInPack(item, pack, type))
194
+ return hit?.label || item.category || '其他'
195
+ }
196
+
166
197
  export function expandPackSelection(scans, { stacks = [], scenes = [], skillsOnly = false } = {}) {
167
198
  const packById = new Map([...STACK_PACKS, ...SCENE_PACKS].map(pack => [pack.id, pack]))
168
199
  const selectedPacks = [...stacks, ...scenes]
package/src/sync-scene.js CHANGED
@@ -1,30 +1,47 @@
1
1
  /**
2
- * 按包勾选交互:技术栈包 → 场景包 → 展示已预勾的子条目(可取消)。
2
+ * 按包勾选交互:技术栈包 → 场景包(包下挂子条目)→ 预勾子条目可取消。
3
3
  * 由 dwy / dwy sync 在「按场景和技术栈」模式下调用。
4
4
  */
5
5
 
6
6
  import { isCancel } from '@clack/prompts'
7
7
  import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
8
- import { SCENE_PACKS, STACK_PACKS, expandPackSelection, normalizePacks } from './sync-packs.js'
8
+ import {
9
+ SCENE_PACKS,
10
+ STACK_PACKS,
11
+ expandPackSelection,
12
+ listPackItems,
13
+ normalizePacks,
14
+ packLabelForItem,
15
+ } from './sync-packs.js'
9
16
  import { chalk } from './utils.js'
10
17
 
11
- const TYPE_LABELS = {
12
- skills: 'Skills',
13
- rules: 'Rules',
14
- hooks: 'Hooks',
15
- }
16
-
17
18
  /**
18
- * 把包目录转成可搜索选项。
19
+ * 包名 + 其下子条目。子条目 disabled,只展示不参与勾选;勾的是整包。
19
20
  *
20
- * @param {Array<{ id: string, label: string, description: string }>} packs
21
+ * @param {object[]} packs
22
+ * @param {object} scans
23
+ * @param {{ skillsOnly?: boolean }} [opts]
21
24
  */
22
- function packOptions(packs) {
23
- return packs.map(pack => ({
24
- value: pack.id,
25
- label: pack.label,
26
- description: pack.description,
27
- }))
25
+ export function packOptionsWithChildren(packs, scans, { skillsOnly = false } = {}) {
26
+ const rows = []
27
+ for (const pack of packs) {
28
+ 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
+ }
43
+ }
44
+ return rows
28
45
  }
29
46
 
30
47
  /**
@@ -38,21 +55,24 @@ export function packItemKey(type, name) {
38
55
  }
39
56
 
40
57
  /**
41
- * 把包展开结果打成一条可搜索列表。label 含类型 + 分类 + 名字。
58
+ * 把包展开结果打成一条可搜索列表,按包名分组,便于对照取消。
42
59
  *
43
60
  * @param {{ skills: object[], rules: object[], hooks: object[] }} selected
61
+ * @param {string[]} packIds
44
62
  */
45
- export function buildPackItemOptions(selected) {
63
+ export function buildPackItemOptions(selected, packIds = []) {
46
64
  const rows = []
47
65
  for (const type of ['skills', 'rules', 'hooks']) {
48
66
  for (const item of selected[type] || []) {
67
+ const packLabel = packLabelForItem(item, type, packIds)
49
68
  rows.push({
50
69
  value: packItemKey(type, item.name),
51
- label: `${TYPE_LABELS[type]} / ${item.category || '其他'} / ${item.name}`,
70
+ label: `${packLabel} / ${item.name}`,
52
71
  description: item.description,
53
72
  })
54
73
  }
55
74
  }
75
+ rows.sort((a, b) => a.label.localeCompare(b.label, 'zh'))
56
76
  return rows
57
77
  }
58
78
 
@@ -89,7 +109,7 @@ function printPackSummary(stacks, scenes, selected) {
89
109
  }
90
110
 
91
111
  /**
92
- * 按包选择条目。栈/场景选完后展示聚合子条目,默认全勾,用户可取消。
112
+ * 按包选择条目。选包时列出包下子条目;确认后默认全勾,用户可取消。
93
113
  * 返回 { skills, rules, commands, hooks, packs } 或 null(取消)。
94
114
  *
95
115
  * @param {object} scans
@@ -101,19 +121,21 @@ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly =
101
121
  let stacks = previous.stacks.length > 0 ? previous.stacks : ['common']
102
122
  let scenes = previous.scenes
103
123
  let step = 'stacks'
124
+ const packOpts = { skillsOnly }
104
125
 
105
126
  while (true) {
106
127
  if (step === 'stacks') {
107
128
  const result = await searchableMultiselect({
108
- message: '选择技术栈(通用默认勾选)',
109
- options: packOptions(STACK_PACKS),
129
+ message: '选择技术栈(通用默认勾选;包下列出子条目,下一步可逐条取消)',
130
+ options: packOptionsWithChildren(STACK_PACKS, scans, packOpts),
110
131
  initialValues: stacks,
111
132
  required: true,
112
- maxItems: 12,
133
+ maxItems: 15,
113
134
  placeholder: SEARCH_PLACEHOLDER,
135
+ descLines: 8,
114
136
  })
115
137
  if (isCancel(result)) return null
116
- stacks = result
138
+ stacks = result.filter(id => !String(id).startsWith('hint:'))
117
139
 
118
140
  const nav = await searchableSelect({
119
141
  message: `已选 ${stacks.length} 个技术栈。下一步:`,
@@ -132,28 +154,30 @@ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly =
132
154
 
133
155
  if (step === 'scenes') {
134
156
  const result = await searchableMultiselect({
135
- message: '选择场景包(可全不选)',
136
- options: packOptions(SCENE_PACKS),
157
+ message: '选择场景包(可全不选;包下列出子条目,下一步可逐条取消)',
158
+ options: packOptionsWithChildren(SCENE_PACKS, scans, packOpts),
137
159
  initialValues: scenes,
138
160
  required: false,
139
- maxItems: 10,
161
+ maxItems: 15,
140
162
  placeholder: SEARCH_PLACEHOLDER,
163
+ descLines: 8,
141
164
  })
142
165
  if (isCancel(result)) return null
143
- scenes = result
166
+ scenes = result.filter(id => !String(id).startsWith('hint:'))
144
167
  step = 'items'
145
168
  continue
146
169
  }
147
170
 
148
171
  const expanded = expandPackSelection(scans, { stacks, scenes, skillsOnly })
149
172
  printPackSummary(stacks, scenes, expanded)
150
- const options = buildPackItemOptions(expanded)
173
+ const packIds = [...stacks, ...scenes]
174
+ const options = buildPackItemOptions(expanded, packIds)
151
175
  if (options.length === 0) {
152
176
  return { skills: [], rules: [], commands: [], hooks: [], packs: { stacks, scenes } }
153
177
  }
154
178
 
155
179
  const keys = await searchableMultiselect({
156
- message: '包已预勾子条目,取消不需要的即可',
180
+ message: '已按包预勾子条目,取消不需要的即可',
157
181
  options,
158
182
  initialValues: options.map(option => option.value),
159
183
  required: false,