create-dwy 0.18.0 → 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,22 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.18.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 技术栈包与场景包都会在包名下列出子条目;选包后统一进入预勾清单,可逐条取消。
8
+
9
+ ---
10
+
11
+ ## 0.18.1
12
+
13
+ ### Patch Changes
14
+
15
+ - `dwy` 菜单只留两项:同步全局 skills、同步项目配置。升级只走 `dwy upgrade`。
16
+ - 同步项目配置里分「按场景和技术栈」与「一个一个选」。按包后直接列出已预勾子条目,可取消。
17
+
18
+ ---
19
+
3
20
  ## 0.18.0
4
21
 
5
22
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli-help.js CHANGED
@@ -14,7 +14,7 @@ export function buildHelpText(version) {
14
14
  `dwy ${version}`,
15
15
  '',
16
16
  'Usage:',
17
- ' dwy 同步项目配置,或刷新全局外部 skill',
17
+ ' dwy 同步全局 skills,或同步项目配置',
18
18
  ' dwy sync 同上',
19
19
  ' dwy upgrade 把 dwy 升级到 npm 最新正式版',
20
20
  ' dwy --help 显示本说明',
@@ -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-all.js CHANGED
@@ -209,28 +209,24 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
209
209
  }
210
210
 
211
211
  /**
212
- * dwy 入口第一问:同步配置还是强制刷新全局外部 skill。
212
+ * dwy 入口第一问:同步全局 skills,或同步项目配置。
213
+ * 自升级只走 `dwy upgrade`,不出现在这张菜单。
213
214
  *
214
- * @returns {Promise<'sync' | 'install-skills' | 'upgrade' | null>}
215
+ * @returns {Promise<'sync' | 'install-skills' | null>}
215
216
  */
