@vibe-forge/workspace-assets 2.0.2 → 2.0.4-alpha.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/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +1 -1
- package/__tests__/adapter-asset-plan.spec.ts +67 -65
- package/__tests__/bundle.spec.ts +208 -138
- package/__tests__/prompt-builders.spec.ts +2 -6
- package/__tests__/skill-dependencies-cli.spec.ts +234 -0
- package/package.json +2 -2
- package/src/bundle-internal.ts +18 -3
- package/src/bundle.ts +2 -0
- package/src/configured-skills.ts +85 -0
- package/src/prompt-selection.ts +2 -2
- package/src/selection-internal.ts +2 -2
- package/src/skill-dependencies.ts +22 -40
- package/src/skills-cli-dependency-helpers.ts +94 -0
- package/src/skills-cli-dependency.ts +104 -0
- package/src/task-tool-guidance.ts +2 -4
- package/src/skill-registry.ts +0 -329
- package/vibe-forge-workspace-assets-2.0.2.tgz +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import type { Config } from '@vibe-forge/types'
|
|
6
|
+
import { findSkillsCli, installSkillsCliRefToTemp, installSkillsCliSkillToTemp } from '@vibe-forge/utils/skills-cli'
|
|
7
|
+
|
|
8
|
+
import type { NormalizedSkillDependency } from './skill-dependencies'
|
|
9
|
+
import {
|
|
10
|
+
buildInstallDir,
|
|
11
|
+
copyRegularFiles,
|
|
12
|
+
pathExists,
|
|
13
|
+
pickSearchResult,
|
|
14
|
+
withInstallLock
|
|
15
|
+
} from './skills-cli-dependency-helpers'
|
|
16
|
+
|
|
17
|
+
export const installSkillsCliDependency = async (params: {
|
|
18
|
+
cwd: string
|
|
19
|
+
configs: [Config?, Config?]
|
|
20
|
+
dependency: NormalizedSkillDependency
|
|
21
|
+
}) => {
|
|
22
|
+
const resolvedTarget = params.dependency.source != null
|
|
23
|
+
? {
|
|
24
|
+
skill: params.dependency.name,
|
|
25
|
+
source: params.dependency.source
|
|
26
|
+
}
|
|
27
|
+
: await (async () => {
|
|
28
|
+
const searchResults = await findSkillsCli({
|
|
29
|
+
registry: params.dependency.registry,
|
|
30
|
+
query: params.dependency.name
|
|
31
|
+
})
|
|
32
|
+
const selected = pickSearchResult(searchResults, params.dependency.name)
|
|
33
|
+
if (selected == null) {
|
|
34
|
+
throw new Error(`Skill ${params.dependency.name} was not found by the skills CLI search.`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
installRef: selected.installRef,
|
|
39
|
+
skill: selected.skill,
|
|
40
|
+
source: selected.source
|
|
41
|
+
}
|
|
42
|
+
})()
|
|
43
|
+
|
|
44
|
+
const installDir = buildInstallDir({
|
|
45
|
+
cwd: params.cwd,
|
|
46
|
+
registry: params.dependency.registry,
|
|
47
|
+
skill: resolvedTarget.skill,
|
|
48
|
+
source: resolvedTarget.source,
|
|
49
|
+
version: params.dependency.version
|
|
50
|
+
})
|
|
51
|
+
const skillPath = resolve(installDir, 'SKILL.md')
|
|
52
|
+
|
|
53
|
+
return await withInstallLock(`${installDir}.lock`, async () => {
|
|
54
|
+
if (await pathExists(skillPath)) {
|
|
55
|
+
return {
|
|
56
|
+
installDir,
|
|
57
|
+
skillPath
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tempInstallDir = `${installDir}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
62
|
+
await rm(tempInstallDir, { recursive: true, force: true })
|
|
63
|
+
await mkdir(tempInstallDir, { recursive: true })
|
|
64
|
+
|
|
65
|
+
const installResult = 'installRef' in resolvedTarget
|
|
66
|
+
? params.dependency.version == null
|
|
67
|
+
? await installSkillsCliRefToTemp({
|
|
68
|
+
installRef: resolvedTarget.installRef,
|
|
69
|
+
registry: params.dependency.registry
|
|
70
|
+
})
|
|
71
|
+
: await installSkillsCliSkillToTemp({
|
|
72
|
+
registry: params.dependency.registry,
|
|
73
|
+
skill: resolvedTarget.skill,
|
|
74
|
+
source: resolvedTarget.source,
|
|
75
|
+
version: params.dependency.version
|
|
76
|
+
})
|
|
77
|
+
: await installSkillsCliSkillToTemp({
|
|
78
|
+
registry: params.dependency.registry,
|
|
79
|
+
skill: resolvedTarget.skill,
|
|
80
|
+
source: resolvedTarget.source,
|
|
81
|
+
version: params.dependency.version
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await copyRegularFiles(installResult.installedSkill.sourcePath, tempInstallDir)
|
|
86
|
+
if (!await pathExists(resolve(tempInstallDir, 'SKILL.md'))) {
|
|
87
|
+
throw new Error(`Skill dependency ${params.dependency.ref} did not include SKILL.md`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await rm(installDir, { recursive: true, force: true })
|
|
91
|
+
await rename(tempInstallDir, installDir)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await rm(tempInstallDir, { recursive: true, force: true })
|
|
94
|
+
throw error
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(installResult.tempDir, { recursive: true, force: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
installDir,
|
|
101
|
+
skillPath
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -5,10 +5,8 @@ export const buildManagedTaskToolGuidance = (serverName: string) => {
|
|
|
5
5
|
`- After starting a task, use \`${serverName}.GetTaskInfo\` with \`{ taskId }\` to inspect one task. It is also the right tool when a task seems stalled, failed, or might be waiting for input.`,
|
|
6
6
|
'- By default, `GetTaskInfo` returns the 10 most recent log entries in descending order, so newer entries appear earlier in the `logs` array. Pass `logLimit` to inspect a different number of recent logs, and set `logOrder` to `"asc"` when you want the selected log window in oldest-to-newest order.',
|
|
7
7
|
`- Use \`${serverName}.ListTasks\` with the same \`logLimit\` and \`logOrder\` fields when you need to find a taskId or inspect multiple tasks at once.`,
|
|
8
|
-
`- Use \`${serverName}.SendTaskMessage\` with \`{ taskId, message
|
|
9
|
-
|
|
10
|
-
'- Choose `mode: "steer"` when the task should finish its current run naturally first, and the follow-up should be queued for the same task afterward.',
|
|
11
|
-
`- Use \`${serverName}.SubmitTaskInput\` only when \`${serverName}.GetTaskInfo\` or \`${serverName}.ListTasks\` shows \`pendingInput\` or \`pendingInteraction\`, or the task status is \`waiting_input\`. Do not use it for ordinary follow-up instructions or queued steer turns.`,
|
|
8
|
+
`- Use \`${serverName}.SendTaskMessage\` with \`{ taskId, message }\` only when a task status is \`running\` and you need to give it another instruction without starting a replacement task.`,
|
|
9
|
+
`- Use \`${serverName}.SubmitTaskInput\` only when \`${serverName}.GetTaskInfo\` or \`${serverName}.ListTasks\` shows \`pendingInput\` or \`pendingInteraction\`, or the task status is \`waiting_input\`. Do not use it for ordinary follow-up instructions.`,
|
|
12
10
|
'- If a task is `completed` or `failed`, start a new task instead of trying to continue the old one.',
|
|
13
11
|
'- When a task is still making progress, use `wait` between checks instead of repeatedly restarting it.'
|
|
14
12
|
].join('\n')
|
package/src/skill-registry.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-lines -- registry install logic keeps URL resolution, locking, and cache writes together */
|
|
2
|
-
import { access, mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
4
|
-
import process from 'node:process'
|
|
5
|
-
import { setTimeout as delay } from 'node:timers/promises'
|
|
6
|
-
|
|
7
|
-
import type { Config } from '@vibe-forge/types'
|
|
8
|
-
import { resolveProjectSharedCachePath } from '@vibe-forge/utils/project-cache-path'
|
|
9
|
-
|
|
10
|
-
import type { NormalizedSkillDependency } from './skill-dependencies'
|
|
11
|
-
|
|
12
|
-
interface RegistryOptions {
|
|
13
|
-
enabled: boolean
|
|
14
|
-
searchBaseUrl: string
|
|
15
|
-
downloadBaseUrl: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface RegistrySearchSkill {
|
|
19
|
-
id?: string
|
|
20
|
-
name?: string
|
|
21
|
-
skillId?: string
|
|
22
|
-
source?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface RegistryDownloadFile {
|
|
26
|
-
path?: string
|
|
27
|
-
contents?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface RegistryDownloadResponse {
|
|
31
|
-
files?: RegistryDownloadFile[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const DEFAULT_SKILL_REGISTRY_URL = 'https://skills.sh'
|
|
35
|
-
const FETCH_TIMEOUT_MS = 10_000
|
|
36
|
-
const INSTALL_LOCK_TIMEOUT_MS = 30_000
|
|
37
|
-
const INSTALL_LOCK_RETRY_MS = 100
|
|
38
|
-
|
|
39
|
-
const asNonEmptyString = (value: unknown) => (
|
|
40
|
-
typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '')
|
|
44
|
-
|
|
45
|
-
const toSkillSlug = (value: string) => (
|
|
46
|
-
value
|
|
47
|
-
.trim()
|
|
48
|
-
.toLowerCase()
|
|
49
|
-
.replace(/[\s_]+/g, '-')
|
|
50
|
-
.replace(/[^a-z0-9-]/g, '')
|
|
51
|
-
.replace(/-+/g, '-')
|
|
52
|
-
.replace(/^-|-$/g, '')
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
const toCacheSegment = (value: string) => (
|
|
56
|
-
value
|
|
57
|
-
.trim()
|
|
58
|
-
.toLowerCase()
|
|
59
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
60
|
-
.replace(/^-+|-+$/g, '') || 'registry'
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
const resolveConfiguredRegistry = (
|
|
64
|
-
projectConfig: Config | undefined,
|
|
65
|
-
userConfig: Config | undefined
|
|
66
|
-
) => userConfig?.skills?.registry ?? projectConfig?.skills?.registry
|
|
67
|
-
|
|
68
|
-
const resolveRegistryOptions = (params: {
|
|
69
|
-
configs: [Config?, Config?]
|
|
70
|
-
registry?: string
|
|
71
|
-
}): RegistryOptions => {
|
|
72
|
-
const [projectConfig, userConfig] = params.configs
|
|
73
|
-
const configuredRegistry = params.registry ?? resolveConfiguredRegistry(projectConfig, userConfig)
|
|
74
|
-
const envSearchBaseUrl = asNonEmptyString(process.env.SKILLS_API_URL)
|
|
75
|
-
const envDownloadBaseUrl = asNonEmptyString(process.env.SKILLS_DOWNLOAD_URL)
|
|
76
|
-
|
|
77
|
-
if (typeof configuredRegistry === 'string' && configuredRegistry.trim() !== '') {
|
|
78
|
-
const baseUrl = normalizeBaseUrl(configuredRegistry.trim())
|
|
79
|
-
return {
|
|
80
|
-
enabled: true,
|
|
81
|
-
searchBaseUrl: baseUrl,
|
|
82
|
-
downloadBaseUrl: baseUrl
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (configuredRegistry != null && typeof configuredRegistry === 'object') {
|
|
87
|
-
const baseUrl = normalizeBaseUrl(
|
|
88
|
-
asNonEmptyString(configuredRegistry.url) ?? DEFAULT_SKILL_REGISTRY_URL
|
|
89
|
-
)
|
|
90
|
-
return {
|
|
91
|
-
enabled: configuredRegistry.enabled !== false,
|
|
92
|
-
searchBaseUrl: normalizeBaseUrl(asNonEmptyString(configuredRegistry.searchUrl) ?? baseUrl),
|
|
93
|
-
downloadBaseUrl: normalizeBaseUrl(asNonEmptyString(configuredRegistry.downloadUrl) ?? baseUrl)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const baseUrl = normalizeBaseUrl(envSearchBaseUrl ?? DEFAULT_SKILL_REGISTRY_URL)
|
|
98
|
-
return {
|
|
99
|
-
enabled: true,
|
|
100
|
-
searchBaseUrl: baseUrl,
|
|
101
|
-
downloadBaseUrl: normalizeBaseUrl(envDownloadBaseUrl ?? baseUrl)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const fetchJson = async <T>(url: string): Promise<T> => {
|
|
106
|
-
const response = await fetch(url, {
|
|
107
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
108
|
-
})
|
|
109
|
-
if (!response.ok) {
|
|
110
|
-
throw new Error(`Request failed with status ${response.status}`)
|
|
111
|
-
}
|
|
112
|
-
return await response.json() as T
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parseSourceAndSlugFromId = (id: string | undefined) => {
|
|
116
|
-
if (id == null) return undefined
|
|
117
|
-
const segments = id.split('/').filter(Boolean)
|
|
118
|
-
if (segments.length < 3) return undefined
|
|
119
|
-
return {
|
|
120
|
-
source: `${segments[0]}/${segments[1]}`,
|
|
121
|
-
slug: segments.slice(2).join('/')
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const pickSearchResult = (
|
|
126
|
-
skills: RegistrySearchSkill[],
|
|
127
|
-
name: string
|
|
128
|
-
) => {
|
|
129
|
-
const slug = toSkillSlug(name)
|
|
130
|
-
return skills.find(skill => (
|
|
131
|
-
skill.skillId === slug ||
|
|
132
|
-
skill.name === name ||
|
|
133
|
-
toSkillSlug(skill.name ?? '') === slug ||
|
|
134
|
-
parseSourceAndSlugFromId(skill.id)?.slug === slug
|
|
135
|
-
)) ?? skills[0]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const resolveRegistrySkillTarget = async (
|
|
139
|
-
dependency: NormalizedSkillDependency,
|
|
140
|
-
registry: RegistryOptions
|
|
141
|
-
) => {
|
|
142
|
-
if (dependency.source != null) {
|
|
143
|
-
return {
|
|
144
|
-
source: dependency.source,
|
|
145
|
-
slug: toSkillSlug(dependency.name)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const searchUrl = `${registry.searchBaseUrl}/api/search?q=${encodeURIComponent(dependency.name)}&limit=10`
|
|
150
|
-
const searchResult = await fetchJson<{
|
|
151
|
-
skills?: RegistrySearchSkill[]
|
|
152
|
-
}>(searchUrl)
|
|
153
|
-
const skills = Array.isArray(searchResult.skills) ? searchResult.skills : []
|
|
154
|
-
const picked = pickSearchResult(skills, dependency.name)
|
|
155
|
-
if (picked == null) {
|
|
156
|
-
throw new Error(`Skill ${dependency.name} was not found in ${registry.searchBaseUrl}`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const parsed = parseSourceAndSlugFromId(picked.id)
|
|
160
|
-
const source = asNonEmptyString(picked.source) ?? parsed?.source
|
|
161
|
-
const slug = asNonEmptyString(picked.skillId) ?? parsed?.slug ?? toSkillSlug(picked.name ?? dependency.name)
|
|
162
|
-
if (source == null || slug === '') {
|
|
163
|
-
throw new Error(`Registry search result for ${dependency.name} did not include a source and skill id`)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
source,
|
|
168
|
-
slug
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const toSafeRelativePath = (filePath: string) => {
|
|
173
|
-
const normalized = filePath.replace(/\\/g, '/')
|
|
174
|
-
if (normalized.startsWith('/') || normalized.split('/').includes('..')) {
|
|
175
|
-
throw new Error(`Unsafe skill dependency file path ${filePath}`)
|
|
176
|
-
}
|
|
177
|
-
return normalized
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const assertInside = (root: string, target: string) => {
|
|
181
|
-
const relativePath = relative(root, target)
|
|
182
|
-
if (
|
|
183
|
-
relativePath === '' ||
|
|
184
|
-
(
|
|
185
|
-
relativePath !== '..' &&
|
|
186
|
-
!relativePath.startsWith(`..${sep}`) &&
|
|
187
|
-
!isAbsolute(relativePath)
|
|
188
|
-
)
|
|
189
|
-
) {
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
throw new Error(`Skill dependency file resolves outside ${root}`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const pathExists = async (path: string) => {
|
|
196
|
-
try {
|
|
197
|
-
await access(path)
|
|
198
|
-
return true
|
|
199
|
-
} catch {
|
|
200
|
-
return false
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const withInstallLock = async <T>(lockDir: string, callback: () => Promise<T>) => {
|
|
205
|
-
const start = Date.now()
|
|
206
|
-
await mkdir(dirname(lockDir), { recursive: true })
|
|
207
|
-
|
|
208
|
-
while (true) {
|
|
209
|
-
try {
|
|
210
|
-
await mkdir(lockDir)
|
|
211
|
-
break
|
|
212
|
-
} catch (error) {
|
|
213
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
214
|
-
if (Date.now() - start > INSTALL_LOCK_TIMEOUT_MS) {
|
|
215
|
-
throw new Error(`Timed out waiting for skill dependency install lock ${lockDir}`)
|
|
216
|
-
}
|
|
217
|
-
await delay(INSTALL_LOCK_RETRY_MS)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
return await callback()
|
|
223
|
-
} finally {
|
|
224
|
-
await rm(lockDir, { recursive: true, force: true })
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const buildInstallDir = (params: {
|
|
229
|
-
cwd: string
|
|
230
|
-
registry: RegistryOptions
|
|
231
|
-
source: string
|
|
232
|
-
slug: string
|
|
233
|
-
}) => {
|
|
234
|
-
let registryKey = params.registry.downloadBaseUrl
|
|
235
|
-
try {
|
|
236
|
-
const parsed = new URL(params.registry.downloadBaseUrl)
|
|
237
|
-
registryKey = `${parsed.hostname}${parsed.pathname}`
|
|
238
|
-
} catch {
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return resolveProjectSharedCachePath(
|
|
242
|
-
params.cwd,
|
|
243
|
-
process.env,
|
|
244
|
-
'skill-dependencies',
|
|
245
|
-
toCacheSegment(registryKey),
|
|
246
|
-
...params.source.split('/').map(toCacheSegment),
|
|
247
|
-
toCacheSegment(params.slug)
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export const installRegistrySkillDependency = async (params: {
|
|
252
|
-
cwd: string
|
|
253
|
-
configs: [Config?, Config?]
|
|
254
|
-
dependency: NormalizedSkillDependency
|
|
255
|
-
}) => {
|
|
256
|
-
const registry = resolveRegistryOptions({
|
|
257
|
-
configs: params.configs,
|
|
258
|
-
registry: params.dependency.registry
|
|
259
|
-
})
|
|
260
|
-
if (!registry.enabled) {
|
|
261
|
-
throw new Error(`Skill dependency registry is disabled; cannot install ${params.dependency.ref}`)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const target = await resolveRegistrySkillTarget(params.dependency, registry)
|
|
265
|
-
const [owner, repo] = target.source.split('/')
|
|
266
|
-
if (owner == null || repo == null || owner === '' || repo === '') {
|
|
267
|
-
throw new Error(`Skill dependency source ${target.source} must use owner/repo format`)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const downloadUrl = `${registry.downloadBaseUrl}/api/download/${encodeURIComponent(owner)}/${
|
|
271
|
-
encodeURIComponent(repo)
|
|
272
|
-
}/${encodeURIComponent(target.slug)}`
|
|
273
|
-
const installDir = buildInstallDir({
|
|
274
|
-
cwd: params.cwd,
|
|
275
|
-
registry,
|
|
276
|
-
source: target.source,
|
|
277
|
-
slug: target.slug
|
|
278
|
-
})
|
|
279
|
-
const skillPath = resolve(installDir, 'SKILL.md')
|
|
280
|
-
|
|
281
|
-
return await withInstallLock(`${installDir}.lock`, async () => {
|
|
282
|
-
if (await pathExists(skillPath)) {
|
|
283
|
-
return {
|
|
284
|
-
installDir,
|
|
285
|
-
skillPath
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const response = await fetchJson<RegistryDownloadResponse>(downloadUrl)
|
|
290
|
-
const files = Array.isArray(response.files) ? response.files : []
|
|
291
|
-
if (files.length === 0) {
|
|
292
|
-
throw new Error(`Skill dependency ${params.dependency.ref} did not include any files`)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const tempDir = `${installDir}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
296
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
297
|
-
await mkdir(tempDir, { recursive: true })
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
let hasSkillMd = false
|
|
301
|
-
for (const file of files) {
|
|
302
|
-
const filePath = asNonEmptyString(file.path)
|
|
303
|
-
if (filePath == null || typeof file.contents !== 'string') continue
|
|
304
|
-
const relativePath = toSafeRelativePath(filePath)
|
|
305
|
-
if (relativePath.toLowerCase() === 'skill.md') hasSkillMd = true
|
|
306
|
-
|
|
307
|
-
const targetPath = resolve(tempDir, relativePath)
|
|
308
|
-
assertInside(tempDir, targetPath)
|
|
309
|
-
await mkdir(dirname(targetPath), { recursive: true })
|
|
310
|
-
await writeFile(targetPath, file.contents, 'utf8')
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!hasSkillMd) {
|
|
314
|
-
throw new Error(`Skill dependency ${params.dependency.ref} did not include SKILL.md`)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
await rm(installDir, { recursive: true, force: true })
|
|
318
|
-
await rename(tempDir, installDir)
|
|
319
|
-
} catch (error) {
|
|
320
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
321
|
-
throw error
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
installDir,
|
|
326
|
-
skillPath
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
}
|
|
Binary file
|