reposizer 0.1.2 → 0.2.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 CHANGED
@@ -24,9 +24,10 @@ Language: C
24
24
  - 📦 Works with `npx` (no global install required)
25
25
  - 🧩 JSON output for scripts and CI tooling
26
26
  - 🏢 Organization repository scanning
27
- - 🔎 Current repository auto-detection (via git remote)
27
+ - 🔎 Current repository auto-detection (via `.git/config`)
28
28
  - 📚 Multi-repository lookup in one command
29
- - 📊 Directory analysis (planned)
29
+ - 📊 Directory analysis (no clone)
30
+ - 🧮 Approximate LOC analysis (no clone)
30
31
 
31
32
  ## Installation
32
33
 
@@ -54,6 +55,7 @@ reposizer openai/gym
54
55
 
55
56
  ```bash
56
57
  reposizer openai/gym vercel/next.js torvalds/linux
58
+ reposizer openai/gym vercel/next.js --sort stars
57
59
  ```
58
60
 
59
61
  ### Detect current repository automatically
@@ -69,6 +71,19 @@ reposizer org openai
69
71
  reposizer org openai --limit 50 --json
70
72
  ```
71
73
 
74
+ ### Analyze top directories (no clone)
75
+
76
+ ```bash
77
+ reposizer vercel/next.js --analyze
78
+ ```
79
+
80
+ ### Estimate lines of code (no clone)
81
+
82
+ ```bash
83
+ reposizer vercel/next.js --loc
84
+ reposizer vercel/next.js --loc --json
85
+ ```
86
+
72
87
  ### JSON output
73
88
 
74
89
  ```bash
@@ -98,7 +113,8 @@ reposizer your-org/private-repo
98
113
  - [x] Organization scanning
99
114
  - [x] Current repository auto-detection
100
115
  - [x] Multi-repository support
101
- - [ ] Directory size analysis
116
+ - [x] Directory size analysis
117
+ - [x] Approximate LOC analysis
102
118
  - [ ] CI/CD threshold mode
103
119
  - [ ] Repository growth tracking
104
120
 
@@ -10,9 +10,18 @@ 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("--loc", "Approximate lines of code using Git tree metadata (no full clone)")
15
+ .option("--sort <field>", "When comparing multiple repositories: sort by size, stars, or name", "size")
13
16
  .action(async (repositories = [], options) => {
14
17
  try {
15
- await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json));
18
+ const sortRaw = options.sort ?? "size";
19
+ if (sortRaw !== "size" &&
20
+ sortRaw !== "stars" &&
21
+ sortRaw !== "name") {
22
+ throw new Error('Invalid --sort. Use "size", "stars", or "name".');
23
+ }
24
+ await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json), Boolean(options.analyze), Boolean(options.loc), sortRaw);
16
25
  }
17
26
  catch (error) {
18
27
  const message = error instanceof Error ? error.message : "Unexpected error occurred";
@@ -20,7 +29,7 @@ program
20
29
  process.exitCode = 1;
21
30
  }
22
31
  })
23
- .addHelpText("after", "\nExamples:\n reposizer openai/gym\n reposizer openai/gym vercel/next.js\n reposizer --json");
32
+ .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 vercel/next.js --loc\n reposizer org vercel --limit 10\n reposizer --json");
24
33
  program
25
34
  .command("org")