216
217
  async function promptAction() {
217
218
  const result = await searchableSelect({
218
219
  message: '选择要做的事',
219
220
  options: [
220
- {
221
- value: ACTION_SYNC,
222
- label: '同步项目 AI 配置',
223
- description: '按包或按条目,把 skills / rules / hooks 写入当前项目',
224
- },
225
221
  {
226
222
  value: ACTION_INSTALL_SKILLS,
227
- label: '刷新全局外部 skill',
223
+ label: '同步全局 skills',
228
224
  description: '强制重装 ~/.dwy/skills(pm-skills / superpowers)',
229
225
  },
230
226
  {
231
- value: ACTION_UPGRADE,
232
- label: '升级 dwy 到最新正式版',
233
- description: '等同 dwy upgrade,把全局 create-dwy 升到 npm latest',
227
+ value: ACTION_SYNC,
228
+ label: '同步项目配置',
229
+ description: '按场景和技术栈,或一条条勾选,写入当前项目',
234
230
  },
235
231
  ],
236
232
  initialValue: ACTION_SYNC,
@@ -276,16 +272,16 @@ async function promptSyncMode(initialValue) {
276
272
  */
277
273
  async function promptSelectionStyle(initialValue) {
278
274
  const result = await searchableSelect({
279
- message: '选择勾选方式',
275
+ message: '项目配置怎么勾',
280
276
  options: [
281
277
  {
282
278
  value: SELECTION_STYLE_PACKS,
283
- label: '按技术栈 / 场景包(推荐)',
284
- description: '先选 Vue/Python 等栈,再选产品0到1/自媒体等场景包',
279
+ label: '按场景和技术栈(推荐)',
280
+ description: '先选栈和场景包,再展示已勾子条目,可取消',
285
281
  },
286
282
  {
287
283
  value: SELECTION_STYLE_ITEMS,
288
- label: '按条目',
284
+ label: '一个一个选',
289
285
  description: '一条条勾选 Skills / Rules / Hooks',
290
286
  },
291
287
  ],
@@ -822,8 +818,8 @@ async function cleanupOpenCodePlatform(projectDir, scans) {
822
818
  }
823
819
 
824
820
  /**
825
- * `dwy` / `dwy sync` 入口。先选动作再分支:同步 / 刷新外部 skill / 自升级。
826
- * 测试注入 selected / action 时跳过动作菜单。
821
+ * `dwy` / `dwy sync` 入口。先选动作再分支:同步全局 skills / 同步项目配置。
822
+ * 自升级只走 `dwy upgrade`。测试注入 selected / action 时跳过动作菜单。
827
823
  *
828
824
  * @param {object} [opts]
829
825
  */
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,29 +1,99 @@
1
1
  /**
2
- * 按包勾选交互:技术栈包 → 场景包 → 确认/微调。
3
- * 由 dwy / dwy sync 在「按技术栈 / 场景包」模式下调用。
2
+ * 按包勾选交互:技术栈包 → 场景包(包下挂子条目)→ 预勾子条目可取消。
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'
9
- import { promptSelection } from './sync.js'
8
+ import {
9
+ SCENE_PACKS,
10
+ STACK_PACKS,
11
+ expandPackSelection,
12
+ listPackItems,
13
+ normalizePacks,
14
+ packLabelForItem,
15
+ } from './sync-packs.js'
10
16
  import { chalk } from './utils.js'
11
17
 
12
18
  /**
13
- * 把包目录转成可搜索选项。
19
+ * 包名 + 其下子条目。子条目 disabled,只展示不参与勾选;勾的是整包。
14
20
  *
15
- * @param {Array<{ id: string, label: string, description: string }>} packs
21
+ * @param {object[]} packs
22
+ * @param {object} scans
23
+ * @param {{ skillsOnly?: boolean }} [opts]
24
+ */
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
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}`
55
+ }
56
+
57
+ /**
58
+ * 把包展开结果打成一条可搜索列表,按包名分组,便于对照取消。
59
+ *
60
+ * @param {{ skills: object[], rules: object[], hooks: object[] }} selected
61
+ * @param {string[]} packIds
16
62
  */
17
- function packOptions(packs) {
18
- return packs.map(pack => ({
19
- value: pack.id,
20
- label: pack.label,
21
- description: pack.description,
22
- }))
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
+ })
73
+ }
74
+ }
75
+ rows.sort((a, b) => a.label.localeCompare(b.label, 'zh'))
76
+ return rows
23
77
  }
24
78
 
25
79
  /**
26
- * 打印包展开条数。确认步默认用这些预选项,微调再加减。
80
+ * 按用户勾选的 key 从展开结果里筛子条目。未勾的丢掉。
81
+ *
82
+ * @param {{ skills: object[], rules: object[], hooks: object[] }} expanded
83
+ * @param {string[]} keys
84
+ */
85
+ export function splitPackItemKeys(expanded, keys) {
86
+ const set = new Set(keys)
87
+ 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))),
91
+ commands: [],
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 打印包展开条数,随后立刻列出预勾子条目。
27
97
  *
28
98
  * @param {string[]} stacks
29
99
  * @param {string[]} scenes
@@ -39,7 +109,7 @@ function printPackSummary(stacks, scenes, selected) {
39
109
  }
40
110
 
41
111
  /**
42
- * 按包选择条目。
112
+ * 按包选择条目。选包时列出包下子条目;确认后默认全勾,用户可取消。
43
113
  * 返回 { skills, rules, commands, hooks, packs } 或 null(取消)。
44
114
  *
45
115
  * @param {object} scans
@@ -51,22 +121,21 @@ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly =
51
121
  let stacks = previous.stacks.length > 0 ? previous.stacks : ['common']
52
122
  let scenes = previous.scenes
53
123
  let step = 'stacks'
54
- /** 微调后的展开结果;返回改包时丢弃,重新 expand。 */
55
- let selected = null
124
+ const packOpts = { skillsOnly }
56
125
 
57
126
  while (true) {
58
127
  if (step === 'stacks') {
59
128
  const result = await searchableMultiselect({
60
- message: '选择技术栈(通用默认勾选)',
61
- options: packOptions(STACK_PACKS),
129
+ message: '选择技术栈(通用默认勾选;包下列出子条目,下一步可逐条取消)',
130
+ options: packOptionsWithChildren(STACK_PACKS, scans, packOpts),
62
131
  initialValues: stacks,
63
132
  required: true,
64
- maxItems: 12,
133
+ maxItems: 15,
65
134
  placeholder: SEARCH_PLACEHOLDER,
135
+ descLines: 8,
66
136
  })
67
137
  if (isCancel(result)) return null
68
- stacks = result
69
- selected = null
138
+ stacks = result.filter(id => !String(id).startsWith('hint:'))
70
139
 
71
140
  const nav = await searchableSelect({
72
141
  message: `已选 ${stacks.length} 个技术栈。下一步:`,
@@ -85,62 +154,40 @@ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly =
85
154
 
86
155
  if (step === 'scenes') {
87
156
  const result = await searchableMultiselect({
88
- message: '选择场景包(可全不选)',
89
- options: packOptions(SCENE_PACKS),
157
+ message: '选择场景包(可全不选;包下列出子条目,下一步可逐条取消)',
158
+ options: packOptionsWithChildren(SCENE_PACKS, scans, packOpts),
90
159
  initialValues: scenes,
91
160
  required: false,
92
- maxItems: 10,
161
+ maxItems: 15,
93
162
  placeholder: SEARCH_PLACEHOLDER,
163
+ descLines: 8,
94
164
  })
95
165
  if (isCancel(result)) return null
96
- scenes = result
97
- selected = null
98
- step = 'confirm'
166
+ scenes = result.filter(id => !String(id).startsWith('hint:'))
167
+ step = 'items'
99
168
  continue
100
169
  }
101
170
 
102
- selected = selected || expandPackSelection(scans, { stacks, scenes, skillsOnly })
103
- printPackSummary(stacks, scenes, selected)
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 } }
177
+ }
104
178
 
105
- const confirmOptions = [
106
- { label: '确认提交(用上面勾好的)', value: 'confirm' },
107
- { label: '微调 Skills(可加减)', value: 'skills' },
108
- ...(!skillsOnly ? [
109
- { label: '微调 Rules(可加减)', value: 'rules' },
110
- { label: '微调 Hooks(可加减)', value: 'hooks' },
111
- ] : []),
112
- { label: '返回改技术栈', value: 'stacks' },
113
- { label: '返回改场景包', value: 'scenes' },
114
- { label: '取消同步', value: 'cancel' },
115
- ]
116
- const final = await searchableSelect({
117
- message: '包已帮你勾好。确认提交,或微调加减条目:',
118
- options: confirmOptions,
119
- initialValue: 'confirm',
179
+ const keys = await searchableMultiselect({
180
+ message: '已按包预勾子条目,取消不需要的即可',
181
+ options,
182
+ initialValues: options.map(option => option.value),
183
+ required: false,
184
+ maxItems: 15,
120
185
  placeholder: SEARCH_PLACEHOLDER,
121
186
  })
122
- if (isCancel(final) || final === 'cancel') return null
123
- if (final === 'confirm') {
124
- return {
125
- ...selected,
126
- commands: [],
127
- packs: { stacks, scenes },
128
- }
187
+ if (isCancel(keys)) return null
188
+ return {
189
+ ...splitPackItemKeys(expanded, keys),
190
+ packs: { stacks, scenes },
129
191
  }
130
- if (final === 'stacks' || final === 'scenes') {
131
- step = final
132
- selected = null
133
- continue
134
- }
135
-
136
- // 完整清单 + 包内项预勾:可减包内项,也可加包外项
137
- const catalog = scans[final] || []
138
- const tuned = await promptSelection(
139
- catalog,
140
- final === 'skills' ? 'Skills' : final === 'rules' ? 'Rules' : 'Hooks',
141
- new Set(selected[final].map(item => item.name)),
142
- )
143
- if (tuned === null) return null
144
- selected = { ...selected, [final]: tuned }
145
192
  }
146
193
  }