@review-my-code/rmcode 0.1.5 → 0.1.6
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 +8 -0
- package/dist/cli.js +278 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,14 +16,22 @@ Set `RMC_API_KEY` before running a review; anonymous CLI reviews are disabled.
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
rmcode # review branch diff from merge base
|
|
19
|
+
rmcode --pr 123 # review GitHub PR #123 for this repo
|
|
20
|
+
rmcode --pr https://github.com/owner/repo/pull/123
|
|
19
21
|
rmcode --staged # review staged changes
|
|
20
22
|
rmcode --unstaged # review unstaged changes
|
|
21
23
|
rmcode --all # review all uncommitted changes, including safe text files
|
|
22
24
|
rmcode src/auth.ts # review one file
|
|
23
25
|
git diff main | rmcode # review a custom diff; language is detected from filenames
|
|
24
26
|
rmcode --json --fail-on-findings
|
|
27
|
+
rmcode cache clear # clear reusable PR checkout cache
|
|
25
28
|
rmcode update # update to the latest CLI
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
Review commands check for the latest published CLI before uploading code. If a
|
|
29
32
|
newer version is available, run `rmcode update` and retry.
|
|
33
|
+
|
|
34
|
+
`rmcode --pr` fetches PR metadata and diff from GitHub, caches a reusable base
|
|
35
|
+
checkout under `~/.cache/rmcode/pr-repos`, and runs the same pinned context
|
|
36
|
+
extractor used for local diff reviews. Use `GH_TOKEN`, `GITHUB_TOKEN`, or
|
|
37
|
+
`gh auth login` for private repositories.
|
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.6";
|
|
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;
|
|
@@ -22,6 +22,7 @@ const MAX_UNTRACKED_FILE_BYTES = 512 * 1024;
|
|
|
22
22
|
const EXTRACTOR_TIMEOUT_MS = 120_000;
|
|
23
23
|
const EXTRACTOR_DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
24
24
|
const EXTRACTOR_INFO_TIMEOUT_MS = 15_000;
|
|
25
|
+
const GITHUB_API_URL = "https://api.github.com";
|
|
25
26
|
// Mirrors the GitHub App workflow template's "Install extractor runtime
|
|
26
27
|
// dependencies" step and REPOMAP_MAX_CHARS env.
|
|
27
28
|
const EXTRACTOR_RUNTIME_DEPS = [
|
|
@@ -89,6 +90,13 @@ function gitQuiet(args) {
|
|
|
89
90
|
stdio: ["ignore", "pipe", "ignore"],
|
|
90
91
|
}).trim();
|
|
91
92
|
}
|
|
93
|
+
function gitQuietIn(cwd, args) {
|
|
94
|
+
return (0, child_process_1.execFileSync)("git", ["-C", cwd, ...args], {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
97
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
98
|
+
}).trim();
|
|
99
|
+
}
|
|
92
100
|
function isGitRepo() {
|
|
93
101
|
try {
|
|
94
102
|
git(["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -200,13 +208,171 @@ function pseudoDiffForNewFile(file, worktreeRoot) {
|
|
|
200
208
|
function gitRepoName() {
|
|
201
209
|
try {
|
|
202
210
|
const remote = gitQuiet(["remote", "get-url", "origin"]);
|
|
203
|
-
|
|
204
|
-
return m ? m[1] : null;
|
|
211
|
+
return ownerRepoFromRemoteUrl(remote);
|
|
205
212
|
}
|
|
206
213
|
catch {
|
|
207
214
|
return null;
|
|
208
215
|
}
|
|
209
216
|
}
|
|
217
|
+
function ownerRepoFromRemoteUrl(remote) {
|
|
218
|
+
const m = remote.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
219
|
+
return m ? m[1].replace(/\.git$/i, "") : null;
|
|
220
|
+
}
|
|
221
|
+
function parsePrReference(value) {
|
|
222
|
+
const trimmed = value.trim();
|
|
223
|
+
const numeric = /^\d+$/.test(trimmed)
|
|
224
|
+
? Number.parseInt(trimmed, 10)
|
|
225
|
+
: undefined;
|
|
226
|
+
if (numeric != null && numeric > 0) {
|
|
227
|
+
const ownerRepo = gitRepoName();
|
|
228
|
+
if (!ownerRepo) {
|
|
229
|
+
throw new Error("Could not infer owner/repo from origin. Use: rmcode --pr https://github.com/owner/repo/pull/123");
|
|
230
|
+
}
|
|
231
|
+
return { ownerRepo, number: numeric };
|
|
232
|
+
}
|
|
233
|
+
const match = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:[/?#].*)?$/i);
|
|
234
|
+
if (!match) {
|
|
235
|
+
throw new Error("Invalid PR reference. Use `rmcode --pr 123` or `rmcode --pr https://github.com/owner/repo/pull/123`.");
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
ownerRepo: match[1].replace(/\.git$/i, ""),
|
|
239
|
+
number: Number.parseInt(match[2], 10),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function githubToken() {
|
|
243
|
+
const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
244
|
+
if (envToken?.trim())
|
|
245
|
+
return envToken.trim();
|
|
246
|
+
try {
|
|
247
|
+
return (0, child_process_1.execFileSync)("gh", ["auth", "token"], {
|
|
248
|
+
encoding: "utf-8",
|
|
249
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
250
|
+
timeout: 5_000,
|
|
251
|
+
}).trim();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function githubHeaders(accept) {
|
|
258
|
+
const headers = {
|
|
259
|
+
Accept: accept,
|
|
260
|
+
"User-Agent": `rmcode/${VERSION}`,
|
|
261
|
+
};
|
|
262
|
+
const token = githubToken();
|
|
263
|
+
if (token)
|
|
264
|
+
headers.Authorization = `Bearer ${token}`;
|
|
265
|
+
return headers;
|
|
266
|
+
}
|
|
267
|
+
async function fetchGithubJson(path) {
|
|
268
|
+
const resp = await fetch(`${GITHUB_API_URL}${path}`, {
|
|
269
|
+
headers: githubHeaders("application/vnd.github+json"),
|
|
270
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
271
|
+
});
|
|
272
|
+
if (!resp.ok) {
|
|
273
|
+
const body = await resp.text().catch(() => "");
|
|
274
|
+
throw new Error(`GitHub API request failed (${resp.status}). ${body.includes("Not Found")
|
|
275
|
+
? "Check the PR URL and GitHub authentication."
|
|
276
|
+
: body.slice(0, 180)}`.trim());
|
|
277
|
+
}
|
|
278
|
+
return (await resp.json());
|
|
279
|
+
}
|
|
280
|
+
async function fetchGithubText(path, accept) {
|
|
281
|
+
const resp = await fetch(`${GITHUB_API_URL}${path}`, {
|
|
282
|
+
headers: githubHeaders(accept),
|
|
283
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
if (!resp.ok) {
|
|
286
|
+
throw new Error(`GitHub diff request failed (${resp.status}).`);
|
|
287
|
+
}
|
|
288
|
+
return resp.text();
|
|
289
|
+
}
|
|
290
|
+
async function fetchPrMetadata(ref) {
|
|
291
|
+
const pr = await fetchGithubJson(`/repos/${ref.ownerRepo}/pulls/${ref.number}`);
|
|
292
|
+
const baseRepo = pr.base?.repo?.full_name;
|
|
293
|
+
const cloneUrl = pr.base?.repo?.clone_url;
|
|
294
|
+
const baseSha = pr.base?.sha;
|
|
295
|
+
const baseRef = pr.base?.ref;
|
|
296
|
+
const headSha = pr.head?.sha;
|
|
297
|
+
if (!baseRepo || !cloneUrl || !baseSha || !baseRef || !headSha) {
|
|
298
|
+
throw new Error("GitHub PR metadata is missing base/head information.");
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
number: pr.number,
|
|
302
|
+
title: pr.title || `PR #${pr.number}`,
|
|
303
|
+
htmlUrl: pr.html_url || `https://github.com/${ref.ownerRepo}/pull/${ref.number}`,
|
|
304
|
+
baseRef,
|
|
305
|
+
baseSha,
|
|
306
|
+
baseRepo,
|
|
307
|
+
cloneUrl,
|
|
308
|
+
headSha,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async function fetchPrDiff(ref) {
|
|
312
|
+
return fetchGithubText(`/repos/${ref.ownerRepo}/pulls/${ref.number}`, "application/vnd.github.v3.diff");
|
|
313
|
+
}
|
|
314
|
+
function rmcodeCacheRoot() {
|
|
315
|
+
return process.env.RMC_CACHE_DIR || (0, path_1.join)((0, os_1.homedir)(), ".cache", "rmcode");
|
|
316
|
+
}
|
|
317
|
+
function prCacheRoot() {
|
|
318
|
+
return process.env.RMC_PR_CACHE_DIR || (0, path_1.join)(rmcodeCacheRoot(), "pr-repos");
|
|
319
|
+
}
|
|
320
|
+
function safeCacheName(ownerRepo) {
|
|
321
|
+
return ownerRepo.toLowerCase().replace(/[^a-z0-9._-]+/g, "__");
|
|
322
|
+
}
|
|
323
|
+
function ensurePrCacheDir(metadata) {
|
|
324
|
+
const root = prCacheRoot();
|
|
325
|
+
(0, fs_1.mkdirSync)(root, { recursive: true });
|
|
326
|
+
const cacheDir = (0, path_1.join)(root, safeCacheName(metadata.baseRepo));
|
|
327
|
+
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.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
try {
|
|
335
|
+
gitQuietIn(cacheDir, ["remote", "set-url", "origin", metadata.cloneUrl]);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// Keep the existing remote; the next fetch will report any real issue.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseSha]);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseRef]);
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
gitQuietIn(cacheDir, ["rev-parse", "--verify", metadata.baseSha]);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
throw new Error(`Could not fetch PR base commit ${metadata.baseSha} into the rmcode cache.`);
|
|
352
|
+
}
|
|
353
|
+
return cacheDir;
|
|
354
|
+
}
|
|
355
|
+
async function diffFromPullRequest(value) {
|
|
356
|
+
const ref = typeof value === "string" ? parsePrReference(value) : value;
|
|
357
|
+
const [metadata, diff] = await Promise.all([
|
|
358
|
+
fetchPrMetadata(ref),
|
|
359
|
+
fetchPrDiff(ref),
|
|
360
|
+
]);
|
|
361
|
+
if (!diff.trim()) {
|
|
362
|
+
throw new Error(`GitHub returned an empty diff for ${metadata.htmlUrl}.`);
|
|
363
|
+
}
|
|
364
|
+
const cacheDir = ensurePrCacheDir(metadata);
|
|
365
|
+
return {
|
|
366
|
+
diff,
|
|
367
|
+
files: filesFromDiff(diff),
|
|
368
|
+
baseRef: metadata.baseSha,
|
|
369
|
+
metadata,
|
|
370
|
+
cacheDir,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function clearPrCache() {
|
|
374
|
+
(0, fs_1.rmSync)(prCacheRoot(), { recursive: true, force: true });
|
|
375
|
+
}
|
|
210
376
|
function currentBranch() {
|
|
211
377
|
try {
|
|
212
378
|
return git(["branch", "--show-current"]);
|
|
@@ -310,8 +476,8 @@ async function fetchReviewJson(url, init) {
|
|
|
310
476
|
clearTimeout(timeout);
|
|
311
477
|
}
|
|
312
478
|
}
|
|
313
|
-
async function callAuthenticatedAPI(code, language, type, files, bundle, progress) {
|
|
314
|
-
const repo = gitRepoName();
|
|
479
|
+
async function callAuthenticatedAPI(code, language, type, files, bundle, progress, repoOverride) {
|
|
480
|
+
const repo = repoOverride || gitRepoName();
|
|
315
481
|
const payload = {
|
|
316
482
|
type,
|
|
317
483
|
value: code,
|
|
@@ -353,8 +519,8 @@ async function callAuthenticatedAPI(code, language, type, files, bundle, progres
|
|
|
353
519
|
}
|
|
354
520
|
return body;
|
|
355
521
|
}
|
|
356
|
-
async function callAnonymousAPI(code, language, type, files, progress) {
|
|
357
|
-
const repo = gitRepoName();
|
|
522
|
+
async function callAnonymousAPI(code, language, type, files, progress, repoOverride) {
|
|
523
|
+
const repo = repoOverride || gitRepoName();
|
|
358
524
|
const { status, body } = await fetchReviewJson(`${API_URL}/api/reviews/anonymous`, {
|
|
359
525
|
method: "POST",
|
|
360
526
|
headers: { "Content-Type": "application/json" },
|
|
@@ -398,9 +564,9 @@ async function pollForResults(url, initialMeta, headers, progress) {
|
|
|
398
564
|
}
|
|
399
565
|
return { success: false, error: "Review timed out after 10 minutes." };
|
|
400
566
|
}
|
|
401
|
-
function callAPI(code, language, type = "snippet", files = [], bundle, progress) {
|
|
567
|
+
function callAPI(code, language, type = "snippet", files = [], bundle, progress, repoOverride) {
|
|
402
568
|
if (API_KEY)
|
|
403
|
-
return callAuthenticatedAPI(code, language, type, files, bundle, progress);
|
|
569
|
+
return callAuthenticatedAPI(code, language, type, files, bundle, progress, repoOverride);
|
|
404
570
|
return Promise.resolve({
|
|
405
571
|
success: false,
|
|
406
572
|
error: "API key required. Run `rmcode login`, then set RMC_API_KEY before sending code for review.",
|
|
@@ -496,7 +662,7 @@ async function ensureExtractor(info) {
|
|
|
496
662
|
* diff's base state and return the ContextBundle, or null when extraction
|
|
497
663
|
* is unavailable. Throws ExtractorIntegrityError on checksum mismatch.
|
|
498
664
|
*/
|
|
499
|
-
async function extractContextBundle(diff, baseRef, contextOptions = {}) {
|
|
665
|
+
async function extractContextBundle(diff, baseRef, contextOptions = {}, repoRoot) {
|
|
500
666
|
const info = await fetchExtractorInfo();
|
|
501
667
|
if (!info)
|
|
502
668
|
return null;
|
|
@@ -508,7 +674,12 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
|
|
|
508
674
|
let worktreeAdded = false;
|
|
509
675
|
try {
|
|
510
676
|
try {
|
|
511
|
-
|
|
677
|
+
if (repoRoot) {
|
|
678
|
+
gitQuietIn(repoRoot, ["worktree", "add", "--detach", baseDir, baseRef]);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
gitQuiet(["worktree", "add", "--detach", baseDir, baseRef]);
|
|
682
|
+
}
|
|
512
683
|
worktreeAdded = true;
|
|
513
684
|
}
|
|
514
685
|
catch {
|
|
@@ -554,12 +725,22 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
|
|
|
554
725
|
finally {
|
|
555
726
|
if (worktreeAdded) {
|
|
556
727
|
try {
|
|
557
|
-
|
|
728
|
+
if (repoRoot) {
|
|
729
|
+
gitQuietIn(repoRoot, ["worktree", "remove", "--force", baseDir]);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
gitQuiet(["worktree", "remove", "--force", baseDir]);
|
|
733
|
+
}
|
|
558
734
|
}
|
|
559
735
|
catch {
|
|
560
736
|
(0, fs_1.rmSync)(baseDir, { recursive: true, force: true });
|
|
561
737
|
try {
|
|
562
|
-
|
|
738
|
+
if (repoRoot) {
|
|
739
|
+
gitQuietIn(repoRoot, ["worktree", "prune"]);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
gitQuiet(["worktree", "prune"]);
|
|
743
|
+
}
|
|
563
744
|
}
|
|
564
745
|
catch {
|
|
565
746
|
// Stale worktree metadata only; git prunes it on its own later.
|
|
@@ -575,11 +756,11 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
|
|
|
575
756
|
* Reports progress on the shared review spinner so the run renders as one
|
|
576
757
|
* phase-aware line. Exits the process on extractor checksum mismatch.
|
|
577
758
|
*/
|
|
578
|
-
async function maybeExtractContext(diff, spinner, baseRef, contextOptions = {}) {
|
|
579
|
-
if (!API_KEY || !baseRef || !isGitRepo())
|
|
759
|
+
async function maybeExtractContext(diff, spinner, baseRef, contextOptions = {}, repoRoot) {
|
|
760
|
+
if (!API_KEY || !baseRef || (!repoRoot && !isGitRepo()))
|
|
580
761
|
return null;
|
|
581
762
|
try {
|
|
582
|
-
const bundle = await extractContextBundle(diff, baseRef, contextOptions);
|
|
763
|
+
const bundle = await extractContextBundle(diff, baseRef, contextOptions, repoRoot);
|
|
583
764
|
if (bundle)
|
|
584
765
|
return bundle;
|
|
585
766
|
}
|
|
@@ -806,7 +987,7 @@ function createSpinner(msg) {
|
|
|
806
987
|
};
|
|
807
988
|
}
|
|
808
989
|
// ── Review runner ─────────────────────────────────────────────────────
|
|
809
|
-
async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef, contextOptions = {}) {
|
|
990
|
+
async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef, contextOptions = {}, contextRepoRoot, repoOverride) {
|
|
810
991
|
const startedAt = Date.now();
|
|
811
992
|
const lang = language || detectLanguage(files);
|
|
812
993
|
const uploadMsg = `Uploading review request (${files.length} file${files.length !== 1 ? "s" : ""}, ${lang})`;
|
|
@@ -814,15 +995,18 @@ async function runReview(content, files, label, jsonMode, failOnFindings, type =
|
|
|
814
995
|
// the diff's base state so the review sees the same repository context.
|
|
815
996
|
// One spinner carries the whole run through its phases. The guard here
|
|
816
997
|
// mirrors maybeExtractContext's own early return.
|
|
817
|
-
const willExtract = type === "diff" &&
|
|
998
|
+
const willExtract = type === "diff" &&
|
|
999
|
+
Boolean(API_KEY) &&
|
|
1000
|
+
Boolean(baseRef) &&
|
|
1001
|
+
(Boolean(contextRepoRoot) || isGitRepo());
|
|
818
1002
|
const spinner = createSpinner(willExtract ? "Extracting repository context" : uploadMsg);
|
|
819
1003
|
const bundle = willExtract
|
|
820
|
-
? await maybeExtractContext(content, spinner, baseRef, contextOptions)
|
|
1004
|
+
? await maybeExtractContext(content, spinner, baseRef, contextOptions, contextRepoRoot)
|
|
821
1005
|
: null;
|
|
822
1006
|
if (willExtract)
|
|
823
1007
|
spinner.update(uploadMsg);
|
|
824
1008
|
try {
|
|
825
|
-
const result = await callAPI(content, lang, type, files, bundle, spinner);
|
|
1009
|
+
const result = await callAPI(content, lang, type, files, bundle, spinner, repoOverride);
|
|
826
1010
|
spinner.stop(`Reviewed ${label}`);
|
|
827
1011
|
if (jsonMode) {
|
|
828
1012
|
printJsonResponse(result);
|
|
@@ -848,6 +1032,38 @@ async function runReview(content, files, label, jsonMode, failOnFindings, type =
|
|
|
848
1032
|
process.exit(1);
|
|
849
1033
|
}
|
|
850
1034
|
}
|
|
1035
|
+
async function runPullRequestReview(prValue, jsonMode, failOnFindings, options) {
|
|
1036
|
+
if (!API_KEY) {
|
|
1037
|
+
console.error(style(" API key required. Run `rmcode login`, then set RMC_API_KEY before reviewing a PR.", c.red));
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
let ref;
|
|
1041
|
+
try {
|
|
1042
|
+
ref = parsePrReference(prValue);
|
|
1043
|
+
}
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
console.error(style(` ${err.message}`, c.red));
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
const spinner = createSpinner(`Fetching GitHub PR ${prValue}`);
|
|
1049
|
+
try {
|
|
1050
|
+
const source = await diffFromPullRequest(ref);
|
|
1051
|
+
spinner.stop(`Fetched ${source.metadata.baseRepo}#${source.metadata.number}`);
|
|
1052
|
+
const contextOptions = {
|
|
1053
|
+
prTitle: firstNonEmpty(options.title, process.env.RMC_PR_TITLE, source.metadata.title),
|
|
1054
|
+
prNumber: source.metadata.number,
|
|
1055
|
+
sourceRepo: firstNonEmpty(options.sourceRepo, process.env.RMC_SOURCE_REPO, source.metadata.baseRepo),
|
|
1056
|
+
};
|
|
1057
|
+
await runReview(source.diff, source.files, `${source.metadata.baseRepo}#${source.metadata.number}`, jsonMode, failOnFindings, "diff", undefined, source.baseRef, contextOptions, source.cacheDir, source.metadata.baseRepo);
|
|
1058
|
+
if (!jsonMode)
|
|
1059
|
+
await maybeShowUpdateNudge();
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
spinner.stop("PR fetch failed");
|
|
1063
|
+
console.error(style(` ${err.message}`, c.red));
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
851
1067
|
// ── Help ──────────────────────────────────────────────────────────────
|
|
852
1068
|
function printHelp() {
|
|
853
1069
|
const header = !noColor && !isCI
|
|
@@ -859,10 +1075,13 @@ ${header}
|
|
|
859
1075
|
${style("COMMANDS", c.bold)}
|
|
860
1076
|
|
|
861
1077
|
${style("rmcode", c.cyan)} Review changes from merge base (default)
|
|
1078
|
+
${style("rmcode --pr 123", c.cyan)} Review GitHub PR #123 for this repo
|
|
1079
|
+
${style("rmcode --pr <url>", c.cyan)} Review a specific GitHub PR URL
|
|
862
1080
|
${style("rmcode --staged", c.cyan)} Review staged changes only
|
|
863
1081
|
${style("rmcode --unstaged", c.cyan)} Review unstaged changes only
|
|
864
1082
|
${style("rmcode --all", c.cyan)} Review all uncommitted changes (staged + unstaged)
|
|
865
1083
|
${style("rmcode <file>", c.cyan)} Review a single local file
|
|
1084
|
+
${style("rmcode cache clear", c.cyan)} Clear cached PR checkouts
|
|
866
1085
|
${style("rmcode login", c.cyan)} Set up your API key
|
|
867
1086
|
${style("rmcode install", c.cyan)} Install the GitHub App for automatic PR reviews
|
|
868
1087
|
${style("rmcode update", c.cyan)} Update rmcode to the latest version
|
|
@@ -872,6 +1091,7 @@ ${header}
|
|
|
872
1091
|
|
|
873
1092
|
--json Output the full API response as JSON (for CI/agents)
|
|
874
1093
|
--fail-on-findings Exit 2 when JSON output contains findings
|
|
1094
|
+
--pr <number|url> Review a GitHub PR by number or URL
|
|
875
1095
|
--lang <language> Optional language hint for raw stdin snippets
|
|
876
1096
|
--title <text> Optional PR title hint for repository-context reviews
|
|
877
1097
|
--pr-number <n> Optional PR number hint for CI/benchmark parity
|
|
@@ -882,6 +1102,7 @@ ${header}
|
|
|
882
1102
|
${style("MODES", c.bold)}
|
|
883
1103
|
|
|
884
1104
|
${style("Default", c.white)} Diff from merge base to HEAD — what your PR would contain
|
|
1105
|
+
${style("--pr", c.white)} Fetch a GitHub PR diff and cached base checkout for context extraction
|
|
885
1106
|
${style("--staged", c.white)} Only changes in the staging area (git add)
|
|
886
1107
|
${style("--unstaged", c.white)} Only working directory changes not yet staged
|
|
887
1108
|
${style("--all", c.white)} Everything not yet committed, including untracked text files
|
|
@@ -899,6 +1120,10 @@ ${header}
|
|
|
899
1120
|
${style("# Review a specific range", c.dim)}
|
|
900
1121
|
git diff abc123..def456 | rmcode
|
|
901
1122
|
|
|
1123
|
+
${style("# Review a GitHub PR without checking it out locally", c.dim)}
|
|
1124
|
+
rmcode --pr 123
|
|
1125
|
+
rmcode --pr https://github.com/owner/repo/pull/123
|
|
1126
|
+
|
|
902
1127
|
${style("# Review one file", c.dim)}
|
|
903
1128
|
rmcode src/auth.ts
|
|
904
1129
|
|
|
@@ -911,6 +1136,7 @@ ${header}
|
|
|
911
1136
|
${style("ENVIRONMENT", c.bold)}
|
|
912
1137
|
|
|
913
1138
|
RMC_API_KEY API key for authenticated reviews (get one: rmcode login)
|
|
1139
|
+
RMC_PR_CACHE_DIR Optional directory for reusable GitHub PR cache clones
|
|
914
1140
|
RMC_PR_TITLE Optional PR title hint for repository-context reviews
|
|
915
1141
|
RMC_PR_NUMBER Optional PR number hint for CI/benchmark parity
|
|
916
1142
|
RMC_SOURCE_REPO Optional owner/repo hint when no origin remote exists
|
|
@@ -1082,6 +1308,7 @@ function parseArgs(args) {
|
|
|
1082
1308
|
const positional = [];
|
|
1083
1309
|
const optionAliases = {
|
|
1084
1310
|
"--lang": "lang",
|
|
1311
|
+
"--pr": "pr",
|
|
1085
1312
|
"--title": "title",
|
|
1086
1313
|
"--pr-title": "title",
|
|
1087
1314
|
"--pr-number": "prNumber",
|
|
@@ -1096,10 +1323,13 @@ function parseArgs(args) {
|
|
|
1096
1323
|
if (eq > 0) {
|
|
1097
1324
|
options[optionName] = arg.slice(eq + 1);
|
|
1098
1325
|
}
|
|
1099
|
-
else {
|
|
1326
|
+
else if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
1100
1327
|
options[optionName] = args[i + 1];
|
|
1101
1328
|
i++;
|
|
1102
1329
|
}
|
|
1330
|
+
else {
|
|
1331
|
+
options[optionName] = undefined;
|
|
1332
|
+
}
|
|
1103
1333
|
}
|
|
1104
1334
|
else if (arg.startsWith("-")) {
|
|
1105
1335
|
flags.add(arg);
|
|
@@ -1146,6 +1376,15 @@ async function main() {
|
|
|
1146
1376
|
await runUpdate();
|
|
1147
1377
|
return;
|
|
1148
1378
|
}
|
|
1379
|
+
if (positional[0] === "cache") {
|
|
1380
|
+
if (positional[1] === "clear" && positional.length === 2) {
|
|
1381
|
+
clearPrCache();
|
|
1382
|
+
console.log(`\n ${style("✓", c.green)} Cleared rmcode PR cache.\n`);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
console.error(style(`\n Unknown cache command. Run: rmcode cache clear\n`, c.red));
|
|
1386
|
+
process.exit(1);
|
|
1387
|
+
}
|
|
1149
1388
|
if (positional[0] === "install") {
|
|
1150
1389
|
console.log(`\n ${style("Install the GitHub App for automatic PR reviews:", c.bold)}\n`);
|
|
1151
1390
|
console.log(` ${style("https://github.com/apps/rmcode-ai", c.cyan, c.bold)}\n`);
|
|
@@ -1177,6 +1416,7 @@ async function main() {
|
|
|
1177
1416
|
const jsonMode = flags.has("--json");
|
|
1178
1417
|
const failOnFindings = flags.has("--fail-on-findings");
|
|
1179
1418
|
const contextOptions = contextOptionsFromCli(options);
|
|
1419
|
+
const hasPrOption = Object.prototype.hasOwnProperty.call(options, "pr");
|
|
1180
1420
|
if (positional.length > 1) {
|
|
1181
1421
|
console.error(style(`\n Unknown command: ${positional[0]}`, c.red));
|
|
1182
1422
|
console.error(style(` Run: rmcode help\n`, c.dim));
|
|
@@ -1188,6 +1428,23 @@ async function main() {
|
|
|
1188
1428
|
if (!jsonMode && !noColor && !isCI) {
|
|
1189
1429
|
process.stderr.write(`\n${owlHeader()}\n\n`);
|
|
1190
1430
|
}
|
|
1431
|
+
if (hasPrOption) {
|
|
1432
|
+
const prValue = options.pr?.trim();
|
|
1433
|
+
if (!prValue) {
|
|
1434
|
+
console.error(style("\n Missing value for --pr.\n", c.red));
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
if (positional.length > 0) {
|
|
1438
|
+
console.error(style("\n --pr cannot be combined with a file path or command.\n", c.red));
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
if (flags.has("--staged") || flags.has("--unstaged") || flags.has("--all")) {
|
|
1442
|
+
console.error(style("\n --pr cannot be combined with --staged, --unstaged, or --all.\n", c.red));
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
await runPullRequestReview(prValue, jsonMode, failOnFindings, options);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1191
1448
|
// Piped stdin
|
|
1192
1449
|
if (hasPipedStdin()) {
|
|
1193
1450
|
const rl = (0, readline_1.createInterface)({ input: process.stdin });
|
package/package.json
CHANGED