create-dwy 0.19.1 → 0.21.0
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 +25 -0
- package/package.json +1 -1
- package/src/cli-help.js +31 -2
- package/src/sync-all.js +168 -577
- package/src/sync-codex.js +42 -28
- package/src/sync-cursor.js +0 -25
- package/src/sync-global-skills.js +2 -2
- package/src/sync-opencode.js +7 -33
- package/src/sync.js +59 -226
- package/src/tab-multiselect.js +15 -53
- package/src/utils.js +0 -1
- package/templates/ai-tools/AGENTS.md +223 -0
- package/templates/ai-tools/CHANGELOG.md +195 -0
- package/templates/ai-tools/VERSION +1 -1
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +5 -4
- package/templates/ai-tools/rules/Android/dwy-android-core.md +147 -13
- package/templates/ai-tools/rules/Android/dwy-android-layering.md +142 -0
- package/templates/ai-tools/rules/Android/dwy-android-mvp.md +55 -0
- package/templates/ai-tools/rules/Android/dwy-android-support-only.md +2 -0
- package/templates/ai-tools/rules/Docker/dwy-docker.md +23 -1
- package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +2 -0
- package/templates/ai-tools/rules/Flutter/dwy-flutter-testing.md +1 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +12 -8
- package/templates/ai-tools/rules/Python/dwy-python-backend.md +4 -43
- package/templates/ai-tools/rules/Python/dwy-python-layering.md +169 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +11 -49
- package/templates/ai-tools/rules/Vue/dwy-vue-layering.md +153 -0
- package/templates/ai-tools/rules/iOS/dwy-apple-layering.md +133 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +2 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-craft.md +83 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +27 -66
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +8 -219
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +27 -123
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prod-branch.md +31 -0
- package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-postgres.md +17 -10
- package/templates/ai-tools/rules//346/225/260/346/215/256/345/272/223/dwy-redis.md +6 -3
- package/templates/ai-tools/skills/Docker/dwy-deploy-first/SKILL.md +14 -14
- package/templates/ai-tools/skills/Docker/dwy-deploy-first/references/chapter-01-mirrors.md +2 -2
- package/templates/ai-tools/skills/Docker/dwy-docker/SKILL.md +21 -13
- package/templates/ai-tools/skills/Docker/dwy-docker/references/ask-templates.md +13 -12
- package/templates/ai-tools/skills/Docker/dwy-docker/references/templates.md +3 -1
- package/templates/ai-tools/skills/Docker/dwy-docker/references/version-rules.md +10 -6
- package/templates/ai-tools/skills/Docker/dwy-docker/scripts/query_dockerhub.py +21 -6
- package/templates/ai-tools/skills/Docker/dwy-docker/scripts/scan_images.sh +2 -2
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/SKILL.md +44 -30
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/dockerfile-snippets.md +2 -0
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/references/mirror-providers.md +8 -8
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/apply_mirrors.py +14 -1
- package/templates/ai-tools/skills/Docker/dwy-mirror-source/scripts/check_mirrors.py +4 -0
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/SKILL.md +8 -6
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/python-sdk.md +3 -3
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/digest/stream-engine.md +2 -2
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/anti-patterns.md +3 -1
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/change-checklist.md +4 -4
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/lethal-violations.md +5 -5
- package/templates/ai-tools/skills/DolphinDB/dwy-dolphindb/references/review-rules/perf-baselines.md +5 -5
- package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +2 -2
- package/templates/ai-tools/skills/Python/dwy-eapi/references/tasks-integration-guide.md +4 -4
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +3 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +28 -3
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +2 -2
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +12 -2
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +1 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +12 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +2 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +10 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +5 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +11 -14
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +1 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +1 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +32 -15
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/electron-release-workflow-template.yml +3 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/release-workflow-template.yml +8 -5
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/aso.md +2 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +4 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +45 -27
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +8 -0
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +7 -7
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +63 -59
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_base.sh +10 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_capacity.sh +21 -23
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_db.sh +9 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +20 -24
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_logs.sh +3 -3
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +4 -2
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +1 -1
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +1 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +7 -5
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +5 -3
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +140 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +113 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-shared/SKILL.md +17 -14
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
- package/templates/project/fullstack-monorepo/{CLAUDE.md.hbs → AGENTS.md.hbs} +1 -1
- package/src/sync-packs.js +0 -223
- package/src/sync-scene.js +0 -178
- package/templates/ai-tools/CLAUDE.md +0 -210
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
- /package/templates/project/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /package/templates/project/fullstack-monorepo/backend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /package/templates/project/fullstack-monorepo/frontend/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
- /package/templates/project/web/{CLAUDE.md.hbs → AGENTS.md.hbs} +0 -0
package/src/sync.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { isCancel } from '@clack/prompts'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
searchableMultiselect,
|
|
7
|
-
searchableSelect,
|
|
8
|
-
} from './searchable-select.js'
|
|
9
|
-
import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } from './utils.js'
|
|
4
|
+
import { tabMultiselect } from './tab-multiselect.js'
|
|
5
|
+
import { chalk, PACKAGE_ROOT } from './utils.js'
|
|
10
6
|
|
|
11
7
|
const CATEGORIES = [
|
|
12
8
|
{ key: 'skills', label: 'Skills' },
|
|
13
9
|
{ key: 'rules', label: 'Rules' },
|
|
14
|
-
{ key: 'commands', label: 'Commands' },
|
|
15
|
-
{ key: 'hooks', label: 'Hooks' },
|
|
16
10
|
]
|
|
17
11
|
|
|
18
12
|
const COPY_EXCLUDE_PATTERNS = [
|
|
@@ -26,14 +20,9 @@ const COPY_EXCLUDE_PATTERNS = [
|
|
|
26
20
|
/\.pyc$/,
|
|
27
21
|
]
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
['templates', 'ai-tools'],
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
export function copyFilter(src) {
|
|
36
|
-
return !COPY_EXCLUDE_PATTERNS.some(re => re.test(src))
|
|
23
|
+
/** 只过滤模板内部缓存,避免 npm 安装路径中的 node_modules 导致整份模板被跳过。 */
|
|
24
|
+
export function copyFilter(sourceRoot) {
|
|
25
|
+
return src => !COPY_EXCLUDE_PATTERNS.some(re => re.test(path.relative(sourceRoot, src)))
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
export function extractDescription(content) {
|
|
@@ -147,27 +136,6 @@ export async function scanRules(sourceDir) {
|
|
|
147
136
|
return rules.sort((a, b) => a.name.localeCompare(b.name))
|
|
148
137
|
}
|
|
149
138
|
|
|
150
|
-
export async function scanCommands(sourceDir) {
|
|
151
|
-
const commandsDir = path.join(sourceDir, 'commands')
|
|
152
|
-
if (!await fs.pathExists(commandsDir)) return []
|
|
153
|
-
|
|
154
|
-
const entries = await fs.readdir(commandsDir, { withFileTypes: true })
|
|
155
|
-
const commands = await Promise.all(entries
|
|
156
|
-
.filter(e => e.name !== '.gitkeep' && e.name !== '.DS_Store')
|
|
157
|
-
.map(async e => ({
|
|
158
|
-
name: e.name,
|
|
159
|
-
description: await readEntryDescription(
|
|
160
|
-
path.join(commandsDir, e.name),
|
|
161
|
-
e.isDirectory() ? '命令目录' : '命令文件',
|
|
162
|
-
),
|
|
163
|
-
category: e.isDirectory() ? '目录命令' : '文件命令',
|
|
164
|
-
sourcePath: path.join(commandsDir, e.name),
|
|
165
|
-
type: 'command',
|
|
166
|
-
})))
|
|
167
|
-
|
|
168
|
-
return commands.sort((a, b) => a.name.localeCompare(b.name))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
139
|
export async function scanHooks(sourceDir) {
|
|
172
140
|
const hooksDir = path.join(sourceDir, 'hooks')
|
|
173
141
|
if (!await fs.pathExists(hooksDir)) return []
|
|
@@ -201,7 +169,7 @@ export async function loadHookManifests(sourceDir) {
|
|
|
201
169
|
}
|
|
202
170
|
|
|
203
171
|
export async function readBaselineDoc(sourceDir) {
|
|
204
|
-
const baselinePath = path.join(sourceDir, '
|
|
172
|
+
const baselinePath = path.join(sourceDir, 'AGENTS.md')
|
|
205
173
|
if (!await fs.pathExists(baselinePath)) return ''
|
|
206
174
|
return fs.readFile(baselinePath, 'utf-8')
|
|
207
175
|
}
|
|
@@ -214,183 +182,69 @@ export async function scanExisting(projectTargetDir, typePlural) {
|
|
|
214
182
|
}
|
|
215
183
|
|
|
216
184
|
/**
|
|
217
|
-
*
|
|
218
|
-
* label 带分类前缀便于筛选;完整 description 进底部公共说明区(不截断塞 hint)。
|
|
185
|
+
* 按模板实际分类生成 Tab,每条独立勾选,不做包展开或跨分类关联。
|
|
219
186
|
*
|
|
220
187
|
* @param {Array<{ name: string, category?: string, description?: string }>} items
|
|
221
|
-
* @
|
|
188
|
+
* @param {'skills' | 'rules'} type
|
|
222
189
|
*/
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
value: item.name,
|
|
235
|
-
label: `${category} / ${item.name}`,
|
|
236
|
-
// 完整描述给底部公共区;搜索也匹配 description
|
|
237
|
-
description: item.description || undefined,
|
|
238
|
-
}
|
|
190
|
+
export function buildCategoryTabs(items, type) {
|
|
191
|
+
const groups = new Map()
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
const category = item.category || '其他'
|
|
194
|
+
if (!groups.has(category)) groups.set(category, [])
|
|
195
|
+
groups.get(category).push({
|
|
196
|
+
value: item.name,
|
|
197
|
+
label: item.name,
|
|
198
|
+
description: item.description,
|
|
199
|
+
type,
|
|
239
200
|
})
|
|
201
|
+
}
|
|
202
|
+
return [...groups.entries()]
|
|
203
|
+
.sort(([a], [b]) => a.localeCompare(b, 'zh'))
|
|
204
|
+
.map(([category, options]) => ({
|
|
205
|
+
id: category,
|
|
206
|
+
label: category,
|
|
207
|
+
options: options.sort((a, b) => a.label.localeCompare(b.label, 'zh')),
|
|
208
|
+
}))
|
|
240
209
|
}
|
|
241
210
|
|
|
242
211
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* @param {Array<{ name: string, category?: string, description?: string }>} items
|
|
247
|
-
* @param {string} label
|
|
248
|
-
* @param {Iterable<string>} defaultNames
|
|
249
|
-
*/
|
|
250
|
-
export async function promptSelection(items, label, defaultNames) {
|
|
251
|
-
if (items.length === 0) return []
|
|
252
|
-
// 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
|
|
253
|
-
const selectedNames = await searchableMultiselect({
|
|
254
|
-
message: `选择 ${label}`,
|
|
255
|
-
options: buildSearchableOptions(items),
|
|
256
|
-
initialValues: [...defaultNames],
|
|
257
|
-
maxItems: 15,
|
|
258
|
-
required: false,
|
|
259
|
-
placeholder: SEARCH_PLACEHOLDER,
|
|
260
|
-
})
|
|
261
|
-
if (isCancel(selectedNames)) return null
|
|
262
|
-
const selectedSet = new Set(selectedNames)
|
|
263
|
-
return items.filter(item => selectedSet.has(item.name))
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 交互式选择:每步可回退到上一步,最后总览支持任意类别重选。
|
|
268
|
-
* categories 可缩成仅 Skills,未出现的类别返回空数组(调用方不得据此删除已有项)。
|
|
269
|
-
* 返回 { skills, rules, commands, hooks } 或 null(取消)。
|
|
212
|
+
* 顺序选择 Skills、Rules;Enter 进入下一类,不再提供回退或重选菜单。
|
|
213
|
+
* 完整同步默认启用全部模板 Hooks;仅 Skills 模式不得写入其他配置。
|
|
214
|
+
* 取消返回 null,调用方必须终止同步。
|
|
270
215
|
*
|
|
271
216
|
* @param {object} scans
|
|
272
|
-
* @param {
|
|
273
|
-
* @param {{
|
|
217
|
+
* @param {{ skills: Iterable<string>, rules: Iterable<string> }} existing
|
|
218
|
+
* @param {{ skillsOnly?: boolean }} [opts]
|
|
274
219
|
*/
|
|
275
|
-
export async function interactiveSelect(scans, existing, {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const { key, label } = categories[i]
|
|
287
|
-
const defaultNames = sel[key]?.length
|
|
288
|
-
? new Set(sel[key].map(x => x.name))
|
|
289
|
-
: existing[key]
|
|
290
|
-
sel[key] = await promptSelection(scans[key], label, defaultNames)
|
|
291
|
-
if (sel[key] === null) return null
|
|
292
|
-
|
|
293
|
-
if (i === categories.length - 1) break
|
|
294
|
-
|
|
295
|
-
const nextLabel = categories[i + 1].label
|
|
296
|
-
const prevLabel = i > 0 ? categories[i - 1].label : null
|
|
297
|
-
// 导航步可搜索单选(选项少,交互与多选一致)
|
|
298
|
-
const nav = await searchableSelect({
|
|
299
|
-
message: `${label} 已选 ${sel[key].length} 项。下一步:`,
|
|
300
|
-
options: [
|
|
301
|
-
{ label: `继续选 ${nextLabel}`, value: 'next' },
|
|
302
|
-
...(prevLabel ? [{ label: `返回重选 ${prevLabel}`, value: 'back' }] : []),
|
|
303
|
-
{ label: '取消同步', value: 'cancel' },
|
|
304
|
-
],
|
|
305
|
-
initialValue: 'next',
|
|
306
|
-
placeholder: SEARCH_PLACEHOLDER,
|
|
307
|
-
})
|
|
308
|
-
if (isCancel(nav) || nav === 'cancel') return null
|
|
309
|
-
i = nav === 'back' ? i - 1 : i + 1
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// 第二轮:总览 + 任意重选
|
|
313
|
-
while (true) {
|
|
314
|
-
console.log(chalk.gray('\n选择汇总:'))
|
|
315
|
-
for (const { key, label } of categories) {
|
|
316
|
-
console.log(chalk.gray(` ${label.padEnd(10)} ${sel[key].length} 项`))
|
|
317
|
-
}
|
|
318
|
-
const final = await searchableSelect({
|
|
319
|
-
message: '确认提交还是重选?',
|
|
320
|
-
options: [
|
|
321
|
-
{ label: '确认提交', value: 'confirm' },
|
|
322
|
-
...categories.map(c => ({ label: `重选 ${c.label}`, value: c.key })),
|
|
323
|
-
{ label: '取消同步', value: 'cancel' },
|
|
324
|
-
],
|
|
325
|
-
initialValue: 'confirm',
|
|
326
|
-
placeholder: SEARCH_PLACEHOLDER,
|
|
220
|
+
export async function interactiveSelect(scans, existing, { skillsOnly = false } = {}) {
|
|
221
|
+
const selected = { skills: [], rules: [], commands: [], hooks: skillsOnly ? [] : scans.hooks }
|
|
222
|
+
for (const { key, label } of skillsOnly ? CATEGORIES.slice(0, 1) : CATEGORIES) {
|
|
223
|
+
const tabs = buildCategoryTabs(scans[key], key)
|
|
224
|
+
const availableNames = new Set(scans[key].map(item => item.name))
|
|
225
|
+
const names = await tabMultiselect({
|
|
226
|
+
message: `选择 ${label}`,
|
|
227
|
+
tabs,
|
|
228
|
+
initialValues: [...existing[key]].filter(name => availableNames.has(name)),
|
|
229
|
+
required: false,
|
|
230
|
+
maxItems: 15,
|
|
327
231
|
})
|
|
328
|
-
if (isCancel(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
sel[final] = await promptSelection(scans[final], cat.label, new Set(sel[final].map(x => x.name)))
|
|
332
|
-
if (sel[final] === null) return null
|
|
232
|
+
if (isCancel(names)) return null
|
|
233
|
+
const selectedNames = new Set(names)
|
|
234
|
+
selected[key] = scans[key].filter(item => selectedNames.has(item.name))
|
|
333
235
|
}
|
|
236
|
+
return selected
|
|
334
237
|
}
|
|
335
238
|
|
|
336
239
|
export function logAction(label, color = 'green', prefix = '✓') {
|
|
337
240
|
console.log(chalk[color](` ${prefix} ${label}`))
|
|
338
241
|
}
|
|
339
242
|
|
|
340
|
-
function getTemplateRepoCacheDir() {
|
|
341
|
-
return path.join(getDwyHomeDir(), 'cache', 'dwy')
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function resolveTemplateDirFromRepo(repoDir) {
|
|
345
|
-
for (const segments of TEMPLATE_SOURCE_CANDIDATES) {
|
|
346
|
-
const candidate = path.join(repoDir, ...segments)
|
|
347
|
-
if (await fs.pathExists(candidate)) return candidate
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
throw new Error(`ai-tools templates not found in repo: ${repoDir}`)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function readGitOriginUrl(repoDir) {
|
|
354
|
-
try {
|
|
355
|
-
return await runGit(['-C', repoDir, 'remote', 'get-url', 'origin'])
|
|
356
|
-
} catch {
|
|
357
|
-
return ''
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function ensureTemplateRepoCache() {
|
|
362
|
-
const repoDir = getTemplateRepoCacheDir()
|
|
363
|
-
const repoRoot = path.dirname(repoDir)
|
|
364
|
-
await fs.ensureDir(repoRoot)
|
|
365
|
-
|
|
366
|
-
const hasGitDir = await fs.pathExists(path.join(repoDir, '.git'))
|
|
367
|
-
if (hasGitDir) {
|
|
368
|
-
const originUrl = await readGitOriginUrl(repoDir)
|
|
369
|
-
if (!originUrl || originUrl !== DEFAULT_TEMPLATE_REPO_URL) {
|
|
370
|
-
console.log(chalk.gray('缓存模板仓库 origin 已变更,重建缓存...'))
|
|
371
|
-
await fs.remove(repoDir)
|
|
372
|
-
}
|
|
373
|
-
} else if (await fs.pathExists(repoDir)) {
|
|
374
|
-
console.log(chalk.gray('缓存目录不是 git 仓库,重建缓存...'))
|
|
375
|
-
await fs.remove(repoDir)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (!await fs.pathExists(path.join(repoDir, '.git'))) {
|
|
379
|
-
console.log(chalk.gray('拉取模板仓库...'))
|
|
380
|
-
await runGit(['clone', DEFAULT_TEMPLATE_REPO_URL, repoDir])
|
|
381
|
-
return repoDir
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
console.log(chalk.gray('更新模板仓库...'))
|
|
385
|
-
await runGit(['-C', repoDir, 'pull', '--ff-only'])
|
|
386
|
-
return repoDir
|
|
387
|
-
}
|
|
388
|
-
|
|
389
243
|
async function copyItems(items, targetDir) {
|
|
390
244
|
for (const item of items) {
|
|
391
245
|
const dest = path.join(targetDir, item.type + 's', item.name)
|
|
392
246
|
await fs.ensureDir(path.dirname(dest))
|
|
393
|
-
await fs.copy(item.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
247
|
+
await fs.copy(item.sourcePath, dest, { overwrite: true, filter: copyFilter(item.sourcePath) })
|
|
394
248
|
logAction(`${item.type}s/${item.name}`)
|
|
395
249
|
}
|
|
396
250
|
return items.length
|
|
@@ -406,7 +260,7 @@ async function copyHooks(items, targetDir) {
|
|
|
406
260
|
continue
|
|
407
261
|
}
|
|
408
262
|
const dest = path.join(hooksTargetDir, hook.name)
|
|
409
|
-
await fs.copy(hook.sourcePath, dest, { overwrite: true, filter: copyFilter })
|
|
263
|
+
await fs.copy(hook.sourcePath, dest, { overwrite: true, filter: copyFilter(hook.sourcePath) })
|
|
410
264
|
if (!await fs.pathExists(dest)) {
|
|
411
265
|
logAction(`hooks/${hook.name}`, 'yellow', '!')
|
|
412
266
|
continue
|
|
@@ -431,7 +285,7 @@ async function removeUnselected(typePlural, existingNames, selectedNames, templa
|
|
|
431
285
|
return count
|
|
432
286
|
}
|
|
433
287
|
|
|
434
|
-
function buildClaudeHooksSettings(manifests, selectedHookNames) {
|
|
288
|
+
function buildClaudeHooksSettings(manifests, selectedHookNames, global) {
|
|
435
289
|
const grouped = new Map()
|
|
436
290
|
|
|
437
291
|
for (const manifest of manifests) {
|
|
@@ -444,7 +298,7 @@ function buildClaudeHooksSettings(manifests, selectedHookNames) {
|
|
|
444
298
|
const hooks = grouped.get(key) || []
|
|
445
299
|
hooks.push({
|
|
446
300
|
type: 'command',
|
|
447
|
-
command: `bash $CLAUDE_PROJECT_DIR/.claude/hooks/${manifest.name}`,
|
|
301
|
+
command: `bash "${global ? '$HOME' : '$CLAUDE_PROJECT_DIR'}/.claude/hooks/${manifest.name}"`,
|
|
448
302
|
})
|
|
449
303
|
grouped.set(key, hooks)
|
|
450
304
|
}
|
|
@@ -489,10 +343,10 @@ function mergeClaudeHooks(existingHooks, generatedHooks, managedHookNames) {
|
|
|
489
343
|
return Object.keys(merged).length > 0 ? merged : undefined
|
|
490
344
|
}
|
|
491
345
|
|
|
492
|
-
async function syncSettings(sourceDir, targetDir, selectedHookNames) {
|
|
346
|
+
async function syncSettings(sourceDir, targetDir, selectedHookNames, global) {
|
|
493
347
|
const settingsDest = path.join(targetDir, 'settings.json')
|
|
494
348
|
const manifests = await loadHookManifests(sourceDir)
|
|
495
|
-
const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames)
|
|
349
|
+
const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames, global)
|
|
496
350
|
const managedHookNames = new Set(
|
|
497
351
|
manifests
|
|
498
352
|
.filter(manifest => !manifest.platforms || manifest.platforms.includes('claude'))
|
|
@@ -527,19 +381,7 @@ async function syncSettings(sourceDir, targetDir, selectedHookNames) {
|
|
|
527
381
|
return 1
|
|
528
382
|
}
|
|
529
383
|
|
|
530
|
-
|
|
531
|
-
const baseline = await readBaselineDoc(sourceDir)
|
|
532
|
-
if (!baseline) return 0
|
|
533
|
-
|
|
534
|
-
const dest = path.join(targetDir, 'CLAUDE.md')
|
|
535
|
-
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
536
|
-
if (current === baseline) return 0
|
|
537
|
-
|
|
538
|
-
await fs.writeFile(dest, baseline)
|
|
539
|
-
logAction('.claude/CLAUDE.md')
|
|
540
|
-
return 1
|
|
541
|
-
}
|
|
542
|
-
|
|
384
|
+
/** 默认读取随包模板;仅开发者显式设置环境变量时改用外部目录,不要求仓库权限。 */
|
|
543
385
|
export async function resolveSourceDir() {
|
|
544
386
|
const explicitSourceDir = process.env.DWY_TEMPLATE_SOURCE_DIR
|
|
545
387
|
if (explicitSourceDir) {
|
|
@@ -549,13 +391,14 @@ export async function resolveSourceDir() {
|
|
|
549
391
|
return explicitSourceDir
|
|
550
392
|
}
|
|
551
393
|
|
|
552
|
-
|
|
553
|
-
return resolveTemplateDirFromRepo(repoDir)
|
|
394
|
+
return path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
|
|
554
395
|
}
|
|
555
396
|
|
|
397
|
+
/** 同步 Claude Skills 与 Hooks;基线和项目 Rules 由总同步入口分别处理。 */
|
|
556
398
|
export async function syncClaude({
|
|
557
399
|
sourceDir: sourceDirOverride,
|
|
558
400
|
projectDir,
|
|
401
|
+
global = false,
|
|
559
402
|
selected,
|
|
560
403
|
staleRemovals = {},
|
|
561
404
|
} = {}) {
|
|
@@ -571,7 +414,6 @@ export async function syncClaude({
|
|
|
571
414
|
const scans = {
|
|
572
415
|
skills: await scanSkills(sourceDir),
|
|
573
416
|
rules: await scanRules(sourceDir),
|
|
574
|
-
commands: await scanCommands(sourceDir),
|
|
575
417
|
hooks: await scanHooks(sourceDir),
|
|
576
418
|
}
|
|
577
419
|
const availableHooks = []
|
|
@@ -588,27 +430,23 @@ export async function syncClaude({
|
|
|
588
430
|
logAction(`hooks skipped: ${missing}`, 'yellow', '!')
|
|
589
431
|
}
|
|
590
432
|
scans.hooks = availableHooks
|
|
591
|
-
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.
|
|
433
|
+
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.hooks.length} hooks\n`))
|
|
592
434
|
|
|
593
435
|
const existing = {
|
|
594
436
|
skills: await scanExisting(projectTargetDir, 'skills'),
|
|
595
437
|
rules: await scanExisting(projectTargetDir, 'rules'),
|
|
596
|
-
commands: await scanExisting(projectTargetDir, 'commands'),
|
|
597
438
|
hooks: await scanExisting(projectTargetDir, 'hooks'),
|
|
598
439
|
}
|
|
599
440
|
|
|
600
441
|
const selectedNames = key => new Set((selected[key] || []).map(item => item.name))
|
|
601
442
|
const syncedSelection = {
|
|
602
443
|
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
603
|
-
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
604
|
-
commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
|
|
605
444
|
hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
|
|
606
445
|
}
|
|
607
446
|
|
|
608
447
|
const selectedHookNames = new Set(syncedSelection.hooks.map(h => h.name))
|
|
609
448
|
const approvedStaleSkills = staleRemovals.skills || new Set()
|
|
610
449
|
const approvedStaleRules = staleRemovals.rules || new Set()
|
|
611
|
-
const approvedStaleCommands = staleRemovals.commands || new Set()
|
|
612
450
|
const approvedStaleHooks = staleRemovals.hooks || new Set()
|
|
613
451
|
|
|
614
452
|
console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
|
|
@@ -617,20 +455,15 @@ export async function syncClaude({
|
|
|
617
455
|
let syncedCount = 0
|
|
618
456
|
let removedCount = 0
|
|
619
457
|
|
|
620
|
-
syncedCount += await syncClaudeBaselineDoc(sourceDir, projectTargetDir)
|
|
621
458
|
syncedCount += await copyItems(syncedSelection.skills, projectTargetDir)
|
|
622
|
-
syncedCount += await
|
|
623
|
-
syncedCount += await copyItems(syncedSelection.commands, projectTargetDir)
|
|
624
|
-
syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames)
|
|
459
|
+
syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames, global)
|
|
625
460
|
syncedCount += await copyHooks(syncedSelection.hooks, projectTargetDir)
|
|
626
461
|
|
|
627
462
|
const skillTemplateNames = new Set([...scans.skills.map(s => s.name), ...approvedStaleSkills])
|
|
628
463
|
const ruleTemplateNames = new Set([...scans.rules.map(r => r.name), ...approvedStaleRules])
|
|
629
|
-
const commandTemplateNames = new Set([...scans.commands.map(c => c.name), ...approvedStaleCommands])
|
|
630
464
|
const hookTemplateNames = new Set([...scans.hooks.map(h => h.name), ...approvedStaleHooks])
|
|
631
465
|
removedCount += await removeUnselected('skills', existing.skills, new Set(syncedSelection.skills.map(s => s.name)), skillTemplateNames, projectTargetDir)
|
|
632
|
-
removedCount += await removeUnselected('rules', existing.rules, new Set(
|
|
633
|
-
removedCount += await removeUnselected('commands', existing.commands, new Set(syncedSelection.commands.map(c => c.name)), commandTemplateNames, projectTargetDir)
|
|
466
|
+
removedCount += await removeUnselected('rules', existing.rules, new Set(), ruleTemplateNames, projectTargetDir)
|
|
634
467
|
removedCount += await removeUnselected('hooks', existing.hooks, selectedHookNames, hookTemplateNames, projectTargetDir)
|
|
635
468
|
|
|
636
469
|
console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
|
package/src/tab-multiselect.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 基于 @clack/core Prompt 自绘,不引入新 UI 库。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Prompt, getColumns, settings } from '@clack/core'
|
|
7
|
+
import { Prompt, getColumns, settings, wrapTextWithPrefix } from '@clack/core'
|
|
8
8
|
import { limitOptions } from '@clack/prompts'
|
|
9
9
|
import { styleText } from 'node:util'
|
|
10
10
|
import process from 'node:process'
|
|
@@ -39,39 +39,8 @@ const S_BAR_H = () => unicodeOr('─', '-')
|
|
|
39
39
|
const S_TAB_MORE = unicodeOr('‹', '<')
|
|
40
40
|
const S_TAB_MORE_RIGHT = unicodeOr('›', '>')
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* skill / rule / hook 的终端标记。
|
|
44
|
-
* 不用 emoji:几何符号 + 固定宽度英文,无 unicode 时退回 S/R/H。
|
|
45
|
-
*/
|
|
46
|
-
const ITEM_TYPE_META = {
|
|
47
|
-
skills: { icon: unicodeOr('◈', 'S'), word: 'skill', color: 'cyan', title: '技能' },
|
|
48
|
-
rules: { icon: unicodeOr('☰', 'R'), word: 'rule', color: 'yellow', title: '规则' },
|
|
49
|
-
hooks: { icon: unicodeOr('⤷', 'H'), word: 'hook', color: 'magenta', title: '钩子' },
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 取类型展示元数据。未知类型当 skill,避免列表画出空白标记。
|
|
54
|
-
*
|
|
55
|
-
* @param {string | undefined} type
|
|
56
|
-
*/
|
|
57
|
-
export function itemTypeMeta(type) {
|
|
58
|
-
return ITEM_TYPE_META[type] || ITEM_TYPE_META.skills
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 行首类型前缀:图标 + 对齐后的 skill/rule/hook。
|
|
63
|
-
*
|
|
64
|
-
* @param {string | undefined} type
|
|
65
|
-
*/
|
|
66
|
-
export function formatItemTypePrefix(type) {
|
|
67
|
-
const meta = itemTypeMeta(type)
|
|
68
|
-
return `${styleText(meta.color, meta.icon)} ${styleText(meta.color, meta.word.padEnd(5, ' '))}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
42
|
/** Tab 之间空格数。过密会糊成一行,过疏又浪费宽度。 */
|
|
72
43
|
const TAB_GAP = 2
|
|
73
|
-
/** 省略角标占位。‹/› 按 1 列计。 */
|
|
74
|
-
const ELLIPSIS_W = 1
|
|
75
44
|
/**
|
|
76
45
|
* 技术栈 / 场景分组之间插入 ` | ` 比普通间隙多出来的宽度。
|
|
77
46
|
* 普通间隙 TAB_GAP=2;再加 `|` 和一侧间隙 → 3。
|
|
@@ -151,35 +120,31 @@ export function tabWindow(segments, activeIndex, maxWidth, groups) {
|
|
|
151
120
|
const n = segments.length
|
|
152
121
|
if (n === 0) return { start: 0, end: 0, showLeft: false, showRight: false }
|
|
153
122
|
const idx = Math.min(Math.max(activeIndex, 0), n - 1)
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
w += groupSepWidth(groups, start, end)
|
|
163
|
-
if (start > 0) w += ELLIPSIS_W + TAB_GAP
|
|
164
|
-
if (end < n) w += TAB_GAP + ELLIPSIS_W
|
|
165
|
-
return w
|
|
123
|
+
const fits = (start, end) => {
|
|
124
|
+
const gap = ' '.repeat(TAB_GAP)
|
|
125
|
+
let text = segments.slice(start, end).join(gap)
|
|
126
|
+
if (start > 0) text = `${S_TAB_MORE}${gap}${text}`
|
|
127
|
+
if (end < n) text += `${gap}${S_TAB_MORE_RIGHT}`
|
|
128
|
+
text += ' '.repeat(groupSepWidth(groups, start, end))
|
|
129
|
+
// 复用 clack 的终端宽度处理,中文分类不能按字符串 length 计算。
|
|
130
|
+
return !wrapTextWithPrefix({ columns: maxWidth }, text, '').includes('\n')
|
|
166
131
|
}
|
|
167
132
|
|
|
168
133
|
let start = idx
|
|
169
134
|
let end = idx + 1
|
|
170
135
|
// 当前段已经超宽:不再扩张,避免把邻 Tab 挤没
|
|
171
|
-
if (
|
|
136
|
+
if (!fits(start, end)) {
|
|
172
137
|
return { start, end, showLeft: start > 0, showRight: end < n }
|
|
173
138
|
}
|
|
174
139
|
|
|
175
140
|
let grew = true
|
|
176
141
|
while (grew) {
|
|
177
142
|
grew = false
|
|
178
|
-
if (end < n &&
|
|
143
|
+
if (end < n && fits(start, end + 1)) {
|
|
179
144
|
end += 1
|
|
180
145
|
grew = true
|
|
181
146
|
}
|
|
182
|
-
if (start > 0 &&
|
|
147
|
+
if (start > 0 && fits(start - 1, end)) {
|
|
183
148
|
start -= 1
|
|
184
149
|
grew = true
|
|
185
150
|
}
|
|
@@ -330,7 +295,7 @@ function renderTabBar(segments, activeIndex, win, tabs, selectedValues) {
|
|
|
330
295
|
|
|
331
296
|
/**
|
|
332
297
|
* 打开顶部 Tab + 下方多选。
|
|
333
|
-
* tabs 为空时不进交互,直接返回 []
|
|
298
|
+
* tabs 为空时不进交互,直接返回 [](调用方应先过滤空分类)。
|
|
334
299
|
*
|
|
335
300
|
* @param {object} opts
|
|
336
301
|
* @param {string} opts.message
|
|
@@ -380,13 +345,11 @@ export function tabMultiselect(opts) {
|
|
|
380
345
|
const contentWidth = Math.max(cols - (withGuide ? 10 : 6), 20)
|
|
381
346
|
const segments = tabBarSegments(this.tabs, selected)
|
|
382
347
|
const groups = this.tabs.map(tab => tab.group)
|
|
383
|
-
const win = tabWindow(segments, this.tabIndex, contentWidth, groups)
|
|
348
|
+
const win = tabWindow(segments, this.tabIndex, contentWidth - 2, groups)
|
|
384
349
|
const tabLine = renderTabBar(segments, this.tabIndex, win, this.tabs, selected)
|
|
385
350
|
|
|
386
351
|
const focused = this.currentOptions[this.cursor]
|
|
387
|
-
const
|
|
388
|
-
const descRaw = (focused?.description || '').trim() || '(无说明)'
|
|
389
|
-
const desc = `${focusedType.title} · ${descRaw}`
|
|
352
|
+
const desc = (focused?.description || '').trim() || '(无说明)'
|
|
390
353
|
const instructions = [
|
|
391
354
|
`${styleText('dim', '←/→')} 切 Tab`,
|
|
392
355
|
`${styleText('dim', '↑/↓')} 移动`,
|
|
@@ -421,7 +384,6 @@ export function tabMultiselect(opts) {
|
|
|
421
384
|
label: opt.label ?? String(opt.value ?? ''),
|
|
422
385
|
selected: selected.includes(opt.value),
|
|
423
386
|
active,
|
|
424
|
-
prefix: formatItemTypePrefix(opt.type),
|
|
425
387
|
}),
|
|
426
388
|
})
|
|
427
389
|
|
package/src/utils.js
CHANGED
|
@@ -11,7 +11,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
11
11
|
const execFileAsync = promisify(execFile)
|
|
12
12
|
|
|
13
13
|
export const PACKAGE_ROOT = path.resolve(__dirname, '..')
|
|
14
|
-
export const DEFAULT_TEMPLATE_REPO_URL = process.env.DWY_REPO_URL || 'https://github.com/danweiyuancircle/dwy-ai-experience.git'
|
|
15
14
|
|
|
16
15
|
export function getDwyHomeDir() {
|
|
17
16
|
return process.env.DWY_HOME || path.join(os.homedir(), '.dwy')
|