Skip to content

Commit acab0dd

Browse files
committed
feat: wrap pipeline errors in SpmError with EINSTALL code
- Add INSTALL_ERROR (EINSTALL) error code - fetchQueue processor wraps fetch failures in SpmError with skill name context - linkQueue processor wraps link failures in SpmError with skill name context - pipeline/index.ts aggregates multiple errors into single SpmError with list - formatErrorForDisplay shows failed skill list for INSTALL_ERROR
1 parent f5478e3 commit acab0dd

5 files changed

Lines changed: 65 additions & 24 deletions

File tree

packages/skills-package-manager/src/errors/codes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export enum ErrorCode {
3333
NETWORK_ERROR = 'ENETWORK',
3434
REPO_NOT_FOUND = 'EREPONOTFOUND',
3535

36+
// Install/Pipeline errors (6xx)
37+
INSTALL_ERROR = 'EINSTALL',
38+
3639
// General errors (9xx)
3740
UNKNOWN_ERROR = 'EUNKNOWN',
3841
NOT_IMPLEMENTED = 'ENOTIMPL',

packages/skills-package-manager/src/errors/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ export function formatErrorForDisplay(error: unknown): string {
128128
output += `\n\nPlease fix the validation errors in "${error.filePath}".`
129129
output += `\nRefer to the JSON Schema at: https://unpkg.com/skills-package-manager@latest/skills.schema.json`
130130
}
131+
} else if (error.code === ErrorCode.INSTALL_ERROR) {
132+
const errorList = error.context.errors as string[] | undefined
133+
if (errorList && errorList.length > 1) {
134+
output += `\n\nFailed skills:`
135+
for (const msg of errorList) {
136+
output += `\n - ${msg}`
137+
}
138+
}
131139
}
132140

133141
if (error.cause && !(error instanceof GitError || error instanceof FileSystemError)) {

packages/skills-package-manager/src/pipeline/fetchQueue.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { access, lstat, readFile, readlink } from 'node:fs/promises'
22
import path from 'node:path'
3+
import { ErrorCode, SpmError } from '../errors'
34
import { fetchSkill } from '../fetchers'
45
import { applySkillPatch } from '../patches/skillPatch'
56
import { createTaskQueue, type TaskQueue } from './queue'
@@ -56,26 +57,35 @@ export function createFetchTaskQueue(
5657
return result
5758
}
5859

59-
const { installPath, fromCache } = await fetchSkill(
60-
ctx.cwd,
61-
task.skillName,
62-
task.entry,
63-
installDir,
64-
ctx.cache,
65-
)
60+
try {
61+
const { installPath, fromCache } = await fetchSkill(
62+
ctx.cwd,
63+
task.skillName,
64+
task.entry,
65+
installDir,
66+
ctx.cache,
67+
)
6668

67-
if (task.entry.patch) {
68-
await applySkillPatch(installPath, path.resolve(ctx.cwd, task.entry.patch.path))
69-
}
69+
if (task.entry.patch) {
70+
await applySkillPatch(installPath, path.resolve(ctx.cwd, task.entry.patch.path))
71+
}
7072

71-
const result: FetchResult = {
72-
skillName: task.skillName,
73-
entry: task.entry,
74-
installPath,
75-
fromCache,
73+
const result: FetchResult = {
74+
skillName: task.skillName,
75+
entry: task.entry,
76+
installPath,
77+
fromCache,
78+
}
79+
bus.emitFetched(result)
80+
return result
81+
} catch (error) {
82+
throw new SpmError({
83+
code: ErrorCode.INSTALL_ERROR,
84+
message: `Failed to fetch skill "${task.skillName}": ${error instanceof Error ? error.message : String(error)}`,
85+
cause: error instanceof Error ? error : undefined,
86+
context: { skillName: task.skillName, phase: 'fetch' },
87+
})
7688
}
77-
bus.emitFetched(result)
78-
return result
7989
}
8090

8191
return createTaskQueue(processor, options)

packages/skills-package-manager/src/pipeline/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { access } from 'node:fs/promises'
22
import path from 'node:path'
33
import type { SkillsLock, SkillsLockEntry } from '../config/types'
4+
import { ErrorCode, SpmError } from '../errors'
45
import { writeInstallState } from '../install/installState'
56
import { pruneManagedSkills } from '../install/pruneManagedSkills'
67
import { installStageHooks } from '../install/withBundledSelfSkillLock'
@@ -146,7 +147,16 @@ export async function runPipeline(input: RunPipelineInput): Promise<PipelineResu
146147
await linkQueue.drain()
147148

148149
if (errors.length > 0) {
149-
throw errors[0]
150+
const first = errors[0]
151+
if (errors.length === 1) {
152+
throw first
153+
}
154+
throw new SpmError({
155+
code: ErrorCode.INSTALL_ERROR,
156+
message: `${errors.length} skills failed to install`,
157+
cause: first instanceof Error ? first : undefined,
158+
context: { errors: errors.map((e) => (e instanceof Error ? e.message : String(e))) },
159+
})
150160
}
151161

152162
const results = bus.getResults()

packages/skills-package-manager/src/pipeline/linkQueue.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ErrorCode, SpmError } from '../errors'
12
import { linkSkill } from '../install/links'
23
import { createTaskQueue, type TaskQueue } from './queue'
34
import type { LinkResult, LinkTask, PipelineBus, WorkspaceContext } from './types'
@@ -13,13 +14,22 @@ export function createLinkTaskQueue(
1314
const linkTargets = ctx.lockfile?.linkTargets ?? ctx.manifest.linkTargets ?? []
1415

1516
async function processor(task: LinkTask): Promise<LinkResult> {
16-
for (const linkTarget of linkTargets) {
17-
await linkSkill(ctx.cwd, installDir, linkTarget, task.skillName)
18-
}
17+
try {
18+
for (const linkTarget of linkTargets) {
19+
await linkSkill(ctx.cwd, installDir, linkTarget, task.skillName)
20+
}
1921

20-
const result: LinkResult = { skillName: task.skillName }
21-
bus.emitLinked(result)
22-
return result
22+
const result: LinkResult = { skillName: task.skillName }
23+
bus.emitLinked(result)
24+
return result
25+
} catch (error) {
26+
throw new SpmError({
27+
code: ErrorCode.INSTALL_ERROR,
28+
message: `Failed to link skill "${task.skillName}": ${error instanceof Error ? error.message : String(error)}`,
29+
cause: error instanceof Error ? error : undefined,
30+
context: { skillName: task.skillName, phase: 'link' },
31+
})
32+
}
2333
}
2434

2535
return createTaskQueue(processor, options)

0 commit comments

Comments
 (0)