@casatwy/deyo 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -6
- package/deyo.mjs +470 -66
- package/package.json +7 -4
- package/skill-manager.mjs +1346 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
import { constants as fsConstants } from 'node:fs'
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
chmod,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
readlink,
|
|
11
|
+
rename,
|
|
12
|
+
unlink,
|
|
13
|
+
} from 'node:fs/promises'
|
|
14
|
+
import { spawn } from 'node:child_process'
|
|
15
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
16
|
+
import { homedir } from 'node:os'
|
|
17
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
|
|
18
|
+
import process from 'node:process'
|
|
19
|
+
|
|
20
|
+
export const SKILL_PLATFORMS = ['codex', 'claude', 'gemini', 'openclaw']
|
|
21
|
+
export const SKILL_SCOPES = ['user', 'workspace', 'global']
|
|
22
|
+
|
|
23
|
+
const MARKETPLACE_NAME = 'deyo-official'
|
|
24
|
+
const PLUGIN_ID = `deyo@${MARKETPLACE_NAME}`
|
|
25
|
+
const GITHUB_REPOSITORY = 'casatwy/deyo-skill'
|
|
26
|
+
const GIT_SOURCE_URL = 'https://github.com/casatwy/deyo-skill.git'
|
|
27
|
+
const OPENCLAW_SKILL = '@casatwy/deyo'
|
|
28
|
+
const OPENCLAW_REGISTRY = 'https://clawhub.ai'
|
|
29
|
+
const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SCOPES = {
|
|
32
|
+
codex: 'user',
|
|
33
|
+
claude: 'user',
|
|
34
|
+
gemini: 'user',
|
|
35
|
+
openclaw: 'global',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SUPPORTED_SCOPES = {
|
|
39
|
+
codex: new Set(['user']),
|
|
40
|
+
claude: new Set(['user', 'workspace']),
|
|
41
|
+
gemini: new Set(['user']),
|
|
42
|
+
openclaw: new Set(['workspace', 'global']),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// These hashes are generated with hashLegacyTree() from official deyo/ trees
|
|
46
|
+
// previously published in github.com/casatwy/deyo-skill. Directory entries,
|
|
47
|
+
// paths, file bytes, modes, and symlink targets all participate in the digest.
|
|
48
|
+
// Unknown trees are intentionally never migrated automatically.
|
|
49
|
+
const OFFICIAL_LEGACY_HASHES = new Map([
|
|
50
|
+
['sha256:0dc4e4a2ac1a616d79e7deaf365dcbf4c26bf70ec5a4554806a5b68760c584ca', 'c98b981237aadae462019a822509b463ff2900cd'],
|
|
51
|
+
['sha256:fe3fc055413da24e48ca4e3a3d24319fa7033f2d494df803243b175e121053ac', '6fe78e74728fbab6aba75f6acc92cee5c143af3d'],
|
|
52
|
+
['sha256:38112030ae205c4c58b90e8ef7497a590b4ee3485311bd6aa1a777b4c7ed29f1', '91d001299aa82a1411e037bb267e463d171885c6'],
|
|
53
|
+
['sha256:a19f75e873ff6d6e28555a08cc83388e3d7a5cace25d8d59c736d07c2764a8bf', '13b2e5d555f1596810b5e6835804bd3ba18dbe4a'],
|
|
54
|
+
['sha256:0e8d8b3d418bd1be05799f35bab08e40d99cae553c706371675da26adf614e38', 'f1e25f0cbfb0740d7834cce2f25f27a5d00392b1'],
|
|
55
|
+
['sha256:bb2349e66a15d6f63914a079556b440e6374e16d6091bf7cb6d667ff77b4cc88', '96d5e851e14e89b8318428f580d4b15660549570'],
|
|
56
|
+
['sha256:6eae6dbb865422639c9d6ab79293c803712b78e7eb223401bc2421e46cfec301', 'da0039cee32a4756378770ece55577da43cac243'],
|
|
57
|
+
['sha256:3c82cc30d4376618ad3c059411fb52ffd82f14523627fcb80c6eff6f3807c5dd', 'b5c884e03f047e5372720c9fe86f2127803d0c58'],
|
|
58
|
+
['sha256:d2debbabc20d4efc1e054e2e46a05e2ab809fd8a84ad325fd38018d0a0c6469e', '0442925dd7a9a18be7adaefaa351dd5b79ace607'],
|
|
59
|
+
['sha256:cb3958fcd77bdf674a85153c978bccfd658ab25292bca138c0a2846580b4ea9c', 'b497324d0091e45f6cd57612e1295ea593b0c2b4'],
|
|
60
|
+
['sha256:6e556a96701f00baa4eaf3d9a10a38eb4d852e6b9c853aea474286cd35565d3b', '4fed0be6d69dd99179403f02b03bf4a5f5c3f334'],
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
class SkillManagerError extends Error {
|
|
64
|
+
constructor(message, code = 'skill_error') {
|
|
65
|
+
super(message)
|
|
66
|
+
this.code = code
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class SkillCommandReportedError extends Error {
|
|
71
|
+
constructor(exitCode = 1) {
|
|
72
|
+
super('Deyo Skill 命令已报告错误')
|
|
73
|
+
this.name = 'SkillCommandReportedError'
|
|
74
|
+
this.exitCode = exitCode
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function expandConfiguredPath(name, value, fallback, expansionHome = homedir()) {
|
|
79
|
+
if (!value) return normalize(fallback)
|
|
80
|
+
let expanded = String(value)
|
|
81
|
+
if (expanded === '~') expanded = expansionHome
|
|
82
|
+
else if (/^~[\\/]/.test(expanded)) expanded = join(expansionHome, expanded.slice(2))
|
|
83
|
+
if (!isAbsolute(expanded)) {
|
|
84
|
+
throw new SkillManagerError(`${name} 必须是绝对路径`, 'invalid_config_path')
|
|
85
|
+
}
|
|
86
|
+
return normalize(expanded)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getHomeDirectory() {
|
|
90
|
+
if (process.env.HOME) {
|
|
91
|
+
return expandConfiguredPath('HOME', process.env.HOME, homedir())
|
|
92
|
+
}
|
|
93
|
+
if (process.env.USERPROFILE) {
|
|
94
|
+
return expandConfiguredPath('USERPROFILE', process.env.USERPROFILE, homedir())
|
|
95
|
+
}
|
|
96
|
+
return normalize(homedir())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getCodexHome() {
|
|
100
|
+
const home = getHomeDirectory()
|
|
101
|
+
return expandConfiguredPath('CODEX_HOME', process.env.CODEX_HOME, join(home, '.codex'), home)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getClaudeHome() {
|
|
105
|
+
const home = getHomeDirectory()
|
|
106
|
+
return expandConfiguredPath(
|
|
107
|
+
'CLAUDE_CONFIG_DIR',
|
|
108
|
+
process.env.CLAUDE_CONFIG_DIR,
|
|
109
|
+
join(home, '.claude'),
|
|
110
|
+
home,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getGeminiHome() {
|
|
115
|
+
const home = getHomeDirectory()
|
|
116
|
+
const geminiRoot = expandConfiguredPath(
|
|
117
|
+
'GEMINI_CLI_HOME',
|
|
118
|
+
process.env.GEMINI_CLI_HOME,
|
|
119
|
+
home,
|
|
120
|
+
home,
|
|
121
|
+
)
|
|
122
|
+
return join(geminiRoot, '.gemini')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getOpenClawHome() {
|
|
126
|
+
const home = getHomeDirectory()
|
|
127
|
+
if (process.env.OPENCLAW_STATE_DIR) {
|
|
128
|
+
return expandConfiguredPath(
|
|
129
|
+
'OPENCLAW_STATE_DIR',
|
|
130
|
+
process.env.OPENCLAW_STATE_DIR,
|
|
131
|
+
join(home, '.openclaw'),
|
|
132
|
+
home,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
const openClawHome = expandConfiguredPath(
|
|
136
|
+
'OPENCLAW_HOME',
|
|
137
|
+
process.env.OPENCLAW_HOME,
|
|
138
|
+
home,
|
|
139
|
+
home,
|
|
140
|
+
)
|
|
141
|
+
return join(openClawHome, '.openclaw')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getDefaultScope(platform) {
|
|
145
|
+
return DEFAULT_SCOPES[platform]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validatePlatform(platform) {
|
|
149
|
+
if (!SKILL_PLATFORMS.includes(platform)) {
|
|
150
|
+
throw new SkillManagerError(
|
|
151
|
+
`--platform 必须是 ${SKILL_PLATFORMS.join('、')} 之一`,
|
|
152
|
+
'invalid_platform',
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateScope(platform, scope) {
|
|
158
|
+
if (!SKILL_SCOPES.includes(scope)) {
|
|
159
|
+
throw new SkillManagerError(
|
|
160
|
+
`--scope 必须是 ${SKILL_SCOPES.join('、')} 之一`,
|
|
161
|
+
'invalid_scope',
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!SUPPORTED_SCOPES[platform].has(scope)) {
|
|
166
|
+
const supported = [...SUPPORTED_SCOPES[platform]].join('、')
|
|
167
|
+
throw new SkillManagerError(
|
|
168
|
+
`${platform} 的原生安装器不支持 ${scope} scope;可用 scope:${supported}`,
|
|
169
|
+
'unsupported_scope',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseSha256(value) {
|
|
175
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
176
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(normalized)) {
|
|
177
|
+
throw new SkillManagerError(
|
|
178
|
+
'--replace-legacy 必须使用 status 显示的完整 sha256:<64位摘要>',
|
|
179
|
+
'invalid_legacy_hash',
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
return normalized
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function printSkillUsage(stream = process.stdout) {
|
|
186
|
+
stream.write(`用法:
|
|
187
|
+
deyo skill install --platform codex|claude|gemini|openclaw
|
|
188
|
+
[--scope user|workspace|global]
|
|
189
|
+
[--replace-legacy sha256:<observed-hash>]
|
|
190
|
+
[--json]
|
|
191
|
+
deyo skill status [--platform codex|claude|gemini|openclaw] [--json]
|
|
192
|
+
|
|
193
|
+
默认 scope:
|
|
194
|
+
codex=user,claude=user,gemini=user,openclaw=global
|
|
195
|
+
|
|
196
|
+
说明:
|
|
197
|
+
status 只读取本地配置和 manifest,不联网、不检查更新,也不修改 cache/lockfile。
|
|
198
|
+
install 只调用各平台原生管理器;不会用 --force、--all、固定 --ref 或 --consent。
|
|
199
|
+
legacy 目录只有在完整树哈希属于官方历史版本,且 --replace-legacy 精确匹配时才会备份迁移。
|
|
200
|
+
`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseSkillCommandArgs(argv) {
|
|
204
|
+
const requestedAction = argv[1] || ''
|
|
205
|
+
const action = requestedAction === '-h' || requestedAction === '--help' ? '' : requestedAction
|
|
206
|
+
const result = {
|
|
207
|
+
action,
|
|
208
|
+
platform: '',
|
|
209
|
+
scope: '',
|
|
210
|
+
replaceLegacy: '',
|
|
211
|
+
json: false,
|
|
212
|
+
help: requestedAction === '-h' || requestedAction === '--help',
|
|
213
|
+
}
|
|
214
|
+
const seen = new Set()
|
|
215
|
+
|
|
216
|
+
function markSeen(option) {
|
|
217
|
+
if (seen.has(option)) {
|
|
218
|
+
throw new SkillManagerError(`skill 参数不能重复:${option}`, 'duplicate_argument')
|
|
219
|
+
}
|
|
220
|
+
seen.add(option)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readOptionValue(option, index) {
|
|
224
|
+
const value = argv[index + 1]
|
|
225
|
+
if (!value || value.startsWith('-')) {
|
|
226
|
+
throw new SkillManagerError(`${option} 缺少参数值`, 'missing_argument_value')
|
|
227
|
+
}
|
|
228
|
+
return value
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
232
|
+
const current = argv[index]
|
|
233
|
+
if (current === '-h' || current === '--help') {
|
|
234
|
+
result.help = true
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
if (current === '--json') {
|
|
238
|
+
markSeen(current)
|
|
239
|
+
result.json = true
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (current === '--platform') {
|
|
243
|
+
markSeen(current)
|
|
244
|
+
result.platform = readOptionValue(current, index)
|
|
245
|
+
index += 1
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if (current === '--scope') {
|
|
249
|
+
markSeen(current)
|
|
250
|
+
result.scope = readOptionValue(current, index)
|
|
251
|
+
index += 1
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (current === '--replace-legacy') {
|
|
255
|
+
markSeen(current)
|
|
256
|
+
result.replaceLegacy = readOptionValue(current, index)
|
|
257
|
+
index += 1
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
throw new SkillManagerError(`未知 skill 参数:${current}`, 'unknown_argument')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!action && !result.help) {
|
|
264
|
+
throw new SkillManagerError('缺少 skill 子命令 install 或 status', 'missing_action')
|
|
265
|
+
}
|
|
266
|
+
if (!['install', 'status'].includes(action) && !result.help) {
|
|
267
|
+
throw new SkillManagerError(`不支持的 skill 子命令:${action}`, 'invalid_action')
|
|
268
|
+
}
|
|
269
|
+
if (action === 'install' && !result.platform && !result.help) {
|
|
270
|
+
throw new SkillManagerError('skill install 必须提供 --platform', 'missing_platform')
|
|
271
|
+
}
|
|
272
|
+
if (action === 'status' && result.scope) {
|
|
273
|
+
throw new SkillManagerError('skill status 不接受 --scope;它检查各平台默认生效 scope', 'status_scope')
|
|
274
|
+
}
|
|
275
|
+
if (action === 'status' && result.replaceLegacy) {
|
|
276
|
+
throw new SkillManagerError('skill status 不接受 --replace-legacy', 'status_replace_legacy')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function pathLstat(path) {
|
|
283
|
+
try {
|
|
284
|
+
return await lstat(path)
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error?.code === 'ENOENT') return null
|
|
287
|
+
throw error
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function readUtf8IfExists(path) {
|
|
292
|
+
try {
|
|
293
|
+
return await readFile(path, 'utf8')
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error?.code === 'ENOENT') return null
|
|
296
|
+
throw error
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function readJsonIfExists(path) {
|
|
301
|
+
const content = await readUtf8IfExists(path)
|
|
302
|
+
if (content === null) return null
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(content)
|
|
305
|
+
} catch {
|
|
306
|
+
throw new SkillManagerError(`JSON 文件无法解析:${path}`, 'invalid_json')
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function inspectJsonMetadata(path) {
|
|
311
|
+
const stats = await pathLstat(path)
|
|
312
|
+
if (!stats) return { exists: false, safe: true, value: null }
|
|
313
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
314
|
+
return { exists: true, safe: false, value: null }
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
return { exists: true, safe: true, value: JSON.parse(await readFile(path, 'utf8')) }
|
|
318
|
+
} catch {
|
|
319
|
+
return { exists: true, safe: false, value: null }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function updateLengthPrefixed(hash, value) {
|
|
324
|
+
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8')
|
|
325
|
+
const length = Buffer.alloc(8)
|
|
326
|
+
length.writeBigUInt64BE(BigInt(buffer.length))
|
|
327
|
+
hash.update(length)
|
|
328
|
+
hash.update(buffer)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function updateMode(hash, mode) {
|
|
332
|
+
const buffer = Buffer.alloc(4)
|
|
333
|
+
buffer.writeUInt32BE(mode & 0o7777)
|
|
334
|
+
hash.update(buffer)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function sortNames(names) {
|
|
338
|
+
return [...names].sort((first, second) => Buffer.compare(
|
|
339
|
+
Buffer.from(first, 'utf8'),
|
|
340
|
+
Buffer.from(second, 'utf8'),
|
|
341
|
+
))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function hashLegacyTree(rootPath) {
|
|
345
|
+
const rootStats = await pathLstat(rootPath)
|
|
346
|
+
if (!rootStats) return null
|
|
347
|
+
|
|
348
|
+
const hash = createHash('sha256')
|
|
349
|
+
hash.update('deyo-legacy-tree-v1\0')
|
|
350
|
+
|
|
351
|
+
async function visit(path, relativePath) {
|
|
352
|
+
const entryStats = await lstat(path)
|
|
353
|
+
if (entryStats.isDirectory()) {
|
|
354
|
+
hash.update('D')
|
|
355
|
+
updateLengthPrefixed(hash, relativePath)
|
|
356
|
+
updateMode(hash, entryStats.mode)
|
|
357
|
+
const names = sortNames(await readdir(path))
|
|
358
|
+
for (const name of names) {
|
|
359
|
+
await visit(join(path, name), relativePath === '.' ? name : `${relativePath}/${name}`)
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (entryStats.isFile()) {
|
|
365
|
+
hash.update('F')
|
|
366
|
+
updateLengthPrefixed(hash, relativePath)
|
|
367
|
+
updateMode(hash, entryStats.mode)
|
|
368
|
+
updateLengthPrefixed(hash, await readFile(path))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (entryStats.isSymbolicLink()) {
|
|
373
|
+
hash.update('L')
|
|
374
|
+
updateLengthPrefixed(hash, relativePath)
|
|
375
|
+
updateMode(hash, entryStats.mode)
|
|
376
|
+
updateLengthPrefixed(hash, await readlink(path))
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
throw new SkillManagerError(
|
|
381
|
+
`legacy 路径包含不支持的文件类型:${path}`,
|
|
382
|
+
'unsupported_legacy_entry',
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
await visit(rootPath, '.')
|
|
387
|
+
return `sha256:${hash.digest('hex')}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getLegacyPaths(platform, scope) {
|
|
391
|
+
const home = getHomeDirectory()
|
|
392
|
+
if (platform === 'codex') {
|
|
393
|
+
return [
|
|
394
|
+
join(getCodexHome(), 'skills', 'deyo'),
|
|
395
|
+
join(home, '.agents', 'skills', 'deyo'),
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
if (platform === 'claude') {
|
|
399
|
+
return scope === 'workspace'
|
|
400
|
+
? [resolve('.claude', 'skills', 'deyo')]
|
|
401
|
+
: [join(getClaudeHome(), 'skills', 'deyo')]
|
|
402
|
+
}
|
|
403
|
+
if (platform === 'gemini') {
|
|
404
|
+
return scope === 'workspace'
|
|
405
|
+
? [resolve('.gemini', 'skills', 'deyo')]
|
|
406
|
+
: [join(getGeminiHome(), 'skills', 'deyo')]
|
|
407
|
+
}
|
|
408
|
+
return scope === 'global'
|
|
409
|
+
? [join(getOpenClawHome(), 'skills', 'deyo')]
|
|
410
|
+
: [resolve('skills', 'deyo')]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getOpenClawOriginPath(skillPath) {
|
|
414
|
+
return join(skillPath, '.clawhub', 'origin.json')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function inspectLegacy(platform, scope) {
|
|
418
|
+
const entries = []
|
|
419
|
+
for (const path of getLegacyPaths(platform, scope)) {
|
|
420
|
+
const stats = await pathLstat(path)
|
|
421
|
+
if (!stats) continue
|
|
422
|
+
if (platform === 'openclaw' && stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
423
|
+
const originStats = await pathLstat(getOpenClawOriginPath(path))
|
|
424
|
+
if (originStats?.isFile() && !originStats.isSymbolicLink()) continue
|
|
425
|
+
}
|
|
426
|
+
const hash = await hashLegacyTree(path)
|
|
427
|
+
entries.push({
|
|
428
|
+
path,
|
|
429
|
+
hash,
|
|
430
|
+
recognized: OFFICIAL_LEGACY_HASHES.has(hash),
|
|
431
|
+
officialRevision: OFFICIAL_LEGACY_HASHES.get(hash) || null,
|
|
432
|
+
type: stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'other',
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
return entries
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function migrateLegacy(entries, expectedHash) {
|
|
439
|
+
if (!entries.length) {
|
|
440
|
+
if (expectedHash) {
|
|
441
|
+
throw new SkillManagerError(
|
|
442
|
+
'--replace-legacy 已提供,但当前 scope 没有发现 legacy Deyo Skill',
|
|
443
|
+
'legacy_not_found',
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
return null
|
|
447
|
+
}
|
|
448
|
+
if (entries.length !== 1) {
|
|
449
|
+
throw new SkillManagerError(
|
|
450
|
+
`发现多个 legacy Deyo Skill,拒绝自动处理:${entries.map(entry => entry.path).join('、')}`,
|
|
451
|
+
'multiple_legacy_installs',
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const entry = entries[0]
|
|
456
|
+
if (!expectedHash) {
|
|
457
|
+
throw new SkillManagerError(
|
|
458
|
+
`发现 legacy Deyo Skill:${entry.path}(${entry.hash})。确认没有本地修改后,用 --replace-legacy ${entry.hash} 重试`,
|
|
459
|
+
'legacy_confirmation_required',
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
if (entry.hash !== expectedHash) {
|
|
463
|
+
throw new SkillManagerError(
|
|
464
|
+
`legacy 哈希已变化:当前 ${entry.hash},参数为 ${expectedHash}`,
|
|
465
|
+
'legacy_hash_changed',
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
if (!entry.recognized) {
|
|
469
|
+
throw new SkillManagerError(
|
|
470
|
+
`legacy 内容不是已知官方历史版本,拒绝覆盖:${entry.path}`,
|
|
471
|
+
'unknown_legacy_content',
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const recheckedHash = await hashLegacyTree(entry.path)
|
|
476
|
+
if (recheckedHash !== expectedHash) {
|
|
477
|
+
throw new SkillManagerError('legacy 内容在迁移前发生变化,已停止', 'legacy_race')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const backupPath = `${entry.path}.deyo-backup-${Date.now()}-${randomBytes(4).toString('hex')}`
|
|
481
|
+
await rename(entry.path, backupPath)
|
|
482
|
+
const backupHash = await hashLegacyTree(backupPath)
|
|
483
|
+
if (backupHash !== expectedHash) {
|
|
484
|
+
await rename(backupPath, entry.path).catch(() => {})
|
|
485
|
+
throw new SkillManagerError('legacy 备份复算哈希不一致,已停止并尝试恢复', 'legacy_backup_mismatch')
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
originalPath: entry.path,
|
|
490
|
+
backupPath,
|
|
491
|
+
hash: expectedHash,
|
|
492
|
+
async restore() {
|
|
493
|
+
if (await pathLstat(entry.path)) {
|
|
494
|
+
const failedInstallPath = `${entry.path}.deyo-failed-install-${Date.now()}-${randomBytes(4).toString('hex')}`
|
|
495
|
+
await rename(entry.path, failedInstallPath)
|
|
496
|
+
}
|
|
497
|
+
await rename(backupPath, entry.path)
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizeRepositorySource(value) {
|
|
503
|
+
let normalized = String(value || '').trim().toLowerCase()
|
|
504
|
+
if (!normalized) return ''
|
|
505
|
+
normalized = normalized.replace(/^git\+/, '')
|
|
506
|
+
normalized = normalized.replace(/^git@github\.com:/, '')
|
|
507
|
+
normalized = normalized.replace(/^ssh:\/\/git@github\.com\//, '')
|
|
508
|
+
normalized = normalized.replace(/^https?:\/\/github\.com\//, '')
|
|
509
|
+
normalized = normalized.replace(/#.*$/, '')
|
|
510
|
+
normalized = normalized.replace(/\.git\/?$/, '')
|
|
511
|
+
normalized = normalized.replace(/^\/+|\/+$/g, '')
|
|
512
|
+
return normalized
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isPlainObject(value) {
|
|
516
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function repositorySourceCandidates(value) {
|
|
520
|
+
if (typeof value === 'string') return [value]
|
|
521
|
+
if (!isPlainObject(value)) return []
|
|
522
|
+
|
|
523
|
+
if (isPlainObject(value.marketplaceSource)) {
|
|
524
|
+
const marketplaceSource = value.marketplaceSource
|
|
525
|
+
if (typeof marketplaceSource.source === 'string') return [marketplaceSource.source]
|
|
526
|
+
return repositorySourceCandidates(marketplaceSource)
|
|
527
|
+
}
|
|
528
|
+
if (isPlainObject(value.installSource)) {
|
|
529
|
+
const installSource = value.installSource
|
|
530
|
+
if (typeof installSource.source === 'string') return [installSource.source]
|
|
531
|
+
return repositorySourceCandidates(installSource)
|
|
532
|
+
}
|
|
533
|
+
if (isPlainObject(value.source)) {
|
|
534
|
+
return repositorySourceCandidates(value.source)
|
|
535
|
+
}
|
|
536
|
+
for (const key of ['repo', 'repository', 'url', 'gitUrl']) {
|
|
537
|
+
if (typeof value[key] === 'string') return [value[key]]
|
|
538
|
+
}
|
|
539
|
+
return typeof value.source === 'string' ? [value.source] : []
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function hasOfficialRepositorySource(value) {
|
|
543
|
+
return repositorySourceCandidates(value)
|
|
544
|
+
.some(item => normalizeRepositorySource(item) === GITHUB_REPOSITORY)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function parseJsonOutput(result, commandLabel) {
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(result.stdout || 'null')
|
|
550
|
+
} catch {
|
|
551
|
+
throw new SkillManagerError(`${commandLabel} 没有返回合法 JSON`, 'manager_invalid_json')
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function flattenArray(value, preferredKeys = []) {
|
|
556
|
+
if (Array.isArray(value)) return value
|
|
557
|
+
if (!value || typeof value !== 'object') return []
|
|
558
|
+
for (const key of preferredKeys) {
|
|
559
|
+
if (Array.isArray(value[key])) return value[key]
|
|
560
|
+
}
|
|
561
|
+
return []
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function findExecutable(command) {
|
|
565
|
+
const pathValue = process.env.PATH || ''
|
|
566
|
+
const suffixes = process.platform === 'win32'
|
|
567
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
|
568
|
+
: ['']
|
|
569
|
+
for (const directory of pathValue.split(process.platform === 'win32' ? ';' : ':')) {
|
|
570
|
+
if (!directory) continue
|
|
571
|
+
for (const suffix of suffixes) {
|
|
572
|
+
const candidate = resolve(directory, `${command}${suffix}`)
|
|
573
|
+
try {
|
|
574
|
+
await access(candidate, fsConstants.X_OK)
|
|
575
|
+
return candidate
|
|
576
|
+
} catch {}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return null
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function requireExecutable(command) {
|
|
583
|
+
const executable = await findExecutable(command)
|
|
584
|
+
if (!executable) {
|
|
585
|
+
throw new SkillManagerError(
|
|
586
|
+
`未找到 ${command}。请先安装对应平台 CLI,再重试`,
|
|
587
|
+
'manager_not_found',
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
return executable
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function runManager(command, args, options = {}) {
|
|
594
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
595
|
+
const child = spawn(command, args, {
|
|
596
|
+
cwd: options.cwd || process.cwd(),
|
|
597
|
+
env: process.env,
|
|
598
|
+
stdio: options.interactive ? ['inherit', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
599
|
+
})
|
|
600
|
+
const stdout = []
|
|
601
|
+
const stderr = []
|
|
602
|
+
|
|
603
|
+
child.stdout.on('data', (chunk) => {
|
|
604
|
+
stdout.push(Buffer.from(chunk))
|
|
605
|
+
if (options.interactive) {
|
|
606
|
+
;(options.jsonMode ? process.stderr : process.stdout).write(chunk)
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
child.stderr.on('data', (chunk) => {
|
|
610
|
+
stderr.push(Buffer.from(chunk))
|
|
611
|
+
if (options.interactive) process.stderr.write(chunk)
|
|
612
|
+
})
|
|
613
|
+
child.on('error', (error) => {
|
|
614
|
+
if (error?.code === 'ENOENT') {
|
|
615
|
+
rejectPromise(new SkillManagerError(`未找到 ${command}`, 'manager_not_found'))
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
rejectPromise(error)
|
|
619
|
+
})
|
|
620
|
+
child.on('close', (exitCode, signal) => {
|
|
621
|
+
const result = {
|
|
622
|
+
exitCode: exitCode ?? 1,
|
|
623
|
+
signal: signal || null,
|
|
624
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
625
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
626
|
+
}
|
|
627
|
+
if (result.exitCode !== 0 && !options.allowFailure) {
|
|
628
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`
|
|
629
|
+
rejectPromise(new SkillManagerError(
|
|
630
|
+
`${command} ${args.join(' ')} 失败:${detail}`,
|
|
631
|
+
'manager_failed',
|
|
632
|
+
))
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
resolvePromise(result)
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function findMarketplace(list, name) {
|
|
641
|
+
return flattenArray(list, ['marketplaces']).find(item => item?.name === name) || null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function findPlugin(list, pluginId) {
|
|
645
|
+
const entries = [
|
|
646
|
+
...flattenArray(list, ['installed', 'plugins']),
|
|
647
|
+
...(Array.isArray(list?.available) ? list.available : []),
|
|
648
|
+
]
|
|
649
|
+
return entries.find(item => (
|
|
650
|
+
item?.pluginId === pluginId
|
|
651
|
+
|| item?.id === pluginId
|
|
652
|
+
|| (item?.name === 'deyo' && item?.marketplaceName === MARKETPLACE_NAME)
|
|
653
|
+
) && item?.installed !== false) || null
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function findForeignDeyoPlugin(list) {
|
|
657
|
+
const entries = [
|
|
658
|
+
...flattenArray(list, ['installed', 'plugins']),
|
|
659
|
+
...(Array.isArray(list?.available) ? list.available : []),
|
|
660
|
+
]
|
|
661
|
+
return entries.find(item => {
|
|
662
|
+
const id = item?.pluginId || item?.id || ''
|
|
663
|
+
const installed = item?.installed !== false
|
|
664
|
+
return installed && (
|
|
665
|
+
(item?.name === 'deyo' && item?.marketplaceName !== MARKETPLACE_NAME)
|
|
666
|
+
|| (id.startsWith('deyo@') && id !== PLUGIN_ID)
|
|
667
|
+
)
|
|
668
|
+
}) || null
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function buildCodexInstallPlan(jsonMode) {
|
|
672
|
+
const codex = await requireExecutable('codex')
|
|
673
|
+
const marketplaceList = parseJsonOutput(
|
|
674
|
+
await runManager(codex, ['plugin', 'marketplace', 'list', '--json']),
|
|
675
|
+
'codex plugin marketplace list',
|
|
676
|
+
)
|
|
677
|
+
const marketplace = findMarketplace(marketplaceList, MARKETPLACE_NAME)
|
|
678
|
+
if (marketplace && !hasOfficialRepositorySource(marketplace)) {
|
|
679
|
+
throw new SkillManagerError(
|
|
680
|
+
`Codex 已有同名 marketplace ${MARKETPLACE_NAME},但来源不是 ${GITHUB_REPOSITORY}`,
|
|
681
|
+
'marketplace_source_conflict',
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const pluginList = parseJsonOutput(
|
|
686
|
+
await runManager(codex, ['plugin', 'list', '--json']),
|
|
687
|
+
'codex plugin list',
|
|
688
|
+
)
|
|
689
|
+
const foreign = findForeignDeyoPlugin(pluginList)
|
|
690
|
+
if (foreign) {
|
|
691
|
+
throw new SkillManagerError('Codex 已安装同名异源 Deyo plugin,拒绝覆盖', 'plugin_source_conflict')
|
|
692
|
+
}
|
|
693
|
+
const installed = findPlugin(pluginList, PLUGIN_ID)
|
|
694
|
+
|
|
695
|
+
return async () => {
|
|
696
|
+
const actions = []
|
|
697
|
+
if (!marketplace) {
|
|
698
|
+
await runManager(codex, ['plugin', 'marketplace', 'add', GITHUB_REPOSITORY], {
|
|
699
|
+
interactive: true,
|
|
700
|
+
jsonMode,
|
|
701
|
+
})
|
|
702
|
+
actions.push('marketplace_added')
|
|
703
|
+
}
|
|
704
|
+
if (!installed) {
|
|
705
|
+
await runManager(codex, ['plugin', 'add', PLUGIN_ID], {
|
|
706
|
+
interactive: true,
|
|
707
|
+
jsonMode,
|
|
708
|
+
})
|
|
709
|
+
actions.push('plugin_installed')
|
|
710
|
+
} else {
|
|
711
|
+
actions.push('already_installed')
|
|
712
|
+
}
|
|
713
|
+
return { actions, activation: 'new_session' }
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getClaudeSettingsPath(scope) {
|
|
718
|
+
return scope === 'workspace'
|
|
719
|
+
? resolve('.claude', 'settings.json')
|
|
720
|
+
: join(getClaudeHome(), 'settings.json')
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function claudeSourceMatches(entry) {
|
|
724
|
+
return hasOfficialRepositorySource(entry)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function validateClaudeSettingsParent(path, create = false) {
|
|
728
|
+
const parent = dirname(path)
|
|
729
|
+
let stats = await pathLstat(parent)
|
|
730
|
+
if (!stats && create) {
|
|
731
|
+
await mkdir(parent, { recursive: true, mode: 0o700 })
|
|
732
|
+
stats = await pathLstat(parent)
|
|
733
|
+
}
|
|
734
|
+
if (stats?.isSymbolicLink()) {
|
|
735
|
+
throw new SkillManagerError(`拒绝通过符号链接设置目录写入:${parent}`, 'settings_symlink')
|
|
736
|
+
}
|
|
737
|
+
if (stats && !stats.isDirectory()) {
|
|
738
|
+
throw new SkillManagerError(`Claude 设置目录不是普通目录:${parent}`, 'settings_not_directory')
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function validateClaudeSettingsShape(settings, path) {
|
|
743
|
+
if (!isPlainObject(settings)) {
|
|
744
|
+
throw new SkillManagerError(`Claude 设置必须是 JSON object:${path}`, 'settings_invalid_shape')
|
|
745
|
+
}
|
|
746
|
+
const marketplaces = settings.extraKnownMarketplaces
|
|
747
|
+
if (marketplaces !== undefined && !isPlainObject(marketplaces)) {
|
|
748
|
+
throw new SkillManagerError(
|
|
749
|
+
`Claude extraKnownMarketplaces 必须是 JSON object:${path}`,
|
|
750
|
+
'settings_invalid_shape',
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
const existing = marketplaces?.[MARKETPLACE_NAME]
|
|
754
|
+
if (existing !== undefined && !isPlainObject(existing)) {
|
|
755
|
+
throw new SkillManagerError(
|
|
756
|
+
`Claude marketplace ${MARKETPLACE_NAME} 配置必须是 JSON object`,
|
|
757
|
+
'settings_invalid_shape',
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
if (existing && !claudeSourceMatches(existing)) {
|
|
761
|
+
throw new SkillManagerError(
|
|
762
|
+
`Claude 设置中已有同名 marketplace ${MARKETPLACE_NAME},但来源不同`,
|
|
763
|
+
'marketplace_source_conflict',
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function readClaudeSettingsSnapshot(scope) {
|
|
769
|
+
const path = getClaudeSettingsPath(scope)
|
|
770
|
+
await validateClaudeSettingsParent(path)
|
|
771
|
+
const stats = await pathLstat(path)
|
|
772
|
+
if (stats?.isSymbolicLink()) {
|
|
773
|
+
throw new SkillManagerError(`拒绝写入符号链接设置文件:${path}`, 'settings_symlink')
|
|
774
|
+
}
|
|
775
|
+
if (stats && !stats.isFile()) {
|
|
776
|
+
throw new SkillManagerError(`Claude 设置路径不是普通文件:${path}`, 'settings_not_file')
|
|
777
|
+
}
|
|
778
|
+
const content = stats ? await readFile(path, 'utf8') : null
|
|
779
|
+
let settings = {}
|
|
780
|
+
if (content !== null) {
|
|
781
|
+
try {
|
|
782
|
+
settings = JSON.parse(content)
|
|
783
|
+
} catch {
|
|
784
|
+
throw new SkillManagerError(`JSON 文件无法解析:${path}`, 'invalid_json')
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
validateClaudeSettingsShape(settings, path)
|
|
788
|
+
return { path, content, settings }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function validateClaudeSettings(scope) {
|
|
792
|
+
return (await readClaudeSettingsSnapshot(scope)).path
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function readAtomicTarget(path) {
|
|
796
|
+
const stats = await pathLstat(path)
|
|
797
|
+
if (stats?.isSymbolicLink()) {
|
|
798
|
+
throw new SkillManagerError(`拒绝写入符号链接设置文件:${path}`, 'settings_symlink')
|
|
799
|
+
}
|
|
800
|
+
if (stats && !stats.isFile()) {
|
|
801
|
+
throw new SkillManagerError(`Claude 设置路径不是普通文件:${path}`, 'settings_not_file')
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
content: stats ? await readFile(path, 'utf8') : null,
|
|
805
|
+
mode: stats ? stats.mode & 0o777 : 0o600,
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function writeJsonAtomic(path, value, expectedContent) {
|
|
810
|
+
await validateClaudeSettingsParent(path, true)
|
|
811
|
+
const existingStats = await pathLstat(path)
|
|
812
|
+
if (existingStats?.isSymbolicLink()) {
|
|
813
|
+
throw new SkillManagerError(`拒绝写入符号链接设置文件:${path}`, 'settings_symlink')
|
|
814
|
+
}
|
|
815
|
+
if (existingStats && !existingStats.isFile()) {
|
|
816
|
+
throw new SkillManagerError(`Claude 设置路径不是普通文件:${path}`, 'settings_not_file')
|
|
817
|
+
}
|
|
818
|
+
const mode = existingStats ? existingStats.mode & 0o777 : 0o600
|
|
819
|
+
const temporaryPath = join(
|
|
820
|
+
dirname(path),
|
|
821
|
+
`.${basename(path)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`,
|
|
822
|
+
)
|
|
823
|
+
const handle = await open(temporaryPath, 'wx', mode)
|
|
824
|
+
let temporaryExists = true
|
|
825
|
+
try {
|
|
826
|
+
try {
|
|
827
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
828
|
+
await handle.sync()
|
|
829
|
+
} finally {
|
|
830
|
+
await handle.close()
|
|
831
|
+
}
|
|
832
|
+
await chmod(temporaryPath, mode)
|
|
833
|
+
const current = await readAtomicTarget(path)
|
|
834
|
+
if (current.content !== expectedContent) {
|
|
835
|
+
throw new SkillManagerError(
|
|
836
|
+
`Claude 设置在写入期间发生变化,已停止以避免覆盖:${path}`,
|
|
837
|
+
'settings_changed',
|
|
838
|
+
)
|
|
839
|
+
}
|
|
840
|
+
await rename(temporaryPath, path)
|
|
841
|
+
temporaryExists = false
|
|
842
|
+
} finally {
|
|
843
|
+
if (temporaryExists) await unlink(temporaryPath).catch(() => {})
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function withClaudeSettingsLock(path, callback) {
|
|
848
|
+
await validateClaudeSettingsParent(path, true)
|
|
849
|
+
const lockPath = join(dirname(path), `.${basename(path)}.deyo.lock`)
|
|
850
|
+
let handle
|
|
851
|
+
try {
|
|
852
|
+
handle = await open(lockPath, 'wx', 0o600)
|
|
853
|
+
} catch (error) {
|
|
854
|
+
if (error?.code === 'EEXIST') {
|
|
855
|
+
throw new SkillManagerError(
|
|
856
|
+
`Claude 设置正被另一个 Deyo 安装进程处理:${path}`,
|
|
857
|
+
'settings_busy',
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
throw error
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
return await callback()
|
|
864
|
+
} finally {
|
|
865
|
+
await handle.close().catch(() => {})
|
|
866
|
+
await unlink(lockPath).catch(() => {})
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function enableClaudeAutoUpdate(scope) {
|
|
871
|
+
const path = getClaudeSettingsPath(scope)
|
|
872
|
+
return await withClaudeSettingsLock(path, async () => {
|
|
873
|
+
const snapshot = await readClaudeSettingsSnapshot(scope)
|
|
874
|
+
const settings = snapshot.settings
|
|
875
|
+
settings.extraKnownMarketplaces ||= {}
|
|
876
|
+
const existing = settings.extraKnownMarketplaces[MARKETPLACE_NAME] || {}
|
|
877
|
+
settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
|
|
878
|
+
...existing,
|
|
879
|
+
source: {
|
|
880
|
+
source: 'github',
|
|
881
|
+
repo: GITHUB_REPOSITORY,
|
|
882
|
+
},
|
|
883
|
+
autoUpdate: true,
|
|
884
|
+
}
|
|
885
|
+
await writeJsonAtomic(path, settings, snapshot.content)
|
|
886
|
+
return path
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function buildClaudeInstallPlan(scope, jsonMode) {
|
|
891
|
+
const claude = await requireExecutable('claude')
|
|
892
|
+
await validateClaudeSettings(scope)
|
|
893
|
+
const nativeScope = scope === 'workspace' ? 'project' : 'user'
|
|
894
|
+
const marketplaces = parseJsonOutput(
|
|
895
|
+
await runManager(claude, ['plugin', 'marketplace', 'list', '--json']),
|
|
896
|
+
'claude plugin marketplace list',
|
|
897
|
+
)
|
|
898
|
+
const marketplace = findMarketplace(marketplaces, MARKETPLACE_NAME)
|
|
899
|
+
if (marketplace && !hasOfficialRepositorySource(marketplace)) {
|
|
900
|
+
throw new SkillManagerError(
|
|
901
|
+
`Claude 已有同名 marketplace ${MARKETPLACE_NAME},但来源不是 ${GITHUB_REPOSITORY}`,
|
|
902
|
+
'marketplace_source_conflict',
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const plugins = parseJsonOutput(
|
|
907
|
+
await runManager(claude, ['plugin', 'list', '--json']),
|
|
908
|
+
'claude plugin list',
|
|
909
|
+
)
|
|
910
|
+
const foreign = findForeignDeyoPlugin(plugins)
|
|
911
|
+
if (foreign) {
|
|
912
|
+
throw new SkillManagerError('Claude 已安装同名异源 Deyo plugin,拒绝覆盖', 'plugin_source_conflict')
|
|
913
|
+
}
|
|
914
|
+
const installed = flattenArray(plugins, ['plugins', 'installed']).find(item => (
|
|
915
|
+
(item?.id === PLUGIN_ID || item?.pluginId === PLUGIN_ID)
|
|
916
|
+
&& (!item?.scope || item.scope === nativeScope)
|
|
917
|
+
)) || null
|
|
918
|
+
|
|
919
|
+
return async () => {
|
|
920
|
+
const actions = []
|
|
921
|
+
if (!marketplace) {
|
|
922
|
+
await runManager(claude, [
|
|
923
|
+
'plugin', 'marketplace', 'add', GITHUB_REPOSITORY, '--scope', nativeScope,
|
|
924
|
+
], { interactive: true, jsonMode })
|
|
925
|
+
actions.push('marketplace_added')
|
|
926
|
+
}
|
|
927
|
+
const settingsPath = await enableClaudeAutoUpdate(scope)
|
|
928
|
+
actions.push('auto_update_enabled')
|
|
929
|
+
if (!installed) {
|
|
930
|
+
await runManager(claude, [
|
|
931
|
+
'plugin', 'install', PLUGIN_ID, '--scope', nativeScope,
|
|
932
|
+
], { interactive: true, jsonMode })
|
|
933
|
+
actions.push('plugin_installed')
|
|
934
|
+
} else {
|
|
935
|
+
actions.push('already_installed')
|
|
936
|
+
}
|
|
937
|
+
return { actions, activation: 'reload_plugins_or_new_session', settingsPath }
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function getGeminiExtensionPath() {
|
|
942
|
+
return join(getGeminiHome(), 'extensions', 'deyo')
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function inspectGeminiExtension() {
|
|
946
|
+
const path = getGeminiExtensionPath()
|
|
947
|
+
const stats = await pathLstat(path)
|
|
948
|
+
if (!stats) {
|
|
949
|
+
return { installed: false, path, manifest: null, install: null }
|
|
950
|
+
}
|
|
951
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
952
|
+
return {
|
|
953
|
+
installed: true,
|
|
954
|
+
path,
|
|
955
|
+
manifest: null,
|
|
956
|
+
install: null,
|
|
957
|
+
version: null,
|
|
958
|
+
officialSource: false,
|
|
959
|
+
autoUpdate: false,
|
|
960
|
+
safe: false,
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const manifestMetadata = await inspectJsonMetadata(join(path, 'gemini-extension.json'))
|
|
964
|
+
const installMetadata = await inspectJsonMetadata(join(path, '.gemini-extension-install.json'))
|
|
965
|
+
const manifest = manifestMetadata.value
|
|
966
|
+
const install = installMetadata.value
|
|
967
|
+
const safe = manifestMetadata.safe && installMetadata.safe
|
|
968
|
+
return {
|
|
969
|
+
installed: true,
|
|
970
|
+
path,
|
|
971
|
+
manifest,
|
|
972
|
+
install,
|
|
973
|
+
version: manifest?.version || install?.version || null,
|
|
974
|
+
officialSource: safe && hasOfficialRepositorySource(install),
|
|
975
|
+
autoUpdate: safe && (install?.autoUpdate === true || install?.auto_update === true),
|
|
976
|
+
safe,
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function buildGeminiInstallPlan(jsonMode) {
|
|
981
|
+
const gemini = await requireExecutable('gemini')
|
|
982
|
+
const extension = await inspectGeminiExtension()
|
|
983
|
+
if (extension.installed && !extension.officialSource) {
|
|
984
|
+
throw new SkillManagerError(
|
|
985
|
+
`Gemini 已存在 deyo extension,但安装来源不是 ${GITHUB_REPOSITORY} 或无法确认`,
|
|
986
|
+
'extension_source_conflict',
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
if (extension.installed && !extension.autoUpdate) {
|
|
990
|
+
throw new SkillManagerError(
|
|
991
|
+
'Gemini 的现有 Deyo extension 未启用 auto-update;原生 CLI 没有安全的原地切换命令,请先用 Gemini 原生管理器卸载后重试',
|
|
992
|
+
'gemini_auto_update_disabled',
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return async () => {
|
|
997
|
+
if (extension.installed) {
|
|
998
|
+
return { actions: ['already_installed'], activation: 'restart' }
|
|
999
|
+
}
|
|
1000
|
+
await runManager(gemini, [
|
|
1001
|
+
'extensions', 'install', GIT_SOURCE_URL, '--auto-update',
|
|
1002
|
+
], { interactive: true, jsonMode })
|
|
1003
|
+
return { actions: ['extension_installed'], activation: 'restart' }
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function getOpenClawSkillPath(scope) {
|
|
1008
|
+
return scope === 'global'
|
|
1009
|
+
? join(getOpenClawHome(), 'skills', 'deyo')
|
|
1010
|
+
: resolve('skills', 'deyo')
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function isOfficialOpenClawOrigin(origin) {
|
|
1014
|
+
if (!isPlainObject(origin)) return false
|
|
1015
|
+
const registry = typeof origin.registry === 'string' ? origin.registry.replace(/\/+$/, '') : ''
|
|
1016
|
+
return origin.version === 1
|
|
1017
|
+
&& registry === OPENCLAW_REGISTRY
|
|
1018
|
+
&& String(origin.ownerHandle || '').replace(/^@/, '').toLowerCase() === 'casatwy'
|
|
1019
|
+
&& String(origin.slug || '').replace(/^@casatwy\//, '').toLowerCase() === 'deyo'
|
|
1020
|
+
&& typeof origin.installedVersion === 'string'
|
|
1021
|
+
&& STABLE_SEMVER.test(origin.installedVersion)
|
|
1022
|
+
&& Number.isFinite(origin.installedAt)
|
|
1023
|
+
&& /^[a-f0-9]{64}$/.test(origin.fingerprint || '')
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function inspectOpenClawSkill(scope) {
|
|
1027
|
+
const path = getOpenClawSkillPath(scope)
|
|
1028
|
+
const stats = await pathLstat(path)
|
|
1029
|
+
if (!stats) {
|
|
1030
|
+
return {
|
|
1031
|
+
path,
|
|
1032
|
+
installed: false,
|
|
1033
|
+
managed: false,
|
|
1034
|
+
officialSource: false,
|
|
1035
|
+
origin: null,
|
|
1036
|
+
version: null,
|
|
1037
|
+
safe: true,
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
1041
|
+
return {
|
|
1042
|
+
path,
|
|
1043
|
+
installed: true,
|
|
1044
|
+
managed: false,
|
|
1045
|
+
officialSource: false,
|
|
1046
|
+
origin: null,
|
|
1047
|
+
version: null,
|
|
1048
|
+
safe: false,
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const originPath = getOpenClawOriginPath(path)
|
|
1052
|
+
const metadata = await inspectJsonMetadata(originPath)
|
|
1053
|
+
const origin = metadata.value
|
|
1054
|
+
return {
|
|
1055
|
+
path,
|
|
1056
|
+
installed: true,
|
|
1057
|
+
managed: metadata.exists,
|
|
1058
|
+
officialSource: metadata.safe && isOfficialOpenClawOrigin(origin),
|
|
1059
|
+
origin,
|
|
1060
|
+
version: isOfficialOpenClawOrigin(origin) ? origin.installedVersion : null,
|
|
1061
|
+
safe: metadata.safe,
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function buildOpenClawInstallPlan(scope, jsonMode) {
|
|
1066
|
+
const openclaw = await requireExecutable('openclaw')
|
|
1067
|
+
const skill = await inspectOpenClawSkill(scope)
|
|
1068
|
+
if (skill.managed && !skill.officialSource) {
|
|
1069
|
+
throw new SkillManagerError(
|
|
1070
|
+
`OpenClaw 当前 scope 已存在异源 deyo skill:${skill.path}`,
|
|
1071
|
+
'openclaw_source_conflict',
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return async () => {
|
|
1076
|
+
const scopeArgs = scope === 'global' ? ['--global'] : []
|
|
1077
|
+
if (skill.managed) {
|
|
1078
|
+
await runManager(openclaw, [
|
|
1079
|
+
'skills', 'update', OPENCLAW_SKILL, ...scopeArgs,
|
|
1080
|
+
], { interactive: true, jsonMode })
|
|
1081
|
+
return { actions: ['skill_updated'], activation: 'invoke_again_if_updated' }
|
|
1082
|
+
}
|
|
1083
|
+
await runManager(openclaw, [
|
|
1084
|
+
'skills', 'install', OPENCLAW_SKILL, ...scopeArgs,
|
|
1085
|
+
], { interactive: true, jsonMode })
|
|
1086
|
+
return { actions: ['skill_installed'], activation: 'next_invocation' }
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function buildPlatformInstallPlan(platform, scope, jsonMode) {
|
|
1091
|
+
if (platform === 'codex') return await buildCodexInstallPlan(jsonMode)
|
|
1092
|
+
if (platform === 'claude') return await buildClaudeInstallPlan(scope, jsonMode)
|
|
1093
|
+
if (platform === 'gemini') return await buildGeminiInstallPlan(jsonMode)
|
|
1094
|
+
return await buildOpenClawInstallPlan(scope, jsonMode)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function extractTomlSection(content, sectionName) {
|
|
1098
|
+
if (!content) return ''
|
|
1099
|
+
const lines = content.split(/\r?\n/)
|
|
1100
|
+
const exactHeader = `[${sectionName}]`
|
|
1101
|
+
const start = lines.findIndex(line => line.trim() === exactHeader)
|
|
1102
|
+
if (start < 0) return ''
|
|
1103
|
+
const section = []
|
|
1104
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
1105
|
+
if (/^\s*\[/.test(lines[index])) break
|
|
1106
|
+
section.push(lines[index])
|
|
1107
|
+
}
|
|
1108
|
+
return section.join('\n')
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function readTomlString(section, key) {
|
|
1112
|
+
const match = section.match(new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']*)["']`, 'm'))
|
|
1113
|
+
return match?.[1] || ''
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function readTomlBoolean(section, key) {
|
|
1117
|
+
const match = section.match(new RegExp(`^\\s*${key}\\s*=\\s*(true|false)\\s*$`, 'm'))
|
|
1118
|
+
return match ? match[1] === 'true' : null
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function inspectCodexManaged() {
|
|
1122
|
+
const configPath = join(getCodexHome(), 'config.toml')
|
|
1123
|
+
const content = await readUtf8IfExists(configPath) || ''
|
|
1124
|
+
const pluginSection = extractTomlSection(content, `plugins."${PLUGIN_ID}"`)
|
|
1125
|
+
const marketplaceSection = extractTomlSection(content, `marketplaces.${MARKETPLACE_NAME}`)
|
|
1126
|
+
const source = readTomlString(marketplaceSection, 'source')
|
|
1127
|
+
let version = null
|
|
1128
|
+
const pluginCache = join(getCodexHome(), 'plugins', 'cache', MARKETPLACE_NAME, 'deyo')
|
|
1129
|
+
try {
|
|
1130
|
+
const versions = (await readdir(pluginCache))
|
|
1131
|
+
.filter(item => /^\d+\.\d+\.\d+$/.test(item))
|
|
1132
|
+
.sort((first, second) => {
|
|
1133
|
+
const firstParts = first.split('.').map(Number)
|
|
1134
|
+
const secondParts = second.split('.').map(Number)
|
|
1135
|
+
return firstParts[0] - secondParts[0]
|
|
1136
|
+
|| firstParts[1] - secondParts[1]
|
|
1137
|
+
|| firstParts[2] - secondParts[2]
|
|
1138
|
+
})
|
|
1139
|
+
version = versions.at(-1) || null
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (error?.code !== 'ENOENT') throw error
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
installed: Boolean(pluginSection),
|
|
1145
|
+
enabled: readTomlBoolean(pluginSection, 'enabled'),
|
|
1146
|
+
version,
|
|
1147
|
+
source: source || null,
|
|
1148
|
+
officialSource: normalizeRepositorySource(source) === GITHUB_REPOSITORY,
|
|
1149
|
+
autoUpdate: 'host_startup_refresh',
|
|
1150
|
+
configPath,
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function inspectClaudeManaged(scope) {
|
|
1155
|
+
const settingsPath = getClaudeSettingsPath(scope)
|
|
1156
|
+
const settings = await readJsonIfExists(settingsPath) || {}
|
|
1157
|
+
const marketplace = settings.extraKnownMarketplaces?.[MARKETPLACE_NAME] || null
|
|
1158
|
+
const installedIndexPath = join(getClaudeHome(), 'plugins', 'installed_plugins.json')
|
|
1159
|
+
const installedIndex = await readJsonIfExists(installedIndexPath) || {}
|
|
1160
|
+
const nativeScope = scope === 'workspace' ? 'project' : 'user'
|
|
1161
|
+
const records = installedIndex.plugins?.[PLUGIN_ID] || []
|
|
1162
|
+
const record = (Array.isArray(records) ? records : [records]).find(item => item?.scope === nativeScope) || null
|
|
1163
|
+
return {
|
|
1164
|
+
installed: Boolean(record || settings.enabledPlugins?.[PLUGIN_ID] === true),
|
|
1165
|
+
enabled: settings.enabledPlugins?.[PLUGIN_ID] ?? null,
|
|
1166
|
+
version: record?.version || null,
|
|
1167
|
+
source: marketplace?.source || null,
|
|
1168
|
+
officialSource: marketplace ? hasOfficialRepositorySource(marketplace) : false,
|
|
1169
|
+
autoUpdate: marketplace?.autoUpdate === true,
|
|
1170
|
+
settingsPath,
|
|
1171
|
+
installPath: record?.installPath || null,
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function inspectPlatformManaged(platform, scope) {
|
|
1176
|
+
if (platform === 'codex') return await inspectCodexManaged()
|
|
1177
|
+
if (platform === 'claude') return await inspectClaudeManaged(scope)
|
|
1178
|
+
if (platform === 'gemini') {
|
|
1179
|
+
const extension = await inspectGeminiExtension()
|
|
1180
|
+
return {
|
|
1181
|
+
installed: extension.installed,
|
|
1182
|
+
version: extension.version || null,
|
|
1183
|
+
source: extension.install || null,
|
|
1184
|
+
officialSource: extension.officialSource || false,
|
|
1185
|
+
autoUpdate: extension.autoUpdate || false,
|
|
1186
|
+
installPath: extension.path,
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const skill = await inspectOpenClawSkill(scope)
|
|
1190
|
+
return {
|
|
1191
|
+
installed: skill.installed,
|
|
1192
|
+
version: skill.version,
|
|
1193
|
+
source: skill.origin,
|
|
1194
|
+
officialSource: skill.officialSource,
|
|
1195
|
+
autoUpdate: 'throttled_on_invocation',
|
|
1196
|
+
installPath: skill.path,
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function getSkillStatus(platform) {
|
|
1201
|
+
const scope = getDefaultScope(platform)
|
|
1202
|
+
const managed = await inspectPlatformManaged(platform, scope)
|
|
1203
|
+
const legacy = await inspectLegacy(platform, scope)
|
|
1204
|
+
const command = platform === 'openclaw' ? 'openclaw' : platform
|
|
1205
|
+
return {
|
|
1206
|
+
platform,
|
|
1207
|
+
scope,
|
|
1208
|
+
managerAvailable: Boolean(await findExecutable(command)),
|
|
1209
|
+
installed: Boolean(managed.installed),
|
|
1210
|
+
version: managed.version || null,
|
|
1211
|
+
officialSource: Boolean(managed.officialSource),
|
|
1212
|
+
autoUpdate: managed.autoUpdate ?? null,
|
|
1213
|
+
managed,
|
|
1214
|
+
legacy,
|
|
1215
|
+
healthy: Boolean(managed.installed && managed.officialSource && legacy.length === 0),
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function installSkill(options) {
|
|
1220
|
+
validatePlatform(options.platform)
|
|
1221
|
+
const scope = options.scope || getDefaultScope(options.platform)
|
|
1222
|
+
validateScope(options.platform, scope)
|
|
1223
|
+
const expectedHash = options.replaceLegacy ? parseSha256(options.replaceLegacy) : ''
|
|
1224
|
+
|
|
1225
|
+
// All source-conflict and manager checks happen before the legacy directory
|
|
1226
|
+
// is moved or any provider configuration is mutated.
|
|
1227
|
+
const execute = await buildPlatformInstallPlan(options.platform, scope, options.json)
|
|
1228
|
+
const legacy = await inspectLegacy(options.platform, scope)
|
|
1229
|
+
let migration = null
|
|
1230
|
+
try {
|
|
1231
|
+
migration = await migrateLegacy(legacy, expectedHash)
|
|
1232
|
+
const provider = await execute()
|
|
1233
|
+
return {
|
|
1234
|
+
command: 'skill.install',
|
|
1235
|
+
platform: options.platform,
|
|
1236
|
+
scope,
|
|
1237
|
+
provider,
|
|
1238
|
+
legacyMigration: migration ? {
|
|
1239
|
+
originalPath: migration.originalPath,
|
|
1240
|
+
backupPath: migration.backupPath,
|
|
1241
|
+
hash: migration.hash,
|
|
1242
|
+
} : null,
|
|
1243
|
+
}
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (migration) {
|
|
1246
|
+
try {
|
|
1247
|
+
await migration.restore()
|
|
1248
|
+
} catch (restoreError) {
|
|
1249
|
+
throw new SkillManagerError(
|
|
1250
|
+
`${error instanceof Error ? error.message : String(error)};legacy 自动恢复失败:${restoreError instanceof Error ? restoreError.message : String(restoreError)}`,
|
|
1251
|
+
'legacy_restore_failed',
|
|
1252
|
+
)
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
throw error
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function printInstallResult(result) {
|
|
1260
|
+
process.stdout.write(`Deyo Skill 已处理:${result.platform}(scope: ${result.scope})\n`)
|
|
1261
|
+
for (const action of result.provider.actions) {
|
|
1262
|
+
process.stdout.write(`- ${action}\n`)
|
|
1263
|
+
}
|
|
1264
|
+
if (result.legacyMigration) {
|
|
1265
|
+
process.stdout.write(`legacy 备份:${result.legacyMigration.backupPath}\n`)
|
|
1266
|
+
}
|
|
1267
|
+
process.stdout.write(`生效方式:${result.provider.activation}\n`)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function formatAutoUpdate(value) {
|
|
1271
|
+
if (value === true) return 'enabled'
|
|
1272
|
+
if (value === false) return 'disabled'
|
|
1273
|
+
return value || 'unknown'
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function printStatusResult(result) {
|
|
1277
|
+
for (const item of result.platforms) {
|
|
1278
|
+
const state = item.installed ? `installed${item.version ? ` v${item.version}` : ''}` : 'not installed'
|
|
1279
|
+
process.stdout.write(`${item.platform} (${item.scope}): ${state}\n`)
|
|
1280
|
+
process.stdout.write(` manager: ${item.managerAvailable ? 'available' : 'missing'}\n`)
|
|
1281
|
+
process.stdout.write(` official source: ${item.officialSource ? 'yes' : 'no'}\n`)
|
|
1282
|
+
process.stdout.write(` auto update: ${formatAutoUpdate(item.autoUpdate)}\n`)
|
|
1283
|
+
for (const legacy of item.legacy) {
|
|
1284
|
+
process.stdout.write(` legacy: ${legacy.path}\n`)
|
|
1285
|
+
process.stdout.write(` hash: ${legacy.hash}\n`)
|
|
1286
|
+
process.stdout.write(` official historical tree: ${legacy.recognized ? 'yes' : 'no'}\n`)
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
export async function handleSkillCommand(argv, cliVersion) {
|
|
1292
|
+
const wantsJson = argv.includes('--json')
|
|
1293
|
+
try {
|
|
1294
|
+
const args = parseSkillCommandArgs(argv)
|
|
1295
|
+
if (args.help) {
|
|
1296
|
+
printSkillUsage()
|
|
1297
|
+
return
|
|
1298
|
+
}
|
|
1299
|
+
if (args.platform) validatePlatform(args.platform)
|
|
1300
|
+
|
|
1301
|
+
if (args.action === 'install') {
|
|
1302
|
+
const result = await installSkill(args)
|
|
1303
|
+
const output = { cliVersion, ...result }
|
|
1304
|
+
if (args.json) process.stdout.write(`${JSON.stringify(output)}\n`)
|
|
1305
|
+
else printInstallResult(output)
|
|
1306
|
+
return
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const platforms = args.platform ? [args.platform] : SKILL_PLATFORMS
|
|
1310
|
+
const result = {
|
|
1311
|
+
command: 'skill.status',
|
|
1312
|
+
cliVersion,
|
|
1313
|
+
offline: true,
|
|
1314
|
+
platforms: [],
|
|
1315
|
+
}
|
|
1316
|
+
for (const platform of platforms) {
|
|
1317
|
+
result.platforms.push(await getSkillStatus(platform))
|
|
1318
|
+
}
|
|
1319
|
+
if (args.json) process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
1320
|
+
else printStatusResult(result)
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
const payload = {
|
|
1323
|
+
command: 'skill.error',
|
|
1324
|
+
error: error?.code || 'skill_error',
|
|
1325
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1326
|
+
}
|
|
1327
|
+
if (wantsJson) process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
1328
|
+
else process.stderr.write(`Deyo Skill 操作失败:${payload.message}\n`)
|
|
1329
|
+
throw new SkillCommandReportedError(1)
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
export const __test = {
|
|
1334
|
+
GITHUB_REPOSITORY,
|
|
1335
|
+
GIT_SOURCE_URL,
|
|
1336
|
+
MARKETPLACE_NAME,
|
|
1337
|
+
OFFICIAL_LEGACY_HASHES,
|
|
1338
|
+
OPENCLAW_SKILL,
|
|
1339
|
+
PLUGIN_ID,
|
|
1340
|
+
getGeminiHome,
|
|
1341
|
+
getLegacyPaths,
|
|
1342
|
+
getOpenClawHome,
|
|
1343
|
+
inspectLegacy,
|
|
1344
|
+
normalizeRepositorySource,
|
|
1345
|
+
writeJsonAtomic,
|
|
1346
|
+
}
|