ants-move 0.0.1 → 0.0.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/README.md +23 -3
- package/dist/index.d.ts +31 -0
- package/dist/index.js +443 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ants-move
|
|
2
2
|
|
|
3
|
-
`ants-move` is an open-source TypeScript CLI for moving data between systems. Its commands are small workers: collectors bring data in, and future automation commands can move that data into forms and other systems. The
|
|
3
|
+
`ants-move` is an open-source TypeScript CLI for moving data between systems. Its commands are small workers: collectors bring data in, and future automation commands can move that data into forms and other systems. The current collectors read 36Kr, Toutiao, Hacker News, and GitHub Trending data.
|
|
4
4
|
|
|
5
5
|
## Installation and requirements
|
|
6
6
|
|
|
@@ -86,11 +86,31 @@ ants hn search <query> [--limit 1..100] [--sort relevance|date] [--format json|t
|
|
|
86
86
|
|
|
87
87
|
Hacker News uses at most eight concurrent detail requests. ID-list and search responses are limited to 5 MB, while each Firebase story detail is limited to 256 KB.
|
|
88
88
|
|
|
89
|
+
## GitHub Trending commands
|
|
90
|
+
|
|
91
|
+
Fetch every repository shown on GitHub's all-language Trending page. The period defaults to `daily`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ants github trending
|
|
95
|
+
ants github trending --since daily
|
|
96
|
+
ants github trending --since weekly
|
|
97
|
+
ants github trending --since monthly
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
JSON is the default output. Use `--format table` or `-t` for a terminal table:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ants github trending --since weekly --format table
|
|
104
|
+
ants github trending --since monthly -t
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The collector reads the official server-rendered HTML with one GitHub request and does not use a third-party Trending API or Playwright. The request has a 30-second timeout and a 5 MB HTML limit. Parsing accepts at most 100 repositories and retains at most ten displayed contributors per repository. The command has no language filter, result limit, automatic retry, or cache; a successful zero-row parse is rejected as a page-structure error instead of returning a misleading empty ranking.
|
|
108
|
+
|
|
89
109
|
## High-concurrency usage warning
|
|
90
110
|
|
|
91
|
-
Resource use is bounded within one process, but identical commands running in separate processes are not globally deduplicated or rate limited. At `Q` concurrent invocations, upstream work can approach `20Q` requests for a 36Kr list, `201Q` requests for a Hacker News list,
|
|
111
|
+
Resource use is bounded within one process, but identical commands running in separate processes are not globally deduplicated or rate limited. At `Q` concurrent invocations, upstream work can approach `20Q` requests for a 36Kr list, `201Q` requests for a Hacker News list, `105Q` browser navigations plus page subresources for a Toutiao author collection, or `Q` requests for GitHub Trending.
|
|
92
112
|
|
|
93
|
-
Retained collector data is also bounded per invocation: 5 MB for a 36Kr list, approximately 50 MB across 200 Hacker News candidate details before result filtering,
|
|
113
|
+
Retained collector data is also bounded per invocation: 5 MB for a 36Kr list, approximately 50 MB across 200 Hacker News candidate details before result filtering, 10 MB for Toutiao author article results, and one 5 MB GitHub HTML response before Cheerio parsing. Toutiao may additionally hold up to five active 5 MB feed buffers inside one browser session; browser process overhead, required page subresources, and HTML parser object overhead are separate from these payload limits.
|
|
94
114
|
|
|
95
115
|
Do not place the CLI directly on a high-QPS request path. Online services must use an external bounded queue and a shared rate limiter to apply backpressure across processes and instances. This project has no distributed lock, shared cache, retry queue, or multi-instance single-flight mechanism.
|
|
96
116
|
|
package/dist/index.d.ts
CHANGED
|
@@ -124,6 +124,36 @@ interface ToutiaoRuntime {
|
|
|
124
124
|
withSession: <T>(operation: (session: ToutiaoSession) => Promise<T>) => Promise<T>;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
type GitHubTrendingPeriod = 'daily' | 'weekly' | 'monthly';
|
|
128
|
+
interface GitHubTrendingContributor {
|
|
129
|
+
avatarUrl: string;
|
|
130
|
+
url: string;
|
|
131
|
+
username: string;
|
|
132
|
+
}
|
|
133
|
+
interface GitHubTrendingRepository {
|
|
134
|
+
builtBy?: GitHubTrendingContributor[];
|
|
135
|
+
description?: string;
|
|
136
|
+
forks: number;
|
|
137
|
+
fullName: string;
|
|
138
|
+
language?: string;
|
|
139
|
+
name: string;
|
|
140
|
+
owner: string;
|
|
141
|
+
rank: number;
|
|
142
|
+
stars: number;
|
|
143
|
+
starsInPeriod: number;
|
|
144
|
+
url: string;
|
|
145
|
+
}
|
|
146
|
+
interface GitHubTrendingRuntimeResult {
|
|
147
|
+
items: GitHubTrendingRepository[];
|
|
148
|
+
period: GitHubTrendingPeriod;
|
|
149
|
+
sourceUrl: string;
|
|
150
|
+
}
|
|
151
|
+
interface GitHubRuntime {
|
|
152
|
+
fetchTrending: (options: {
|
|
153
|
+
period: GitHubTrendingPeriod;
|
|
154
|
+
}) => Promise<GitHubTrendingRuntimeResult>;
|
|
155
|
+
}
|
|
156
|
+
|
|
127
157
|
interface CliIo {
|
|
128
158
|
stderr: (value: string) => void;
|
|
129
159
|
stdout: (value: string) => void;
|
|
@@ -132,6 +162,7 @@ interface CliRunner {
|
|
|
132
162
|
run: (args: string[]) => Promise<number>;
|
|
133
163
|
}
|
|
134
164
|
interface CreateCliOptions extends Partial<CliIo> {
|
|
165
|
+
githubRuntime?: GitHubRuntime;
|
|
135
166
|
hackerNewsRuntime?: HackerNewsRuntime;
|
|
136
167
|
kr36Runtime?: Kr36Runtime;
|
|
137
168
|
toutiaoRuntime?: ToutiaoRuntime;
|
package/dist/index.js
CHANGED
|
@@ -2119,6 +2119,440 @@ async function loadPlaywright() {
|
|
|
2119
2119
|
return await import("playwright");
|
|
2120
2120
|
}
|
|
2121
2121
|
|
|
2122
|
+
// src/github/command.ts
|
|
2123
|
+
import { z as z4 } from "zod";
|
|
2124
|
+
|
|
2125
|
+
// src/github/output.ts
|
|
2126
|
+
function renderGitHubTrendingAsJson(result) {
|
|
2127
|
+
return JSON.stringify(
|
|
2128
|
+
{
|
|
2129
|
+
ok: true,
|
|
2130
|
+
data: result
|
|
2131
|
+
},
|
|
2132
|
+
null,
|
|
2133
|
+
2
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
function renderGitHubTrendingAsTable(result) {
|
|
2137
|
+
return renderSafeTable([
|
|
2138
|
+
[
|
|
2139
|
+
"rank",
|
|
2140
|
+
"repository",
|
|
2141
|
+
"language",
|
|
2142
|
+
"stars",
|
|
2143
|
+
"forks",
|
|
2144
|
+
"period stars",
|
|
2145
|
+
"description",
|
|
2146
|
+
"url"
|
|
2147
|
+
],
|
|
2148
|
+
...result.items.map((item) => [
|
|
2149
|
+
String(item.rank),
|
|
2150
|
+
item.fullName,
|
|
2151
|
+
item.language ?? "",
|
|
2152
|
+
String(item.stars),
|
|
2153
|
+
String(item.forks),
|
|
2154
|
+
String(item.starsInPeriod),
|
|
2155
|
+
item.description ?? "",
|
|
2156
|
+
item.url
|
|
2157
|
+
])
|
|
2158
|
+
]);
|
|
2159
|
+
}
|
|
2160
|
+
function renderGitHubCommandErrorAsJson(code, message, details) {
|
|
2161
|
+
return JSON.stringify(
|
|
2162
|
+
{
|
|
2163
|
+
ok: false,
|
|
2164
|
+
error: {
|
|
2165
|
+
code,
|
|
2166
|
+
message,
|
|
2167
|
+
...details === void 0 ? {} : { details }
|
|
2168
|
+
}
|
|
2169
|
+
},
|
|
2170
|
+
null,
|
|
2171
|
+
2
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/github/types.ts
|
|
2176
|
+
var GitHubCommandError = class extends Error {
|
|
2177
|
+
constructor(code, message, exitCode, details) {
|
|
2178
|
+
super(message);
|
|
2179
|
+
this.code = code;
|
|
2180
|
+
this.exitCode = exitCode;
|
|
2181
|
+
this.details = details;
|
|
2182
|
+
this.name = "GitHubCommandError";
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
|
|
2186
|
+
// src/github/service.ts
|
|
2187
|
+
var SUPPORTED_PERIODS = ["daily", "weekly", "monthly"];
|
|
2188
|
+
var SUPPORTED_PERIOD_SET = new Set(SUPPORTED_PERIODS);
|
|
2189
|
+
var GitHubService = class {
|
|
2190
|
+
constructor(dependencies) {
|
|
2191
|
+
this.dependencies = dependencies;
|
|
2192
|
+
}
|
|
2193
|
+
async trending(options) {
|
|
2194
|
+
const period = parsePeriod(options.period);
|
|
2195
|
+
const result = await this.dependencies.runtime.fetchTrending({ period });
|
|
2196
|
+
return {
|
|
2197
|
+
...result,
|
|
2198
|
+
meta: {
|
|
2199
|
+
totalItems: result.items.length
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
function parsePeriod(period) {
|
|
2205
|
+
const value = period ?? "daily";
|
|
2206
|
+
if (SUPPORTED_PERIOD_SET.has(value)) {
|
|
2207
|
+
return value;
|
|
2208
|
+
}
|
|
2209
|
+
throw new GitHubCommandError(
|
|
2210
|
+
"GITHUB_INVALID_PERIOD",
|
|
2211
|
+
"GitHub Trending period must be daily, weekly, or monthly.",
|
|
2212
|
+
2,
|
|
2213
|
+
{
|
|
2214
|
+
period: value,
|
|
2215
|
+
supportedPeriods: [...SUPPORTED_PERIODS]
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// src/github/command.ts
|
|
2221
|
+
var trendingOptionsSchema = z4.object({
|
|
2222
|
+
format: z4.enum(["json", "table"]).default("json"),
|
|
2223
|
+
since: z4.string().default("daily"),
|
|
2224
|
+
table: z4.boolean().optional()
|
|
2225
|
+
});
|
|
2226
|
+
function registerGitHubCommands(program, dependencies) {
|
|
2227
|
+
const github = program.command("github").description("Fetch GitHub Trending repositories.");
|
|
2228
|
+
const service = new GitHubService({ runtime: dependencies.runtime });
|
|
2229
|
+
github.command("trending").description("Fetch the all-language GitHub Trending page.").option(
|
|
2230
|
+
"--since <period>",
|
|
2231
|
+
"trending period: daily, weekly, or monthly",
|
|
2232
|
+
"daily"
|
|
2233
|
+
).option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (options) => {
|
|
2234
|
+
const parsed = trendingOptionsSchema.parse(applyTableShortcut4(options));
|
|
2235
|
+
const result = await service.trending({ period: parsed.since });
|
|
2236
|
+
dependencies.stdout(
|
|
2237
|
+
parsed.format === "table" ? `${renderGitHubTrendingAsTable(result)}
|
|
2238
|
+
` : `${renderGitHubTrendingAsJson(result)}
|
|
2239
|
+
`
|
|
2240
|
+
);
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
function handleGitHubCommandError(error, dependencies) {
|
|
2244
|
+
if (error instanceof GitHubCommandError) {
|
|
2245
|
+
dependencies.stderr(
|
|
2246
|
+
`${renderGitHubCommandErrorAsJson(error.code, error.message, error.details)}
|
|
2247
|
+
`
|
|
2248
|
+
);
|
|
2249
|
+
return error.exitCode;
|
|
2250
|
+
}
|
|
2251
|
+
return void 0;
|
|
2252
|
+
}
|
|
2253
|
+
function applyTableShortcut4(options) {
|
|
2254
|
+
if (options.table) {
|
|
2255
|
+
return {
|
|
2256
|
+
...options,
|
|
2257
|
+
format: "table"
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
return {
|
|
2261
|
+
...options,
|
|
2262
|
+
format: options.format
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/github/parser.ts
|
|
2267
|
+
import { load } from "cheerio";
|
|
2268
|
+
var GITHUB_ORIGIN = "https://github.com";
|
|
2269
|
+
var PERIOD_LABEL = {
|
|
2270
|
+
daily: "today",
|
|
2271
|
+
monthly: "this month",
|
|
2272
|
+
weekly: "this week"
|
|
2273
|
+
};
|
|
2274
|
+
function parseGitHubTrendingHtml(html, options) {
|
|
2275
|
+
const $ = load(html);
|
|
2276
|
+
const items = [];
|
|
2277
|
+
$("article.Box-row").each((_, element) => {
|
|
2278
|
+
const row = $(element);
|
|
2279
|
+
const repository = parseRepositoryLink(
|
|
2280
|
+
row.find("h2 a[href]").first().attr("href")
|
|
2281
|
+
);
|
|
2282
|
+
if (!repository) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (items.length >= options.maxItems) {
|
|
2286
|
+
throw new GitHubCommandError(
|
|
2287
|
+
"GITHUB_PARSE_FAILED",
|
|
2288
|
+
"GitHub Trending returned more repositories than the configured limit.",
|
|
2289
|
+
2,
|
|
2290
|
+
{
|
|
2291
|
+
maxItems: options.maxItems,
|
|
2292
|
+
sourceUrl: options.sourceUrl
|
|
2293
|
+
}
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const description = normalizeText(row.find("p.col-9").first().text());
|
|
2297
|
+
const language = normalizeText(
|
|
2298
|
+
row.find('[itemprop="programmingLanguage"]').first().text()
|
|
2299
|
+
);
|
|
2300
|
+
const builtBy = parseContributors(
|
|
2301
|
+
$,
|
|
2302
|
+
row.find('a[data-hovercard-type="user"][href]'),
|
|
2303
|
+
options.maxBuiltBy
|
|
2304
|
+
);
|
|
2305
|
+
items.push({
|
|
2306
|
+
forks: parseCount(findRepositoryLinkText(
|
|
2307
|
+
$,
|
|
2308
|
+
row.find("a[href]"),
|
|
2309
|
+
`${repository.path}/forks`
|
|
2310
|
+
)),
|
|
2311
|
+
fullName: `${repository.owner}/${repository.name}`,
|
|
2312
|
+
name: repository.name,
|
|
2313
|
+
owner: repository.owner,
|
|
2314
|
+
rank: items.length + 1,
|
|
2315
|
+
stars: parseCount(findRepositoryLinkText(
|
|
2316
|
+
$,
|
|
2317
|
+
row.find("a[href]"),
|
|
2318
|
+
`${repository.path}/stargazers`
|
|
2319
|
+
)),
|
|
2320
|
+
starsInPeriod: parsePeriodStars(
|
|
2321
|
+
row.find("span.float-sm-right").first().text(),
|
|
2322
|
+
options.period
|
|
2323
|
+
),
|
|
2324
|
+
url: repository.url,
|
|
2325
|
+
...builtBy.length === 0 ? {} : { builtBy },
|
|
2326
|
+
...description.length === 0 ? {} : { description },
|
|
2327
|
+
...language.length === 0 ? {} : { language }
|
|
2328
|
+
});
|
|
2329
|
+
});
|
|
2330
|
+
if (items.length === 0) {
|
|
2331
|
+
throw new GitHubCommandError(
|
|
2332
|
+
"GITHUB_PARSE_FAILED",
|
|
2333
|
+
"Failed to parse GitHub Trending repositories.",
|
|
2334
|
+
2,
|
|
2335
|
+
{
|
|
2336
|
+
sourceUrl: options.sourceUrl
|
|
2337
|
+
}
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
return {
|
|
2341
|
+
items,
|
|
2342
|
+
period: options.period,
|
|
2343
|
+
sourceUrl: options.sourceUrl
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function parseRepositoryLink(href) {
|
|
2347
|
+
const url = parseGitHubUrl(href);
|
|
2348
|
+
if (!url) return void 0;
|
|
2349
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
2350
|
+
if (segments.length !== 2) return void 0;
|
|
2351
|
+
const [owner, name] = segments;
|
|
2352
|
+
const path = `/${owner}/${name}`;
|
|
2353
|
+
return {
|
|
2354
|
+
name,
|
|
2355
|
+
owner,
|
|
2356
|
+
path,
|
|
2357
|
+
url: `${GITHUB_ORIGIN}${path}`
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
function parseContributors($, links, maxBuiltBy) {
|
|
2361
|
+
const contributors = [];
|
|
2362
|
+
links.each((_, element) => {
|
|
2363
|
+
if (contributors.length >= maxBuiltBy) return false;
|
|
2364
|
+
const link = $(element);
|
|
2365
|
+
const user = parseGitHubUser(link.attr("href"));
|
|
2366
|
+
const avatarUrl = parseHttpUrl(link.find("img").first().attr("src"));
|
|
2367
|
+
if (!user || !avatarUrl) return;
|
|
2368
|
+
contributors.push({
|
|
2369
|
+
avatarUrl,
|
|
2370
|
+
url: `${GITHUB_ORIGIN}/${user}`,
|
|
2371
|
+
username: user
|
|
2372
|
+
});
|
|
2373
|
+
});
|
|
2374
|
+
return contributors;
|
|
2375
|
+
}
|
|
2376
|
+
function parseGitHubUser(href) {
|
|
2377
|
+
const url = parseGitHubUrl(href);
|
|
2378
|
+
if (!url) return void 0;
|
|
2379
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
2380
|
+
return segments.length === 1 ? segments[0] : void 0;
|
|
2381
|
+
}
|
|
2382
|
+
function parseGitHubUrl(value) {
|
|
2383
|
+
if (!value) return void 0;
|
|
2384
|
+
try {
|
|
2385
|
+
const url = new URL(value, GITHUB_ORIGIN);
|
|
2386
|
+
return url.origin === GITHUB_ORIGIN ? url : void 0;
|
|
2387
|
+
} catch {
|
|
2388
|
+
return void 0;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
function parseHttpUrl(value) {
|
|
2392
|
+
if (!value) return void 0;
|
|
2393
|
+
try {
|
|
2394
|
+
const url = new URL(value, GITHUB_ORIGIN);
|
|
2395
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : void 0;
|
|
2396
|
+
} catch {
|
|
2397
|
+
return void 0;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function findRepositoryLinkText($, links, expectedPath) {
|
|
2401
|
+
let text = "";
|
|
2402
|
+
links.each((_, element) => {
|
|
2403
|
+
const link = $(element);
|
|
2404
|
+
const url = parseGitHubUrl(link.attr("href"));
|
|
2405
|
+
if (url?.pathname === expectedPath) {
|
|
2406
|
+
text = link.text();
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
return text;
|
|
2411
|
+
}
|
|
2412
|
+
function parsePeriodStars(value, period) {
|
|
2413
|
+
const suffix = new RegExp(`\\s+stars?\\s+${PERIOD_LABEL[period]}\\s*$`, "i");
|
|
2414
|
+
if (!suffix.test(value)) return 0;
|
|
2415
|
+
return parseCount(value.replace(suffix, ""));
|
|
2416
|
+
}
|
|
2417
|
+
function parseCount(value) {
|
|
2418
|
+
const digits = value.replace(/[,\s]/g, "");
|
|
2419
|
+
if (!/^\d+$/.test(digits)) return 0;
|
|
2420
|
+
const count = Number(digits);
|
|
2421
|
+
return Number.isSafeInteger(count) ? count : 0;
|
|
2422
|
+
}
|
|
2423
|
+
function normalizeText(value) {
|
|
2424
|
+
return value.replace(/\s+/g, " ").trim();
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/github/runtime.ts
|
|
2428
|
+
var DEFAULT_MAX_RESPONSE_BYTES2 = 5 * 1024 * 1024;
|
|
2429
|
+
var DEFAULT_REQUEST_TIMEOUT_MS2 = 3e4;
|
|
2430
|
+
var MAX_BUILT_BY = 10;
|
|
2431
|
+
var MAX_ITEMS = 100;
|
|
2432
|
+
function createDefaultGitHubRuntime(options = {}) {
|
|
2433
|
+
const dependencies = {
|
|
2434
|
+
fetch: options.fetch ?? fetch,
|
|
2435
|
+
maxResponseBytes: options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES2,
|
|
2436
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2
|
|
2437
|
+
};
|
|
2438
|
+
return {
|
|
2439
|
+
fetchTrending: async ({ period }) => fetchTrending(dependencies, period)
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
async function fetchTrending(dependencies, period) {
|
|
2443
|
+
const sourceUrl = createSourceUrl(period);
|
|
2444
|
+
const controller = new AbortController();
|
|
2445
|
+
const timeout = setTimeout(
|
|
2446
|
+
() => controller.abort(),
|
|
2447
|
+
dependencies.requestTimeoutMs
|
|
2448
|
+
);
|
|
2449
|
+
try {
|
|
2450
|
+
let response;
|
|
2451
|
+
try {
|
|
2452
|
+
response = await dependencies.fetch(sourceUrl, {
|
|
2453
|
+
headers: {
|
|
2454
|
+
accept: "text/html,application/xhtml+xml",
|
|
2455
|
+
"user-agent": "ants-move GitHub collector"
|
|
2456
|
+
},
|
|
2457
|
+
redirect: "error",
|
|
2458
|
+
signal: controller.signal
|
|
2459
|
+
});
|
|
2460
|
+
} catch {
|
|
2461
|
+
throw fetchFailed(sourceUrl);
|
|
2462
|
+
}
|
|
2463
|
+
if (!response.ok) {
|
|
2464
|
+
await cancelResponseBody2(response);
|
|
2465
|
+
throw new GitHubCommandError(
|
|
2466
|
+
"GITHUB_FETCH_FAILED",
|
|
2467
|
+
`Failed to fetch GitHub Trending data. HTTP ${response.status}.`,
|
|
2468
|
+
2,
|
|
2469
|
+
{
|
|
2470
|
+
status: response.status,
|
|
2471
|
+
url: sourceUrl
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
let body;
|
|
2476
|
+
try {
|
|
2477
|
+
body = await readBoundedBody2(
|
|
2478
|
+
response,
|
|
2479
|
+
dependencies.maxResponseBytes,
|
|
2480
|
+
sourceUrl
|
|
2481
|
+
);
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
if (error instanceof GitHubCommandError) throw error;
|
|
2484
|
+
throw fetchFailed(sourceUrl);
|
|
2485
|
+
}
|
|
2486
|
+
return parseGitHubTrendingHtml(new TextDecoder().decode(body), {
|
|
2487
|
+
maxBuiltBy: MAX_BUILT_BY,
|
|
2488
|
+
maxItems: MAX_ITEMS,
|
|
2489
|
+
period,
|
|
2490
|
+
sourceUrl
|
|
2491
|
+
});
|
|
2492
|
+
} finally {
|
|
2493
|
+
clearTimeout(timeout);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function createSourceUrl(period) {
|
|
2497
|
+
const url = new URL("/trending", "https://github.com");
|
|
2498
|
+
url.searchParams.set("since", period);
|
|
2499
|
+
return url.toString();
|
|
2500
|
+
}
|
|
2501
|
+
async function readBoundedBody2(response, maxBytes, url) {
|
|
2502
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
2503
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
2504
|
+
await cancelResponseBody2(response);
|
|
2505
|
+
throw responseTooLarge2(url, maxBytes);
|
|
2506
|
+
}
|
|
2507
|
+
const reader = response.body?.getReader();
|
|
2508
|
+
if (!reader) {
|
|
2509
|
+
return new Uint8Array();
|
|
2510
|
+
}
|
|
2511
|
+
const chunks = [];
|
|
2512
|
+
let totalBytes = 0;
|
|
2513
|
+
while (true) {
|
|
2514
|
+
const { done, value } = await reader.read();
|
|
2515
|
+
if (done) break;
|
|
2516
|
+
totalBytes += value.byteLength;
|
|
2517
|
+
if (totalBytes > maxBytes) {
|
|
2518
|
+
await reader.cancel().catch(() => void 0);
|
|
2519
|
+
throw responseTooLarge2(url, maxBytes);
|
|
2520
|
+
}
|
|
2521
|
+
chunks.push(value);
|
|
2522
|
+
}
|
|
2523
|
+
const body = new Uint8Array(totalBytes);
|
|
2524
|
+
let offset = 0;
|
|
2525
|
+
for (const chunk of chunks) {
|
|
2526
|
+
body.set(chunk, offset);
|
|
2527
|
+
offset += chunk.byteLength;
|
|
2528
|
+
}
|
|
2529
|
+
return body;
|
|
2530
|
+
}
|
|
2531
|
+
async function cancelResponseBody2(response) {
|
|
2532
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2533
|
+
}
|
|
2534
|
+
function fetchFailed(url) {
|
|
2535
|
+
return new GitHubCommandError(
|
|
2536
|
+
"GITHUB_FETCH_FAILED",
|
|
2537
|
+
"Failed to fetch GitHub Trending data.",
|
|
2538
|
+
2,
|
|
2539
|
+
{
|
|
2540
|
+
url
|
|
2541
|
+
}
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
function responseTooLarge2(url, maxBytes) {
|
|
2545
|
+
return new GitHubCommandError(
|
|
2546
|
+
"GITHUB_RESPONSE_TOO_LARGE",
|
|
2547
|
+
"GitHub Trending response exceeded the configured size limit.",
|
|
2548
|
+
2,
|
|
2549
|
+
{
|
|
2550
|
+
maxBytes,
|
|
2551
|
+
url
|
|
2552
|
+
}
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2122
2556
|
// src/cli.ts
|
|
2123
2557
|
function createCli(options = {}) {
|
|
2124
2558
|
const io = {
|
|
@@ -2141,6 +2575,11 @@ function createCli(options = {}) {
|
|
|
2141
2575
|
stderr: io.stderr,
|
|
2142
2576
|
stdout: io.stdout
|
|
2143
2577
|
});
|
|
2578
|
+
registerGitHubCommands(program, {
|
|
2579
|
+
runtime: options.githubRuntime ?? createDefaultGitHubRuntime(),
|
|
2580
|
+
stderr: io.stderr,
|
|
2581
|
+
stdout: io.stdout
|
|
2582
|
+
});
|
|
2144
2583
|
registerToutiaoCommands(program, {
|
|
2145
2584
|
runtime: options.toutiaoRuntime ?? createDefaultToutiaoRuntime(),
|
|
2146
2585
|
stderr: io.stderr,
|
|
@@ -2155,6 +2594,10 @@ function createCli(options = {}) {
|
|
|
2155
2594
|
if (error instanceof CommanderError && error.exitCode === 0) {
|
|
2156
2595
|
return 0;
|
|
2157
2596
|
}
|
|
2597
|
+
const githubExitCode = handleGitHubCommandError(error, io);
|
|
2598
|
+
if (githubExitCode !== void 0) {
|
|
2599
|
+
return githubExitCode;
|
|
2600
|
+
}
|
|
2158
2601
|
const toutiaoExitCode = handleToutiaoCommandError(error, io);
|
|
2159
2602
|
if (toutiaoExitCode !== void 0) {
|
|
2160
2603
|
return toutiaoExitCode;
|