26
35
  .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
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLocPayload = getLocPayload;
4
+ exports.printLocResult = printLocResult;
5
+ exports.runLocCommand = runLocCommand;
6
+ const tree_1 = require("../services/tree");
7
+ const IGNORED_SEGMENTS = new Set([
8
+ "node_modules",
9
+ "dist",
10
+ "build",
11
+ ".next",
12
+ "coverage",
13
+ ".git"
14
+ ]);
15
+ const EXTENSION_TO_LANGUAGE = {
16
+ ts: "TypeScript",
17
+ tsx: "TypeScript",
18
+ js: "JavaScript",
19
+ jsx: "JavaScript",
20
+ mjs: "JavaScript",
21
+ cjs: "JavaScript",
22
+ py: "Python",
23
+ rb: "Ruby",
24
+ php: "PHP",
25
+ java: "Java",
26
+ kt: "Kotlin",
27
+ rs: "Rust",
28
+ go: "Go",
29
+ c: "C",
30
+ h: "C",
31
+ cc: "C++",
32
+ cpp: "C++",
33
+ cxx: "C++",
34
+ hpp: "C++",
35
+ cs: "C#",
36
+ swift: "Swift",
37
+ scala: "Scala",
38
+ r: "R",
39
+ sh: "Shell",
40
+ bash: "Shell",
41
+ zsh: "Shell",
42
+ ps1: "PowerShell",
43
+ sql: "SQL",
44
+ html: "HTML",
45
+ css: "CSS",
46
+ scss: "SCSS",
47
+ less: "Less",
48
+ vue: "Vue",
49
+ svelte: "Svelte",
50
+ json: "JSON",
51
+ yml: "YAML",
52
+ yaml: "YAML",
53
+ toml: "TOML",
54
+ md: "Markdown",
55
+ mdx: "Markdown",
56
+ txt: "Text"
57
+ };
58
+ const AVG_BYTES_PER_LINE = {
59
+ TypeScript: 33,
60
+ JavaScript: 34,
61
+ Python: 28,
62
+ Ruby: 30,
63
+ PHP: 32,
64
+ Java: 36,
65
+ Kotlin: 36,
66
+ Rust: 34,
67
+ Go: 31,
68
+ C: 30,
69
+ "C++": 33,
70
+ "C#": 35,
71
+ Swift: 35,
72
+ Scala: 37,
73
+ R: 29,
74
+ Shell: 27,
75
+ PowerShell: 32,
76
+ SQL: 36,
77
+ HTML: 40,
78
+ CSS: 34,
79
+ SCSS: 36,
80
+ Less: 35,
81
+ Vue: 38,
82
+ Svelte: 36,
83
+ JSON: 48,
84
+ YAML: 42,
85
+ TOML: 32,
86
+ Markdown: 52,
87
+ Text: 62,
88
+ Other: 38
89
+ };
90
+ function shouldIgnore(path) {
91
+ const segments = path.split("/");
92
+ return segments.some((segment) => IGNORED_SEGMENTS.has(segment));
93
+ }
94
+ function getLanguageFromPath(path) {
95
+ const fileName = path.split("/").pop() ?? path;
96
+ if (fileName.endsWith(".min.js") || fileName.endsWith(".min.css")) {
97
+ return "Other";
98
+ }
99
+ const ext = fileName.includes(".") ? fileName.split(".").pop() ?? "" : "";
100
+ if (!ext) {
101
+ return "Other";
102
+ }
103
+ return EXTENSION_TO_LANGUAGE[ext.toLowerCase()] ?? "Other";
104
+ }
105
+ function estimateLines(language, sizeBytes) {
106
+ const divisor = AVG_BYTES_PER_LINE[language] ?? AVG_BYTES_PER_LINE.Other;
107
+ return Math.max(1, Math.round(sizeBytes / divisor));
108
+ }
109
+ function toSortedEntries(record, limit) {
110
+ return Object.entries(record)
111
+ .sort((a, b) => b[1] - a[1])
112
+ .slice(0, limit)
113
+ .map(([key, value]) => ({ key, value }));
114
+ }
115
+ async function getLocPayload(owner, repo) {
116
+ const { tree, truncated } = await (0, tree_1.getRepoTree)(owner, repo);
117
+ const byLanguage = {};
118
+ const byDirectory = {};
119
+ let totalLines = 0;
120
+ for (const item of tree) {
121
+ if (item.type !== "blob") {
122
+ continue;
123
+ }
124
+ const path = item.path;
125
+ if (shouldIgnore(path)) {
126
+ continue;
127
+ }
128
+ const sizeBytes = item.size ?? 0;
129
+ if (sizeBytes === 0) {
130
+ continue;
131
+ }
132
+ const language = getLanguageFromPath(path);
133
+ const lines = estimateLines(language, sizeBytes);
134
+ totalLines += lines;
135
+ byLanguage[language] = (byLanguage[language] ?? 0) + lines;
136
+ const topDir = path.includes("/") ? path.split("/")[0] : "root";
137
+ byDirectory[topDir] = (byDirectory[topDir] ?? 0) + lines;
138
+ }
139
+ const topLanguages = toSortedEntries(byLanguage, 10).map(({ key, value }) => ({
140
+ language: key,
141
+ lines: value
142
+ }));
143
+ const topDirectories = toSortedEntries(byDirectory, 10).map(({ key, value }) => ({
144
+ directory: key,
145
+ lines: value
146
+ }));
147
+ return {
148
+ repository: `${owner}/${repo}`,
149
+ approx: true,
150
+ truncated,
151
+ total_lines: totalLines,
152
+ by_language: topLanguages,
153
+ by_directory: topDirectories,
154
+ method: "size-based-estimate"
155
+ };
156
+ }
157
+ function printLocResult(payload) {
158
+ const formatLineCount = (value) => new Intl.NumberFormat("en-US").format(value);
159
+ if (payload.truncated) {
160
+ console.error("Warning: Git tree response was truncated by GitHub; LOC totals may be incomplete.");
161
+ }
162
+ console.log(`Repository: ${payload.repository}`);
163
+ console.log(`Total LOC (approx): ${formatLineCount(payload.total_lines)}`);
164
+ console.log("");
165
+ console.log("Top languages:");
166
+ console.log("--------------");
167
+ for (const row of payload.by_language) {
168
+ console.log(`${row.language.padEnd(15)} ${formatLineCount(row.lines)}`);
169
+ }
170
+ console.log("");
171
+ console.log("Top directories:");
172
+ console.log("----------------");
173
+ for (const row of payload.by_directory) {
174
+ console.log(`${row.directory.padEnd(15)} ${formatLineCount(row.lines)}`);
175
+ }
176
+ }
177
+ async function runLocCommand(owner, repo) {
178
+ const payload = await getLocPayload(owner, repo);
179
+ printLocResult(payload);
180
+ }
@@ -4,18 +4,78 @@ 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");
8
+ const loc_1 = require("./loc");
7
9
  const git_1 = require("../utils/git");
