fix(cli): resolve SKILL.md not found errors from duplicate path prefixes
When skillPath from the registry had a prefix (e.g., "skills/foo"), fallback paths were incorrectly generated with duplicate prefixes like ".claude/skills/skills/foo/SKILL.md". Now extracts the leaf skill name to build correct fallback paths. Also adds raw.githubusercontent.com as a universal fallback for 404s, improves error messages with actual paths tried, and skips API results that lack the main instruction file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,10 +103,16 @@ export async function install(skillId: string, options: InstallOptions): Promise
|
|||||||
sourceFormat = cachedFiles.sourceFormat as SourceFormat;
|
sourceFormat = cachedFiles.sourceFormat as SourceFormat;
|
||||||
}
|
}
|
||||||
// Convert API response to SkillContent format
|
// Convert API response to SkillContent format
|
||||||
content = convertCachedFilesToSkillContent(cachedFiles, sourceFormat);
|
const converted = convertCachedFilesToSkillContent(cachedFiles, sourceFormat);
|
||||||
|
// Only use API result if the main instruction file was found
|
||||||
|
if (converted.skillMd) {
|
||||||
|
content = converted;
|
||||||
spinner.text = cachedFiles.fromCache
|
spinner.text = cachedFiles.fromCache
|
||||||
? `Using cached files (${cachedFiles.files.length} files)`
|
? `Using cached files (${cachedFiles.files.length} files)`
|
||||||
: `Downloaded ${cachedFiles.files.length} files via API`;
|
: `Downloaded ${cachedFiles.files.length} files via API`;
|
||||||
|
} else {
|
||||||
|
spinner.text = 'API returned files but main instruction file missing, falling back...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// API was reachable but file fetch failed (timeout, server error, etc.)
|
// API was reachable but file fetch failed (timeout, server error, etc.)
|
||||||
|
|||||||
@@ -100,12 +100,26 @@ export async function fetchSkillContent(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SKILL.md - try multiple common paths
|
// SKILL.md - try multiple common paths
|
||||||
pathsToTry = [
|
// Extract the leaf skill name to avoid duplicate prefixes
|
||||||
basePath,
|
// e.g., "skills/claude-md-architect" → "claude-md-architect"
|
||||||
...(skillPath && !skillPath.startsWith('skills/') ? [`skills/${skillPath}/SKILL.md`] : []),
|
const skillName = skillPath
|
||||||
...(skillPath && !skillPath.startsWith('.claude/') ? [`.claude/skills/${skillPath}/SKILL.md`] : []),
|
? skillPath.replace(/^(skills|\.claude\/skills|\.github\/skills)\//, '')
|
||||||
...(skillPath && !skillPath.startsWith('.github/') ? [`.github/skills/${skillPath}/SKILL.md`] : []),
|
: '';
|
||||||
|
|
||||||
|
pathsToTry = [basePath];
|
||||||
|
if (skillName) {
|
||||||
|
// Add all common locations, deduplicating against basePath
|
||||||
|
const candidates = [
|
||||||
|
`skills/${skillName}/${filename}`,
|
||||||
|
`.claude/skills/${skillName}/${filename}`,
|
||||||
|
`.github/skills/${skillName}/${filename}`,
|
||||||
];
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (candidate !== basePath) {
|
||||||
|
pathsToTry.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pathToTry of pathsToTry) {
|
for (const pathToTry of pathsToTry) {
|
||||||
@@ -123,7 +137,6 @@ export async function fetchSkillContent(
|
|||||||
if (error.message?.includes('timeout') || error.message?.includes('network')) {
|
if (error.message?.includes('timeout') || error.message?.includes('network')) {
|
||||||
try {
|
try {
|
||||||
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
|
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
|
||||||
// Create a mock response compatible with Octokit
|
|
||||||
skillMdResponse = {
|
skillMdResponse = {
|
||||||
data: {
|
data: {
|
||||||
content: Buffer.from(rawContent).toString('base64'),
|
content: Buffer.from(rawContent).toString('base64'),
|
||||||
@@ -131,22 +144,51 @@ export async function fetchSkillContent(
|
|||||||
}
|
}
|
||||||
} as any;
|
} as any;
|
||||||
break;
|
break;
|
||||||
} catch (rawError) {
|
} catch {
|
||||||
// If raw fetch also fails, throw original error
|
// Raw fetch also failed, continue to next path
|
||||||
throw new Error(`GitHub API timeout. Try using --no-api flag or check your network connection.`);
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If 404, try next path
|
// If 404, try next path
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Rate limiting - provide a clear error
|
||||||
|
if (error.status === 403) {
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable for higher rate limits.`
|
||||||
|
);
|
||||||
|
}
|
||||||
// Other errors, throw immediately
|
// Other errors, throw immediately
|
||||||
throw new Error(`Failed to fetch from GitHub: ${error.message}`);
|
throw new Error(`Failed to fetch from GitHub: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Octokit failed on all paths, try raw.githubusercontent.com as last resort
|
||||||
|
// (handles cases where GitHub API returns 404 but raw CDN has the file,
|
||||||
|
// e.g., during API caching delays or edge cases)
|
||||||
if (!skillMdResponse) {
|
if (!skillMdResponse) {
|
||||||
throw new Error(`${filename} not found at ${owner}/${repo} (tried ${pathsToTry.length} paths)`);
|
for (const pathToTry of pathsToTry) {
|
||||||
|
try {
|
||||||
|
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
|
||||||
|
skillMdResponse = {
|
||||||
|
data: {
|
||||||
|
content: Buffer.from(rawContent).toString('base64'),
|
||||||
|
encoding: 'base64' as const,
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skillMdResponse) {
|
||||||
|
const triedPaths = pathsToTry.map(p => ` - ${owner}/${repo}/${p}`).join('\n');
|
||||||
|
throw new Error(
|
||||||
|
`${filename} not found at ${owner}/${repo} (tried ${pathsToTry.length} paths):\n${triedPaths}\n\nThe skill may have been moved or deleted from the repository.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!('content' in skillMdResponse.data)) {
|
if (!('content' in skillMdResponse.data)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user