@review-my-code/rmcode 0.1.6 → 0.1.8

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.
Files changed (2) hide show
  1. package/dist/cli.js +137 -16
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ const path_1 = require("path");
9
9
  const readline_1 = require("readline");
10
10
  const API_URL = process.env.RMC_API_URL || "https://review-my-code.com";
11
11
  const APP_URL = process.env.RMC_APP_URL || "https://review-my-code.com";
12
- const VERSION = "0.1.6";
12
+ const VERSION = "0.1.8";
13
13
  const FREE_PLAN_CREDITS_PER_MONTH = 30;
14
14
  const REQUEST_TIMEOUT_MS = 290_000;
15
15
  const POLL_TIMEOUT_MS = 10 * 60_000;
@@ -97,6 +97,62 @@ function gitQuietIn(cwd, args) {
97
97
  stdio: ["ignore", "pipe", "ignore"],
98
98
  }).trim();
99
99
  }
100
+ function runQuietAsync(cmd, args, options = {}) {
101
+ return new Promise((resolve, reject) => {
102
+ let settled = false;
103
+ let stdout = "";
104
+ let stderr = "";
105
+ const child = (0, child_process_1.spawn)(cmd, args, {
106
+ cwd: options.cwd,
107
+ env: options.env ? { ...process.env, ...options.env } : process.env,
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ });
110
+ const timer = options.timeoutMs && options.timeoutMs > 0
111
+ ? setTimeout(() => {
112
+ child.kill("SIGTERM");
113
+ finish(new Error(`${cmd} ${args.join(" ")} timed out after ${formatElapsed(options.timeoutMs)}.`));
114
+ }, options.timeoutMs)
115
+ : null;
116
+ function finish(err, output = "") {
117
+ if (settled)
118
+ return;
119
+ settled = true;
120
+ if (timer)
121
+ clearTimeout(timer);
122
+ if (err)
123
+ reject(err);
124
+ else
125
+ resolve(output.trim());
126
+ }
127
+ child.stdout.setEncoding("utf8");
128
+ child.stderr.setEncoding("utf8");
129
+ child.stdout.on("data", (chunk) => {
130
+ stdout += chunk;
131
+ });
132
+ child.stderr.on("data", (chunk) => {
133
+ stderr += chunk;
134
+ });
135
+ child.on("error", (err) => finish(err));
136
+ child.on("close", (code, signal) => {
137
+ if (code === 0) {
138
+ finish(undefined, stdout);
139
+ return;
140
+ }
141
+ finish(new Error(`${cmd} ${args.join(" ")} failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}. ${stderr.slice(0, 240).trim()}`.trim()));
142
+ });
143
+ });
144
+ }
145
+ function gitQuietInAsync(cwd, args) {
146
+ return runQuietAsync("git", ["-C", cwd, ...args], {
147
+ timeoutMs: REQUEST_TIMEOUT_MS,
148
+ });
149
+ }
150
+ function gitQuietInAsyncWithEnv(cwd, args, env) {
151
+ return runQuietAsync("git", ["-C", cwd, ...args], {
152
+ env,
153
+ timeoutMs: REQUEST_TIMEOUT_MS,
154
+ });
155
+ }
100
156
  function isGitRepo() {
101
157
  try {
102
158
  git(["rev-parse", "--is-inside-work-tree"]);
@@ -239,21 +295,42 @@ function parsePrReference(value) {
239
295
  number: Number.parseInt(match[2], 10),
240
296
  };
241
297
  }
298
+ let cachedGithubToken;
242
299
  function githubToken() {
300
+ if (cachedGithubToken !== undefined)
301
+ return cachedGithubToken || undefined;
243
302
  const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
244
- if (envToken?.trim())
245
- return envToken.trim();
303
+ if (envToken?.trim()) {
304
+ cachedGithubToken = envToken.trim();
305
+ return cachedGithubToken;
306
+ }
246
307
  try {
247
- return (0, child_process_1.execFileSync)("gh", ["auth", "token"], {
308
+ const token = (0, child_process_1.execFileSync)("gh", ["auth", "token"], {
248
309
  encoding: "utf-8",
249
310
  stdio: ["ignore", "pipe", "ignore"],
250
311
  timeout: 5_000,
251
312
  }).trim();
313
+ cachedGithubToken = token || null;
314
+ return cachedGithubToken || undefined;
252
315
  }
253
316
  catch {
317
+ cachedGithubToken = null;
254
318
  return undefined;
255
319
  }
256
320
  }
321
+ function githubGitAuthEnv(cloneUrl) {
322
+ if (!/^https:\/\/github\.com\//i.test(cloneUrl))
323
+ return undefined;
324
+ const token = githubToken();
325
+ if (!token)
326
+ return undefined;
327
+ const basicToken = Buffer.from(`x-access-token:${token}`).toString("base64");
328
+ return {
329
+ GIT_CONFIG_COUNT: "1",
330
+ GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
331
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${basicToken}`,
332
+ };
333
+ }
257
334
  function githubHeaders(accept) {
258
335
  const headers = {
259
336
  Accept: accept,
@@ -312,7 +389,7 @@ async function fetchPrDiff(ref) {
312
389
  return fetchGithubText(`/repos/${ref.ownerRepo}/pulls/${ref.number}`, "application/vnd.github.v3.diff");
313
390
  }
314
391
  function rmcodeCacheRoot() {
315
- return process.env.RMC_CACHE_DIR || (0, path_1.join)((0, os_1.homedir)(), ".cache", "rmcode");
392
+ return process.env.RMC_CACHE_DIR || (0, path_1.join)((0, os_1.homedir)(), ".rmcode");
316
393
  }
317
394
  function prCacheRoot() {
318
395
  return process.env.RMC_PR_CACHE_DIR || (0, path_1.join)(rmcodeCacheRoot(), "pr-repos");
@@ -320,39 +397,82 @@ function prCacheRoot() {
320
397
  function safeCacheName(ownerRepo) {
321
398
  return ownerRepo.toLowerCase().replace(/[^a-z0-9._-]+/g, "__");
322
399
  }
323
- function ensurePrCacheDir(metadata) {
400
+ async function ensurePrCacheDir(metadata) {
324
401
  const root = prCacheRoot();
325
402
  (0, fs_1.mkdirSync)(root, { recursive: true });
326
403
  const cacheDir = (0, path_1.join)(root, safeCacheName(metadata.baseRepo));
404
+ const gitAuthEnv = githubGitAuthEnv(metadata.cloneUrl);
405
+ // We only need the PR base commit's tree so the extractor can check it out
406
+ // in a detached worktree. A full `git clone` pulls the repo's entire history
407
+ // and every branch — gigabytes for large monorepos, which reliably blew past
408
+ // the timeout and left an unusable cache. Instead, init a local repo once and
409
+ // shallow-fetch just the single base commit (one snapshot, no history).
327
410
  if (!(0, fs_1.existsSync)((0, path_1.join)(cacheDir, ".git"))) {
328
- const clone = (0, child_process_1.spawnSync)("git", ["clone", "--no-checkout", metadata.cloneUrl, cacheDir], { stdio: "ignore", timeout: REQUEST_TIMEOUT_MS });
329
- if (clone.status !== 0) {
330
- throw new Error(`Could not clone ${metadata.baseRepo} into rmcode cache.`);
411
+ if ((0, fs_1.existsSync)(cacheDir))
412
+ (0, fs_1.rmSync)(cacheDir, { recursive: true, force: true });
413
+ (0, fs_1.mkdirSync)(cacheDir, { recursive: true });
414
+ try {
415
+ await gitQuietInAsync(cacheDir, ["init", "--quiet"]);
416
+ await gitQuietInAsync(cacheDir, [
417
+ "remote",
418
+ "add",
419
+ "origin",
420
+ metadata.cloneUrl,
421
+ ]);
422
+ }
423
+ catch (err) {
424
+ (0, fs_1.rmSync)(cacheDir, { recursive: true, force: true });
425
+ throw new Error(`Could not initialize the rmcode PR cache for ${metadata.baseRepo}. ${err.message}`);
331
426
  }
332
427
  }
333
428
  else {
334
429
  try {
335
- gitQuietIn(cacheDir, ["remote", "set-url", "origin", metadata.cloneUrl]);
430
+ await gitQuietInAsync(cacheDir, [
431
+ "remote",
432
+ "set-url",
433
+ "origin",
434
+ metadata.cloneUrl,
435
+ ]);
336
436
  }
337
437
  catch {
338
438
  // Keep the existing remote; the next fetch will report any real issue.
339
439
  }
340
440
  }
441
+ // Shallow-fetch only the base commit. Fetching by SHA works on GitHub for
442
+ // commits reachable from a ref; fall back to the base branch ref if the
443
+ // server rejects an explicit SHA in the want (the rev-parse below then
444
+ // confirms the SHA actually arrived).
341
445
  try {
342
- gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseSha]);
446
+ await gitQuietInAsyncWithEnv(cacheDir, [
447
+ "fetch",
448
+ "--no-tags",
449
+ "--depth=1",
450
+ "origin",
451
+ metadata.baseSha,
452
+ ], gitAuthEnv);
343
453
  }
344
454
  catch {
345
- gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseRef]);
455
+ await gitQuietInAsyncWithEnv(cacheDir, [
456
+ "fetch",
457
+ "--no-tags",
458
+ "--depth=1",
459
+ "origin",
460
+ metadata.baseRef,
461
+ ], gitAuthEnv);
346
462
  }
347
463
  try {
348
- gitQuietIn(cacheDir, ["rev-parse", "--verify", metadata.baseSha]);
464
+ await gitQuietInAsync(cacheDir, [
465
+ "rev-parse",
466
+ "--verify",
467
+ metadata.baseSha,
468
+ ]);
349
469
  }
350
470
  catch {
351
471
  throw new Error(`Could not fetch PR base commit ${metadata.baseSha} into the rmcode cache.`);
352
472
  }
353
473
  return cacheDir;
354
474
  }
355
- async function diffFromPullRequest(value) {
475
+ async function diffFromPullRequest(value, progress) {
356
476
  const ref = typeof value === "string" ? parsePrReference(value) : value;
357
477
  const [metadata, diff] = await Promise.all([
358
478
  fetchPrMetadata(ref),
@@ -361,7 +481,8 @@ async function diffFromPullRequest(value) {
361
481
  if (!diff.trim()) {
362
482
  throw new Error(`GitHub returned an empty diff for ${metadata.htmlUrl}.`);
363
483
  }
364
- const cacheDir = ensurePrCacheDir(metadata);
484
+ progress?.update(`Preparing PR cache for ${metadata.baseRepo}`);
485
+ const cacheDir = await ensurePrCacheDir(metadata);
365
486
  return {
366
487
  diff,
367
488
  files: filesFromDiff(diff),
@@ -1047,7 +1168,7 @@ async function runPullRequestReview(prValue, jsonMode, failOnFindings, options)
1047
1168
  }
1048
1169
  const spinner = createSpinner(`Fetching GitHub PR ${prValue}`);
1049
1170
  try {
1050
- const source = await diffFromPullRequest(ref);
1171
+ const source = await diffFromPullRequest(ref, spinner);
1051
1172
  spinner.stop(`Fetched ${source.metadata.baseRepo}#${source.metadata.number}`);
1052
1173
  const contextOptions = {
1053
1174
  prTitle: firstNonEmpty(options.title, process.env.RMC_PR_TITLE, source.metadata.title),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@review-my-code/rmcode",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "AI code review from your terminal. Catches logic errors, null risks, security holes, and broken error handling.",
5
5
  "keywords": [
6
6
  "code-review",