@1e0zj/dsh-plugin-mall 0.1.0 → 0.1.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/src/github.js CHANGED
@@ -1,136 +1,136 @@
1
- // GitHub API helpers for the dsh plugin marketplace.
2
- // Pure functions with no harness imports, so this module is unit-testable
3
- // standalone (node src/github.js --self-test).
4
-
5
- const SEARCH_TOPIC = "topic:dsh-plugin";
6
-
7
- export function buildHeaders(token) {
8
- const headers = {
9
- "User-Agent": "dsh-plugin-mall",
10
- Accept: "application/vnd.github+json",
11
- "X-GitHub-Api-Version": "2022-11-28",
12
- };
13
- if (token) headers.Authorization = `Bearer ${token}`;
14
- return headers;
15
- }
16
-
17
- function apiUrl(apiBase, path) {
18
- const base = apiBase.endsWith("/") ? apiBase : `${apiBase}/`;
19
- return `${base}${path.replace(/^\//, "")}`;
20
- }
21
-
22
- async function requestJson(path, { apiBase, token, signal }) {
23
- let response;
24
- try {
25
- response = await fetch(apiUrl(apiBase, path), {
26
- headers: buildHeaders(token),
27
- signal,
28
- });
29
- } catch (error) {
30
- if (error?.name === "AbortError") throw error;
31
- throw new Error(`GitHub API request failed: ${error?.message ?? String(error)}`);
32
- }
33
- const remaining = response.headers.get("x-ratelimit-remaining");
34
- const resetAt = response.headers.get("x-ratelimit-reset");
35
- const body = await response.json().catch(() => undefined);
36
- if (response.status === 403 && remaining === "0" && resetAt !== null) {
37
- const reset = new Date(Number(resetAt) * 1000).toISOString();
38
- throw new Error(`GitHub API rate limit exceeded; resets at ${reset} (UTC). Set GITHUB_TOKEN or DSH_MARKET_GITHUB_TOKEN for a higher limit.`);
39
- }
40
- if (response.status === 404) {
41
- throw new Error(`GitHub API 404: ${body?.message ?? "not found"}`);
42
- }
43
- if (!response.ok) {
44
- throw new Error(`GitHub API ${response.status}: ${body?.message ?? response.statusText}`);
45
- }
46
- return body;
47
- }
48
-
49
- /** Pick the stable, compact fields the tools render. */
50
- function pickRepo(item) {
51
- return {
52
- fullName: item.full_name ?? "",
53
- htmlUrl: item.html_url ?? "",
54
- description: item.description ?? "",
55
- stars: item.stargazers_count ?? 0,
56
- forks: item.forks_count ?? 0,
57
- language: item.language,
58
- license: item.license?.spdx_id,
59
- topics: item.topics ?? [],
60
- updatedAt: item.updated_at ?? "",
61
- archived: item.archived ?? false,
62
- defaultBranch: item.default_branch ?? "main",
63
- };
64
- }
65
-
66
- /**
67
- * Search repositories tagged `topic:dsh-plugin`, optionally narrowed by
68
- * keywords (name/description/readme match), star-ranked by default.
69
- */
70
- export async function searchPlugins({ query, sort = "stars", perPage = 10, page = 1, apiBase, token, signal }) {
71
- const trimmed = typeof query === "string" ? query.trim() : "";
72
- const q = trimmed.length > 0 ? `${SEARCH_TOPIC} ${trimmed}` : SEARCH_TOPIC;
73
- const safePerPage = Math.min(Math.max(Math.trunc(perPage) || 10, 1), 100);
74
- const safePage = Math.max(Math.trunc(page) || 1, 1);
75
- const path = `/search/repositories?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}&order=desc&per_page=${safePerPage}&page=${safePage}`;
76
- const body = await requestJson(path, { apiBase, token, signal });
77
- return {
78
- total: body.total_count ?? 0,
79
- page: safePage,
80
- perPage: safePerPage,
81
- items: (body.items ?? []).map(pickRepo),
82
- };
83
- }
84
-
85
- /**
86
- * Fetch one repository's metadata plus its package.json (base64-decoded),
87
- * which is what tells us whether it declares a dsh bundle patch.
88
- */
89
- export async function repoInfo({ repo, apiBase, token, signal }) {
90
- const trimmed = String(repo ?? "").trim();
91
- if (!/^[^/\s]+\/[^/\s]+$/.test(trimmed) || trimmed.includes("..")) {
92
- throw new Error(`market_info: repo must be "owner/name", got ${JSON.stringify(trimmed)}`);
93
- }
94
- let meta;
95
- try {
96
- meta = await requestJson(`/repos/${trimmed}`, { apiBase, token, signal });
97
- } catch (error) {
98
- throw new Error(`market_info: repository ${trimmed} not found on GitHub (${error.message})`);
99
- }
100
- let packageJson;
101
- try {
102
- const contents = await requestJson(`/repos/${trimmed}/contents/package.json`, { apiBase, token, signal });
103
- if (typeof contents.content === "string") {
104
- packageJson = JSON.parse(Buffer.from(contents.content, "base64").toString("utf8"));
105
- }
106
- } catch {
107
- packageJson = undefined; // no package.json at the repo root
108
- }
109
- return {
110
- meta: pickRepo(meta),
111
- packageJson: packageJson === undefined ? undefined : {
112
- name: packageJson.name,
113
- version: packageJson.version,
114
- description: packageJson.description,
115
- type: packageJson.type,
116
- dshBundlePatch: typeof packageJson.dsh?.bundle?.patch === "string" ? packageJson.dsh.bundle.patch : undefined,
117
- dshClientPlatform: packageJson.dsh?.client?.platform,
118
- dshClientInjectCount: Array.isArray(packageJson.dsh?.client?.inject) ? packageJson.dsh.client.inject.length : undefined,
119
- dependencyCount: Object.keys(packageJson.dependencies ?? {}).length,
120
- peerDependencyCount: Object.keys(packageJson.peerDependencies ?? {}).length,
121
- },
122
- };
123
- }
124
-
125
- // Self-test entry: node src/github.js
126
- if (process.argv[1]?.endsWith("github.js") && process.argv.includes("--self-test")) {
127
- const apiBase = "https://api.github.com";
128
- const result = await searchPlugins({ query: "", perPage: 3, apiBase });
129
- console.log(`total=${result.total} page=${result.page} perPage=${result.perPage}`);
130
- for (const item of result.items) console.log(`${item.fullName} ★${item.stars} ${item.language ?? ""}`);
131
- if (result.items.length > 0) {
132
- const info = await repoInfo({ repo: result.items[0].fullName, apiBase });
133
- console.log(`repo=${info.meta.fullName} defaultBranch=${info.meta.defaultBranch} archived=${info.meta.archived}`);
134
- console.log(`packageJson=${info.packageJson ? `${info.packageJson.name}@${info.packageJson.version} bundle=${info.packageJson.dshBundlePatch ?? "none"}` : "absent"}`);
135
- }
136
- }
1
+ // GitHub API helpers for the dsh plugin marketplace.
2
+ // Pure functions with no harness imports, so this module is unit-testable
3
+ // standalone (node src/github.js --self-test).
4
+
5
+ const SEARCH_TOPIC = "topic:dsh-plugin";
6
+
7
+ export function buildHeaders(token) {
8
+ const headers = {
9
+ "User-Agent": "dsh-plugin-mall",
10
+ Accept: "application/vnd.github+json",
11
+ "X-GitHub-Api-Version": "2022-11-28",
12
+ };
13
+ if (token) headers.Authorization = `Bearer ${token}`;
14
+ return headers;
15
+ }
16
+
17
+ function apiUrl(apiBase, path) {
18
+ const base = apiBase.endsWith("/") ? apiBase : `${apiBase}/`;
19
+ return `${base}${path.replace(/^\//, "")}`;
20
+ }
21
+
22
+ async function requestJson(path, { apiBase, token, signal }) {
23
+ let response;
24
+ try {
25
+ response = await fetch(apiUrl(apiBase, path), {
26
+ headers: buildHeaders(token),
27
+ signal,
28
+ });
29
+ } catch (error) {
30
+ if (error?.name === "AbortError") throw error;
31
+ throw new Error(`GitHub API request failed: ${error?.message ?? String(error)}`);
32
+ }
33
+ const remaining = response.headers.get("x-ratelimit-remaining");
34
+ const resetAt = response.headers.get("x-ratelimit-reset");
35
+ const body = await response.json().catch(() => undefined);
36
+ if (response.status === 403 && remaining === "0" && resetAt !== null) {
37
+ const reset = new Date(Number(resetAt) * 1000).toISOString();
38
+ throw new Error(`GitHub API rate limit exceeded; resets at ${reset} (UTC). Set GITHUB_TOKEN or DSH_MARKET_GITHUB_TOKEN for a higher limit.`);
39
+ }
40
+ if (response.status === 404) {
41
+ throw new Error(`GitHub API 404: ${body?.message ?? "not found"}`);
42
+ }
43
+ if (!response.ok) {
44
+ throw new Error(`GitHub API ${response.status}: ${body?.message ?? response.statusText}`);
45
+ }
46
+ return body;
47
+ }
48
+
49
+ /** Pick the stable, compact fields the tools render. */
50
+ function pickRepo(item) {
51
+ return {
52
+ fullName: item.full_name ?? "",
53
+ htmlUrl: item.html_url ?? "",
54
+ description: item.description ?? "",
55
+ stars: item.stargazers_count ?? 0,
56
+ forks: item.forks_count ?? 0,
57
+ language: item.language,
58
+ license: item.license?.spdx_id,
59
+ topics: item.topics ?? [],
60
+ updatedAt: item.updated_at ?? "",
61
+ archived: item.archived ?? false,
62
+ defaultBranch: item.default_branch ?? "main",
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Search repositories tagged `topic:dsh-plugin`, optionally narrowed by
68
+ * keywords (name/description/readme match), star-ranked by default.
69
+ */
70
+ export async function searchPlugins({ query, sort = "stars", perPage = 10, page = 1, apiBase, token, signal }) {
71
+ const trimmed = typeof query === "string" ? query.trim() : "";
72
+ const q = trimmed.length > 0 ? `${SEARCH_TOPIC} ${trimmed}` : SEARCH_TOPIC;
73
+ const safePerPage = Math.min(Math.max(Math.trunc(perPage) || 10, 1), 100);
74
+ const safePage = Math.max(Math.trunc(page) || 1, 1);
75
+ const path = `/search/repositories?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}&order=desc&per_page=${safePerPage}&page=${safePage}`;
76
+ const body = await requestJson(path, { apiBase, token, signal });
77
+ return {
78
+ total: body.total_count ?? 0,
79
+ page: safePage,
80
+ perPage: safePerPage,
81
+ items: (body.items ?? []).map(pickRepo),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Fetch one repository's metadata plus its package.json (base64-decoded),
87
+ * which is what tells us whether it declares a dsh bundle patch.
88
+ */
89
+ export async function repoInfo({ repo, apiBase, token, signal }) {
90
+ const trimmed = String(repo ?? "").trim();
91
+ if (!/^[^/\s]+\/[^/\s]+$/.test(trimmed) || trimmed.includes("..")) {
92
+ throw new Error(`market_info: repo must be "owner/name", got ${JSON.stringify(trimmed)}`);
93
+ }
94
+ let meta;
95
+ try {
96
+ meta = await requestJson(`/repos/${trimmed}`, { apiBase, token, signal });
97
+ } catch (error) {
98
+ throw new Error(`market_info: repository ${trimmed} not found on GitHub (${error.message})`);
99
+ }
100
+ let packageJson;
101
+ try {
102
+ const contents = await requestJson(`/repos/${trimmed}/contents/package.json`, { apiBase, token, signal });
103
+ if (typeof contents.content === "string") {
104
+ packageJson = JSON.parse(Buffer.from(contents.content, "base64").toString("utf8"));
105
+ }
106
+ } catch {
107
+ packageJson = undefined; // no package.json at the repo root
108
+ }
109
+ return {
110
+ meta: pickRepo(meta),
111
+ packageJson: packageJson === undefined ? undefined : {
112
+ name: packageJson.name,
113
+ version: packageJson.version,
114
+ description: packageJson.description,
115
+ type: packageJson.type,
116
+ dshBundlePatch: typeof packageJson.dsh?.bundle?.patch === "string" ? packageJson.dsh.bundle.patch : undefined,
117
+ dshClientPlatform: packageJson.dsh?.client?.platform,
118
+ dshClientInjectCount: Array.isArray(packageJson.dsh?.client?.inject) ? packageJson.dsh.client.inject.length : undefined,
119
+ dependencyCount: Object.keys(packageJson.dependencies ?? {}).length,
120
+ peerDependencyCount: Object.keys(packageJson.peerDependencies ?? {}).length,
121
+ },
122
+ };
123
+ }
124
+
125
+ // Self-test entry: node src/github.js
126
+ if (process.argv[1]?.endsWith("github.js") && process.argv.includes("--self-test")) {
127
+ const apiBase = "https://api.github.com";
128
+ const result = await searchPlugins({ query: "", perPage: 3, apiBase });
129
+ console.log(`total=${result.total} page=${result.page} perPage=${result.perPage}`);
130
+ for (const item of result.items) console.log(`${item.fullName} ★${item.stars} ${item.language ?? ""}`);
131
+ if (result.items.length > 0) {
132
+ const info = await repoInfo({ repo: result.items[0].fullName, apiBase });
133
+ console.log(`repo=${info.meta.fullName} defaultBranch=${info.meta.defaultBranch} archived=${info.meta.archived}`);
134
+ console.log(`packageJson=${info.packageJson ? `${info.packageJson.name}@${info.packageJson.version} bundle=${info.packageJson.dshBundlePatch ?? "none"}` : "absent"}`);
135
+ }
136
+ }