repos / neovimcraft

website that makes it easy to find neovim plugins
git clone https://github.com/neurosnap/neovimcraft.git

commit
dd84033
parent
0aed6a1
author
Eric Bower
date
2022-11-27 19:11:27 +0000 UTC
feat: support srht for plugin codeforge source (#202)

12 files changed,  +547, -216
M .github/workflows/deploy.yml
+1, -0
1@@ -7,6 +7,7 @@ on:
2 env:
3   GITHUB_ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
4   GITHUB_USERNAME: ${{secrets.USERNAME}}
5+  SRHT_ACCESS_TOKEN: ${{secrets.SRHT_ACCESS_TOKEN}}
6 
7 jobs:
8   build:
M Makefile
+17, -1
 1@@ -6,11 +6,27 @@ resource:
 2 	deno run --allow-write src/scripts/resource.ts
 3 .PHONY: resource
 4 
 5-scrape:
 6+download:
 7 	deno run --allow-write --allow-net src/scripts/scrape.ts
 8+.PHONY: download
 9+
10+patch:
11 	deno run --allow-write src/scripts/patch.ts
12+.PHONY: patch
13+
14+process:
15 	deno run --allow-write --allow-env --allow-net src/scripts/process.ts
16+.PHONY: process
17+
18+missing:
19+	deno run --allow-write --allow-env --allow-net --allow-read src/scripts/process.ts missing
20+.PHONY: missing
21+
22+html:
23 	deno run --allow-write --allow-read src/scripts/html.ts
24+.PHONY: html
25+
26+scrape: download patch process html
27 .PHONY: scrape
28 
29 clean:
M data/manual.json
+105, -28
  1@@ -4,115 +4,155 @@
  2       "type": "github",
  3       "username": "frabjous",
  4       "repo": "knap",
  5-      "tags": ["latex", "markdown"]
  6+      "tags": [
  7+        "latex",
  8+        "markdown"
  9+      ]
 10     },
 11     {
 12       "type": "github",
 13       "username": "jceb",
 14       "repo": "blinds.nvim",
 15-      "tags": ["color"]
 16+      "tags": [
 17+        "color"
 18+      ]
 19     },
 20     {
 21       "type": "github",
 22       "username": "theory-of-everything",
 23       "repo": "nii-nvim",
 24-      "tags": ["preconfigured-configuration"]
 25+      "tags": [
 26+        "preconfigured-configuration"
 27+      ]
 28     },
 29     {
 30       "type": "github",
 31       "username": "nvim-neo-tree",
 32       "repo": "neo-tree.nvim",
 33-      "tags": ["file-explorer"]
 34+      "tags": [
 35+        "file-explorer"
 36+      ]
 37     },
 38     {
 39       "type": "github",
 40       "username": "j-hui",
 41       "repo": "fidget.nvim",
 42-      "tags": ["neovim-0.5"]
 43+      "tags": [
 44+        "neovim-0.5"
 45+      ]
 46     },
 47     {
 48       "type": "github",
 49       "username": "slugbyte",
 50       "repo": "unruly-worker",
 51-      "tags": ["keybinding", "workman-layout"]
 52+      "tags": [
 53+        "keybinding",
 54+        "workman-layout"
 55+      ]
 56     },
 57     {
 58       "type": "github",
 59       "username": "phha",
 60       "repo": "zenburn.nvim",
 61-      "tags": ["tree-sitter-supported-colorscheme"]
 62+      "tags": [
 63+        "tree-sitter-supported-colorscheme"
 64+      ]
 65     },
 66     {
 67       "type": "github",
 68       "username": "mrjones2014",
 69       "repo": "smart-splits.nvim",
 70-      "tags": ["split-and-window"]
 71+      "tags": [
 72+        "split-and-window"
 73+      ]
 74     },
 75     {
 76       "type": "github",
 77       "username": "linty-org",
 78       "repo": "readline.nvim",
 79-      "tags": ["editing-support"]
 80+      "tags": [
 81+        "editing-support"
 82+      ]
 83     },
 84     {
 85       "type": "github",
 86       "username": "linty-org",
 87       "repo": "key-menu.nvim",
 88-      "tags": ["keybinding"]
 89+      "tags": [
 90+        "keybinding"
 91+      ]
 92     },
 93     {
 94       "type": "github",
 95       "username": "koenverburg",
 96       "repo": "minimal-tabline.nvim",
 97-      "tags": ["tabline"]
 98+      "tags": [
 99+        "tabline"
100+      ]
101     },
102     {
103       "type": "github",
104       "username": "koenverburg",
105       "repo": "peepsight.nvim",
106-      "tags": ["color"]
107+      "tags": [
108+        "color"
109+      ]
110     },
111     {
112       "type": "github",
113       "username": "koenverburg",
114       "repo": "cmd-palette.nvim",
115-      "tags": ["utility"]
116+      "tags": [
117+        "utility"
118+      ]
119     },
120     {
121       "type": "github",
122       "username": "wuelnerdotexe",
123       "repo": "vim-enfocado",
124-      "tags": ["tree-sitter-supported-colorscheme"]
125+      "tags": [
126+        "tree-sitter-supported-colorscheme"
127+      ]
128     },
129     {
130       "type": "github",
131       "username": "strash",
132       "repo": "everybody-wants-that-line.nvim",
133-      "tags": ["statusline"]
134+      "tags": [
135+        "statusline"
136+      ]
137     },
138     {
139       "type": "github",
140       "username": "Massolari",
141       "repo": "forem.nvim",
142-      "tags": ["utility"]
143+      "tags": [
144+        "utility"
145+      ]
146     },
147     {
148       "type": "github",
149       "username": "kylechui",
150       "repo": "nvim-surround",
151-      "tags": ["formatting"]
152+      "tags": [
153+        "formatting"
154+      ]
155     },
156     {
157       "type": "github",
158       "username": "ktunprasert",
159       "repo": "gui-font-resize.nvim",
160-      "tags": ["utility"]
161+      "tags": [
162+        "utility"
163+      ]
164     },
165     {
166       "type": "github",
167       "username": "brenoprata10",
168       "repo": "nvim-highlight-colors",
169-      "tags": ["color"]
170+      "tags": [
171+        "color"
172+      ]
173     },
174     {
175       "type": "github",
176@@ -128,49 +168,86 @@
177       "type": "github",
178       "username": "lcheylus",
179       "repo": "overlength.nvim",
180-      "tags": ["editing-support"]
181+      "tags": [
182+        "editing-support"
183+      ]
184     },
185     {
186       "type": "github",
187       "username": "justinhj",
188       "repo": "battery.nvim",
189-      "tags": ["statusline", "diagnostics"]
190+      "tags": [
191+        "statusline",
192+        "diagnostics"
193+      ]
194     },
195     {
196       "type": "github",
197       "username": "danielfalk",
198       "repo": "smart-open.nvim",
199-      "tags": ["fuzzy-finder"]
200+      "tags": [
201+        "fuzzy-finder"
202+      ]
203     },
204     {
205       "type": "github",
206       "username": "Saverio976",
207       "repo": "music.nvim",
208-      "tags": ["media", "utility"]
209+      "tags": [
210+        "media",
211+        "utility"
212+      ]
213     },
214     {
215       "type": "github",
216       "username": "Vonr",
217       "repo": "align.nvim",
218-      "tags": ["formatting"]
219+      "tags": [
220+        "formatting"
221+      ]
222     },
223     {
224       "type": "github",
225       "username": "Wansmer",
226       "repo": "treesj",
227-      "tags": ["refactoring", "treesitter", "splitjoin"]
228+      "tags": [
229+        "refactoring",
230+        "treesitter",
231+        "splitjoin"
232+      ]
233     },
234     {
235       "type": "github",
236       "username": "lvim-tech",
237       "repo": "lvim",
238-      "tags": ["preconfigured-configuration"]
239+      "tags": [
240+        "preconfigured-configuration"
241+      ]
242     },
243     {
244       "type": "github",
245       "username": "TimotheeSai",
246       "repo": "git-sessions.nvim",
247-      "tags": ["session", "git"]
248+      "tags": [
249+        "session",
250+        "git"
251+      ]
252+    },
253+    {
254+      "type": "srht",
255+      "username": "vigoux",
256+      "repo": "azy.nvim",
257+      "tags": [
258+        "fuzzy-finder"
259+      ]
260+    },
261+    {
262+      "type": "srht",
263+      "username": "vigoux",
264+      "repo": "complementree.nvim",
265+      "tags": [
266+        "completion"
267+      ]
268     }
269   ]
270-}
271+}
M data/resources.json
+9, -1
 1@@ -4040,7 +4040,15 @@
 2       ]
 3     },
 4     {
 5-      "type": "github",
 6+      "type": "srht",
 7+      "username": "vigoux",
 8+      "repo": "azy.nvim",
 9+      "tags": [
10+        "fuzzy-finder"
11+      ]
12+    },
13+    {
14+      "type": "srht",
15       "username": "vigoux",
16       "repo": "complementree.nvim",
17       "tags": [
M src/entities.ts
+1, -0
1@@ -2,6 +2,7 @@ import type { Plugin, Resource } from "./types.ts";
2 
3 export const createPlugin = (p: Partial<Plugin> = {}): Plugin => {
4   return {
5+    type: "github",
6     id: "",
7     name: "",
8     username: "",
A src/github.ts
+172, -0
  1@@ -0,0 +1,172 @@
  2+import type { FetchRepoProps, Resp } from "./types.ts";
  3+
  4+function delay(ms: number): Promise<void> {
  5+  return new Promise((resolve) => setTimeout(() => resolve(), ms));
  6+}
  7+
  8+async function githubApi<D = any>(
  9+  endpoint: string,
 10+  token: string,
 11+): Promise<Resp<D>> {
 12+  const url = `https://api.github.com${endpoint}`;
 13+  console.log(`Fetching ${url}`);
 14+  const res = await fetch(url, {
 15+    headers: { Authorization: `Basic ${token}` },
 16+  });
 17+
 18+  const rateLimitRemaining = parseInt(
 19+    res.headers.get("X-RateLimit-Remaining") || "0",
 20+  );
 21+  const rateLimitReset = parseInt(res.headers.get("X-RateLimit-Reset") || "0");
 22+  console.log(`rate limit remaining: ${rateLimitRemaining}`);
 23+  if (rateLimitRemaining === 1) {
 24+    const now = Date.now();
 25+    const RESET_BUFFER = 500;
 26+    const wait = rateLimitReset + RESET_BUFFER - now;
 27+    console.log(
 28+      `About to hit github rate limit, waiting ${wait * 1000} seconds`,
 29+    );
 30+    await delay(wait);
 31+  }
 32+
 33+  let data = null;
 34+  try {
 35+    data = await res.json();
 36+  } catch {
 37+    return {
 38+      ok: false,
 39+      data: {
 40+        status: res.status,
 41+        error: new Error(`JSON parsing error [${url}]`),
 42+      },
 43+    };
 44+  }
 45+
 46+  if (res.ok) {
 47+    return {
 48+      ok: true,
 49+      data,
 50+    };
 51+  }
 52+
 53+  return {
 54+    ok: false,
 55+    data: {
 56+      status: res.status,
 57+      error: new Error(`Could not load [${url}]`),
 58+    },
 59+  };
 60+}
 61+
 62+async function fetchReadme({
 63+  username,
 64+  repo,
 65+  token,
 66+}: FetchRepoProps): Promise<Resp<string>> {
 67+  const result = await githubApi(`/repos/${username}/${repo}/readme`, token);
 68+  if (!result.ok) {
 69+    return {
 70+      ok: false,
 71+      data: result.data as any,
 72+    };
 73+  }
 74+
 75+  const url = result.data.download_url;
 76+  console.log(`Fetching ${url}`);
 77+  const readme = await fetch(url);
 78+  const data = await readme.text();
 79+  return {
 80+    ok: true,
 81+    data,
 82+  };
 83+}
 84+
 85+interface RepoData {
 86+  name: string;
 87+  html_url: string;
 88+  homepage: string;
 89+  default_branch: string;
 90+  open_issues_count: number;
 91+  watchers_count: number;
 92+  forks: number;
 93+  stargazers_count: number;
 94+  subscribers_count: number;
 95+  network_count: number;
 96+  description: string;
 97+  created_at: string;
 98+  updated_at: string;
 99+}
100+
101+async function fetchRepo({
102+  username,
103+  repo,
104+  token,
105+}: FetchRepoProps): Promise<Resp<RepoData>> {
106+  const result = await githubApi(`/repos/${username}/${repo}`, token);
107+  return result;
108+}
109+
110+interface BranchData {
111+  commit: {
112+    commit: {
113+      committer: {
114+        date: string;
115+      };
116+    };
117+  };
118+}
119+
120+async function fetchBranch({
121+  username,
122+  repo,
123+  branch,
124+  token,
125+}: FetchRepoProps & { branch: string }): Promise<Resp<BranchData>> {
126+  const result = await githubApi(
127+    `/repos/${username}/${repo}/branches/${branch}`,
128+    token,
129+  );
130+  return result;
131+}
132+
133+interface GithubData {
134+  readme: string;
135+  repo: RepoData;
136+  branch: Resp<BranchData>;
137+}
138+
139+export async function fetchGithubData(
140+  props: FetchRepoProps,
141+): Promise<Resp<GithubData>> {
142+  const repo = await fetchRepo(props);
143+  if (repo.ok === false) {
144+    console.log(`${repo.data.status}: ${repo.data.error.message}`);
145+    return repo;
146+  }
147+
148+  const branch = await fetchBranch({
149+    ...props,
150+    branch: repo.data.default_branch,
151+  });
152+  if (branch.ok === false) {
153+    console.log(`${branch.data.status}: ${branch.data.error.message}`);
154+  }
155+
156+  const readme = await fetchReadme({
157+    username: props.username,
158+    repo: props.repo,
159+    token: props.token,
160+  });
161+  if (readme.ok === false) {
162+    console.log(`${readme.data.status}: ${readme.data.error.message}`);
163+  }
164+
165+  return {
166+    ok: true,
167+    data: {
168+      readme: readme.ok ? readme.data : "",
169+      repo: repo.data,
170+      branch,
171+    },
172+  };
173+}
M src/scripts/html.ts
+2, -1
 1@@ -16,7 +16,8 @@ async function clean() {
 2     marked.use({
 3       walkTokens: (token: any) => {
 4         const domain = "https://github.com";
 5-        const pre = `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
 6+        const pre =
 7+          `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
 8 
 9         if (token.type === "link" || token.type === "image") {
10           if (
M src/scripts/process.ts
+62, -171
  1@@ -3,9 +3,12 @@ import resourceFile from "../../data/resources.json" assert { type: "json" };
  2 import { encode } from "../deps.ts";
  3 import type { Plugin, Resource } from "../types.ts";
  4 import { createPlugin } from "../entities.ts";
  5+import { fetchGithubData } from "../github.ts";
  6+import { fetchSrhtData } from "../stht.ts";
  7 
  8 const accessToken = Deno.env.get("GITHUB_ACCESS_TOKEN") || "";
  9 const accessUsername = Deno.env.get("GITHUB_USERNAME") || "";
 10+const srhtToken = Deno.env.get("SRHT_ACCESS_TOKEN") || "";
 11 
 12 const option = Deno.args[0];
 13 if (option === "missing") {
 14@@ -18,11 +21,6 @@ if (option === "missing") {
 15     .catch(console.error);
 16 }
 17 
 18-interface Props {
 19-  username: string;
 20-  repo: string;
 21-}
 22-
 23 async function processMissingResources() {
 24   const dbFile = await Deno.readTextFile("./data/db.json");
 25   const db = JSON.parse(dbFile.toString());
 26@@ -45,182 +43,75 @@ async function processMissingResources() {
 27   return { plugins, markdown };
 28 }
 29 
 30-function delay(ms: number): Promise<void> {
 31-  return new Promise((resolve) => setTimeout(() => resolve(), ms));
 32-}
 33-
 34-async function githubApi(
 35-  endpoint: string,
 36-): Promise<Resp<{ [key: string]: any }>> {
 37-  const url = `https://api.github.com${endpoint}`;
 38-  console.log(`Fetching ${url}`);
 39-  const token = encode(`${accessUsername}:${accessToken}`);
 40-  const res = await fetch(url, {
 41-    headers: { Authorization: `Basic ${token}` },
 42-  });
 43-
 44-  const rateLimitRemaining = parseInt(
 45-    res.headers.get("X-RateLimit-Remaining") || "0",
 46-  );
 47-  const rateLimitReset = parseInt(res.headers.get("X-RateLimit-Reset") || "0");
 48-  console.log(`rate limit remaining: ${rateLimitRemaining}`);
 49-  if (rateLimitRemaining === 1) {
 50-    const now = Date.now();
 51-    const RESET_BUFFER = 500;
 52-    const wait = rateLimitReset + RESET_BUFFER - now;
 53-    console.log(
 54-      `About to hit github rate limit, waiting ${wait * 1000} seconds`,
 55-    );
 56-    await delay(wait);
 57-  }
 58-
 59-  let data = null;
 60-  try {
 61-    data = await res.json();
 62-  } catch {
 63-    return {
 64-      ok: false,
 65-      data: {
 66-        status: res.status,
 67-        error: new Error(`JSON parsing error [${url}]`),
 68-      },
 69-    };
 70-  }
 71-
 72-  if (res.ok) {
 73-    return {
 74-      ok: true,
 75-      data,
 76-    };
 77-  }
 78-
 79-  return {
 80-    ok: false,
 81-    data: {
 82-      status: res.status,
 83-      error: new Error(`Could not load [${url}]`),
 84-    },
 85-  };
 86-}
 87-
 88-async function fetchReadme({ username, repo }: Props): Promise<Resp<string>> {
 89-  const result = await githubApi(`/repos/${username}/${repo}/readme`);
 90-  if (!result.ok) {
 91-    return {
 92-      ok: false,
 93-      data: result.data as any,
 94-    };
 95-  }
 96-
 97-  const url = result.data.download_url;
 98-  console.log(`Fetching ${url}`);
 99-  const readme = await fetch(url);
100-  const data = await readme.text();
101-  return {
102-    ok: true,
103-    data,
104-  };
105-}
106-
107-async function fetchRepo(
108-  { username, repo }: Props,
109-): Promise<Resp<{ [key: string]: any }>> {
110-  const result = await githubApi(`/repos/${username}/${repo}`);
111-  return result;
112-}
113-
114-async function fetchBranch({
115-  username,
116-  repo,
117-  branch,
118-}: Props & { branch: string }): Promise<Resp<{ [key: string]: any }>> {
119-  const result = await githubApi(
120-    `/repos/${username}/${repo}/branches/${branch}`,
121-  );
122-  return result;
123-}
124-
125-interface ApiSuccess<D = any> {
126-  ok: true;
127-  data: D;
128-}
129-
130-interface ApiFailure {
131-  ok: false;
132-  data: { status: number; error: Error };
133-}
134-
135-type Resp<D> = ApiSuccess<D> | ApiFailure;
136-
137-async function fetchGithubData(props: Props): Promise<Resp<any>> {
138-  const repo = await fetchRepo(props);
139-  if (repo.ok === false) {
140-    console.log(`${repo.data.status}: ${repo.data.error.message}`);
141-    return repo;
142-  }
143-
144-  const branch = await fetchBranch({
145-    ...props,
146-    branch: repo.data.default_branch,
147-  });
148-  if (branch.ok === false) {
149-    console.log(`${branch.data.status}: ${branch.data.error.message}`);
150-  }
151-
152-  const readme = await fetchReadme({
153-    username: props.username,
154-    repo: props.repo,
155-  });
156-  if (readme.ok === false) {
157-    console.log(`${readme.data.status}: ${readme.data.error.message}`);
158-  }
159-
160-  return {
161-    ok: true,
162-    data: {
163-      readme: readme.ok ? readme.data : "",
164-      repo: repo.data,
165-      branch: branch.data,
166-    },
167-  };
168-}
169-
170 async function processResources(resources: Resource[]) {
171   const plugins: { [key: string]: Plugin } = {};
172   const markdown: { [key: string]: string } = {};
173 
174+  const ghToken = encode(`${accessUsername}:${accessToken}`);
175+
176   console.log(`Fetching ${resources.length} resources`);
177 
178   for (let i = 0; i < resources.length; i += 1) {
179     const d = resources[i];
180 
181-    if (d.type === "github") {
182-      const result = await fetchGithubData(d);
183-      if (result.ok) {
184-        const resp = result.data;
185-        const id = `${d.username}/${d.repo}`;
186-
187-        markdown[id] = resp.readme;
188-        plugins[id] = createPlugin({
189-          id,
190-          username: d.username,
191-          repo: d.repo,
192-          tags: d.tags,
193-          name: resp.repo.name,
194-          link: resp.repo.html_url,
195-          homepage: resp.repo.homepage,
196-          branch: resp.repo.default_branch,
197-          openIssues: resp.repo.open_issues_count,
198-          watchers: resp.repo.watchers_count,
199-          forks: resp.repo.forks,
200-          stars: resp.repo.stargazers_count,
201-          subscribers: resp.repo.subscribers_count,
202-          network: resp.repo.network_count,
203-          description: resp.repo.description,
204-          createdAt: resp.repo.created_at,
205-          updatedAt: resp.branch.commit.commit.committer.date,
206-        });
207+    if (d.type === "srht") {
208+      const result = await fetchSrhtData({ ...d, token: srhtToken });
209+      const id = `${d.username}/${d.repo}`;
210+      if (!result.ok) {
211+        console.log(result);
212+        continue;
213       }
214+
215+      const repo = result.data.repo;
216+      markdown[id] = result.data.readme;
217+      plugins[id] = createPlugin({
218+        type: "srht",
219+        id,
220+        username: d.username,
221+        repo: d.repo,
222+        tags: d.tags,
223+        link: `https://git.sr.ht/~${id}`,
224+        name: repo.name,
225+        description: repo.description,
226+        createdAt: repo.created,
227+        updatedAt: repo.updated,
228+        branch: result.data.branch,
229+      });
230+    } else if (d.type === "github") {
231+      const result = await fetchGithubData({ ...d, token: ghToken });
232+      if (!result.ok) {
233+        continue;
234+      }
235+
236+      const resp = result.data;
237+      const id = `${d.username}/${d.repo}`;
238+
239+      let updatedAt = "";
240+      if (result.data.branch.ok) {
241+        updatedAt = result.data.branch.data.commit.commit.committer.date;
242+      }
243+
244+      markdown[id] = resp.readme;
245+      plugins[id] = createPlugin({
246+        type: "github",
247+        id,
248+        username: d.username,
249+        repo: d.repo,
250+        tags: d.tags,
251+        name: resp.repo.name,
252+        link: resp.repo.html_url,
253+        homepage: resp.repo.homepage,
254+        branch: resp.repo.default_branch,
255+        openIssues: resp.repo.open_issues_count,
256+        watchers: resp.repo.watchers_count,
257+        forks: resp.repo.forks,
258+        stars: resp.repo.stargazers_count,
259+        subscribers: resp.repo.subscribers_count,
260+        network: resp.repo.network_count,
261+        description: resp.repo.description,
262+        createdAt: resp.repo.created_at,
263+        updatedAt: updatedAt,
264+      });
265     }
266   }
267 
M src/scripts/resource.ts
+29, -6
 1@@ -1,14 +1,31 @@
 2+import dbFile from "../../data/db.json" assert { type: "json" };
 3 import resourceFile from "../../data/resources.json" assert { type: "json" };
 4 import manualFile from "../../data/manual.json" assert { type: "json" };
 5+import { derivePluginData } from "../plugin-data.ts";
 6 
 7-import type { Resource } from "../types.ts";
 8+import type { PluginMap, Resource } from "../types.ts";
 9 import { createResource } from "../entities.ts";
10 
11+const forges = ["github", "srht"];
12+const forgesStr = forges.join(",");
13+
14 async function init() {
15+  const pluginMap = (dbFile as any).plugins as PluginMap;
16+  const pluginData = derivePluginData(pluginMap);
17+  const allTags = pluginData.tags.map((t) => t.id);
18+
19+  const type = prompt(`code forge [${forgesStr}] (default: github):`) ||
20+    "github";
21+  if (!forges.includes(type)) {
22+    throw new Error(`${type} is not a valid code forge, choose ${forgesStr}`);
23+  }
24+
25   const name = prompt("name (username/repo):") || "";
26-  const [username, repo] = name.split("/");
27-  const tagsRes = prompt("tags (comma separated):") || "";
28-  const tags = tagsRes.split(",");
29+  let [username, repo] = name.split("/");
30+  if (type === "srht" && username[0] === "~") {
31+    username = username.replace("~", "");
32+  }
33+
34   const foundResource = (resourceFile.resources as Resource[]).find(
35     (r) => `${r.username}/${r.repo}` === name,
36   );
37@@ -17,9 +34,16 @@ async function init() {
38     return;
39   }
40 
41+  console.log(
42+    "\nNOTICE: Please review all current tags and see if any fit, only add new tags if absolutely necessary:\n",
43+  );
44+  console.log(`[${allTags.join(", ")}]\n`);
45+  const tagsRes = prompt("tags (comma separated):") || "";
46+  const tags = tagsRes.split(",");
47+
48   manualFile.resources.push(
49     createResource({
50-      type: "github",
51+      type: type as any,
52       username,
53       repo,
54       tags,
55@@ -27,7 +51,6 @@ async function init() {
56   );
57 
58   const json = JSON.stringify(manualFile, null, 2);
59-
60   await Deno.writeTextFile("./data/manual.json", json);
61 }
62 
M src/scripts/static.ts
+1, -7
 1@@ -4,13 +4,7 @@ import htmlFile from "../../data/html.json" assert { type: "json" };
 2 import { dirname } from "../deps.ts";
 3 import { format, relativeTimeFromDates } from "../date.ts";
 4 import { derivePluginData } from "../plugin-data.ts";
 5-import type {
 6-  Plugin,
 7-  PluginData,
 8-  PluginMap,
 9-  Tag,
10-  TagMap,
11-} from "../types.ts";
12+import type { Plugin, PluginData, PluginMap, Tag, TagMap } from "../types.ts";
13 
14 async function createFile(fname: string, data: string) {
15   await Deno.mkdir(dirname(fname), { recursive: true });
A src/stht.ts
+128, -0
  1@@ -0,0 +1,128 @@
  2+import type { FetchRepoProps, Resp } from "./types.ts";
  3+
  4+interface SrhtRepo {
  5+  id: string;
  6+  name: string;
  7+  created: string;
  8+  updated: string;
  9+  readme: string | null;
 10+  description: string;
 11+  HEAD: { name: string };
 12+}
 13+
 14+interface SrhtRepoResp {
 15+  data: {
 16+    user: {
 17+      repository: SrhtRepo;
 18+    };
 19+  };
 20+}
 21+
 22+async function fetchReadme(
 23+  props: FetchRepoProps,
 24+  branch: string,
 25+  fpath = "README.md",
 26+): Promise<Resp<string>> {
 27+  const url =
 28+    `https://git.sr.ht/~${props.username}/${props.repo}/blob/${branch}/${fpath}`;
 29+  console.log(`Fetching ${url}`);
 30+  const resp = await fetch(url);
 31+  const readme = await resp.text();
 32+  if (!resp.ok) {
 33+    return {
 34+      ok: false,
 35+      data: {
 36+        status: resp.status,
 37+        error: new Error(`failed to fetch srht readme: ${readme}`),
 38+      },
 39+    };
 40+  }
 41+
 42+  return { ok: true, data: readme };
 43+}
 44+
 45+async function recurseReadme(
 46+  props: FetchRepoProps,
 47+  branch: string,
 48+  fnames: string[],
 49+) {
 50+  for (let i = 0; i < fnames.length; i += 1) {
 51+    const readme = await fetchReadme(props, branch, fnames[i]);
 52+
 53+    if (readme.ok) {
 54+      return readme.data;
 55+    } else {
 56+      console.log(`${readme.data.status}: ${readme.data.error.message}`);
 57+    }
 58+  }
 59+
 60+  return "";
 61+}
 62+
 63+interface SrhtData {
 64+  repo: SrhtRepo;
 65+  branch: string;
 66+  readme: string;
 67+}
 68+
 69+export async function fetchSrhtData(
 70+  props: FetchRepoProps,
 71+): Promise<Resp<SrhtData>> {
 72+  const query = `\
 73+    query {\
 74+      user(username: "${props.username}") {\
 75+        repository(name: "${props.repo}") {\
 76+          id,\
 77+          name,\
 78+          created,\
 79+          updated,\
 80+          readme,\
 81+          description,\
 82+          HEAD { name }\
 83+        }\
 84+      }\
 85+    }`;
 86+  const body = { query };
 87+
 88+  const payload = {
 89+    method: "POST",
 90+    headers: {
 91+      Authorization: `Bearer ${props.token}`,
 92+      ["Content-Type"]: "application/json",
 93+    },
 94+    body: JSON.stringify(body),
 95+  };
 96+  const url = "https://git.sr.ht/query";
 97+  console.log(`Fetching ${url} [${props.username}/${props.repo}]`);
 98+  const resp = await fetch(url, payload);
 99+
100+  if (!resp.ok) {
101+    return {
102+      ok: false,
103+      data: { status: resp.status, error: new Error("request failed") },
104+    };
105+  }
106+
107+  const data: SrhtRepoResp = await resp.json();
108+  const repo = data.data.user.repository;
109+  const name = repo.HEAD.name;
110+  const ref = name.split("/");
111+  const branch = ref[ref.length - 1];
112+  let readmeData = "";
113+  if (repo.readme) {
114+    readmeData = repo.readme;
115+  } else {
116+    // supported readme: https://git.sr.ht/~sircmpwn/scm.sr.ht/tree/83185bf27e1e67ab2ce88851dc7a3f7766075a60/item/scmsrht/formatting.py#L25
117+    const fnames = ["README.md", "README.markdown", "README"];
118+    readmeData = await recurseReadme(props, branch, fnames);
119+  }
120+
121+  return {
122+    ok: true,
123+    data: {
124+      repo,
125+      branch,
126+      readme: readmeData,
127+    },
128+  };
129+}
M src/types.ts
+20, -1
 1@@ -1,4 +1,5 @@
 2 export interface Plugin {
 3+  type: "github" | "srht";
 4   id: string;
 5   name: string;
 6   username: string;
 7@@ -34,10 +35,28 @@ export interface PluginData {
 8 }
 9 
10 export interface Resource {
11-  type: "github";
12+  type: "github" | "srht";
13   username: string;
14   repo: string;
15   tags: string[];
16 }
17 
18 export type ResourceMap = { [key: string]: Resource };
19+
20+export interface ApiSuccess<D = any> {
21+  ok: true;
22+  data: D;
23+}
24+
25+export interface ApiFailure {
26+  ok: false;
27+  data: { status: number; error: Error };
28+}
29+
30+export type Resp<D> = ApiSuccess<D> | ApiFailure;
31+
32+export interface FetchRepoProps {
33+  username: string;
34+  repo: string;
35+  token: string;
36+}