create-dwy 0.25.1 → 0.25.3
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 +19 -0
- package/package.json +1 -1
- package/src/cli-help.js +8 -9
- package/src/sync-all.js +3 -2
- package/src/sync-plan.js +1 -1
- package/src/sync.js +10 -5
- package/src/tab-multiselect.js +2 -2
- package/src/template-source.js +0 -218
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# create-dwy
|
|
2
2
|
|
|
3
|
+
## [0.25.3] - 2026-11-06
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 修复项目中全局 Skills 禁选后,已启用分类 Tab 失去颜色区分的问题。包含项目已勾选或全局已启用 Skills 的非当前分类保持青色,当前分类保持青色底;禁选逻辑和可勾选项计数保持不变。
|
|
8
|
+
- 补充部分/全部全局覆盖状态以及真实交互输出的颜色回归。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [0.25.2] - 2026-11-06
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 恢复 AI 模板随 CLI 包分发,移除启动时的远端 Git 检查、下载和模板缓存读取,避免运行 `dwy` 长时间等待。Skills 变更通过发布 CLI 交付,`dwy refresh` 从当前包内模板刷新已安装副本;保留显式开发目录覆盖。
|
|
17
|
+
- 补齐普通项目交互的全局 Skills 禁选:有效全局入口已覆盖的条目标注“全局已启用”,禁止重复勾选;末步新增其他 CLI 时只补缺失入口,保留已有全局内容。
|
|
18
|
+
- 保留范围优先、CLI 末尾选择、全局规范与 Hooks 跳页以及升级后的实际版本校验。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
3
22
|
## [0.25.1] - 2026-11-06
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
package/src/cli-help.js
CHANGED
|
@@ -17,7 +17,7 @@ export function buildHelpText(version) {
|
|
|
17
17
|
' dwy 选择范围、配置和 CLI,预览清单后同步',
|
|
18
18
|
' dwy sync 同上',
|
|
19
19
|
' dwy upgrade 升级到 npm 最新正式版,并刷新共享 Skills',
|
|
20
|
-
' dwy refresh
|
|
20
|
+
' dwy refresh 用当前 CLI 包内模板刷新已安装的共享 Skills',
|
|
21
21
|
' dwy migrate 一键迁移老项目已有的托管 Skills,原目录先备份',
|
|
22
22
|
' dwy --help 显示本说明',
|
|
23
23
|
' dwy --version 显示版本',
|
|
@@ -59,16 +59,16 @@ export function buildHelpText(version) {
|
|
|
59
59
|
'用户主目录不能作为项目范围同步,请选择全局或进入项目目录。',
|
|
60
60
|
'关闭选项只移除当前范围托管内容;项目操作不移除全局配置。',
|
|
61
61
|
'Skills 副本集中在 ~/.dwy/skills;项目和全局使用目录链接,同平台全局入口优先。',
|
|
62
|
-
'项目 Skills
|
|
63
|
-
'
|
|
62
|
+
'项目 Skills 全部适用平台均有有效全局入口时显示「全局已启用」,不能重复勾选。',
|
|
63
|
+
'部分平台已有有效全局入口时仍可选,只为缺少全局入口的平台安装项目链接。',
|
|
64
64
|
'Hooks 与 Skills 独立;整组管理,项目仅安装缺少有效全局注册的 Hooks。',
|
|
65
65
|
'全局 Hook 的脚本与对应事件、matcher、完整命令注册均有效时,项目禁止重复安装。',
|
|
66
|
-
'任一适用 CLI 已有 dwy
|
|
66
|
+
'任一适用 CLI 已有 dwy 全局通用规范时,项目继承并清理重复基线;其他 CLI 缺失的全局规范另选全局范围补齐。',
|
|
67
67
|
'手写 AGENTS.md 本身不算全局已安装;通用规范与 Rules 共用文件但独立管理。',
|
|
68
68
|
'移除通用规范仅删除基线块;项目 Rules 独立管理,保留手写内容。',
|
|
69
69
|
'全局启用会明确列出当前项目重复项清理;全局移除不会自动向项目补装。',
|
|
70
70
|
'项目同步只清理项目重复入口,不改写有效全局提供者;真实目录先备份。',
|
|
71
|
-
'
|
|
71
|
+
'确认前不改项目/全局配置或已安装共享内容;确认后才同步配置,并刷新所选共享 Skills。',
|
|
72
72
|
'由有效全局入口提供的 Skills 不借项目同步改写;全部已安装副本用 dwy refresh 更新。',
|
|
73
73
|
'sync / migrate 脚本接口仍须显式指定范围。',
|
|
74
74
|
'产品流程缺失的外部依赖也列入清单,确认后联网补齐,已有内容保留。',
|
|
@@ -77,10 +77,9 @@ export function buildHelpText(version) {
|
|
|
77
77
|
'Rules / Hooks 的模板更新仍需执行 dwy sync;已有会话缓存的 Skill 可能需要重新加载。',
|
|
78
78
|
'',
|
|
79
79
|
'模板更新:',
|
|
80
|
-
'
|
|
81
|
-
' DWY_TEMPLATE_SOURCE_DIR
|
|
82
|
-
'
|
|
83
|
-
' 新增 Skills 只需更新模板分发分支,无需发布 CLI;dwy upgrade 更新程序。',
|
|
80
|
+
' 模板随 CLI 包分发,启动不联网检查模板。',
|
|
81
|
+
' DWY_TEMPLATE_SOURCE_DIR 开发覆盖优先;未设置时读取当前 CLI 的 dwy-cli/templates/ai-tools。',
|
|
82
|
+
' Skills 新增或变更需要 CLI 发版;dwy upgrade 更新程序与包内模板。',
|
|
84
83
|
'',
|
|
85
84
|
].join('\n')
|
|
86
85
|
}
|
package/src/sync-all.js
CHANGED
|
@@ -279,8 +279,7 @@ export async function syncAll({
|
|
|
279
279
|
unionSets(...platforms.map(platform => platform === 'cursor' && type === 'rules' ? cursorRules : existing[platform][type]))]))
|
|
280
280
|
const defaults = buildSelectionDefaultsFromSyncState(current, targetState, platforms)
|
|
281
281
|
for (const type of ['skills', 'rules', 'hooks']) defaults[type] = unionSets(defaults[type], current[type])
|
|
282
|
-
|
|
283
|
-
const choices = platformsOverride && !global && contentTypes.includes('skills')
|
|
282
|
+
const choices = !global && contentTypes.includes('skills')
|
|
284
283
|
? { ...scans, skills: await projectSkillChoices(scans.skills, platforms, { homeDir, codexHome }) } : scans
|
|
285
284
|
const selected = selectedOverride
|
|
286
285
|
? { skills: selectedOverride.skills || [], rules: selectedOverride.rules || [], hooks: [], commands: [] }
|
|
@@ -324,6 +323,8 @@ export async function syncAll({
|
|
|
324
323
|
platforms = await selectPlatforms(targetRoot, scope, platforms)
|
|
325
324
|
if (platforms === null) { console.log(chalk.yellow('已取消同步。')); return }
|
|
326
325
|
}
|
|
326
|
+
// 禁选的全局 Skills 仍处于启用状态;末步新增 CLI 时只补缺失入口,已有全局源保持原样。
|
|
327
|
+
if (!selectedOverride && !global) selected.skills.push(...choices.skills.filter(skill => skill.disabled))
|
|
327
328
|
const options = contentOptions(scope, platforms)
|
|
328
329
|
if (mainFlow) contentTypes = contentTypes.filter(type => options.some(option => option.value === type))
|
|
329
330
|
const stale = staleEntries(scans, targetState, existing, platforms)
|
package/src/sync-plan.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function projectSkillChoices(skills, platforms, { homeDir, codexHom
|
|
|
39
39
|
const disabled = applicable.length > 0 && covered.length === applicable.length
|
|
40
40
|
const missing = applicable.filter(platform => !covered.includes(platform))
|
|
41
41
|
const status = disabled ? '全局已启用' : covered.length ? `${missing.map(p => PLATFORM_LABELS[p]).join('、')} 可安装到项目` : ''
|
|
42
|
-
return { ...skill, disabled, label: status ? `${skill.name}(${status})` : skill.name,
|
|
42
|
+
return { ...skill, disabled, inherited: covered.length > 0, label: status ? `${skill.name}(${status})` : skill.name,
|
|
43
43
|
description: [covered.length ? `全局已提供:${covered.map(p => PLATFORM_LABELS[p]).join('、')}` : '', skill.description].filter(Boolean).join(';') }
|
|
44
44
|
}))
|
|
45
45
|
}
|
package/src/sync.js
CHANGED
|
@@ -2,9 +2,8 @@ import fs from 'fs-extra'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { isCancel } from '@clack/prompts'
|
|
4
4
|
import { tabMultiselect } from './tab-multiselect.js'
|
|
5
|
-
import { chalk } from './utils.js'
|
|
5
|
+
import { chalk, PACKAGE_ROOT } from './utils.js'
|
|
6
6
|
import { linkSkill, ensureSharedSkills, removeSkillEntry } from './skill-store.js'
|
|
7
|
-
import { resolveTemplateSource } from './template-source.js'
|
|
8
7
|
|
|
9
8
|
const CATEGORIES = [
|
|
10
9
|
{ key: 'skills', label: 'Skills' },
|
|
@@ -199,6 +198,7 @@ export function buildCategoryTabs(items, type) {
|
|
|
199
198
|
label: item.label || item.name,
|
|
200
199
|
description: item.description,
|
|
201
200
|
disabled: item.disabled,
|
|
201
|
+
inherited: item.inherited,
|
|
202
202
|
type,
|
|
203
203
|
})
|
|
204
204
|
}
|
|
@@ -391,9 +391,14 @@ async function syncSettings(sourceDir, targetDir, selectedHookNames, global, sta
|
|
|
391
391
|
return 1
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
/**
|
|
395
|
-
export async function resolveSourceDir(
|
|
396
|
-
|
|
394
|
+
/** 只读取当前 CLI 的随包模板;开发时允许显式覆盖,不联网检查更新。 */
|
|
395
|
+
export async function resolveSourceDir() {
|
|
396
|
+
const override = process.env.DWY_TEMPLATE_SOURCE_DIR
|
|
397
|
+
const sourceDir = override || path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
|
|
398
|
+
if (!await fs.pathExists(sourceDir)) {
|
|
399
|
+
throw new Error(override ? `DWY_TEMPLATE_SOURCE_DIR not found: ${override}` : 'ai-tools templates not found in package')
|
|
400
|
+
}
|
|
401
|
+
return sourceDir
|
|
397
402
|
}
|
|
398
403
|
|
|
399
404
|
/** 同步 Claude Skills 与 Hooks;基线和项目 Rules 由总同步入口分别处理。 */
|
package/src/tab-multiselect.js
CHANGED
|
@@ -264,7 +264,7 @@ class TabMultiSelectPrompt extends Prompt {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
267
|
+
* 当前 Tab 反底;项目已勾选或全局已启用的分类用青色,不把禁选状态当成未启用。
|
|
268
268
|
*
|
|
269
269
|
* @param {string[]} segments
|
|
270
270
|
* @param {number} activeIndex
|
|
@@ -284,7 +284,7 @@ function renderTabBar(segments, activeIndex, win, tabs, selectedValues) {
|
|
|
284
284
|
const { selected } = tabSelectionStats(tabs[i], set)
|
|
285
285
|
if (i === activeIndex) {
|
|
286
286
|
parts.push(styleText(['bgCyan', 'black'], ` ${text} `))
|
|
287
|
-
} else if (selected > 0) {
|
|
287
|
+
} else if (selected > 0 || tabs[i].options.some(option => option.inherited)) {
|
|
288
288
|
parts.push(styleText('cyan', text))
|
|
289
289
|
} else {
|
|
290
290
|
parts.push(styleText('dim', text))
|
package/src/template-source.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { execFile } from 'node:child_process'
|
|
5
|
-
import { promisify } from 'node:util'
|
|
6
|
-
import { randomUUID } from 'node:crypto'
|
|
7
|
-
import { spinner } from '@clack/prompts'
|
|
8
|
-
import { PACKAGE_ROOT } from './utils.js'
|
|
9
|
-
|
|
10
|
-
const execFileAsync = promisify(execFile)
|
|
11
|
-
const REMOTE = 'https://github.com/danweiyuancircle/dwy-ai-experience.git'
|
|
12
|
-
const TEMPLATE_PATH = ['dwy-cli', 'templates', 'ai-tools']
|
|
13
|
-
const REVISION_FILE = '.dwy-source-revision'
|
|
14
|
-
|
|
15
|
-
async function statEntry(file) {
|
|
16
|
-
return fs.lstat(file).catch(error => {
|
|
17
|
-
if (error.code === 'ENOENT') return null
|
|
18
|
-
throw error
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function requireDirectory(directory) {
|
|
23
|
-
const stat = await fs.lstat(directory)
|
|
24
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`模板目录不能是软链接或文件:${directory}`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function validateTemplates(directory) {
|
|
28
|
-
await requireDirectory(directory)
|
|
29
|
-
await validateTree(directory)
|
|
30
|
-
for (const name of ['skills', 'rules', 'hooks']) await requireDirectory(path.join(directory, name))
|
|
31
|
-
const baseline = path.join(directory, 'AGENTS.md')
|
|
32
|
-
if (!(await fs.lstat(baseline)).isFile() || !(await fs.readFile(baseline, 'utf8')).trim()) {
|
|
33
|
-
throw new Error('模板缺少有效 AGENTS.md')
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function validateTree(directory) {
|
|
38
|
-
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
39
|
-
if (entry.isSymbolicLink()) throw new Error(`模板不能包含软链接:${entry.name}`)
|
|
40
|
-
if (entry.isDirectory()) await validateTree(path.join(directory, entry.name))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function deadLockExists(lockPath) {
|
|
45
|
-
const stat = await statEntry(lockPath)
|
|
46
|
-
if (!stat) return false
|
|
47
|
-
if (!stat.isFile()) throw new Error(`模板锁无效,保留原文件:${lockPath}`)
|
|
48
|
-
const owner = await fs.readFile(lockPath, 'utf8').catch(error => {
|
|
49
|
-
if (error.code === 'ENOENT') return null
|
|
50
|
-
throw error
|
|
51
|
-
})
|
|
52
|
-
if (owner === null) return false
|
|
53
|
-
const pid = Number(owner.trim())
|
|
54
|
-
if (!/^[1-9]\d*\s*$/.test(owner) || !Number.isSafeInteger(pid) || pid > 2147483647) {
|
|
55
|
-
throw new Error(`模板锁无效,保留原文件:${lockPath}`)
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
process.kill(pid, 0)
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (error.code === 'ESRCH') return true
|
|
61
|
-
if (error.code !== 'EPERM') throw error
|
|
62
|
-
}
|
|
63
|
-
throw new Error(`进程 ${pid} 正在更新模板,保留现有锁。`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function acquireLock(lockPath, candidate) {
|
|
67
|
-
while (true) {
|
|
68
|
-
try {
|
|
69
|
-
await fs.link(candidate, lockPath)
|
|
70
|
-
return
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (error.code !== 'EEXIST') throw error
|
|
73
|
-
}
|
|
74
|
-
if (!await deadLockExists(lockPath)) continue
|
|
75
|
-
// 回收也须互斥,避免两个进程删掉对方的新锁;回收中断时按同一协议恢复。
|
|
76
|
-
const recovery = `${lockPath}.recovery`
|
|
77
|
-
await acquireLock(recovery, candidate)
|
|
78
|
-
try {
|
|
79
|
-
if (await deadLockExists(lockPath)) await fs.unlink(lockPath)
|
|
80
|
-
try {
|
|
81
|
-
await fs.link(candidate, lockPath)
|
|
82
|
-
return
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (error.code !== 'EEXIST') throw error
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
await fs.unlink(recovery)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function git(args, cwd) {
|
|
93
|
-
try {
|
|
94
|
-
const { stdout } = await execFileAsync('git', ['-c', 'core.hooksPath=/dev/null', ...args], {
|
|
95
|
-
cwd,
|
|
96
|
-
// 不加载用户安装的 hooks 或过滤器,避免检出远端内容时执行脚本。
|
|
97
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' },
|
|
98
|
-
// 首次检出要获取整套模板,不能沿用轻量版本检查的短超时。
|
|
99
|
-
timeout: ['clone', 'checkout', 'fetch', 'reset'].includes(args[0]) ? 300000 : 15000,
|
|
100
|
-
maxBuffer: 1024 * 1024,
|
|
101
|
-
})
|
|
102
|
-
return stdout.trim()
|
|
103
|
-
} catch (error) {
|
|
104
|
-
throw new Error(`Git 获取模板失败(${error.code || error.signal})`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function refreshTemplates(root, cache) {
|
|
109
|
-
await fs.ensureDir(root)
|
|
110
|
-
await requireDirectory(root)
|
|
111
|
-
const lockPath = path.join(root, 'template-source.lock')
|
|
112
|
-
const candidate = path.join(root, `.template-source-lock-${randomUUID()}`)
|
|
113
|
-
await fs.writeFile(candidate, `${process.pid}\n`, { flag: 'wx' })
|
|
114
|
-
try {
|
|
115
|
-
await acquireLock(lockPath, candidate)
|
|
116
|
-
} finally {
|
|
117
|
-
await fs.unlink(candidate)
|
|
118
|
-
}
|
|
119
|
-
let staging
|
|
120
|
-
const previous = path.join(root, '.template-source.previous')
|
|
121
|
-
try {
|
|
122
|
-
if (await statEntry(cache)) await requireDirectory(cache)
|
|
123
|
-
if (await statEntry(previous)) {
|
|
124
|
-
await validateTemplates(previous)
|
|
125
|
-
if (await statEntry(cache)) {
|
|
126
|
-
await validateTemplates(cache)
|
|
127
|
-
await fs.remove(previous)
|
|
128
|
-
} else {
|
|
129
|
-
await fs.rename(previous, cache)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const revision = (await git(['ls-remote', '--exit-code', REMOTE, 'refs/heads/master'], root)).split(/\s/)[0]
|
|
133
|
-
if (!/^[a-f\d]{40,64}$/.test(revision)) throw new Error('远端 master 版本无效')
|
|
134
|
-
if (await statEntry(cache)) {
|
|
135
|
-
try {
|
|
136
|
-
await validateTemplates(cache)
|
|
137
|
-
if ((await fs.readFile(path.join(cache, REVISION_FILE), 'utf8')).trim() === revision) return
|
|
138
|
-
} catch {
|
|
139
|
-
// 本地缓存不完整时重新获取,不能只凭版本标记跳过内容校验。
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
staging = await fs.mkdtemp(path.join(root, '.template-source-'))
|
|
143
|
-
const storedRepository = path.join(root, 'template-repository')
|
|
144
|
-
const reuse = !!await statEntry(storedRepository)
|
|
145
|
-
const repository = reuse ? storedRepository : path.join(staging, 'repository')
|
|
146
|
-
if (reuse) {
|
|
147
|
-
await requireDirectory(repository)
|
|
148
|
-
await requireDirectory(path.join(repository, '.git'))
|
|
149
|
-
await git(['fetch', '--depth=1', '--filter=blob:none', REMOTE, 'master'], repository)
|
|
150
|
-
await git(['reset', '--hard', 'FETCH_HEAD'], repository)
|
|
151
|
-
} else {
|
|
152
|
-
await git(['clone', '--depth=1', '--filter=blob:none', '--no-checkout', '--single-branch',
|
|
153
|
-
'--branch', 'master', REMOTE, repository], staging)
|
|
154
|
-
await git(['sparse-checkout', 'set', '--cone', TEMPLATE_PATH.join('/')], repository)
|
|
155
|
-
await git(['checkout', '--force', 'master'], repository)
|
|
156
|
-
}
|
|
157
|
-
let candidate = repository
|
|
158
|
-
for (const segment of TEMPLATE_PATH) {
|
|
159
|
-
candidate = path.join(candidate, segment)
|
|
160
|
-
await requireDirectory(candidate)
|
|
161
|
-
}
|
|
162
|
-
await validateTemplates(candidate)
|
|
163
|
-
const next = path.join(staging, 'templates')
|
|
164
|
-
await fs.copy(candidate, next)
|
|
165
|
-
await fs.writeFile(path.join(next, REVISION_FILE), await git(['rev-parse', 'HEAD'], repository))
|
|
166
|
-
if (!reuse) await fs.rename(repository, storedRepository)
|
|
167
|
-
const hadCache = !!await statEntry(cache)
|
|
168
|
-
if (hadCache) await fs.rename(cache, previous)
|
|
169
|
-
try {
|
|
170
|
-
await fs.rename(next, cache)
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (hadCache) await fs.rename(previous, cache)
|
|
173
|
-
throw error
|
|
174
|
-
}
|
|
175
|
-
if (hadCache) await fs.remove(previous)
|
|
176
|
-
} finally {
|
|
177
|
-
try {
|
|
178
|
-
if (staging) await fs.remove(staging)
|
|
179
|
-
} finally {
|
|
180
|
-
await fs.unlink(lockPath)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 从固定公开仓库更新用户模板缓存;只读取已验证的目录,不执行模板脚本。
|
|
187
|
-
* 显式目录优先;DWY_HOME 优先于注入 homeDir,更新失败保留缓存,无有效缓存才用随包模板。
|
|
188
|
-
*/
|
|
189
|
-
export async function resolveTemplateSource({ homeDir = os.homedir() } = {}) {
|
|
190
|
-
const explicitSourceDir = process.env.DWY_TEMPLATE_SOURCE_DIR
|
|
191
|
-
if (explicitSourceDir) {
|
|
192
|
-
if (!await fs.pathExists(explicitSourceDir)) {
|
|
193
|
-
throw new Error(`DWY_TEMPLATE_SOURCE_DIR not found: ${explicitSourceDir}`)
|
|
194
|
-
}
|
|
195
|
-
return explicitSourceDir
|
|
196
|
-
}
|
|
197
|
-
const root = process.env.DWY_HOME || path.join(homeDir, '.dwy')
|
|
198
|
-
const cache = path.join(root, 'template-source')
|
|
199
|
-
const progress = process.stdout.isTTY ? spinner() : null
|
|
200
|
-
progress?.start('正在检查 AI 模板更新,首次获取可能较慢')
|
|
201
|
-
try {
|
|
202
|
-
await refreshTemplates(root, cache)
|
|
203
|
-
progress?.stop('AI 模板已就绪')
|
|
204
|
-
return cache
|
|
205
|
-
} catch (error) {
|
|
206
|
-
progress?.stop('模板更新未完成,正在检查本地来源')
|
|
207
|
-
try {
|
|
208
|
-
await validateTemplates(cache)
|
|
209
|
-
} catch {
|
|
210
|
-
const bundled = path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
|
|
211
|
-
await validateTemplates(bundled)
|
|
212
|
-
console.warn(`模板更新失败(${error.message});无有效缓存,回退随包模板。`)
|
|
213
|
-
return bundled
|
|
214
|
-
}
|
|
215
|
-
console.warn(`模板更新失败(${error.message});继续使用本地缓存。`)
|
|
216
|
-
return cache
|
|
217
|
-
}
|
|
218
|
-
}
|