create-dwy 0.19.0 → 0.20.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 +21 -0
- package/package.json +1 -1
- package/src/cli-help.js +28 -2
- package/src/prompt-style.js +82 -0
- package/src/searchable-select.js +12 -29
- package/src/sync-all.js +95 -583
- package/src/sync-codex.js +14 -7
- package/src/sync-opencode.js +8 -22
- package/src/sync.js +60 -149
- package/src/tab-multiselect.js +21 -64
- package/templates/ai-tools/CHANGELOG.md +195 -0
- package/templates/ai-tools/CLAUDE.md +39 -58
- 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-base-lib.md +1 -1
- 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 +59 -200
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/anticorruption.md +13 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/layering.md +5 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/response-envelope.md +20 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/timezone.md +7 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +15 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +43 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +103 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +49 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +13 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +62 -306
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +8 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +0 -31
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/overlay.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/reka.md +5 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v1/index.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/breaking.md +14 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +55 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/lookup.md +29 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +12 -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//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 +16 -13
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-whiteboard-prototype/SKILL.md +1 -1
- package/src/sync-packs.js +0 -223
- package/src/sync-scene.js +0 -178
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +0 -115
package/src/sync-codex.js
CHANGED
|
@@ -114,8 +114,9 @@ function upsertBaselineBlock(content, baselineBody) {
|
|
|
114
114
|
|
|
115
115
|
export async function syncProjectAgentsMd(sourceDir, projectDir, selectedRules, {
|
|
116
116
|
preservedRuleNames = new Set(),
|
|
117
|
+
targetPath = path.join(projectDir, 'AGENTS.md'),
|
|
117
118
|
} = {}) {
|
|
118
|
-
const dest =
|
|
119
|
+
const dest = targetPath
|
|
119
120
|
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
120
121
|
const baselineBody = await readBaselineDoc(sourceDir)
|
|
121
122
|
const selectedRuleNames = new Set(selectedRules.map(rule => rule.name))
|
|
@@ -132,6 +133,7 @@ export async function syncProjectAgentsMd(sourceDir, projectDir, selectedRules,
|
|
|
132
133
|
const withBaseline = upsertBaselineBlock(current, baselineBody)
|
|
133
134
|
const next = upsertManagedBlock(withBaseline, blockBody)
|
|
134
135
|
if (next === current) return 0
|
|
136
|
+
await fs.ensureDir(path.dirname(dest))
|
|
135
137
|
await fs.writeFile(dest, next)
|
|
136
138
|
const parts = []
|
|
137
139
|
if (baselineBody.trim()) parts.push('baseline')
|
|
@@ -223,7 +225,7 @@ async function copyCodexHooks(hooks, proj) {
|
|
|
223
225
|
return hooks.length
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
function buildCodexHooksJson(manifests, selectedHookNames) {
|
|
228
|
+
function buildCodexHooksJson(manifests, selectedHookNames, global) {
|
|
227
229
|
const hooksJson = { hooks: {} }
|
|
228
230
|
const grouped = new Map()
|
|
229
231
|
|
|
@@ -237,7 +239,9 @@ function buildCodexHooksJson(manifests, selectedHookNames) {
|
|
|
237
239
|
const hooks = grouped.get(key) || []
|
|
238
240
|
hooks.push({
|
|
239
241
|
type: 'command',
|
|
240
|
-
command:
|
|
242
|
+
command: global
|
|
243
|
+
? `bash "$HOME/.codex/hooks/${manifest.name}"`
|
|
244
|
+
: `bash "$(git rev-parse --show-toplevel)/.codex/hooks/${manifest.name}"`,
|
|
241
245
|
...(manifest.timeout ? { timeout: manifest.timeout } : {}),
|
|
242
246
|
})
|
|
243
247
|
grouped.set(key, hooks)
|
|
@@ -280,10 +284,10 @@ function mergeCodexHooks(existingHooks, generatedHooks, managedHookNames) {
|
|
|
280
284
|
return Object.keys(merged).length > 0 ? merged : undefined
|
|
281
285
|
}
|
|
282
286
|
|
|
283
|
-
async function syncCodexHooksJson(sourceDir, proj, selectedHookNames) {
|
|
287
|
+
async function syncCodexHooksJson(sourceDir, proj, selectedHookNames, global) {
|
|
284
288
|
const dest = path.join(proj, '.codex', 'hooks.json')
|
|
285
289
|
const manifests = await loadHookManifests(sourceDir)
|
|
286
|
-
const generated = buildCodexHooksJson(manifests, selectedHookNames)
|
|
290
|
+
const generated = buildCodexHooksJson(manifests, selectedHookNames, global)
|
|
287
291
|
const managedHookNames = new Set(
|
|
288
292
|
manifests
|
|
289
293
|
.filter(manifest => !manifest.platforms || manifest.platforms.includes('codex'))
|
|
@@ -317,9 +321,11 @@ async function syncCodexHooksJson(sourceDir, proj, selectedHookNames) {
|
|
|
317
321
|
return 1
|
|
318
322
|
}
|
|
319
323
|
|
|
324
|
+
/** 同步 Codex 配置;global 模式将规则写入用户 .codex/AGENTS.md。 */
|
|
320
325
|
export async function syncCodex({
|
|
321
326
|
sourceDir: sourceDirOverride,
|
|
322
327
|
projectDir,
|
|
328
|
+
global = false,
|
|
323
329
|
selected,
|
|
324
330
|
staleRemovals = {},
|
|
325
331
|
preserveMissingRules = new Set(),
|
|
@@ -340,7 +346,7 @@ export async function syncCodex({
|
|
|
340
346
|
}
|
|
341
347
|
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.hooks.length} hooks\n`))
|
|
342
348
|
|
|
343
|
-
const agentsMdPath = path.join(proj, 'AGENTS.md')
|
|
349
|
+
const agentsMdPath = global ? path.join(proj, '.codex', 'AGENTS.md') : path.join(proj, 'AGENTS.md')
|
|
344
350
|
const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
|
|
345
351
|
const existing = {
|
|
346
352
|
skills: await scanExistingCodexSkills(proj),
|
|
@@ -363,9 +369,10 @@ export async function syncCodex({
|
|
|
363
369
|
syncedCount += await copySkillsFlat(syncedSelection.skills, proj)
|
|
364
370
|
syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules, {
|
|
365
371
|
preservedRuleNames: preserveMissingRules,
|
|
372
|
+
targetPath: agentsMdPath,
|
|
366
373
|
})
|
|
367
374
|
syncedCount += await copyCodexHooks(syncedSelection.hooks, proj)
|
|
368
|
-
syncedCount += await syncCodexHooksJson(sourceDir, proj, selectedHookNames)
|
|
375
|
+
syncedCount += await syncCodexHooksJson(sourceDir, proj, selectedHookNames, global)
|
|
369
376
|
|
|
370
377
|
const skillTemplateNames = new Set([
|
|
371
378
|
...scans.skills.map(s => s.name),
|
package/src/sync-opencode.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
copyFilter,
|
|
6
6
|
logAction,
|
|
7
7
|
resolveSourceDir,
|
|
8
|
-
scanCommands,
|
|
9
8
|
scanExisting,
|
|
10
9
|
scanRules,
|
|
11
10
|
scanSkills,
|
|
@@ -27,8 +26,8 @@ async function copyItemsTo(baseDir, typeDir, items) {
|
|
|
27
26
|
return count
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
async function removeUnselected(typeDir, existingNames, selectedNames, templateNames,
|
|
31
|
-
const targetDir = path.join(
|
|
29
|
+
async function removeUnselected(typeDir, existingNames, selectedNames, templateNames, baseDir) {
|
|
30
|
+
const targetDir = path.join(baseDir, typeDir)
|
|
32
31
|
let count = 0
|
|
33
32
|
|
|
34
33
|
for (const name of existingNames) {
|
|
@@ -42,9 +41,11 @@ async function removeUnselected(typeDir, existingNames, selectedNames, templateN
|
|
|
42
41
|
return count
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
/** 同步 OpenCode 规则和技能;global 模式使用用户 .config/opencode 目录。 */
|
|
45
45
|
export async function syncOpenCode({
|
|
46
46
|
sourceDir: sourceDirOverride,
|
|
47
47
|
projectDir,
|
|
48
|
+
global = false,
|
|
48
49
|
selected,
|
|
49
50
|
staleRemovals = {},
|
|
50
51
|
preserveMissingRules = new Set(),
|
|
@@ -55,7 +56,7 @@ export async function syncOpenCode({
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const proj = projectDir || process.cwd()
|
|
58
|
-
const openCodeDir = path.join(proj, '.opencode')
|
|
59
|
+
const openCodeDir = global ? path.join(proj, '.config', 'opencode') : path.join(proj, '.opencode')
|
|
59
60
|
await fs.ensureDir(openCodeDir)
|
|
60
61
|
|
|
61
62
|
console.log(chalk.blue('\nSyncing OpenCode configuration...\n'))
|
|
@@ -63,31 +64,28 @@ export async function syncOpenCode({
|
|
|
63
64
|
const scans = {
|
|
64
65
|
skills: await scanSkills(sourceDir),
|
|
65
66
|
rules: await scanRules(sourceDir),
|
|
66
|
-
commands: await scanCommands(sourceDir),
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const selectedNames = key => new Set((selected[key] || []).map(item => item.name))
|
|
70
70
|
const syncedSelection = {
|
|
71
71
|
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
72
72
|
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
73
|
-
commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const existing = {
|
|
77
76
|
skills: await scanExisting(openCodeDir, 'skills'),
|
|
78
|
-
commands: await scanExisting(openCodeDir, 'commands'),
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
let syncedCount = 0
|
|
82
80
|
let removedCount = 0
|
|
83
81
|
|
|
84
|
-
const agentsPath = path.join(proj, 'AGENTS.md')
|
|
82
|
+
const agentsPath = path.join(global ? openCodeDir : proj, 'AGENTS.md')
|
|
85
83
|
syncedCount += await syncProjectAgentsMd(sourceDir, proj, syncedSelection.rules, {
|
|
86
84
|
preservedRuleNames: preserveMissingRules,
|
|
85
|
+
targetPath: agentsPath,
|
|
87
86
|
})
|
|
88
87
|
|
|
89
88
|
syncedCount += await copyItemsTo(openCodeDir, 'skills', syncedSelection.skills)
|
|
90
|
-
syncedCount += await copyItemsTo(openCodeDir, 'commands', syncedSelection.commands)
|
|
91
89
|
|
|
92
90
|
removedCount += await removeUnselected(
|
|
93
91
|
'skills',
|
|
@@ -97,22 +95,10 @@ export async function syncOpenCode({
|
|
|
97
95
|
...scans.skills.map(item => item.name),
|
|
98
96
|
...(staleRemovals.skills || new Set()),
|
|
99
97
|
]),
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
removedCount += await removeUnselected(
|
|
103
|
-
'commands',
|
|
104
|
-
existing.commands,
|
|
105
|
-
new Set(syncedSelection.commands.map(item => item.name)),
|
|
106
|
-
new Set([
|
|
107
|
-
...scans.commands.map(item => item.name),
|
|
108
|
-
...(staleRemovals.commands || new Set()),
|
|
109
|
-
]),
|
|
110
|
-
proj,
|
|
98
|
+
openCodeDir,
|
|
111
99
|
)
|
|
112
|
-
|
|
113
100
|
console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
|
|
114
101
|
console.log(chalk.gray(` Rules → ${agentsPath}`))
|
|
115
102
|
console.log(chalk.gray(` Skills → ${path.join(openCodeDir, 'skills')}`))
|
|
116
|
-
console.log(chalk.gray(` Commands → ${path.join(openCodeDir, 'commands')}`))
|
|
117
103
|
console.log(chalk.gray(' Hooks → 当前适配未支持'))
|
|
118
104
|
}
|
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
|
-
SEARCH_PLACEHOLDER,
|
|
6
|
-
searchableMultiselect,
|
|
7
|
-
searchableSelect,
|
|
8
|
-
} from './searchable-select.js'
|
|
4
|
+
import { tabMultiselect } from './tab-multiselect.js'
|
|
9
5
|
import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } 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 = [
|
|
@@ -147,27 +141,6 @@ export async function scanRules(sourceDir) {
|
|
|
147
141
|
return rules.sort((a, b) => a.name.localeCompare(b.name))
|
|
148
142
|
}
|
|
149
143
|
|
|
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
144
|
export async function scanHooks(sourceDir) {
|
|
172
145
|
const hooksDir = path.join(sourceDir, 'hooks')
|
|
173
146
|
if (!await fs.pathExists(hooksDir)) return []
|
|
@@ -214,123 +187,58 @@ export async function scanExisting(projectTargetDir, typePlural) {
|
|
|
214
187
|
}
|
|
215
188
|
|
|
216
189
|
/**
|
|
217
|
-
*
|
|
218
|
-
* label 带分类前缀便于筛选;完整 description 进底部公共说明区(不截断塞 hint)。
|
|
190
|
+
* 按模板实际分类生成 Tab,每条独立勾选,不做包展开或跨分类关联。
|
|
219
191
|
*
|
|
220
192
|
* @param {Array<{ name: string, category?: string, description?: string }>} items
|
|
221
|
-
* @
|
|
193
|
+
* @param {'skills' | 'rules'} type
|
|
222
194
|
*/
|
|
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
|
-
}
|
|
195
|
+
export function buildCategoryTabs(items, type) {
|
|
196
|
+
const groups = new Map()
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
const category = item.category || '其他'
|
|
199
|
+
if (!groups.has(category)) groups.set(category, [])
|
|
200
|
+
groups.get(category).push({
|
|
201
|
+
value: item.name,
|
|
202
|
+
label: item.name,
|
|
203
|
+
description: item.description,
|
|
204
|
+
type,
|
|
239
205
|
})
|
|
206
|
+
}
|
|
207
|
+
return [...groups.entries()]
|
|
208
|
+
.sort(([a], [b]) => a.localeCompare(b, 'zh'))
|
|
209
|
+
.map(([category, options]) => ({
|
|
210
|
+
id: category,
|
|
211
|
+
label: category,
|
|
212
|
+
options: options.sort((a, b) => a.label.localeCompare(b.label, 'zh')),
|
|
213
|
+
}))
|
|
240
214
|
}
|
|
241
215
|
|
|
242
216
|
/**
|
|
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(取消)。
|
|
217
|
+
* 顺序选择 Skills、Rules;Enter 进入下一类,不再提供回退或重选菜单。
|
|
218
|
+
* 完整同步默认启用全部模板 Hooks;仅 Skills 模式不得写入其他配置。
|
|
219
|
+
* 取消返回 null,调用方必须终止同步。
|
|
270
220
|
*
|
|
271
221
|
* @param {object} scans
|
|
272
|
-
* @param {
|
|
273
|
-
* @param {{
|
|
222
|
+
* @param {{ skills: Iterable<string>, rules: Iterable<string> }} existing
|
|
223
|
+
* @param {{ skillsOnly?: boolean }} [opts]
|
|
274
224
|
*/
|
|
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,
|
|
225
|
+
export async function interactiveSelect(scans, existing, { skillsOnly = false } = {}) {
|
|
226
|
+
const selected = { skills: [], rules: [], commands: [], hooks: skillsOnly ? [] : scans.hooks }
|
|
227
|
+
for (const { key, label } of skillsOnly ? CATEGORIES.slice(0, 1) : CATEGORIES) {
|
|
228
|
+
const tabs = buildCategoryTabs(scans[key], key)
|
|
229
|
+
const availableNames = new Set(scans[key].map(item => item.name))
|
|
230
|
+
const names = await tabMultiselect({
|
|
231
|
+
message: `选择 ${label}`,
|
|
232
|
+
tabs,
|
|
233
|
+
initialValues: [...existing[key]].filter(name => availableNames.has(name)),
|
|
234
|
+
required: false,
|
|
235
|
+
maxItems: 15,
|
|
327
236
|
})
|
|
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
|
|
237
|
+
if (isCancel(names)) return null
|
|
238
|
+
const selectedNames = new Set(names)
|
|
239
|
+
selected[key] = scans[key].filter(item => selectedNames.has(item.name))
|
|
333
240
|
}
|
|
241
|
+
return selected
|
|
334
242
|
}
|
|
335
243
|
|
|
336
244
|
export function logAction(label, color = 'green', prefix = '✓') {
|
|
@@ -431,7 +339,7 @@ async function removeUnselected(typePlural, existingNames, selectedNames, templa
|
|
|
431
339
|
return count
|
|
432
340
|
}
|
|
433
341
|
|
|
434
|
-
function buildClaudeHooksSettings(manifests, selectedHookNames) {
|
|
342
|
+
function buildClaudeHooksSettings(manifests, selectedHookNames, global) {
|
|
435
343
|
const grouped = new Map()
|
|
436
344
|
|
|
437
345
|
for (const manifest of manifests) {
|
|
@@ -444,7 +352,7 @@ function buildClaudeHooksSettings(manifests, selectedHookNames) {
|
|
|
444
352
|
const hooks = grouped.get(key) || []
|
|
445
353
|
hooks.push({
|
|
446
354
|
type: 'command',
|
|
447
|
-
command: `bash $CLAUDE_PROJECT_DIR/.claude/hooks/${manifest.name}`,
|
|
355
|
+
command: `bash "${global ? '$HOME' : '$CLAUDE_PROJECT_DIR'}/.claude/hooks/${manifest.name}"`,
|
|
448
356
|
})
|
|
449
357
|
grouped.set(key, hooks)
|
|
450
358
|
}
|
|
@@ -489,10 +397,10 @@ function mergeClaudeHooks(existingHooks, generatedHooks, managedHookNames) {
|
|
|
489
397
|
return Object.keys(merged).length > 0 ? merged : undefined
|
|
490
398
|
}
|
|
491
399
|
|
|
492
|
-
async function syncSettings(sourceDir, targetDir, selectedHookNames) {
|
|
400
|
+
async function syncSettings(sourceDir, targetDir, selectedHookNames, global) {
|
|
493
401
|
const settingsDest = path.join(targetDir, 'settings.json')
|
|
494
402
|
const manifests = await loadHookManifests(sourceDir)
|
|
495
|
-
const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames)
|
|
403
|
+
const generatedSettings = buildClaudeHooksSettings(manifests, selectedHookNames, global)
|
|
496
404
|
const managedHookNames = new Set(
|
|
497
405
|
manifests
|
|
498
406
|
.filter(manifest => !manifest.platforms || manifest.platforms.includes('claude'))
|
|
@@ -527,7 +435,7 @@ async function syncSettings(sourceDir, targetDir, selectedHookNames) {
|
|
|
527
435
|
return 1
|
|
528
436
|
}
|
|
529
437
|
|
|
530
|
-
async function syncClaudeBaselineDoc(sourceDir, targetDir) {
|
|
438
|
+
async function syncClaudeBaselineDoc(sourceDir, targetDir, global) {
|
|
531
439
|
const baseline = await readBaselineDoc(sourceDir)
|
|
532
440
|
if (!baseline) return 0
|
|
533
441
|
|
|
@@ -535,7 +443,15 @@ async function syncClaudeBaselineDoc(sourceDir, targetDir) {
|
|
|
535
443
|
const current = await fs.pathExists(dest) ? await fs.readFile(dest, 'utf-8') : ''
|
|
536
444
|
if (current === baseline) return 0
|
|
537
445
|
|
|
538
|
-
|
|
446
|
+
const start = '<!-- DWY-BASELINE:START -->'
|
|
447
|
+
const end = '<!-- DWY-BASELINE:END -->'
|
|
448
|
+
const block = `${start}\n\n${baseline.trim()}\n\n${end}`
|
|
449
|
+
const next = global
|
|
450
|
+
? (current === baseline ? `${block}\n` : /<!-- DWY-BASELINE:START[^>]*-->[\s\S]*?<!-- DWY-BASELINE:END -->/.test(current)
|
|
451
|
+
? current.replace(/<!-- DWY-BASELINE:START[^>]*-->[\s\S]*?<!-- DWY-BASELINE:END -->/, block)
|
|
452
|
+
: `${block}\n\n${current}`)
|
|
453
|
+
: baseline
|
|
454
|
+
await fs.writeFile(dest, next)
|
|
539
455
|
logAction('.claude/CLAUDE.md')
|
|
540
456
|
return 1
|
|
541
457
|
}
|
|
@@ -553,9 +469,11 @@ export async function resolveSourceDir() {
|
|
|
553
469
|
return resolveTemplateDirFromRepo(repoDir)
|
|
554
470
|
}
|
|
555
471
|
|
|
472
|
+
/** 同步 Claude 配置;global 模式用传入的用户目录,并保留个人 CLAUDE.md 内容。 */
|
|
556
473
|
export async function syncClaude({
|
|
557
474
|
sourceDir: sourceDirOverride,
|
|
558
475
|
projectDir,
|
|
476
|
+
global = false,
|
|
559
477
|
selected,
|
|
560
478
|
staleRemovals = {},
|
|
561
479
|
} = {}) {
|
|
@@ -571,7 +489,6 @@ export async function syncClaude({
|
|
|
571
489
|
const scans = {
|
|
572
490
|
skills: await scanSkills(sourceDir),
|
|
573
491
|
rules: await scanRules(sourceDir),
|
|
574
|
-
commands: await scanCommands(sourceDir),
|
|
575
492
|
hooks: await scanHooks(sourceDir),
|
|
576
493
|
}
|
|
577
494
|
const availableHooks = []
|
|
@@ -588,12 +505,11 @@ export async function syncClaude({
|
|
|
588
505
|
logAction(`hooks skipped: ${missing}`, 'yellow', '!')
|
|
589
506
|
}
|
|
590
507
|
scans.hooks = availableHooks
|
|
591
|
-
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.
|
|
508
|
+
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.hooks.length} hooks\n`))
|
|
592
509
|
|
|
593
510
|
const existing = {
|
|
594
511
|
skills: await scanExisting(projectTargetDir, 'skills'),
|
|
595
512
|
rules: await scanExisting(projectTargetDir, 'rules'),
|
|
596
|
-
commands: await scanExisting(projectTargetDir, 'commands'),
|
|
597
513
|
hooks: await scanExisting(projectTargetDir, 'hooks'),
|
|
598
514
|
}
|
|
599
515
|
|
|
@@ -601,14 +517,12 @@ export async function syncClaude({
|
|
|
601
517
|
const syncedSelection = {
|
|
602
518
|
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
603
519
|
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
604
|
-
commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
|
|
605
520
|
hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
|
|
606
521
|
}
|
|
607
522
|
|
|
608
523
|
const selectedHookNames = new Set(syncedSelection.hooks.map(h => h.name))
|
|
609
524
|
const approvedStaleSkills = staleRemovals.skills || new Set()
|
|
610
525
|
const approvedStaleRules = staleRemovals.rules || new Set()
|
|
611
|
-
const approvedStaleCommands = staleRemovals.commands || new Set()
|
|
612
526
|
const approvedStaleHooks = staleRemovals.hooks || new Set()
|
|
613
527
|
|
|
614
528
|
console.log(chalk.blue(`\nSyncing to ${projectTargetDir}...\n`))
|
|
@@ -617,20 +531,17 @@ export async function syncClaude({
|
|
|
617
531
|
let syncedCount = 0
|
|
618
532
|
let removedCount = 0
|
|
619
533
|
|
|
620
|
-
syncedCount += await syncClaudeBaselineDoc(sourceDir, projectTargetDir)
|
|
534
|
+
syncedCount += await syncClaudeBaselineDoc(sourceDir, projectTargetDir, global)
|
|
621
535
|
syncedCount += await copyItems(syncedSelection.skills, projectTargetDir)
|
|
622
536
|
syncedCount += await copyItems(syncedSelection.rules, projectTargetDir)
|
|
623
|
-
syncedCount += await
|
|
624
|
-
syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames)
|
|
537
|
+
syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames, global)
|
|
625
538
|
syncedCount += await copyHooks(syncedSelection.hooks, projectTargetDir)
|
|
626
539
|
|
|
627
540
|
const skillTemplateNames = new Set([...scans.skills.map(s => s.name), ...approvedStaleSkills])
|
|
628
541
|
const ruleTemplateNames = new Set([...scans.rules.map(r => r.name), ...approvedStaleRules])
|
|
629
|
-
const commandTemplateNames = new Set([...scans.commands.map(c => c.name), ...approvedStaleCommands])
|
|
630
542
|
const hookTemplateNames = new Set([...scans.hooks.map(h => h.name), ...approvedStaleHooks])
|
|
631
543
|
removedCount += await removeUnselected('skills', existing.skills, new Set(syncedSelection.skills.map(s => s.name)), skillTemplateNames, projectTargetDir)
|
|
632
544
|
removedCount += await removeUnselected('rules', existing.rules, new Set(syncedSelection.rules.map(r => r.name)), ruleTemplateNames, projectTargetDir)
|
|
633
|
-
removedCount += await removeUnselected('commands', existing.commands, new Set(syncedSelection.commands.map(c => c.name)), commandTemplateNames, projectTargetDir)
|
|
634
545
|
removedCount += await removeUnselected('hooks', existing.hooks, selectedHookNames, hookTemplateNames, projectTargetDir)
|
|
635
546
|
|
|
636
547
|
console.log(chalk.blue(`\nDone! ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
|
package/src/tab-multiselect.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
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'
|
|
11
|
+
import { formatCheckboxRow } from './prompt-style.js'
|
|
11
12
|
|
|
12
13
|
/** 是否支持 unicode 符号(与 clack / searchable-select 判定一致) */
|
|
13
14
|
function isUnicodeSupported() {
|
|
@@ -35,44 +36,11 @@ const S_STEP_SUBMIT = unicodeOr('◇', 'o')
|
|
|
35
36
|
const S_BAR = unicodeOr('│', '|')
|
|
36
37
|
const S_BAR_END = unicodeOr('└', '—')
|
|
37
38
|
const S_BAR_H = () => unicodeOr('─', '-')
|
|
38
|
-
const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]')
|
|
39
|
-
const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]')
|
|
40
39
|
const S_TAB_MORE = unicodeOr('‹', '<')
|
|
41
40
|
const S_TAB_MORE_RIGHT = unicodeOr('›', '>')
|
|
42
41
|
|
|
43
|
-
/**
|
|
44
|
-
* skill / rule / hook 的终端标记。
|
|
45
|
-
* 不用 emoji:几何符号 + 固定宽度英文,无 unicode 时退回 S/R/H。
|
|
46
|
-
*/
|
|
47
|
-
const ITEM_TYPE_META = {
|
|
48
|
-
skills: { icon: unicodeOr('◈', 'S'), word: 'skill', color: 'cyan', title: '技能' },
|
|
49
|
-
rules: { icon: unicodeOr('☰', 'R'), word: 'rule', color: 'yellow', title: '规则' },
|
|
50
|
-
hooks: { icon: unicodeOr('⤷', 'H'), word: 'hook', color: 'magenta', title: '钩子' },
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 取类型展示元数据。未知类型当 skill,避免列表画出空白标记。
|
|
55
|
-
*
|
|
56
|
-
* @param {string | undefined} type
|
|
57
|
-
*/
|
|
58
|
-
export function itemTypeMeta(type) {
|
|
59
|
-
return ITEM_TYPE_META[type] || ITEM_TYPE_META.skills
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 行首类型前缀:图标 + 对齐后的 skill/rule/hook。
|
|
64
|
-
*
|
|
65
|
-
* @param {string | undefined} type
|
|
66
|
-
*/
|
|
67
|
-
export function formatItemTypePrefix(type) {
|
|
68
|
-
const meta = itemTypeMeta(type)
|
|
69
|
-
return `${styleText(meta.color, meta.icon)} ${styleText(meta.color, meta.word.padEnd(5, ' '))}`
|
|
70
|
-
}
|
|
71
|
-
|
|
72
42
|
/** Tab 之间空格数。过密会糊成一行,过疏又浪费宽度。 */
|
|
73
43
|
const TAB_GAP = 2
|
|
74
|
-
/** 省略角标占位。‹/› 按 1 列计。 */
|
|
75
|
-
const ELLIPSIS_W = 1
|
|
76
44
|
/**
|
|
77
45
|
* 技术栈 / 场景分组之间插入 ` | ` 比普通间隙多出来的宽度。
|
|
78
46
|
* 普通间隙 TAB_GAP=2;再加 `|` 和一侧间隙 → 3。
|
|
@@ -152,35 +120,31 @@ export function tabWindow(segments, activeIndex, maxWidth, groups) {
|
|
|
152
120
|
const n = segments.length
|
|
153
121
|
if (n === 0) return { start: 0, end: 0, showLeft: false, showRight: false }
|
|
154
122
|
const idx = Math.min(Math.max(activeIndex, 0), n - 1)
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
w += groupSepWidth(groups, start, end)
|
|
164
|
-
if (start > 0) w += ELLIPSIS_W + TAB_GAP
|
|
165
|
-
if (end < n) w += TAB_GAP + ELLIPSIS_W
|
|
166
|
-
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')
|
|
167
131
|
}
|
|
168
132
|
|
|
169
133
|
let start = idx
|
|
170
134
|
let end = idx + 1
|
|
171
135
|
// 当前段已经超宽:不再扩张,避免把邻 Tab 挤没
|
|
172
|
-
if (
|
|
136
|
+
if (!fits(start, end)) {
|
|
173
137
|
return { start, end, showLeft: start > 0, showRight: end < n }
|
|
174
138
|
}
|
|
175
139
|
|
|
176
140
|
let grew = true
|
|
177
141
|
while (grew) {
|
|
178
142
|
grew = false
|
|
179
|
-
if (end < n &&
|
|
143
|
+
if (end < n && fits(start, end + 1)) {
|
|
180
144
|
end += 1
|
|
181
145
|
grew = true
|
|
182
146
|
}
|
|
183
|
-
if (start > 0 &&
|
|
147
|
+
if (start > 0 && fits(start - 1, end)) {
|
|
184
148
|
start -= 1
|
|
185
149
|
grew = true
|
|
186
150
|
}
|
|
@@ -331,7 +295,7 @@ function renderTabBar(segments, activeIndex, win, tabs, selectedValues) {
|
|
|
331
295
|
|
|
332
296
|
/**
|
|
333
297
|
* 打开顶部 Tab + 下方多选。
|
|
334
|
-
* tabs 为空时不进交互,直接返回 []
|
|
298
|
+
* tabs 为空时不进交互,直接返回 [](调用方应先过滤空分类)。
|
|
335
299
|
*
|
|
336
300
|
* @param {object} opts
|
|
337
301
|
* @param {string} opts.message
|
|
@@ -381,13 +345,11 @@ export function tabMultiselect(opts) {
|
|
|
381
345
|
const contentWidth = Math.max(cols - (withGuide ? 10 : 6), 20)
|
|
382
346
|
const segments = tabBarSegments(this.tabs, selected)
|
|
383
347
|
const groups = this.tabs.map(tab => tab.group)
|
|
384
|
-
const win = tabWindow(segments, this.tabIndex, contentWidth, groups)
|
|
348
|
+
const win = tabWindow(segments, this.tabIndex, contentWidth - 2, groups)
|
|
385
349
|
const tabLine = renderTabBar(segments, this.tabIndex, win, this.tabs, selected)
|
|
386
350
|
|
|
387
351
|
const focused = this.currentOptions[this.cursor]
|
|
388
|
-
const
|
|
389
|
-
const descRaw = (focused?.description || '').trim() || '(无说明)'
|
|
390
|
-
const desc = `${focusedType.title} · ${descRaw}`
|
|
352
|
+
const desc = (focused?.description || '').trim() || '(无说明)'
|
|
391
353
|
const instructions = [
|
|
392
354
|
`${styleText('dim', '←/→')} 切 Tab`,
|
|
393
355
|
`${styleText('dim', '↑/↓')} 移动`,
|
|
@@ -418,16 +380,11 @@ export function tabMultiselect(opts) {
|
|
|
418
380
|
maxItems: opts.maxItems ?? 12,
|
|
419
381
|
output: opts.output,
|
|
420
382
|
rowPadding: header.length + footer.length,
|
|
421
|
-
style: (opt, active) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
: styleText('dim', S_CHECKBOX_INACTIVE)
|
|
427
|
-
const prefix = formatItemTypePrefix(opt.type)
|
|
428
|
-
if (active) return `${box} ${prefix} ${label}`
|
|
429
|
-
return `${box} ${prefix} ${styleText('dim', label)}`
|
|
430
|
-
},
|
|
383
|
+
style: (opt, active) => formatCheckboxRow({
|
|
384
|
+
label: opt.label ?? String(opt.value ?? ''),
|
|
385
|
+
selected: selected.includes(opt.value),
|
|
386
|
+
active,
|
|
387
|
+
}),
|
|
431
388
|
})
|
|
432
389
|
|
|
433
390
|
return [
|