reposizer 0.1.2 → 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.
@@ -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
- await (0, repo_1.runRepositoriesCommand)(repositories, Boolean(options.json));
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 openai/gym vercel/next.js\n reposizer --json");
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("Invalid repository format. Use owner/repo.");
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 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
- }));
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(results.length === 1 ? results[0] : results, null, 2));
158
+ console.log(JSON.stringify(sorted.length === 1 ? sorted[0] : sorted, null, 2));
80
159
  return;
81
160
  }
82
- for (const result of results) {
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
- console.log("");
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
- 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,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.1.2",
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
- "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",