@review-my-code/rmcode 0.1.6 → 0.1.7

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 +117 -15
  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.7";
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,
@@ -320,39 +397,63 @@ 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);
327
405
  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.`);
406
+ if ((0, fs_1.existsSync)(cacheDir))
407
+ (0, fs_1.rmSync)(cacheDir, { recursive: true, force: true });
408
+ try {
409
+ await runQuietAsync("git", ["clone", "--no-checkout", metadata.cloneUrl, cacheDir], { env: gitAuthEnv, timeoutMs: REQUEST_TIMEOUT_MS });
410
+ }
411
+ catch (err) {
412
+ throw new Error(`Could not clone ${metadata.baseRepo} into rmcode cache. Make sure GitHub CLI is authenticated with access to the repository. ${err.message}`);
331
413
  }
332
414
  }
333
415
  else {
334
416
  try {
335
- gitQuietIn(cacheDir, ["remote", "set-url", "origin", metadata.cloneUrl]);
417
+ await gitQuietInAsync(cacheDir, [
418
+ "remote",
419
+ "set-url",
420
+ "origin",
421
+ metadata.cloneUrl,
422
+ ]);
336
423
  }
337
424
  catch {
338
425
  // Keep the existing remote; the next fetch will report any real issue.
339
426
  }
340
427
  }
341
428
  try {
342
- gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseSha]);
429
+ await gitQuietInAsyncWithEnv(cacheDir, [
430
+ "fetch",
431
+ "--no-tags",
432
+ "origin",
433
+ metadata.baseSha,
434
+ ], gitAuthEnv);
343
435
  }
344
436
  catch {
345
- gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseRef]);
437
+ await gitQuietInAsyncWithEnv(cacheDir, [
438
+ "fetch",
439
+ "--no-tags",
440
+ "origin",
441
+ metadata.baseRef,
442
+ ], gitAuthEnv);
346
443
  }
347
444
  try {
348
- gitQuietIn(cacheDir, ["rev-parse", "--verify", metadata.baseSha]);
445
+ await gitQuietInAsync(cacheDir, [
446
+ "rev-parse",
447
+ "--verify",
448
+ metadata.baseSha,
449
+ ]);
349
450
  }
350
451
  catch {
351
452
  throw new Error(`Could not fetch PR base commit ${metadata.baseSha} into the rmcode cache.`);
352
453
  }
353
454
  return cacheDir;
354
455
  }
355
- async function diffFromPullRequest(value) {
456
+ async function diffFromPullRequest(value, progress) {
356
457
  const ref = typeof value === "string" ? parsePrReference(value) : value;
357
458
  const [metadata, diff] = await Promise.all([
358
459
  fetchPrMetadata(ref),
@@ -361,7 +462,8 @@ async function diffFromPullRequest(value) {
361
462
  if (!diff.trim()) {
362
463
  throw new Error(`GitHub returned an empty diff for ${metadata.htmlUrl}.`);
363
464
  }
364
- const cacheDir = ensurePrCacheDir(metadata);
465
+ progress?.update(`Preparing PR cache for ${metadata.baseRepo}`);
466
+ const cacheDir = await ensurePrCacheDir(metadata);
365
467
  return {
366
468
  diff,
367
469
  files: filesFromDiff(diff),
@@ -1047,7 +1149,7 @@ async function runPullRequestReview(prValue, jsonMode, failOnFindings, options)
1047
1149
  }
1048
1150
  const spinner = createSpinner(`Fetching GitHub PR ${prValue}`);
1049
1151
  try {
1050
- const source = await diffFromPullRequest(ref);
1152
+ const source = await diffFromPullRequest(ref, spinner);
1051
1153
  spinner.stop(`Fetched ${source.metadata.baseRepo}#${source.metadata.number}`);
1052
1154
  const contextOptions = {
1053
1155
  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.7",
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",