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.
- package/CHANGELOG.md +16 -0
- package/bin/index.js +5 -3
- package/package.json +3 -2
- package/src/searchable-select.js +419 -0
- package/src/skills-install.js +48 -7
- package/src/sync-all.js +17 -25
- package/src/sync.js +38 -32
- package/templates/ai-tools/CHANGELOG.md +76 -0
- package/templates/ai-tools/CLAUDE.md +27 -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/Android/dwy-android-pitfalls.md +52 -0
- 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/Python/dwy-python-backend.md +16 -63
- package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +89 -0
- package/templates/ai-tools/rules/Python/dwy-python-pitfalls.md +49 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-base-lib.md +127 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +22 -86
- package/templates/ai-tools/rules/Vue/dwy-vue-pitfalls.md +66 -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-dependency-freshness.md +59 -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/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +47 -2
- 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//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +3 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +51 -9
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +58 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-explore/SKILL.md +83 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +14 -9
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +65 -13
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +30 -11
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +3 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +17 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +42 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +22 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +13 -4
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +13 -4
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +12 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +15 -6
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -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/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-ideate/SKILL.md +0 -21
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +0 -113
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +0 -56
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md +0 -117
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +0 -92
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +0 -72
- 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
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +0 -76
- 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
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +0 -104
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +0 -81
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +0 -212
- 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
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +0 -105
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +0 -64
- 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.
|
|
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/skills-install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
await
|
|
112
|
-
|
|
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
|
|
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',
|
|
38
|
-
{ value: 'codex', label: 'Codex',
|
|
39
|
-
{ value: 'cursor', label: 'Cursor',
|
|
40
|
-
{ 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' },
|
|
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
|
-
|
|
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
|
|
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
|
}
|