reposizer 0.2.1 → 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
 
@@ -11,6 +11,7 @@ program
11
11
  .argument("[repositories...]", "One or more repositories in owner/repo format")
12
12
  .option("--json", "Return machine-readable JSON output")
13
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)")
14
15
  .option("--sort <field>", "When comparing multiple repositories: sort by size, stars, or name", "size")
15
16
  .action(async (repositories = [], options) => {
16
17
  try {
@@ -20,7 +21,7 @@ program
20
21
  sortRaw !== "name") {
21
22
  throw new Error('Invalid --sort. Use "size", "stars", or "name".');
22
23
  }
23
- await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json), Boolean(options.analyze), sortRaw);
24
+ await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json), Boolean(options.analyze), Boolean(options.loc), sortRaw);
24
25
  }
25
26
  catch (error) {
26
27
  const message = error instanceof Error ? error.message : "Unexpected error occurred";
@@ -28,7 +29,7 @@ program
28
29
  process.exitCode = 1;
29
30
  }
30
31
  })
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");
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");
32
33
  program
33
34
  .command("org")
34
35
  .description("Scan repositories in a GitHub organization")
@@ -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
+ }
@@ -5,6 +5,7 @@ exports.runRepositoriesCommand = runRepositoriesCommand;
5
5
  exports.runOrganizationCommand = runOrganizationCommand;
6
6
  const github_1 = require("../services/github");
7
7
  const analyze_1 = require("./analyze");
8
+ const loc_1 = require("./loc");
8
9
  const git_1 = require("../utils/git");
9
10
  const size_1 = require("../utils/size");
10
11
  function parseRepository(input, position) {
@@ -59,6 +60,7 @@ async function mapWithConcurrency(items, concurrency, mapper) {
59
60
  }
60
61
  const REPO_METADATA_CONCURRENCY = 4;
61
62
  const ANALYZE_FETCH_CONCURRENCY = 2;
63
+ const LOC_FETCH_CONCURRENCY = 2;
62
64
  function sortRepoRows(rows, sort) {
63
65
  const copy = [...rows];
64
66
  if (sort === "size") {
@@ -87,6 +89,8 @@ async function runRepoCommand(repositoryInput, jsonOutput) {
87
89
  }
88
90
  console.log(`Repository: ${metadata.fullName}`);
89
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)}`);
90
94
  console.log(`Stars: ${(0, size_1.formatStars)(metadata.stargazersCount)}`);
91
95
  console.log(`Language: ${metadata.language ?? "Unknown"}`);
92
96
  }
@@ -124,7 +128,10 @@ function buildPayload(repositoryInput, metadata) {
124
128
  language: metadata.language ?? "Unknown"
125
129
  };
126
130
  }
127
- async function runRepositoriesCommand(repositoryInputs, jsonOutput, analyze = false, sort = "size") {
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
+ }
128
135
  const effectiveInputs = repositoryInputs.length > 0
129
136
  ? repositoryInputs
130
137
  : [(0, git_1.detectCurrentRepositoryFromGitRemote)()];
@@ -149,6 +156,26 @@ async function runRepositoriesCommand(repositoryInputs, jsonOutput, analyze = fa
149
156
  }
150
157
  return;
151
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
+ }
152
179
  const results = await mapWithConcurrency(parsedArgs, REPO_METADATA_CONCURRENCY, async (item) => {
153
180
  const metadata = await (0, github_1.fetchRepositoryMetadata)(item.owner, item.repo);
154
181
  return buildPayload(item.input, metadata);
@@ -160,8 +187,11 @@ async function runRepositoriesCommand(repositoryInputs, jsonOutput, analyze = fa
160
187
  }
161
188
  if (sorted.length === 1) {
162
189
  const result = sorted[0];
190
+ const only = parsedArgs[0];
191
+ const loc = await (0, loc_1.getLocPayload)(only.owner, only.repo);
163
192
  console.log(`Repository: ${result.repository}`);
164
193
  console.log(`Size: ${(0, size_1.formatSizeFromKb)(result.size_mb * 1024)}`);
194
+ console.log(`Lines: ${(0, size_1.formatCompactCount)(loc.total_lines)} `);
165
195
  console.log(`Stars: ${(0, size_1.formatStars)(result.stars)}`);
166
196
  console.log(`Language: ${result.language}`);
167
197
  return;
@@ -4,6 +4,7 @@ exports.formatBytes = formatBytes;
4
4
  exports.formatSizeFromKb = formatSizeFromKb;
5
5
  exports.kbToMbRounded = kbToMbRounded;
6
6
  exports.formatStars = formatStars;
7
+ exports.formatCompactCount = formatCompactCount;
7
8
  function formatBytes(bytes) {
8
9
  const mb = bytes / (1024 * 1024);
9
10
  const gb = mb / 1024;
@@ -35,3 +36,15 @@ function formatStars(value) {
35
36
  }
36
37
  return String(value);
37
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.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Fast CLI to inspect GitHub repository sizes",
5
5
  "license": "MIT",
6
6
  "author": "Hanif",