reposizer 0.1.1 → 0.2.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/bin/reposizer.js +10 -2
- package/dist/src/commands/analyze.js +53 -0
- package/dist/src/commands/repo.js +99 -12
- package/dist/src/services/github.js +53 -25
- package/dist/src/services/tree.js +47 -0
- package/dist/src/utils/git.js +69 -11
- package/dist/src/utils/size.js +15 -0
- package/package.json +10 -4
package/dist/bin/reposizer.js
CHANGED
|
@@ -10,9 +10,17 @@ program
|
|
|
10
10
|
program
|
|
11
11
|
.argument("[repositories...]", "One or more repositories in owner/repo format")
|
|
12
12
|
.option("--json", "Return machine-readable JSON output")
|
|
13
|
+
.option("--analyze", "Approximate top-level directory sizes from Git tree metadata (no full clone)")
|
|
14
|
+
.option("--sort <field>", "When comparing multiple repositories: sort by size, stars, or name", "size")
|
|
13
15
|
.action(async (repositories = [], options) => {
|
|
14
16
|
try {
|
|
15
|
-
|
|
17
|
+
const sortRaw = options.sort ?? "size";
|
|
18
|
+
if (sortRaw !== "size" &&
|
|
19
|
+
sortRaw !== "stars" &&
|
|
20
|
+
sortRaw !== "name") {
|
|
21
|
+
throw new Error('Invalid --sort. Use "size", "stars", or "name".');
|
|
22
|
+
}
|
|
23
|
+
await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json), Boolean(options.analyze), sortRaw);
|
|
16
24
|
}
|
|
17
25
|
catch (error) {
|
|
18
26
|
const message = error instanceof Error ? error.message : "Unexpected error occurred";
|
|
@@ -20,7 +28,7 @@ program
|
|
|
20
28
|
process.exitCode = 1;
|
|
21
29
|
}
|
|
22
30
|
})
|
|
23
|
-
.addHelpText("after", "\nExamples:\n reposizer openai/gym\n reposizer
|
|
31
|
+
.addHelpText("after", "\nExamples:\n reposizer openai/gym\n reposizer vercel/next.js facebook/react\n reposizer vercel/next.js facebook/react --sort stars\n reposizer vercel/next.js --analyze\n reposizer org vercel --limit 10\n reposizer --json");
|
|
24
32
|
program
|
|
25
33
|
.command("org")
|
|
26
34
|
.description("Scan repositories in a GitHub organization")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAnalyzePayload = getAnalyzePayload;
|
|
4
|
+
exports.printAnalyzeResult = printAnalyzeResult;
|
|
5
|
+
exports.runAnalyzeCommand = runAnalyzeCommand;
|
|
6
|
+
const tree_1 = require("../services/tree");
|
|
7
|
+
const size_1 = require("../utils/size");
|
|
8
|
+
function aggregateTopDirectories(tree, limit) {
|
|
9
|
+
const dirSizes = {};
|
|
10
|
+
let totalBytes = 0;
|
|
11
|
+
for (const item of tree) {
|
|
12
|
+
if (item.type !== "blob") {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const size = item.size ?? 0;
|
|
16
|
+
const path = item.path;
|
|
17
|
+
totalBytes += size;
|
|
18
|
+
const topDir = path.includes("/") ? path.split("/")[0] : "root";
|
|
19
|
+
dirSizes[topDir] = (dirSizes[topDir] ?? 0) + size;
|
|
20
|
+
}
|
|
21
|
+
const dirs = Object.entries(dirSizes)
|
|
22
|
+
.sort((a, b) => b[1] - a[1])
|
|
23
|
+
.slice(0, limit)
|
|
24
|
+
.map(([directory, bytes]) => ({ directory, bytes }));
|
|
25
|
+
return { totalBytes, dirs };
|
|
26
|
+
}
|
|
27
|
+
async function getAnalyzePayload(owner, repo) {
|
|
28
|
+
const { tree, truncated } = await (0, tree_1.getRepoTree)(owner, repo);
|
|
29
|
+
const { totalBytes, dirs } = aggregateTopDirectories(tree, 10);
|
|
30
|
+
return {
|
|
31
|
+
repository: `${owner}/${repo}`,
|
|
32
|
+
truncated,
|
|
33
|
+
total_bytes: totalBytes,
|
|
34
|
+
top_directories: dirs
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function printAnalyzeResult(payload) {
|
|
38
|
+
if (payload.truncated) {
|
|
39
|
+
console.error("Warning: Git tree response was truncated by GitHub; directory totals may be incomplete.");
|
|
40
|
+
}
|
|
41
|
+
console.log(`Repository: ${payload.repository}`);
|
|
42
|
+
console.log(`Total (approx): ${(0, size_1.formatBytes)(payload.total_bytes)}`);
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log("Top directories:");
|
|
45
|
+
console.log("----------------");
|
|
46
|
+
for (const { directory, bytes } of payload.top_directories) {
|
|
47
|
+
console.log(`${directory.padEnd(15)} ${(0, size_1.formatBytes)(bytes)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function runAnalyzeCommand(owner, repo) {
|
|
51
|
+
const payload = await getAnalyzePayload(owner, repo);
|
|
52
|
+
printAnalyzeResult(payload);
|
|
53
|
+
}
|
|
@@ -4,18 +4,76 @@ exports.runRepoCommand = runRepoCommand;
|
|
|
4
4
|
exports.runRepositoriesCommand = runRepositoriesCommand;
|
|
5
5
|
exports.runOrganizationCommand = runOrganizationCommand;
|
|
6
6
|
const github_1 = require("../services/github");
|
|
7
|
+
const analyze_1 = require("./analyze");
|
|
7
8
|
const git_1 = require("../utils/git");
|
|
8
9
|
const size_1 = require("../utils/size");
|
|
9
|
-
function parseRepository(input) {
|
|
10
|
+
function parseRepository(input, position) {
|
|
10
11
|
const trimmed = input.trim();
|
|
11
12
|
const match = /^([^/\s]+)\/([^/\s]+)$/.exec(trimmed);
|
|
12
13
|
if (!match) {
|
|
13
|
-
throw new Error(
|
|
14
|
+
throw new Error(`Invalid repository at position ${position}: "${input}". Use owner/repo.`);
|
|
14
15
|
}
|
|
15
16
|
return { owner: match[1], repo: match[2] };
|
|
16
17
|
}
|
|
18
|
+
function dedupeParsedArgs(items) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const key = `${item.owner.toLowerCase()}/${item.repo.toLowerCase()}`;
|
|
23
|
+
if (seen.has(key)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seen.add(key);
|
|
27
|
+
out.push(item);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function parseRepositoryArgs(inputs) {
|
|
32
|
+
const parsed = [];
|
|
33
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
34
|
+
const input = inputs[i];
|
|
35
|
+
const { owner, repo } = parseRepository(input, i + 1);
|
|
36
|
+
parsed.push({ input, owner, repo });
|
|
37
|
+
}
|
|
38
|
+
return dedupeParsedArgs(parsed);
|
|
39
|
+
}
|
|
40
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
41
|
+
if (items.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const results = new Array(items.length);
|
|
45
|
+
let nextIndex = 0;
|
|
46
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
47
|
+
const worker = async () => {
|
|
48
|
+
while (true) {
|
|
49
|
+
const index = nextIndex;
|
|
50
|
+
nextIndex += 1;
|
|
51
|
+
if (index >= items.length) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
results[index] = await mapper(items[index], index);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
const REPO_METADATA_CONCURRENCY = 4;
|
|
61
|
+
const ANALYZE_FETCH_CONCURRENCY = 2;
|
|
62
|
+
function sortRepoRows(rows, sort) {
|
|
63
|
+
const copy = [...rows];
|
|
64
|
+
if (sort === "size") {
|
|
65
|
+
copy.sort((a, b) => b.size_mb - a.size_mb || a.repository.localeCompare(b.repository));
|
|
66
|
+
}
|
|
67
|
+
else if (sort === "stars") {
|
|
68
|
+
copy.sort((a, b) => b.stars - a.stars || a.repository.localeCompare(b.repository));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
copy.sort((a, b) => a.repository.localeCompare(b.repository));
|
|
72
|
+
}
|
|
73
|
+
return copy;
|
|
74
|
+
}
|
|
17
75
|
async function runRepoCommand(repositoryInput, jsonOutput) {
|
|
18
|
-
const { owner, repo } = parseRepository(repositoryInput);
|
|
76
|
+
const { owner, repo } = parseRepository(repositoryInput, 1);
|
|
19
77
|
const metadata = await (0, github_1.fetchRepositoryMetadata)(owner, repo);
|
|
20
78
|
if (jsonOutput) {
|
|
21
79
|
const payload = {
|
|
@@ -66,26 +124,51 @@ function buildPayload(repositoryInput, metadata) {
|
|
|
66
124
|
language: metadata.language ?? "Unknown"
|
|
67
125
|
};
|
|
68
126
|
}
|
|
69
|
-
async function runRepositoriesCommand(repositoryInputs, jsonOutput) {
|
|
127
|
+
async function runRepositoriesCommand(repositoryInputs, jsonOutput, analyze = false, sort = "size") {
|
|
70
128
|
const effectiveInputs = repositoryInputs.length > 0
|
|
71
129
|
? repositoryInputs
|
|
72
130
|
: [(0, git_1.detectCurrentRepositoryFromGitRemote)()];
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
const parsedArgs = parseRepositoryArgs(effectiveInputs);
|
|
132
|
+
if (analyze) {
|
|
133
|
+
if (jsonOutput) {
|
|
134
|
+
const payloads = await mapWithConcurrency(parsedArgs, ANALYZE_FETCH_CONCURRENCY, async (item) => (0, analyze_1.getAnalyzePayload)(item.owner, item.repo));
|
|
135
|
+
console.log(JSON.stringify(payloads.length === 1 ? payloads[0] : payloads, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (parsedArgs.length === 1) {
|
|
139
|
+
const only = parsedArgs[0];
|
|
140
|
+
await (0, analyze_1.runAnalyzeCommand)(only.owner, only.repo);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const payloads = await mapWithConcurrency(parsedArgs, ANALYZE_FETCH_CONCURRENCY, async (item) => (0, analyze_1.getAnalyzePayload)(item.owner, item.repo));
|
|
144
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
145
|
+
(0, analyze_1.printAnalyzeResult)(payloads[i]);
|
|
146
|
+
if (i < payloads.length - 1) {
|
|
147
|
+
console.log("");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const results = await mapWithConcurrency(parsedArgs, REPO_METADATA_CONCURRENCY, async (item) => {
|
|
153
|
+
const metadata = await (0, github_1.fetchRepositoryMetadata)(item.owner, item.repo);
|
|
154
|
+
return buildPayload(item.input, metadata);
|
|
155
|
+
});
|
|
156
|
+
const sorted = sortRepoRows(results, sort);
|
|
78
157
|
if (jsonOutput) {
|
|
79
|
-
console.log(JSON.stringify(
|
|
158
|
+
console.log(JSON.stringify(sorted.length === 1 ? sorted[0] : sorted, null, 2));
|
|
80
159
|
return;
|
|
81
160
|
}
|
|
82
|
-
|
|
161
|
+
if (sorted.length === 1) {
|
|
162
|
+
const result = sorted[0];
|
|
83
163
|
console.log(`Repository: ${result.repository}`);
|
|
84
164
|
console.log(`Size: ${(0, size_1.formatSizeFromKb)(result.size_mb * 1024)}`);
|
|
85
165
|
console.log(`Stars: ${(0, size_1.formatStars)(result.stars)}`);
|
|
86
166
|
console.log(`Language: ${result.language}`);
|
|
87
|
-
|
|
167
|
+
return;
|
|
88
168
|
}
|
|
169
|
+
console.log(`Comparing ${sorted.length} repositories (sorted by ${sort}):`);
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(renderOrgTable(sorted));
|
|
89
172
|
}
|
|
90
173
|
async function runOrganizationCommand(organization, jsonOutput, limit) {
|
|
91
174
|
const repositories = await (0, github_1.fetchOrganizationRepositories)(organization, limit);
|
|
@@ -104,5 +187,9 @@ async function runOrganizationCommand(organization, jsonOutput, limit) {
|
|
|
104
187
|
console.log(`Organization: ${organization}`);
|
|
105
188
|
console.log(`Repositories scanned: ${payload.length}`);
|
|
106
189
|
console.log("");
|
|
190
|
+
if (payload.length === 0) {
|
|
191
|
+
console.log("No repositories to show. The organization may have no visible repos, or all of them may be archived or disabled.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
107
194
|
console.log(renderOrgTable(payload));
|
|
108
195
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHeaders = createHeaders;
|
|
4
|
+
exports.getErrorMessage = getErrorMessage;
|
|
3
5
|
exports.fetchRepositoryMetadata = fetchRepositoryMetadata;
|
|
4
6
|
exports.fetchOrganizationRepositories = fetchOrganizationRepositories;
|
|
5
7
|
function createHeaders(token) {
|
|
@@ -45,33 +47,59 @@ async function fetchRepositoryMetadata(owner, repo, token = process.env.GITHUB_T
|
|
|
45
47
|
language: data.language
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let response;
|
|
52
|
-
try {
|
|
53
|
-
response = await fetch(endpoint, {
|
|
54
|
-
method: "GET",
|
|
55
|
-
headers: createHeaders(token),
|
|
56
|
-
signal: AbortSignal.timeout(7000)
|
|
57
|
-
});
|
|
50
|
+
function getOrganizationListErrorMessage(status) {
|
|
51
|
+
if (status === 404) {
|
|
52
|
+
return "Organization not found.";
|
|
58
53
|
}
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
if (status === 401 || status === 403) {
|
|
55
|
+
return "Cannot access this organization’s repositories. If the org or its repos are private, set GITHUB_TOKEN. Otherwise verify rate limits and permissions.";
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
return getErrorMessage(status);
|
|
58
|
+
}
|
|
59
|
+
async function fetchOrganizationRepositories(org, limit = 30, token = process.env.GITHUB_TOKEN) {
|
|
60
|
+
const safeLimit = Math.max(1, Math.min(limit, 100));
|
|
61
|
+
const collected = [];
|
|
62
|
+
const perPage = 100;
|
|
63
|
+
let page = 1;
|
|
64
|
+
const maxPages = 25;
|
|
65
|
+
while (collected.length < safeLimit && page <= maxPages) {
|
|
66
|
+
const endpoint = `https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=${perPage}&page=${page}&sort=updated&direction=desc`;
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetch(endpoint, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers: createHeaders(token),
|
|
72
|
+
signal: AbortSignal.timeout(7000)
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
|
-
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error("Unable to reach GitHub API. Check your network and try again.");
|
|
77
|
+
}
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(getOrganizationListErrorMessage(response.status));
|
|
80
|
+
}
|
|
81
|
+
const data = (await response.json());
|
|
82
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
for (const repo of data) {
|
|
86
|
+
if (repo.archived || repo.disabled) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
collected.push({
|
|
90
|
+
fullName: repo.full_name,
|
|
91
|
+
sizeKb: repo.size,
|
|
92
|
+
stargazersCount: repo.stargazers_count,
|
|
93
|
+
language: repo.language
|
|
94
|
+
});
|
|
95
|
+
if (collected.length >= safeLimit) {
|
|
96
|
+
return collected;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (data.length < perPage) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
page += 1;
|
|
67
103
|
}
|
|
68
|
-
|
|
69
|
-
return data
|
|
70
|
-
.filter((repo) => !repo.archived && !repo.disabled)
|
|
71
|
-
.map((repo) => ({
|
|
72
|
-
fullName: repo.full_name,
|
|
73
|
-
sizeKb: repo.size,
|
|
74
|
-
stargazersCount: repo.stargazers_count,
|
|
75
|
-
language: repo.language
|
|
76
|
-
}));
|
|
104
|
+
return collected;
|
|
77
105
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRepoTree = getRepoTree;
|
|
4
|
+
const github_1 = require("./github");
|
|
5
|
+
const BASE = "https://api.github.com";
|
|
6
|
+
async function getRepoTree(owner, repo, token = process.env.GITHUB_TOKEN) {
|
|
7
|
+
const repoEndpoint = `${BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
8
|
+
let repoResponse;
|
|
9
|
+
try {
|
|
10
|
+
repoResponse = await fetch(repoEndpoint, {
|
|
11
|
+
method: "GET",
|
|
12
|
+
headers: (0, github_1.createHeaders)(token),
|
|
13
|
+
signal: AbortSignal.timeout(5000)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error("Unable to reach GitHub API. Check your network and try again.");
|
|
18
|
+
}
|
|
19
|
+
if (!repoResponse.ok) {
|
|
20
|
+
throw new Error((0, github_1.getErrorMessage)(repoResponse.status));
|
|
21
|
+
}
|
|
22
|
+
const repoData = (await repoResponse.json());
|
|
23
|
+
const branch = repoData.default_branch;
|
|
24
|
+
if (!branch) {
|
|
25
|
+
throw new Error("GitHub API response missing default branch.");
|
|
26
|
+
}
|
|
27
|
+
const treeEndpoint = `${BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
|
|
28
|
+
let treeResponse;
|
|
29
|
+
try {
|
|
30
|
+
treeResponse = await fetch(treeEndpoint, {
|
|
31
|
+
method: "GET",
|
|
32
|
+
headers: (0, github_1.createHeaders)(token),
|
|
33
|
+
signal: AbortSignal.timeout(30_000)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("Unable to reach GitHub API. Check your network and try again.");
|
|
38
|
+
}
|
|
39
|
+
if (!treeResponse.ok) {
|
|
40
|
+
throw new Error((0, github_1.getErrorMessage)(treeResponse.status));
|
|
41
|
+
}
|
|
42
|
+
const treeData = (await treeResponse.json());
|
|
43
|
+
return {
|
|
44
|
+
tree: treeData.tree ?? [],
|
|
45
|
+
truncated: Boolean(treeData.truncated)
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/src/utils/git.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectCurrentRepositoryFromGitRemote = detectCurrentRepositoryFromGitRemote;
|
|
4
|
-
const
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
5
6
|
function parseRemoteToOwnerRepo(remoteUrl) {
|
|
6
7
|
const normalized = remoteUrl.trim().replace(/\.git$/, "");
|
|
7
8
|
const sshMatch = /^git@github\.com:([^/]+)\/([^/]+)$/.exec(normalized);
|
|
@@ -12,23 +13,80 @@ function parseRemoteToOwnerRepo(remoteUrl) {
|
|
|
12
13
|
if (httpsMatch) {
|
|
13
14
|
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
14
15
|
}
|
|
16
|
+
const sshProtocolMatch = /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+)$/.exec(normalized);
|
|
17
|
+
if (sshProtocolMatch) {
|
|
18
|
+
return `${sshProtocolMatch[1]}/${sshProtocolMatch[2]}`;
|
|
19
|
+
}
|
|
15
20
|
return null;
|
|
16
21
|
}
|
|
22
|
+
function findGitDirectory(startDir) {
|
|
23
|
+
let current = (0, node_path_1.resolve)(startDir);
|
|
24
|
+
while (true) {
|
|
25
|
+
const dotGitPath = (0, node_path_1.join)(current, ".git");
|
|
26
|
+
if ((0, node_fs_1.existsSync)(dotGitPath)) {
|
|
27
|
+
const stats = (0, node_fs_1.statSync)(dotGitPath);
|
|
28
|
+
if (stats.isDirectory()) {
|
|
29
|
+
return dotGitPath;
|
|
30
|
+
}
|
|
31
|
+
if (stats.isFile()) {
|
|
32
|
+
const fileContent = (0, node_fs_1.readFileSync)(dotGitPath, "utf8");
|
|
33
|
+
const match = /^\s*gitdir:\s*(.+)\s*$/im.exec(fileContent);
|
|
34
|
+
if (match?.[1]) {
|
|
35
|
+
return (0, node_path_1.resolve)(current, match[1].trim());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const parent = (0, node_path_1.dirname)(current);
|
|
40
|
+
if (parent === current) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function getOriginRemoteUrlFromConfig(configPath) {
|
|
47
|
+
const configContent = (0, node_fs_1.readFileSync)(configPath, "utf8");
|
|
48
|
+
const lines = configContent.split(/\r?\n/);
|
|
49
|
+
let inOriginSection = false;
|
|
50
|
+
const originUrls = [];
|
|
51
|
+
for (const rawLine of lines) {
|
|
52
|
+
const line = rawLine.trim();
|
|
53
|
+
if (line.length === 0 || line.startsWith(";") || line.startsWith("#")) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const sectionMatch = /^\[(.+)\]$/.exec(line);
|
|
57
|
+
if (sectionMatch) {
|
|
58
|
+
inOriginSection = sectionMatch[1]?.trim() === 'remote "origin"';
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!inOriginSection) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const keyValueMatch = /^([A-Za-z0-9\-.]+)\s*=\s*(.+)$/.exec(line);
|
|
65
|
+
if (keyValueMatch?.[1] === "url") {
|
|
66
|
+
originUrls.push(keyValueMatch[2].trim());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (originUrls.length === 0) {
|
|
70
|
+
throw new Error("No repository argument provided and no remote \"origin\" URL was found in .git/config.");
|
|
71
|
+
}
|
|
72
|
+
if (originUrls.length > 1) {
|
|
73
|
+
throw new Error("Multiple remote \"origin\" URLs were found in .git/config. Pass owner/repo explicitly.");
|
|
74
|
+
}
|
|
75
|
+
return originUrls[0];
|
|
76
|
+
}
|
|
17
77
|
function detectCurrentRepositoryFromGitRemote() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
encoding: "utf8",
|
|
22
|
-
timeout: 2000,
|
|
23
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
24
|
-
}).trim();
|
|
78
|
+
const gitDir = findGitDirectory(process.cwd());
|
|
79
|
+
if (!gitDir) {
|
|
80
|
+
throw new Error("No repository argument provided and no .git directory was found in this path.");
|
|
25
81
|
}
|
|
26
|
-
|
|
27
|
-
|
|
82
|
+
const configPath = (0, node_path_1.join)(gitDir, "config");
|
|
83
|
+
if (!(0, node_fs_1.existsSync)(configPath)) {
|
|
84
|
+
throw new Error("Found .git directory but no config file. Pass owner/repo explicitly.");
|
|
28
85
|
}
|
|
86
|
+
const remoteUrl = getOriginRemoteUrlFromConfig(configPath);
|
|
29
87
|
const ownerRepo = parseRemoteToOwnerRepo(remoteUrl);
|
|
30
88
|
if (!ownerRepo) {
|
|
31
|
-
throw new Error("Unable to parse
|
|
89
|
+
throw new Error("Unable to parse origin remote from .git/config. Expected a GitHub URL.");
|
|
32
90
|
}
|
|
33
91
|
return ownerRepo;
|
|
34
92
|
}
|
package/dist/src/utils/size.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatBytes = formatBytes;
|
|
3
4
|
exports.formatSizeFromKb = formatSizeFromKb;
|
|
4
5
|
exports.kbToMbRounded = kbToMbRounded;
|
|
5
6
|
exports.formatStars = formatStars;
|
|
7
|
+
function formatBytes(bytes) {
|
|
8
|
+
const mb = bytes / (1024 * 1024);
|
|
9
|
+
const gb = mb / 1024;
|
|
10
|
+
if (gb >= 1) {
|
|
11
|
+
return `${gb.toFixed(2)} GB`;
|
|
12
|
+
}
|
|
13
|
+
if (mb >= 1) {
|
|
14
|
+
return `${mb.toFixed(2)} MB`;
|
|
15
|
+
}
|
|
16
|
+
if (bytes >= 1024) {
|
|
17
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
18
|
+
}
|
|
19
|
+
return `${bytes} B`;
|
|
20
|
+
}
|
|
6
21
|
function formatSizeFromKb(sizeKb) {
|
|
7
22
|
const sizeMb = sizeKb / 1024;
|
|
8
23
|
if (sizeMb < 1024) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reposizer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Fast CLI to inspect GitHub repository sizes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hanif",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
21
|
"dev": "tsx bin/reposizer.ts",
|
|
22
|
-
"check": "tsc --noEmit -p tsconfig.json"
|
|
22
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
23
|
+
"video": "remotion studio remotion/index.js",
|
|
24
|
+
"video:render": "remotion render remotion/index.js ReposizerIntro out/reposizer-intro.mp4"
|
|
23
25
|
},
|
|
24
26
|
"keywords": [
|
|
25
27
|
"cli",
|
|
@@ -29,7 +31,11 @@
|
|
|
29
31
|
"typescript"
|
|
30
32
|
],
|
|
31
33
|
"dependencies": {
|
|
32
|
-
"
|
|
34
|
+
"@remotion/cli": "^4.0.452",
|
|
35
|
+
"commander": "^12.1.0",
|
|
36
|
+
"react": "^19.2.5",
|
|
37
|
+
"react-dom": "^19.2.5",
|
|
38
|
+
"remotion": "^4.0.452"
|
|
33
39
|
},
|
|
34
40
|
"devDependencies": {
|
|
35
41
|
"@types/node": "^22.13.10",
|
|
@@ -47,5 +53,5 @@
|
|
|
47
53
|
"bugs": {
|
|
48
54
|
"url": "https://github.com/Hanif-adedotun/reposizer/issues"
|
|
49
55
|
},
|
|
50
|
-
"homepage": "https://reposizer.hanif.one
|
|
56
|
+
"homepage": "https://reposizer.hanif.one"
|
|
51
57
|
}
|