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;
|
||||
}
|
||||
// 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
|
||||
? `Using cached files (${cachedFiles.files.length} files)`
|
||||
: `Downloaded ${cachedFiles.files.length} files via API`;
|
||||
} else {
|
||||
spinner.text = 'API returned files but main instruction file missing, falling back...';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// API was reachable but file fetch failed (timeout, server error, etc.)
|
||||
|
||||
@@ -100,12 +100,26 @@ export async function fetchSkillContent(
|
||||
}
|
||||
} else {
|
||||
// SKILL.md - try multiple common paths
|
||||
pathsToTry = [
|
||||
basePath,
|
||||
...(skillPath && !skillPath.startsWith('skills/') ? [`skills/${skillPath}/SKILL.md`] : []),
|
||||
...(skillPath && !skillPath.startsWith('.claude/') ? [`.claude/skills/${skillPath}/SKILL.md`] : []),
|
||||
...(skillPath && !skillPath.startsWith('.github/') ? [`.github/skills/${skillPath}/SKILL.md`] : []),
|
||||
// Extract the leaf skill name to avoid duplicate prefixes
|
||||
// e.g., "skills/claude-md-architect" → "claude-md-architect"
|
||||
const skillName = skillPath
|
||||
? skillPath.replace(/^(skills|\.claude\/skills|\.github\/skills)\//, '')
|
||||
: '';
|
||||
|
||||
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) {
|
||||
@@ -123,7 +137,6 @@ export async function fetchSkillContent(
|
||||
if (error.message?.includes('timeout') || error.message?.includes('network')) {
|
||||
try {
|
||||
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
|
||||
// Create a mock response compatible with Octokit
|
||||
skillMdResponse = {
|
||||
data: {
|
||||
content: Buffer.from(rawContent).toString('base64'),
|
||||
@@ -131,22 +144,51 @@ export async function fetchSkillContent(
|
||||
}
|
||||
} as any;
|
||||
break;
|
||||
} catch (rawError) {
|
||||
// If raw fetch also fails, throw original error
|
||||
throw new Error(`GitHub API timeout. Try using --no-api flag or check your network connection.`);
|
||||
} catch {
|
||||
// Raw fetch also failed, continue to next path
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If 404, try next path
|
||||
if (error.status === 404) {
|
||||
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
|
||||
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) {
|
||||
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)) {
|
||||
|
||||
Reference in New Issue
Block a user