@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.
Files changed (2) hide show
  1. package/lib/github.mjs +68 -43
  2. 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
- try {
54
- const r = await fetch(buildRawUrl(entry), { headers });
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
- try {
86
- const listRes = await fetch(`${CONTENTS_API}/${dir.name}`, { headers });
87
- if (!listRes.ok) {
88
- console.warn(` Warning: Failed to list skill ${dir.name}, skipping`);
89
- return null;
90
- }
91
- const files = (await listRes.json()).filter((e) => e.type === "file");
92
-
93
- const fetched = await Promise.all(
94
- files.map(async (f) => {
95
- const r = await fetch(`${RAW_BASE}/${dir.name}/${f.name}`, { headers });
96
- if (!r.ok) {
97
- console.warn(` Warning: Failed to fetch ${dir.name}/${f.name}, skipping`);
98
- return null;
99
- }
100
- return { name: f.name, content: await r.text(), executable: f.name.endsWith(".sh") };
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.30.0",
3
+ "version": "2.30.2",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {