create-dwy 0.15.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/index.js +5 -3
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/skills-install.js +48 -7
  6. package/src/sync-all.js +17 -25
  7. package/src/sync.js +38 -32
  8. package/templates/ai-tools/CHANGELOG.md +76 -0
  9. package/templates/ai-tools/CLAUDE.md +27 -0
  10. package/templates/ai-tools/VERSION +1 -0
  11. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  12. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  13. package/templates/ai-tools/rules/Android/dwy-android-core.md +215 -4
  14. package/templates/ai-tools/rules/Android/dwy-android-pitfalls.md +52 -0
  15. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  17. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  18. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  19. package/templates/ai-tools/rules/Python/dwy-python-backend.md +16 -63
  20. package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +89 -0
  21. package/templates/ai-tools/rules/Python/dwy-python-pitfalls.md +49 -0
  22. package/templates/ai-tools/rules/Vue/dwy-vue-base-lib.md +127 -0
  23. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +22 -86
  24. package/templates/ai-tools/rules/Vue/dwy-vue-pitfalls.md +66 -0
  25. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  26. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +59 -0
  27. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  28. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +47 -2
  29. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +511 -0
  30. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +126 -0
  31. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +88 -0
  32. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +195 -0
  33. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +179 -0
  34. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +147 -0
  37. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +230 -0
  38. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +100 -0
  42. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +265 -0
  44. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +94 -0
  45. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +3 -2
  48. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +51 -9
  49. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +58 -0
  50. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +2 -1
  51. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-explore/SKILL.md +83 -0
  52. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +2 -1
  53. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -0
  54. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +14 -9
  55. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +65 -13
  56. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +30 -11
  57. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +3 -2
  58. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +17 -10
  59. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +42 -10
  60. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +22 -10
  61. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +13 -4
  62. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +13 -4
  63. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +12 -2
  64. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +15 -6
  65. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +2 -1
  66. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -0
  67. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  68. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  69. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  70. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  71. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  72. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  73. 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
  74. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-ideate/SKILL.md +0 -21
  75. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +0 -113
  76. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +0 -56
  77. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md +0 -117
  78. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +0 -92
  79. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +0 -72
  80. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/templates/prd-coarse.md +0 -61
  81. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +0 -76
  82. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/templates/prd-detailed.md +0 -119
  83. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +0 -104
  84. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +0 -81
  85. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +0 -212
  86. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/templates/impl-plan.md +0 -115
  87. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +0 -105
  88. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +0 -64
  89. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/templates/version-plan.md +0 -72
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
11
+ ## 0.16.0
12
+
13
+ ### Minor Changes
14
+
15
+ - `dwy` 开头自动检测更新全局外部 skill:未装 / 清单变更(cli 加了 skill 或改了 tag)才联网重装,已装且清单一致零开销;失败不阻塞 sync。用户无需再另记 `dwy skills install`。
16
+ - 新增 `dwy-commercial` 商业分析 skill(立项阶段 validate 之后、poc 之前):商业模式 / 变现路径 / 定价 / 单位经济(CAC/LTV/毛利/回本)/ GTM 获客 + 商业可行性结论;startup 模式为硬闸门、builder 模式跳过。包装 pm-skills 的 `business-model` / `monetization-strategy` / `pricing-strategy` / `startup-canvas` / `porters-five-forces` + `gtm-strategy`。
17
+ - `dwy skills install` 降级为隐藏的强制刷新兜底(向后兼容保留,日常无需手动);`printHelp` 只露 `dwy` 主入口,`dwy sync` 为别名。
18
+
3
19
  ## 0.15.0
4
20
 
5
21
  ### Minor Changes
package/bin/index.js CHANGED
@@ -15,14 +15,16 @@ function printHelp() {
15
15
  `dwy ${pkg.version}`,
16
16
  '',
17
17
  'Usage:',
18
- ' dwy 运行交互式同步',
19
- ' dwy sync 运行交互式同步',
20
- ' dwy skills install 安装/更新全局外部 skill 到 ~/.dwy/skills/',
18
+ ' dwy 运行交互式同步(自动更新全局外部 skill)',
21
19
  ' dwy --help 显示本说明',
22
20
  ' dwy -h 显示本说明',
23
21
  ' dwy --version 显示当前版本',
24
22
  ' dwy -V 显示当前版本',
25
23
  '',
24
+ '以下为隐藏命令(向后兼容保留,日常无需手动调用):',
25
+ ' dwy sync 同上的别名',
26
+ ' dwy skills install 强制刷新全局外部 skill',
27
+ '',
26
28
  ]
27
29
  console.log(lines.join('\n'))
28
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.15.0",
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/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
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import os from 'node:os'
4
+ import { createHash } from 'node:crypto'
4
5
  import { getDwyHomeDir, runGit, chalk } from './utils.js'
5
6
 