8
10
  const size_1 = require("../utils/size");
9
- function parseRepository(input) {
11
+ function parseRepository(input, position) {
10
12
  const trimmed = input.trim();
11
13
  const match = /^([^/\s]+)\/([^/\s]+)$/.exec(trimmed);
12
14
  if (!match) {
13
- throw new Error("Invalid repository format. Use owner/repo.");
15
+ throw new Error(`Invalid repository at position ${position}: "${input}". Use owner/repo.`);
14
16
  }
15
17
  return { owner: match[1], repo: match[2] };
16
18
  }
19
+ function dedupeParsedArgs(items) {
20
+ const seen = new Set();
21
+ const out = [];
22
+ for (const item of items) {
23
+ const key = `${item.owner.toLowerCase()}/${item.repo.toLowerCase()}`;
24
+ if (seen.has(key)) {
25
+ continue;
26
+ }
27
+ seen.add(key);
28
+ out.push(item);
29
+ }
30
+ return out;
31
+ }
32
+ function parseRepositoryArgs(inputs) {
33
+ const parsed = [];
34
+ for (let i = 0; i < inputs.length; i++) {
35
+ const input = inputs[i];
36
+ const { owner, repo } = parseRepository(input, i + 1);
37
+ parsed.push({ input, owner, repo });
38
+ }
39
+ return dedupeParsedArgs(parsed);
40
+ }
41
+ async function mapWithConcurrency(items, concurrency, mapper) {
42
+ if (items.length === 0) {
43
+ return [];
44
+ }
45
+ const results = new Array(items.length);
46
+ let nextIndex = 0;
47
+ const workerCount = Math.min(concurrency, items.length);
48
+ const worker = async () => {
49
+ while (true) {
50
+ const index = nextIndex;
51
+ nextIndex += 1;
52
+ if (index >= items.length) {
53
+ return;
54
+ }
55
+ results[index] = await mapper(items[index], index);
56
+ }
57
+ };
58
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
59
+ return results;
60
+ }
61
+ const REPO_METADATA_CONCURRENCY = 4;
62
+ const ANALYZE_FETCH_CONCURRENCY = 2;
63
+ const LOC_FETCH_CONCURRENCY = 2;
64
+ function sortRepoRows(rows, sort) {
65
+ const copy = [...rows];
66
+ if (sort === "size") {
67
+ copy.sort((a, b) => b.size_mb - a.size_mb || a.repository.localeCompare(b.repository));
68
+ }
69
+ else if (sort === "stars") {
70
+ copy.sort((a, b) => b.stars - a.stars || a.repository.localeCompare(b.repository));
71
+ }
72
+ else {
73
+ copy.sort((a, b) => a.repository.localeCompare(b.repository));
74
+ }
75
+ return copy;
76
+ }
17
77
  async function runRepoCommand(repositoryInput, jsonOutput) {
18
- const { owner, repo } = parseRepository(repositoryInput);
78
+ const { owner, repo } = parseRepository(repositoryInput, 1);
19
79
  const metadata = await (0, github_1.fetchRepositoryMetadata)(owner, repo);
20
80
  if (jsonOutput) {
21
81
  const payload = {
@@ -29,6 +89,8 @@ async function runRepoCommand(repositoryInput, jsonOutput) {
29
89
  }
30
90
  console.log(`Repository: ${metadata.fullName}`);
31
91
  console.log(`Size: ${(0, size_1.formatSizeFromKb)(metadata.sizeKb)}`);
92
+ const loc = await (0, loc_1.getLocPayload)(owner, repo);
93
+ console.log(`Lines: ${(0, size_1.formatCompactCount)(loc.total_lines)}`);
32
94
  console.log(`Stars: ${(0, size_1.formatStars)(metadata.stargazersCount)}`);
33
95
  console.log(`Language: ${metadata.language ?? "Unknown"}`);
34
96
  }
@@ -66,26 +128,77 @@ function buildPayload(repositoryInput, metadata) {
66
128
  language: metadata.language ?? "Unknown"
67
129
  };
68
130
  }
69
- async function runRepositoriesCommand(repositoryInputs, jsonOutput) {
131
+ async function runRepositoriesCommand(repositoryInputs, jsonOutput, analyze = false, loc = false, sort = "size") {
132
+ if (analyze && loc) {
133
+ throw new Error('Use either "--analyze" or "--loc", not both together.');
134
+ }
70
135
  const effectiveInputs = repositoryInputs.length > 0
71
136
  ? repositoryInputs
72
137
  : [(0, git_1.detectCurrentRepositoryFromGitRemote)()];
73
- const results = await Promise.all(effectiveInputs.map(async (input) => {
74
- const { owner, repo } = parseRepository(input);
75
- const metadata = await (0, github_1.fetchRepositoryMetadata)(owner, repo);
76
- return buildPayload(input, metadata);
77
- }));
138
+ const parsedArgs = parseRepositoryArgs(effectiveInputs);
139
+ if (analyze) {
140
+ if (jsonOutput) {
141
+ const payloads = await mapWithConcurrency(parsedArgs, ANALYZE_FETCH_CONCURRENCY, async (item) => (0, analyze_1.getAnalyzePayload)(item.owner, item.repo));
142
+ console.log(JSON.stringify(payloads.length === 1 ? payloads[0] : payloads, null, 2));
143
+ return;
144
+ }
145
+ if (parsedArgs.length === 1) {
146
+ const only = parsedArgs[0];
147
+ await (0, analyze_1.runAnalyzeCommand)(only.owner, only.repo);
148
+ return;
149
+ }
150
+ const payloads = await mapWithConcurrency(parsedArgs, ANALYZE_FETCH_CONCURRENCY, async (item) => (0, analyze_1.getAnalyzePayload)(item.owner, item.repo));
151
+ for (let i = 0; i < payloads.length; i++) {
152
+ (0, analyze_1.printAnalyzeResult)(payloads[i]);
153
+ if (i < payloads.length - 1) {
154
+ console.log("");
155
+ }
156
+ }
157
+ return;
158
+ }
159
+ if (loc) {
160
+ if (jsonOutput) {
161
+ const payloads = await mapWithConcurrency(parsedArgs, LOC_FETCH_CONCURRENCY, async (item) => (0, loc_1.getLocPayload)(item.owner, item.repo));
162
+ console.log(JSON.stringify(payloads.length === 1 ? payloads[0] : payloads, null, 2));
163
+ return;
164
+ }
165
+ if (parsedArgs.length === 1) {
166
+ const only = parsedArgs[0];
167
+ await (0, loc_1.runLocCommand)(only.owner, only.repo);
168
+ return;
169
+ }
170
+ const payloads = await mapWithConcurrency(parsedArgs, LOC_FETCH_CONCURRENCY, async (item) => (0, loc_1.getLocPayload)(item.owner, item.repo));
171
+ for (let i = 0; i < payloads.length; i++) {
172
+ (0, loc_1.printLocResult)(payloads[i]);
173
+ if (i < payloads.length - 1) {
174
+ console.log("");
175
+ }
176
+ }
177
+ return;
178
+ }
179
+ const results = await mapWithConcurrency(parsedArgs, REPO_METADATA_CONCURRENCY, async (item) => {
180
+ const metadata = await (0, github_1.fetchRepositoryMetadata)(item.owner, item.repo);
181
+ return buildPayload(item.input, metadata);
182
+ });
183
+ const sorted = sortRepoRows(results, sort);
78
184
  if (jsonOutput) {
79
- console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
185
+ console.log(JSON.stringify(sorted.length === 1 ? sorted[0] : sorted, null, 2));
80
186
  return;
81
187
  }
82
- for (const result of results) {
188
+ if (sorted.length === 1) {
189
+ const result = sorted[0];
190
+ const only = parsedArgs[0];
191
+ const loc = await (0, loc_1.getLocPayload)(only.owner, only.repo);
83
192
  console.log(`Repository: ${result.repository}`);
84
193
  console.log(`Size: ${(0, size_1.formatSizeFromKb)(result.size_mb * 1024)}`);
194
+ console.log(`Lines: ${(0, size_1.formatCompactCount)(loc.total_lines)} `);
85
195
  console.log(`Stars: ${(0, size_1.formatStars)(result.stars)}`);
86
196
  console.log(`Language: ${result.language}`);
87
- console.log("");
197
+ return;
88
198
  }
199
+ console.log(`Comparing ${sorted.length} repositories (sorted by ${sort}):`);
200
+ console.log("");
201
+ console.log(renderOrgTable(sorted));
89
202
  }
90
203
  async function runOrganizationCommand(organization, jsonOutput, limit) {
91
204
  const repositories = await (0, github_1.fetchOrganizationRepositories)(organization, limit);
@@ -104,5 +217,9 @@ async function runOrganizationCommand(organization, jsonOutput, limit) {
104
217
  console.log(`Organization: ${organization}`);
105
218
  console.log(`Repositories scanned: ${payload.length}`);
106
219
  console.log("");
220
+ if (payload.length === 0) {
221
+ console.log("No repositories to show. The organization may have no visible repos, or all of them may be archived or disabled.");
222
+ return;
223
+ }
107
224
  console.log(renderOrgTable(payload));
108
225
  }
@@ -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
- async function fetchOrganizationRepositories(org, limit = 30, token = process.env.GITHUB_TOKEN) {
49
- const safeLimit = Math.max(1, Math.min(limit, 100));
50
- const endpoint = `https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=${safeLimit}&sort=updated&direction=desc`;
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
- catch {
60
- throw new Error("Unable to reach GitHub API. Check your network and try again.");
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
- if (!response.ok) {
63
- if (response.status === 404) {
64
- throw new Error("Organization not found.");
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
- throw new Error(getErrorMessage(response.status));
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
- const data = (await response.json());
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
+ }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectCurrentRepositoryFromGitRemote = detectCurrentRepositoryFromGitRemote;
4
- const node_child_process_1 = require("node:child_process");
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
- let remoteUrl = "";
19
- try {
20
- remoteUrl = (0, node_child_process_1.execFileSync)("git", ["remote", "get-url", "origin"], {
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
- catch {
27
- throw new Error("No repository argument provided and no git origin remote was detected.");
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 git remote. Expected a GitHub origin remote URL.");
89
+ throw new Error("Unable to parse origin remote from .git/config. Expected a GitHub URL.");
32
90
  }
33
91
  return ownerRepo;
34
92
  }
@@ -1,8 +1,24 @@
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
+ exports.formatCompactCount = formatCompactCount;
8
+ function formatBytes(bytes) {
9
+ const mb = bytes / (1024 * 1024);
10
+ const gb = mb / 1024;
11
+ if (gb >= 1) {
12
+ return `${gb.toFixed(2)} GB`;
13
+ }
14
+ if (mb >= 1) {
15
+ return `${mb.toFixed(2)} MB`;
16
+ }
17
+ if (bytes >= 1024) {
18
+ return `${(bytes / 1024).toFixed(2)} KB`;
19
+ }
20
+ return `${bytes} B`;
21
+ }
6
22
  function formatSizeFromKb(sizeKb) {
7
23
  const sizeMb = sizeKb / 1024;
8
24
  if (sizeMb < 1024) {
@@ -20,3 +36,15 @@ function formatStars(value) {
20
36
  }
21
37
  return String(value);
22
38
  }
39
+ function formatCompactCount(value) {
40
+ if (value >= 1_000_000_000) {
41
+ return `${(value / 1_000_000_000).toFixed(1)}B`;
42
+ }
43
+ if (value >= 1_000_000) {
44
+ return `${(value / 1_000_000).toFixed(1)}M`;
45
+ }
46
+ if (value >= 1_000) {
47
+ return `${(value / 1_000).toFixed(1)}K`;
48
+ }
49
+ return String(value);
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reposizer",
3
- "version": "0.1.2",
3
+ "version": "0.2.2",
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
- "commander": "^12.1.0"
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",