github-discover 0.1.0 → 0.1.1
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/dist/cli.js +71 -0
- package/dist/commands/popular.js +40 -0
- package/dist/commands/topic.js +46 -0
- package/dist/commands/trending.js +45 -0
- package/dist/config.js +10 -0
- package/dist/github/client.js +57 -0
- package/dist/github/repoSearch.js +83 -0
- package/dist/github/topicStats.js +38 -0
- package/dist/utils/dateRange.js +27 -0
- package/dist/utils/output.js +88 -0
- package/package.json +1 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const popular_1 = require("./commands/popular");
|
|
6
|
+
const trending_1 = require("./commands/trending");
|
|
7
|
+
const topic_1 = require("./commands/topic");
|
|
8
|
+
const program = new commander_1.Command();
|
|
9
|
+
program
|
|
10
|
+
.name("github-discover")
|
|
11
|
+
.description("Discover newly created popular GitHub repositories and trending topics (daily/weekly/monthly/yearly).")
|
|
12
|
+
.version("0.1.0");
|
|
13
|
+
program
|
|
14
|
+
.command("trending")
|
|
15
|
+
.description("List trending repositories based on lifetime stars per day, adjusted by repository age.")
|
|
16
|
+
.option("-p, --period <period>", "time period for repository creation window: daily | weekly | monthly | yearly", "daily")
|
|
17
|
+
.option("-n, --limit <number>", "maximum number of repositories to return (1-100)", "50")
|
|
18
|
+
.option("-l, --language <language>", "optional programming language filter, e.g. TypeScript")
|
|
19
|
+
.option("-s, --min-stars <number>", "minimum star count threshold (defaults: daily 60, weekly 500, monthly 4000, yearly 10000)")
|
|
20
|
+
.option("--json", "output JSON instead of a table", false)
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
try {
|
|
23
|
+
await (0, trending_1.runTrendingCommand)(options);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const error = err;
|
|
27
|
+
console.error("Error:", error.message);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command("popular")
|
|
33
|
+
.description("List newly created popular repositories for a given period.")
|
|
34
|
+
.option("-p, --period <period>", "time period: daily | weekly | monthly | yearly", "daily")
|
|
35
|
+
.option("-n, --limit <number>", "maximum number of repositories to return (1-100)", "50")
|
|
36
|
+
.option("-l, --language <language>", "optional programming language filter, e.g. TypeScript")
|
|
37
|
+
.option("-s, --min-stars <number>", "minimum star count threshold (defaults: daily 60, weekly 500, monthly 4000, yearly 10000)")
|
|
38
|
+
.option("--json", "output JSON instead of a table", false)
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
await (0, popular_1.runPopularCommand)(options);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const error = err;
|
|
45
|
+
console.error("Error:", error.message);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command("topic")
|
|
51
|
+
.description("List trending repository topics for a given period based on frequency and stars.")
|
|
52
|
+
.option("-p, --period <period>", "time period: daily | weekly | monthly | yearly", "daily")
|
|
53
|
+
.option("-n, --limit <number>", "maximum number of topics to return (1-100)", "30")
|
|
54
|
+
.option("-l, --language <language>", "optional programming language filter, e.g. TypeScript")
|
|
55
|
+
.option("-s, --min-stars <number>", "minimum star count threshold (defaults: daily 60, weekly 500, monthly 4000, yearly 10000)")
|
|
56
|
+
.option("--json", "output JSON instead of a table", false)
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
try {
|
|
59
|
+
await (0, topic_1.runTopicCommand)(options);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const error = err;
|
|
63
|
+
console.error("Error:", error.message);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
68
|
+
const error = err;
|
|
69
|
+
console.error("Error:", error.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runPopularCommand = runPopularCommand;
|
|
4
|
+
const client_1 = require("../github/client");
|
|
5
|
+
const repoSearch_1 = require("../github/repoSearch");
|
|
6
|
+
const dateRange_1 = require("../utils/dateRange");
|
|
7
|
+
const output_1 = require("../utils/output");
|
|
8
|
+
async function runPopularCommand(options) {
|
|
9
|
+
const periodInput = (options.period ?? "daily").toLowerCase();
|
|
10
|
+
const validPeriods = ["daily", "weekly", "monthly", "yearly"];
|
|
11
|
+
const defaultMinStarsByPeriod = {
|
|
12
|
+
daily: 60,
|
|
13
|
+
weekly: 500,
|
|
14
|
+
monthly: 4000,
|
|
15
|
+
yearly: 10000,
|
|
16
|
+
};
|
|
17
|
+
if (!validPeriods.includes(periodInput)) {
|
|
18
|
+
throw new Error(`Invalid period "${options.period}". Expected one of: daily, weekly, monthly, yearly.`);
|
|
19
|
+
}
|
|
20
|
+
const period = periodInput;
|
|
21
|
+
const limit = clampToInt(options.limit ?? "50", 1, 100, "limit");
|
|
22
|
+
const minStars = clampToInt(options.minStars ?? String(defaultMinStarsByPeriod[period]), 1, Number.MAX_SAFE_INTEGER, "min-stars");
|
|
23
|
+
const startDate = (0, dateRange_1.getStartDate)(period);
|
|
24
|
+
const client = new client_1.GitHubClient();
|
|
25
|
+
const repos = await (0, repoSearch_1.searchNewPopularRepositories)(client, {
|
|
26
|
+
startDate,
|
|
27
|
+
minStars,
|
|
28
|
+
language: options.language,
|
|
29
|
+
limit,
|
|
30
|
+
});
|
|
31
|
+
(0, output_1.printRepositories)(repos, { json: Boolean(options.json) });
|
|
32
|
+
}
|
|
33
|
+
function clampToInt(valueStr, min, max, label) {
|
|
34
|
+
const value = Number.parseInt(valueStr, 10);
|
|
35
|
+
if (Number.isNaN(value)) {
|
|
36
|
+
throw new Error(`Invalid number for ${label}: "${valueStr}".`);
|
|
37
|
+
}
|
|
38
|
+
const clamped = Math.min(Math.max(value, min), max);
|
|
39
|
+
return clamped;
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runTopicCommand = runTopicCommand;
|
|
4
|
+
const client_1 = require("../github/client");
|
|
5
|
+
const repoSearch_1 = require("../github/repoSearch");
|
|
6
|
+
const topicStats_1 = require("../github/topicStats");
|
|
7
|
+
const dateRange_1 = require("../utils/dateRange");
|
|
8
|
+
const output_1 = require("../utils/output");
|
|
9
|
+
async function runTopicCommand(options) {
|
|
10
|
+
const periodInput = (options.period ?? "daily").toLowerCase();
|
|
11
|
+
const validPeriods = ["daily", "weekly", "monthly", "yearly"];
|
|
12
|
+
const defaultMinStarsByPeriod = {
|
|
13
|
+
daily: 60,
|
|
14
|
+
weekly: 500,
|
|
15
|
+
monthly: 4000,
|
|
16
|
+
yearly: 10000,
|
|
17
|
+
};
|
|
18
|
+
if (!validPeriods.includes(periodInput)) {
|
|
19
|
+
throw new Error(`Invalid period "${options.period}". Expected one of: daily, weekly, monthly, yearly.`);
|
|
20
|
+
}
|
|
21
|
+
const period = periodInput;
|
|
22
|
+
const limit = clampToInt(options.limit ?? "30", 1, 100, "limit");
|
|
23
|
+
const minStars = clampToInt(options.minStars ?? String(defaultMinStarsByPeriod[period]), 1, Number.MAX_SAFE_INTEGER, "min-stars");
|
|
24
|
+
const startDate = (0, dateRange_1.getStartDate)(period);
|
|
25
|
+
const client = new client_1.GitHubClient();
|
|
26
|
+
const searchOptions = {
|
|
27
|
+
startDate,
|
|
28
|
+
minStars,
|
|
29
|
+
language: options.language,
|
|
30
|
+
limit,
|
|
31
|
+
};
|
|
32
|
+
const { totalCount, repos } = await (0, repoSearch_1.searchRepositoriesForTopics)(client, searchOptions);
|
|
33
|
+
const stats = (0, topicStats_1.aggregateTopicStats)(repos, limit);
|
|
34
|
+
if (stats.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
(0, output_1.printTopicStats)(stats, { json: Boolean(options.json) });
|
|
38
|
+
}
|
|
39
|
+
function clampToInt(valueStr, min, max, label) {
|
|
40
|
+
const value = Number.parseInt(valueStr, 10);
|
|
41
|
+
if (Number.isNaN(value)) {
|
|
42
|
+
throw new Error(`Invalid number for ${label}: "${valueStr}".`);
|
|
43
|
+
}
|
|
44
|
+
const clamped = Math.min(Math.max(value, min), max);
|
|
45
|
+
return clamped;
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runTrendingCommand = runTrendingCommand;
|
|
4
|
+
const client_1 = require("../github/client");
|
|
5
|
+
const repoSearch_1 = require("../github/repoSearch");
|
|
6
|
+
const output_1 = require("../utils/output");
|
|
7
|
+
async function runTrendingCommand(options) {
|
|
8
|
+
const periodInput = (options.period ?? "daily").toLowerCase();
|
|
9
|
+
const validPeriods = ["daily", "weekly", "monthly", "yearly"];
|
|
10
|
+
const windowDaysByPeriod = {
|
|
11
|
+
daily: 7,
|
|
12
|
+
weekly: 28,
|
|
13
|
+
monthly: 90,
|
|
14
|
+
yearly: 730,
|
|
15
|
+
};
|
|
16
|
+
const defaultMinStarsByPeriod = {
|
|
17
|
+
daily: 60,
|
|
18
|
+
weekly: 500,
|
|
19
|
+
monthly: 4000,
|
|
20
|
+
yearly: 10000,
|
|
21
|
+
};
|
|
22
|
+
if (!validPeriods.includes(periodInput)) {
|
|
23
|
+
throw new Error(`Invalid period "${options.period}". Expected one of: daily, weekly, monthly, yearly.`);
|
|
24
|
+
}
|
|
25
|
+
const period = periodInput;
|
|
26
|
+
const limit = clampToInt(options.limit ?? "50", 1, 100, "limit");
|
|
27
|
+
const minStars = clampToInt(options.minStars ?? String(defaultMinStarsByPeriod[period]), 1, Number.MAX_SAFE_INTEGER, "min-stars");
|
|
28
|
+
const windowDays = windowDaysByPeriod[period];
|
|
29
|
+
const client = new client_1.GitHubClient();
|
|
30
|
+
const repos = await (0, repoSearch_1.searchLifetimeFastGrowingRepositories)(client, {
|
|
31
|
+
windowDays,
|
|
32
|
+
minStars,
|
|
33
|
+
language: options.language,
|
|
34
|
+
limit,
|
|
35
|
+
});
|
|
36
|
+
(0, output_1.printRepositories)(repos, { json: Boolean(options.json) });
|
|
37
|
+
}
|
|
38
|
+
function clampToInt(valueStr, min, max, label) {
|
|
39
|
+
const value = Number.parseInt(valueStr, 10);
|
|
40
|
+
if (Number.isNaN(value)) {
|
|
41
|
+
throw new Error(`Invalid number for ${label}: "${valueStr}".`);
|
|
42
|
+
}
|
|
43
|
+
const clamped = Math.min(Math.max(value, min), max);
|
|
44
|
+
return clamped;
|
|
45
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getGitHubToken = getGitHubToken;
|
|
4
|
+
exports.getUserAgent = getUserAgent;
|
|
5
|
+
function getGitHubToken() {
|
|
6
|
+
return process.env.GITHUB_TOKEN;
|
|
7
|
+
}
|
|
8
|
+
function getUserAgent() {
|
|
9
|
+
return "github-discover-cli/0.1.0";
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitHubClient = void 0;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
class GitHubClient {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.baseUrl = "https://api.github.com";
|
|
8
|
+
}
|
|
9
|
+
async request(path, params = {}) {
|
|
10
|
+
const url = new URL(this.baseUrl + path);
|
|
11
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return;
|
|
14
|
+
url.searchParams.set(key, String(value));
|
|
15
|
+
});
|
|
16
|
+
const headers = {
|
|
17
|
+
"User-Agent": (0, config_1.getUserAgent)(),
|
|
18
|
+
Accept: "application/vnd.github+json",
|
|
19
|
+
};
|
|
20
|
+
const token = (0, config_1.getGitHubToken)();
|
|
21
|
+
if (token) {
|
|
22
|
+
headers.Authorization = `token ${token}`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof fetch === "undefined") {
|
|
25
|
+
throw new Error("Global fetch is not available. Please run this CLI with Node.js 18+ where fetch is built-in.");
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url.toString(), {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers,
|
|
30
|
+
});
|
|
31
|
+
const text = await response.text();
|
|
32
|
+
let json;
|
|
33
|
+
try {
|
|
34
|
+
json = text ? JSON.parse(text) : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
json = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const status = response.status;
|
|
41
|
+
const message = (json && json.message) ||
|
|
42
|
+
`GitHub API request failed with status ${status}`;
|
|
43
|
+
if (status === 401 || status === 403) {
|
|
44
|
+
const hint = (0, config_1.getGitHubToken)()
|
|
45
|
+
? "Check that your GITHUB_TOKEN is valid and has sufficient scopes."
|
|
46
|
+
: "You may be hitting anonymous rate limits. Set GITHUB_TOKEN to increase limits.";
|
|
47
|
+
throw new Error(`${message} (${status}). ${hint}`);
|
|
48
|
+
}
|
|
49
|
+
if (status === 422) {
|
|
50
|
+
throw new Error(`${message} (${status}). Please check your search parameters.`);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`${message} (${status}).`);
|
|
53
|
+
}
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.GitHubClient = GitHubClient;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchNewPopularRepositories = searchNewPopularRepositories;
|
|
4
|
+
exports.searchRepositoriesForTopics = searchRepositoriesForTopics;
|
|
5
|
+
exports.searchLifetimeFastGrowingRepositories = searchLifetimeFastGrowingRepositories;
|
|
6
|
+
async function searchNewPopularRepositories(client, options) {
|
|
7
|
+
const { startDate, minStars, language, limit } = options;
|
|
8
|
+
const qualifiers = [
|
|
9
|
+
`created:>=${startDate}`,
|
|
10
|
+
`stars:>=${minStars}`,
|
|
11
|
+
];
|
|
12
|
+
if (language) {
|
|
13
|
+
qualifiers.push(`language:${language}`);
|
|
14
|
+
}
|
|
15
|
+
const q = qualifiers.join(" ");
|
|
16
|
+
const perPage = Math.min(Math.max(limit, 1), 100);
|
|
17
|
+
const data = await client.request("/search/repositories", {
|
|
18
|
+
q,
|
|
19
|
+
sort: "stars",
|
|
20
|
+
order: "desc",
|
|
21
|
+
per_page: perPage,
|
|
22
|
+
});
|
|
23
|
+
return data.items.slice(0, limit);
|
|
24
|
+
}
|
|
25
|
+
async function searchRepositoriesForTopics(client, options) {
|
|
26
|
+
const { startDate, minStars, language } = options;
|
|
27
|
+
const qualifiers = [`created:>=${startDate}`, `stars:>=${minStars}`];
|
|
28
|
+
if (language) {
|
|
29
|
+
qualifiers.push(`language:${language}`);
|
|
30
|
+
}
|
|
31
|
+
const q = qualifiers.join(" ");
|
|
32
|
+
const data = await client.request("/search/repositories", {
|
|
33
|
+
q,
|
|
34
|
+
sort: "stars",
|
|
35
|
+
order: "desc",
|
|
36
|
+
per_page: 100,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
totalCount: data.total_count,
|
|
40
|
+
repos: data.items,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function searchLifetimeFastGrowingRepositories(client, options) {
|
|
44
|
+
const { windowDays, minStars, language, limit } = options;
|
|
45
|
+
const startDate = getDateNDaysAgo(windowDays);
|
|
46
|
+
const qualifiers = [
|
|
47
|
+
`created:>=${startDate}`,
|
|
48
|
+
`stars:>=${minStars}`,
|
|
49
|
+
];
|
|
50
|
+
if (language) {
|
|
51
|
+
qualifiers.push(`language:${language}`);
|
|
52
|
+
}
|
|
53
|
+
const q = qualifiers.join(" ");
|
|
54
|
+
const data = await client.request("/search/repositories", {
|
|
55
|
+
q,
|
|
56
|
+
sort: "stars",
|
|
57
|
+
order: "desc",
|
|
58
|
+
per_page: 100,
|
|
59
|
+
});
|
|
60
|
+
const items = data.items;
|
|
61
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
62
|
+
const SMOOTHING_DAYS = 30;
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
items.sort((a, b) => {
|
|
65
|
+
const ageDaysARaw = Math.max((now - new Date(a.created_at).getTime()) / MS_PER_DAY, 1);
|
|
66
|
+
const ageDaysBRaw = Math.max((now - new Date(b.created_at).getTime()) / MS_PER_DAY, 1);
|
|
67
|
+
const ageDaysAEff = ageDaysARaw + SMOOTHING_DAYS;
|
|
68
|
+
const ageDaysBEff = ageDaysBRaw + SMOOTHING_DAYS;
|
|
69
|
+
const growthA = a.stargazers_count / ageDaysAEff;
|
|
70
|
+
const growthB = b.stargazers_count / ageDaysBEff;
|
|
71
|
+
return growthB - growthA;
|
|
72
|
+
});
|
|
73
|
+
return items.slice(0, limit);
|
|
74
|
+
}
|
|
75
|
+
function getDateNDaysAgo(days) {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const start = new Date(now);
|
|
78
|
+
start.setDate(now.getDate() - days);
|
|
79
|
+
const year = start.getFullYear();
|
|
80
|
+
const month = String(start.getMonth() + 1).padStart(2, "0");
|
|
81
|
+
const day = String(start.getDate()).padStart(2, "0");
|
|
82
|
+
return `${year}-${month}-${day}`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aggregateTopicStats = aggregateTopicStats;
|
|
4
|
+
function aggregateTopicStats(repos, limit) {
|
|
5
|
+
const topicMap = new Map();
|
|
6
|
+
for (const repo of repos) {
|
|
7
|
+
if (!repo.topics || repo.topics.length === 0)
|
|
8
|
+
continue;
|
|
9
|
+
const uniqueTopics = new Set(repo.topics
|
|
10
|
+
.map((t) => t.trim())
|
|
11
|
+
.filter((t) => t.length > 0));
|
|
12
|
+
for (const topic of uniqueTopics) {
|
|
13
|
+
const current = topicMap.get(topic) ?? { repoCount: 0, starSum: 0 };
|
|
14
|
+
current.repoCount += 1;
|
|
15
|
+
current.starSum += repo.stargazers_count;
|
|
16
|
+
topicMap.set(topic, current);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const stats = [];
|
|
20
|
+
for (const [topic, { repoCount, starSum }] of topicMap.entries()) {
|
|
21
|
+
const score = repoCount * Math.log10(Math.max(starSum + 10, 1)); // 防止 log10(0)
|
|
22
|
+
stats.push({
|
|
23
|
+
topic,
|
|
24
|
+
score,
|
|
25
|
+
repoCount,
|
|
26
|
+
starSum,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
stats.sort((a, b) => {
|
|
30
|
+
if (b.score !== a.score)
|
|
31
|
+
return b.score - a.score;
|
|
32
|
+
if (b.repoCount !== a.repoCount)
|
|
33
|
+
return b.repoCount - a.repoCount;
|
|
34
|
+
return a.topic.localeCompare(b.topic);
|
|
35
|
+
});
|
|
36
|
+
const safeLimit = Math.max(1, Math.min(limit, stats.length));
|
|
37
|
+
return stats.slice(0, safeLimit);
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getStartDate = getStartDate;
|
|
4
|
+
function getStartDate(period) {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
const start = new Date(now);
|
|
7
|
+
switch (period) {
|
|
8
|
+
case "daily":
|
|
9
|
+
start.setDate(now.getDate() - 2);
|
|
10
|
+
break;
|
|
11
|
+
case "weekly":
|
|
12
|
+
start.setDate(now.getDate() - 7);
|
|
13
|
+
break;
|
|
14
|
+
case "monthly":
|
|
15
|
+
start.setDate(now.getDate() - 30);
|
|
16
|
+
break;
|
|
17
|
+
case "yearly":
|
|
18
|
+
start.setDate(now.getDate() - 365);
|
|
19
|
+
break;
|
|
20
|
+
default:
|
|
21
|
+
start.setDate(now.getDate() - 2);
|
|
22
|
+
}
|
|
23
|
+
const year = start.getFullYear();
|
|
24
|
+
const month = String(start.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const day = String(start.getDate()).padStart(2, "0");
|
|
26
|
+
return `${year}-${month}-${day}`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printRepositories = printRepositories;
|
|
4
|
+
exports.printTopicStats = printTopicStats;
|
|
5
|
+
function printRepositories(repos, options) {
|
|
6
|
+
const filteredRepos = repos.filter((r) => {
|
|
7
|
+
const langRaw = r.language ?? "";
|
|
8
|
+
const lang = langRaw.trim();
|
|
9
|
+
if (!lang || lang === "-") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const lower = lang.toLowerCase();
|
|
13
|
+
return lower !== "html" && lower !== "shell";
|
|
14
|
+
});
|
|
15
|
+
if (options.json) {
|
|
16
|
+
const minimal = filteredRepos.map((r) => ({
|
|
17
|
+
name: r.name,
|
|
18
|
+
full_name: r.full_name,
|
|
19
|
+
html_url: r.html_url,
|
|
20
|
+
stars: r.stargazers_count,
|
|
21
|
+
language: r.language,
|
|
22
|
+
created_at: r.created_at,
|
|
23
|
+
description: r.description,
|
|
24
|
+
}));
|
|
25
|
+
console.log(JSON.stringify(minimal, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (filteredRepos.length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const truncate = (text, maxLength) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
32
|
+
const rows = filteredRepos.map((r) => {
|
|
33
|
+
const lang = r.language ?? "-";
|
|
34
|
+
const created = r.created_at.slice(0, 10);
|
|
35
|
+
const description = r.description ? truncate(r.description, 150) : "-";
|
|
36
|
+
return {
|
|
37
|
+
name: r.full_name,
|
|
38
|
+
stars: String(r.stargazers_count),
|
|
39
|
+
lang,
|
|
40
|
+
created,
|
|
41
|
+
url: r.html_url,
|
|
42
|
+
description,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
const headers = ["Name", "Stars", "Lang", "Created", "URL", "Description"];
|
|
46
|
+
const colWidths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => Object.values(row)[index].length)));
|
|
47
|
+
const formatRow = (cols) => cols
|
|
48
|
+
.map((col, i) => col.padEnd(colWidths[i], " "))
|
|
49
|
+
.join(" | ");
|
|
50
|
+
console.log(formatRow(headers));
|
|
51
|
+
console.log(formatRow(headers.map((h, i) => "".padEnd(colWidths[i], "-"))));
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
console.log(formatRow([
|
|
54
|
+
row.name,
|
|
55
|
+
row.stars,
|
|
56
|
+
row.lang,
|
|
57
|
+
row.created,
|
|
58
|
+
row.url,
|
|
59
|
+
row.description,
|
|
60
|
+
]));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function printTopicStats(stats, options) {
|
|
64
|
+
if (options.json) {
|
|
65
|
+
const minimal = stats.map((s) => ({
|
|
66
|
+
topic: s.topic,
|
|
67
|
+
}));
|
|
68
|
+
console.log(JSON.stringify(minimal, null, 2));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (stats.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const rows = stats.map((s, index) => ({
|
|
75
|
+
rank: String(index + 1),
|
|
76
|
+
topic: s.topic,
|
|
77
|
+
}));
|
|
78
|
+
const headers = ["Rank", "Topic"];
|
|
79
|
+
const colWidths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => Object.values(row)[index].length)));
|
|
80
|
+
const formatRow = (cols) => cols
|
|
81
|
+
.map((col, i) => col.padEnd(colWidths[i], " "))
|
|
82
|
+
.join(" | ");
|
|
83
|
+
console.log(formatRow(headers));
|
|
84
|
+
console.log(formatRow(headers.map((h, i) => "".padEnd(colWidths[i], "-"))));
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
console.log(formatRow([row.rank, row.topic]));
|
|
87
|
+
}
|
|
88
|
+
}
|