@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.
- package/dist/cli.js +137 -16
- 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.
|
|
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
|
-
|
|
303
|
+
if (envToken?.trim()) {
|
|
304
|
+
cachedGithubToken = envToken.trim();
|
|
305
|
+
return cachedGithubToken;
|
|
306
|
+
}
|
|
246
307
|
try {
|
|
247
|
-
|
|
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)(), ".
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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