6
7
  function logAction(label) {
@@ -29,6 +30,12 @@ const SKILL_SOURCES = {
29
30
  'test-scenarios': 'pm-execution/skills/test-scenarios',
30
31
  'release-notes': 'pm-execution/skills/release-notes',
31
32
  'gtm-strategy': 'pm-go-to-market/skills/gtm-strategy',
33
+ // 商业分析(dwy-commercial 依赖):商业模式 / 变现 / 定价 / 创业画布 / 赛道五力
34
+ 'business-model': 'pm-product-strategy/skills/business-model',
35
+ 'monetization-strategy': 'pm-product-strategy/skills/monetization-strategy',
36
+ 'pricing-strategy': 'pm-product-strategy/skills/pricing-strategy',
37
+ 'startup-canvas': 'pm-product-strategy/skills/startup-canvas',
38
+ 'porters-five-forces': 'pm-product-strategy/skills/porters-five-forces',
32
39
  'intended-vs-implemented': 'pm-ai-shipping/skills/intended-vs-implemented',
33
40
  'north-star-metric': 'pm-marketing-growth/skills/north-star-metric',
34
41
  },
@@ -47,6 +54,13 @@ const SKILL_SOURCES = {
47
54
  },
48
55
  }
49
56
 
57
+ // 计算 SKILL_SOURCES 清单的内容指纹,用于检测「cli 加了 skill / 改了 tag」→ 需重装。
58
+ // SKILL_SOURCES 是源码内固定字面量,JSON.stringify 稳定(插入顺序固定)。
59
+ // 取 sha256 前 12 位,足以区分任意清单变更,无碰撞风险。
60
+ function computeManifestHash() {
61
+ return createHash('sha256').update(JSON.stringify(SKILL_SOURCES)).digest('hex').slice(0, 12)
62
+ }
63
+
50
64
  // 全局外部 skill 安装目录:~/.dwy/skills/
51
65
  function getGlobalSkillsDir() {
52
66
  return path.join(getDwyHomeDir(), 'skills')
@@ -92,22 +106,49 @@ export async function installSkills() {
92
106
  }
93
107
  }
94
108
 
95
- // 写版本清单,供查看与后续更新比对
96
- await fs.writeJson(path.join(targetDir, 'VERSIONS.json'), versions, { spaces: 2 })
109
+ // 写版本清单 + 清单指纹,供查看与后续更新比对。
110
+ // manifest_hash 变化(cli 加了 skill / 改了 tag)→ 下次 ensureSkillsInstalled 触发重装。
111
+ await fs.writeJson(
112
+ path.join(targetDir, 'VERSIONS.json'),
113
+ { manifest_hash: computeManifestHash(), versions },
114
+ { spaces: 2 },
115
+ )
97
116
  console.log(chalk.green(`\n✓ 外部 skill 已安装到 ${targetDir}`))
98
117
  } finally {
99
118
  await fs.remove(tmpRoot)
100
119
  }
101
120
  }
102
121
 
103
- // 全局 skill 是否已就绪(供 sync 自检:缺则自动装)。
122
+ // 全局 skill 是否已就绪(VERSIONS.json 存在)。
104
123
  export async function skillsInstalled() {
105
124
  return fs.pathExists(path.join(getGlobalSkillsDir(), 'VERSIONS.json'))
106
125
  }
107
126
 
127
+ // 确保 ~/.dwy/skills/ 与 cli 当前 SKILL_SOURCES 清单一致:未装 / 清单变更 → 自动重装。
128
+ // hash 一致 → 零开销跳过;失败 → warn 一行不阻塞主流程(离线/网络故障不挡 sync)。
129
+ // 返回 true = 本次执行了安装,false = 跳过。
108
130
  export async function ensureSkillsInstalled() {
109
- if (await skillsInstalled()) return false
110
- console.log(chalk.gray('未检测到全局外部 skill,自动安装...'))
111
- await installSkills()
112
- return true
131
+ const versionsFile = path.join(getGlobalSkillsDir(), 'VERSIONS.json')
132
+ // 已装:读 manifest_hash 比对,一致即跳过,不联网
133
+ if (await fs.pathExists(versionsFile)) {
134
+ let stored
135
+ try {
136
+ stored = await fs.readJson(versionsFile)
137
+ } catch {
138
+ stored = {}
139
+ }
140
+ // 兼容老格式(无 manifest_hash)→ 视为不一致,触发一次重装补上指纹
141
+ if (stored.manifest_hash === computeManifestHash()) return false
142
+ console.log(chalk.gray('全局外部 skill 清单已更新,重新拉取...'))
143
+ } else {
144
+ console.log(chalk.gray('未检测到全局外部 skill,自动安装...'))
145
+ }
146
+ try {
147
+ await installSkills()
148
+ return true
149
+ } catch (error) {
150
+ // 离线/网络故障:不阻塞主流程,下次再来
151
+ console.log(chalk.yellow(`外部 skill 自动更新失败:${error.message}`))
152
+ return false
153
+ }
113
154
  }
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, multiselect } from '@clack/prompts'
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,
@@ -26,18 +27,11 @@ import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
26
27
  import { syncOpenCode } from './sync-opencode.js'
