@spardutti/claude-skills 2.30.0 → 2.30.2
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/lib/github.mjs +68 -43
- package/package.json +1 -1
package/lib/github.mjs
CHANGED
|
@@ -34,6 +34,42 @@ function getAuthHeaders() {
|
|
|
34
34
|
return headers;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// raw.githubusercontent.com is not the API and the token buys nothing there, so
|
|
38
|
+
// it is not sent. This is hygiene, not a fix: it was first committed as the
|
|
39
|
+
// cause of a wave of 503s, and that was wrong. Measured after: bare requests
|
|
40
|
+
// returned 503 at the same rate as authenticated ones, and both went back to
|
|
41
|
+
// 200 once the window passed. raw was simply unwell for a few minutes.
|
|
42
|
+
function getRawHeaders() {
|
|
43
|
+
return { "User-Agent": "claude-skills-cli" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// What actually cost the user seven skills was the handling below, not the
|
|
47
|
+
// headers: a failed fetch printed a warning, returned null, and the install
|
|
48
|
+
// exited 0 having quietly dropped whatever it could not get. A 503 that clears
|
|
49
|
+
// in two seconds should never reach the user, and one that does not clear must
|
|
50
|
+
// not look like success.
|
|
51
|
+
const RETRIES = 4;
|
|
52
|
+
|
|
53
|
+
async function fetchRaw(url) {
|
|
54
|
+
let last = "";
|
|
55
|
+
for (let attempt = 0; attempt < RETRIES; attempt++) {
|
|
56
|
+
// 400ms, 800ms, 1600ms. The outage that prompted this cleared well inside
|
|
57
|
+
// that; a longer wait would only make a real outage slower to report.
|
|
58
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, 400 * 2 ** (attempt - 1)));
|
|
59
|
+
try {
|
|
60
|
+
const r = await fetch(url, { headers: getRawHeaders() });
|
|
61
|
+
if (r.ok) return await r.text();
|
|
62
|
+
last = `${r.status} ${r.statusText}`;
|
|
63
|
+
// A missing file is an answer, not a hiccup. Retrying it wastes the
|
|
64
|
+
// user's time and still ends in the same place.
|
|
65
|
+
if (r.status === 404) break;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
last = e.message;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`${url} — ${last || "unreachable"} after ${RETRIES} attempts`);
|
|
71
|
+
}
|
|
72
|
+
|
|
37
73
|
async function fetchListing({ apiUrl, label, entryFilter, buildRawUrl, mapEntry, allow404 = false }) {
|
|
38
74
|
const headers = getAuthHeaders();
|
|
39
75
|
const res = await fetch(apiUrl, { headers });
|
|
@@ -48,20 +84,13 @@ async function fetchListing({ apiUrl, label, entryFilter, buildRawUrl, mapEntry,
|
|
|
48
84
|
|
|
49
85
|
const entries = (await res.json()).filter(entryFilter);
|
|
50
86
|
|
|
87
|
+
// No try/catch: a file this listing named and the network could not deliver
|
|
88
|
+
// is a failed install, not a smaller one. Promise.all rejects on the first
|
|
89
|
+
// failure and the error carries the URL and the last status.
|
|
51
90
|
const results = await Promise.all(
|
|
52
|
-
entries.map(async (entry) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!r.ok) {
|
|
56
|
-
console.warn(` Warning: Failed to fetch ${label} ${entry.name}, skipping`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
return mapEntry(entry, await r.text());
|
|
60
|
-
} catch {
|
|
61
|
-
console.warn(` Warning: Failed to fetch ${entry.name}, skipping`);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
})
|
|
91
|
+
entries.map(async (entry) =>
|
|
92
|
+
mapEntry(entry, await fetchRaw(buildRawUrl(entry)))
|
|
93
|
+
)
|
|
65
94
|
);
|
|
66
95
|
|
|
67
96
|
return results.filter(Boolean);
|
|
@@ -80,40 +109,36 @@ export async function fetchSkills() {
|
|
|
80
109
|
|
|
81
110
|
const dirs = (await res.json()).filter((e) => e.type === "dir");
|
|
82
111
|
|
|
112
|
+
// A transient raw outage used to land here as seven "skipping" warnings and
|
|
113
|
+
// an exit code of 0 — the catalog simply came back smaller, and the picker
|
|
114
|
+
// offered whatever survived. Every failure now throws, after fetchRaw has
|
|
115
|
+
// already retried it.
|
|
83
116
|
const skills = await Promise.all(
|
|
84
117
|
dirs.map(async (dir) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const valid = fetched.filter(Boolean);
|
|
105
|
-
const skillMd = valid.find((f) => f.name === "SKILL.md");
|
|
106
|
-
if (!skillMd) {
|
|
107
|
-
console.warn(` Warning: ${dir.name}/SKILL.md missing, skipping`);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
const peerFiles = valid.filter((f) => f.name !== "SKILL.md");
|
|
111
|
-
const { name, description, category } = parseFrontmatter(skillMd.content, dir.name);
|
|
112
|
-
return { dirName: dir.name, name, description, category, content: skillMd.content, peerFiles };
|
|
113
|
-
} catch {
|
|
114
|
-
console.warn(` Warning: Failed to fetch ${dir.name}, skipping`);
|
|
118
|
+
const listRes = await fetch(`${CONTENTS_API}/${dir.name}`, { headers });
|
|
119
|
+
if (!listRes.ok) {
|
|
120
|
+
throw new Error(`Failed to list skill ${dir.name}: ${listRes.status} ${listRes.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
const files = (await listRes.json()).filter((e) => e.type === "file");
|
|
123
|
+
|
|
124
|
+
const fetched = await Promise.all(
|
|
125
|
+
files.map(async (f) => ({
|
|
126
|
+
name: f.name,
|
|
127
|
+
content: await fetchRaw(`${RAW_BASE}/${dir.name}/${f.name}`),
|
|
128
|
+
executable: f.name.endsWith(".sh"),
|
|
129
|
+
}))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const skillMd = fetched.find((f) => f.name === "SKILL.md");
|
|
133
|
+
// Now genuinely a repo problem rather than a lost download, so it is
|
|
134
|
+
// still a skip — but the listing said this directory exists, so say so.
|
|
135
|
+
if (!skillMd) {
|
|
136
|
+
console.warn(` Warning: ${dir.name} has no SKILL.md in the repo, skipping`);
|
|
115
137
|
return null;
|
|
116
138
|
}
|
|
139
|
+
const peerFiles = fetched.filter((f) => f.name !== "SKILL.md");
|
|
140
|
+
const { name, description, category } = parseFrontmatter(skillMd.content, dir.name);
|
|
141
|
+
return { dirName: dir.name, name, description, category, content: skillMd.content, peerFiles };
|
|
117
142
|
})
|
|
118
143
|
);
|
|
119
144
|
|