27
28
  import { ensureSkillsInstalled } from './skills-install.js'
28
29
 
29
- // 产品 0→1 流程里依赖全局外部 skill 的包装型原子 skill。
30
- // 同步选中其中任一个时,触发全局外部 skill 自检安装。
31
- const WRAPPER_SKILL_NAMES = new Set([
32
- 'dwy-ideate', 'dwy-competitor', 'dwy-validate', 'dwy-mvp', 'dwy-prd',
33
- 'dwy-version', 'dwy-tasks', 'dwy-tdd-dev', 'dwy-acceptance', 'dwy-release',
34
- ])
35
-
36
30
  const PLATFORM_OPTIONS = [
37
- { value: 'claude', label: 'Claude Code', hint: '支持 rules / skills / commands / hooks' },
38
- { value: 'codex', label: 'Codex', hint: '支持 rules / skills / hooks' },
39
- { value: 'cursor', label: 'Cursor', hint: '支持 rules' },
40
- { value: 'opencode', label: 'OpenCode', hint: '支持 rules / skills / commands' },
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' },
41
35
  ]
42
36
 
43
37
  const CURSOR_BASELINE_RULE_NAME = '00-dwy-global.mdc'
@@ -159,12 +153,14 @@ async function selectPlatforms(projectDir) {
159
153
  console.log(chalk.gray(' Cursor → rules'))
160
154
  console.log(chalk.gray(' OpenCode → rules / skills / commands\n'))
161
155
 
162
- const result = await multiselect({
156
+ // 可搜索多选 + 底部说明区
157
+ const result = await searchableMultiselect({
163
158
  message: '选择要同步的平台',
164
159
  options: PLATFORM_OPTIONS,
165
160
  initialValues,
166
161
  required: true,
167
162
  maxItems: 6,
163
+ placeholder: SEARCH_PLACEHOLDER,
168
164
  })
169
165
  if (isCancel(result)) return null
170
166
  return result
@@ -317,11 +313,12 @@ async function promptStaleRemovals(staleEntries) {
317
313
  console.log(chalk.gray(` - ${name}`))
318
314
  }
319
315
 
320
- const selected = await multiselect({
316
+ const selected = await searchableMultiselect({
321
317
  message: `选择要删除的 ${PLATFORM_LABELS[platform]} ${TYPE_LABELS[type]}`,
322
318
  options: names.map(name => ({ value: name, label: name })),
323
319
  initialValues: names,
324
320
  required: false,
321
+ placeholder: SEARCH_PLACEHOLDER,
325
322
  })
326
323
  if (isCancel(selected)) return null
327
324
  approvals[platform][type] = new Set(selected)
@@ -567,6 +564,12 @@ export async function syncAll({
567
564
 
568
565
  const projectDir = projectDirOverride || process.cwd()
569
566
 
567
+ // 开头无条件自检全局外部 skill:未装 / 清单变更(cli 加了 skill 或改了 tag)则自动更新,
568
+ // 已装且清单一致则零开销跳过。失败不阻塞 sync(离线/网络故障,下次再来)。
569
+ // 这样用户跑一次 `dwy` 就把 skill 更新到位,无需另记 `dwy skills install`。
570
+ console.log(chalk.gray('检查全局外部 skill 更新...'))
571
+ await ensureSkillsInstalled()
572
+
570
573
  console.log(chalk.blue('\nScanning available Claude Code/Codex configuration...\n'))
571
574
  const scans = {
572
575
  skills: await scanSkills(sourceDir),
@@ -698,17 +701,6 @@ export async function syncAll({
698
701
  }
699
702
  await saveSyncState(projectDir, syncState)
700
703
 
701
- // 产品 0→1 流程的包装型原子 skill 依赖全局外部 skill(~/.dwy/skills/)。
702
- // 若本次同步选中了这些 skill 且全局未安装,自动装一次(缺则装、有则跳过)。
703
- if (selected.skills?.some((s) => WRAPPER_SKILL_NAMES.has(s.name))) {
704
- try {
705
- await ensureSkillsInstalled()
706
- } catch (error) {
707
- console.log(chalk.yellow(`\n外部 skill 自动安装失败:${error.message}`))
708
- console.log(chalk.gray('可稍后手动运行 `dwy skills install` 重试。'))
709
- }
710
- }
711
-
712
704
  if (!syncedAnySupportedPlatform) {
713
705
  console.log(chalk.yellow('未选择已接入平台,本次未执行同步。'))
714
706
  }