@review-my-code/rmcode 0.1.3 → 0.1.5
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 -8
- package/dist/cli.js +105 -22
- package/package.json +1 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RMCode CLI
|
|
2
2
|
|
|
3
|
-
Run
|
|
3
|
+
Run RMCode from your terminal.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @review-my-code/rmcode
|
|
@@ -9,9 +9,8 @@ export RMC_API_KEY=rmc_...
|
|
|
9
9
|
rmcode --staged
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The CLI sends the selected diff, file, or stdin content to RMCode for
|
|
13
|
-
|
|
14
|
-
running a review.
|
|
12
|
+
The CLI sends the selected diff, file, or stdin content to RMCode for review.
|
|
13
|
+
Set `RMC_API_KEY` before running a review; anonymous CLI reviews are disabled.
|
|
15
14
|
|
|
16
15
|
## Common Commands
|
|
17
16
|
|
|
@@ -19,11 +18,12 @@ running a review.
|
|
|
19
18
|
rmcode # review branch diff from merge base
|
|
20
19
|
rmcode --staged # review staged changes
|
|
21
20
|
rmcode --unstaged # review unstaged changes
|
|
22
|
-
rmcode --all # review uncommitted
|
|
21
|
+
rmcode --all # review all uncommitted changes, including safe text files
|
|
23
22
|
rmcode src/auth.ts # review one file
|
|
24
|
-
git diff main | rmcode
|
|
23
|
+
git diff main | rmcode # review a custom diff; language is detected from filenames
|
|
25
24
|
rmcode --json --fail-on-findings
|
|
25
|
+
rmcode update # update to the latest CLI
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
`rmcode
|
|
28
|
+
Review commands check for the latest published CLI before uploading code. If a
|
|
29
|
+
newer version is available, run `rmcode update` and retry.
|
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.5";
|
|
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;
|
|
@@ -199,7 +199,7 @@ function pseudoDiffForNewFile(file, worktreeRoot) {
|
|
|
199
199
|
}
|
|
200
200
|
function gitRepoName() {
|
|
201
201
|
try {
|
|
202
|
-
const remote =
|
|
202
|
+
const remote = gitQuiet(["remote", "get-url", "origin"]);
|
|
203
203
|
const m = remote.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
204
204
|
return m ? m[1] : null;
|
|
205
205
|
}
|
|
@@ -215,6 +215,36 @@ function currentBranch() {
|
|
|
215
215
|
return "unknown";
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
+
function headCommitSubject() {
|
|
219
|
+
try {
|
|
220
|
+
return gitQuiet(["log", "-1", "--pretty=%s"]) || null;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function firstNonEmpty(...values) {
|
|
227
|
+
for (const value of values) {
|
|
228
|
+
const trimmed = value?.trim();
|
|
229
|
+
if (trimmed)
|
|
230
|
+
return trimmed;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
function parseOptionalPositiveInt(value) {
|
|
235
|
+
const trimmed = value?.trim();
|
|
236
|
+
if (!trimmed)
|
|
237
|
+
return undefined;
|
|
238
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
239
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
240
|
+
}
|
|
241
|
+
function contextOptionsFromCli(options) {
|
|
242
|
+
return {
|
|
243
|
+
prTitle: firstNonEmpty(options.title, process.env.RMC_PR_TITLE, headCommitSubject()),
|
|
244
|
+
prNumber: parseOptionalPositiveInt(firstNonEmpty(options.prNumber, process.env.RMC_PR_NUMBER)),
|
|
245
|
+
sourceRepo: firstNonEmpty(options.sourceRepo, process.env.RMC_SOURCE_REPO, gitRepoName(), "local/repo"),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
218
248
|
function filesFromDiff(diff) {
|
|
219
249
|
const files = new Set();
|
|
220
250
|
const re = /^diff --git a\/.+ b\/(.+)$/gm;
|
|
@@ -466,7 +496,7 @@ async function ensureExtractor(info) {
|
|
|
466
496
|
* diff's base state and return the ContextBundle, or null when extraction
|
|
467
497
|
* is unavailable. Throws ExtractorIntegrityError on checksum mismatch.
|
|
468
498
|
*/
|
|
469
|
-
async function extractContextBundle(diff, baseRef) {
|
|
499
|
+
async function extractContextBundle(diff, baseRef, contextOptions = {}) {
|
|
470
500
|
const info = await fetchExtractorInfo();
|
|
471
501
|
if (!info)
|
|
472
502
|
return null;
|
|
@@ -490,10 +520,11 @@ async function extractContextBundle(diff, baseRef) {
|
|
|
490
520
|
(0, fs_1.writeFileSync)(diffPath, diff);
|
|
491
521
|
const result = (0, child_process_1.spawnSync)(process.execPath, [
|
|
492
522
|
extractorPath,
|
|
493
|
-
|
|
523
|
+
`--pr=${contextOptions.prNumber ?? 0}`,
|
|
494
524
|
`--diff-file=${diffPath}`,
|
|
495
525
|
`--output-bundle=${bundlePath}`,
|
|
496
|
-
`--source-repo=${
|
|
526
|
+
`--source-repo=${contextOptions.sourceRepo || "local/repo"}`,
|
|
527
|
+
`--pr-title=${contextOptions.prTitle || ""}`,
|
|
497
528
|
`--tier=${info.tier || "free"}`,
|
|
498
529
|
], {
|
|
499
530
|
cwd: baseDir,
|
|
@@ -544,11 +575,11 @@ async function extractContextBundle(diff, baseRef) {
|
|
|
544
575
|
* Reports progress on the shared review spinner so the run renders as one
|
|
545
576
|
* phase-aware line. Exits the process on extractor checksum mismatch.
|
|
546
577
|
*/
|
|
547
|
-
async function maybeExtractContext(diff, spinner, baseRef) {
|
|
578
|
+
async function maybeExtractContext(diff, spinner, baseRef, contextOptions = {}) {
|
|
548
579
|
if (!API_KEY || !baseRef || !isGitRepo())
|
|
549
580
|
return null;
|
|
550
581
|
try {
|
|
551
|
-
const bundle = await extractContextBundle(diff, baseRef);
|
|
582
|
+
const bundle = await extractContextBundle(diff, baseRef, contextOptions);
|
|
552
583
|
if (bundle)
|
|
553
584
|
return bundle;
|
|
554
585
|
}
|
|
@@ -775,7 +806,7 @@ function createSpinner(msg) {
|
|
|
775
806
|
};
|
|
776
807
|
}
|
|
777
808
|
// ── Review runner ─────────────────────────────────────────────────────
|
|
778
|
-
async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef) {
|
|
809
|
+
async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef, contextOptions = {}) {
|
|
779
810
|
const startedAt = Date.now();
|
|
780
811
|
const lang = language || detectLanguage(files);
|
|
781
812
|
const uploadMsg = `Uploading review request (${files.length} file${files.length !== 1 ? "s" : ""}, ${lang})`;
|
|
@@ -786,7 +817,7 @@ async function runReview(content, files, label, jsonMode, failOnFindings, type =
|
|
|
786
817
|
const willExtract = type === "diff" && Boolean(API_KEY) && Boolean(baseRef) && isGitRepo();
|
|
787
818
|
const spinner = createSpinner(willExtract ? "Extracting repository context" : uploadMsg);
|
|
788
819
|
const bundle = willExtract
|
|
789
|
-
? await maybeExtractContext(content, spinner, baseRef)
|
|
820
|
+
? await maybeExtractContext(content, spinner, baseRef, contextOptions)
|
|
790
821
|
: null;
|
|
791
822
|
if (willExtract)
|
|
792
823
|
spinner.update(uploadMsg);
|
|
@@ -841,7 +872,10 @@ ${header}
|
|
|
841
872
|
|
|
842
873
|
--json Output the full API response as JSON (for CI/agents)
|
|
843
874
|
--fail-on-findings Exit 2 when JSON output contains findings
|
|
844
|
-
--lang <language>
|
|
875
|
+
--lang <language> Optional language hint for raw stdin snippets
|
|
876
|
+
--title <text> Optional PR title hint for repository-context reviews
|
|
877
|
+
--pr-number <n> Optional PR number hint for CI/benchmark parity
|
|
878
|
+
--source-repo <repo> Optional owner/repo hint when no origin remote exists
|
|
845
879
|
--help, -h Show this help
|
|
846
880
|
--version, -v Show version
|
|
847
881
|
|
|
@@ -863,7 +897,7 @@ ${header}
|
|
|
863
897
|
rmcode --staged
|
|
864
898
|
|
|
865
899
|
${style("# Review a specific range", c.dim)}
|
|
866
|
-
git diff abc123..def456 | rmcode
|
|
900
|
+
git diff abc123..def456 | rmcode
|
|
867
901
|
|
|
868
902
|
${style("# Review one file", c.dim)}
|
|
869
903
|
rmcode src/auth.ts
|
|
@@ -877,8 +911,9 @@ ${header}
|
|
|
877
911
|
${style("ENVIRONMENT", c.bold)}
|
|
878
912
|
|
|
879
913
|
RMC_API_KEY API key for authenticated reviews (get one: rmcode login)
|
|
880
|
-
|
|
881
|
-
|
|
914
|
+
RMC_PR_TITLE Optional PR title hint for repository-context reviews
|
|
915
|
+
RMC_PR_NUMBER Optional PR number hint for CI/benchmark parity
|
|
916
|
+
RMC_SOURCE_REPO Optional owner/repo hint when no origin remote exists
|
|
882
917
|
|
|
883
918
|
${style("PRIVACY", c.bold)}
|
|
884
919
|
|
|
@@ -892,7 +927,8 @@ ${header}
|
|
|
892
927
|
}
|
|
893
928
|
// ── Self-update ───────────────────────────────────────────────────────
|
|
894
929
|
const PACKAGE_NAME = "@review-my-code/rmcode";
|
|
895
|
-
const REGISTRY_LATEST_URL =
|
|
930
|
+
const REGISTRY_LATEST_URL = process.env.RMC_REGISTRY_LATEST_URL ||
|
|
931
|
+
"https://registry.npmjs.org/@review-my-code%2Frmcode/latest";
|
|
896
932
|
/** Resolved real path of the running entry script, for install detection. */
|
|
897
933
|
function entryScriptPath() {
|
|
898
934
|
const binPath = process.argv[1] || "";
|
|
@@ -1010,15 +1046,60 @@ async function maybeShowUpdateNudge() {
|
|
|
1010
1046
|
/* silent */
|
|
1011
1047
|
}
|
|
1012
1048
|
}
|
|
1049
|
+
async function enforceLatestCliForReview(jsonMode) {
|
|
1050
|
+
let latest = null;
|
|
1051
|
+
try {
|
|
1052
|
+
latest = await fetchLatestVersion();
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (!latest || compareVersions(latest, VERSION) <= 0)
|
|
1058
|
+
return;
|
|
1059
|
+
const update = updateCommandFor(entryScriptPath());
|
|
1060
|
+
const command = [update.cmd, ...update.args].join(" ");
|
|
1061
|
+
const error = `Update required: rmcode v${latest} is available, but this is v${VERSION}. Run \`${command}\` before reviewing.`;
|
|
1062
|
+
if (jsonMode) {
|
|
1063
|
+
console.log(JSON.stringify({
|
|
1064
|
+
success: false,
|
|
1065
|
+
error,
|
|
1066
|
+
code: "RMC-1006",
|
|
1067
|
+
meta: {
|
|
1068
|
+
currentVersion: VERSION,
|
|
1069
|
+
latestVersion: latest,
|
|
1070
|
+
updateCommand: command,
|
|
1071
|
+
},
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
console.error(style(`\n ${error}\n`, c.yellow));
|
|
1076
|
+
}
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1013
1079
|
function parseArgs(args) {
|
|
1014
1080
|
const flags = new Set();
|
|
1015
1081
|
const options = {};
|
|
1016
1082
|
const positional = [];
|
|
1083
|
+
const optionAliases = {
|
|
1084
|
+
"--lang": "lang",
|
|
1085
|
+
"--title": "title",
|
|
1086
|
+
"--pr-title": "title",
|
|
1087
|
+
"--pr-number": "prNumber",
|
|
1088
|
+
"--source-repo": "sourceRepo",
|
|
1089
|
+
};
|
|
1017
1090
|
for (let i = 0; i < args.length; i++) {
|
|
1018
1091
|
const arg = args[i];
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1092
|
+
const eq = arg.indexOf("=");
|
|
1093
|
+
const rawOption = eq > 0 ? arg.slice(0, eq) : arg;
|
|
1094
|
+
const optionName = optionAliases[rawOption];
|
|
1095
|
+
if (optionName) {
|
|
1096
|
+
if (eq > 0) {
|
|
1097
|
+
options[optionName] = arg.slice(eq + 1);
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
options[optionName] = args[i + 1];
|
|
1101
|
+
i++;
|
|
1102
|
+
}
|
|
1022
1103
|
}
|
|
1023
1104
|
else if (arg.startsWith("-")) {
|
|
1024
1105
|
flags.add(arg);
|
|
@@ -1095,11 +1176,13 @@ async function main() {
|
|
|
1095
1176
|
}
|
|
1096
1177
|
const jsonMode = flags.has("--json");
|
|
1097
1178
|
const failOnFindings = flags.has("--fail-on-findings");
|
|
1179
|
+
const contextOptions = contextOptionsFromCli(options);
|
|
1098
1180
|
if (positional.length > 1) {
|
|
1099
1181
|
console.error(style(`\n Unknown command: ${positional[0]}`, c.red));
|
|
1100
1182
|
console.error(style(` Run: rmcode help\n`, c.dim));
|
|
1101
1183
|
process.exit(1);
|
|
1102
1184
|
}
|
|
1185
|
+
await enforceLatestCliForReview(jsonMode);
|
|
1103
1186
|
// Brand header for review runs — stderr so any stdout consumers stay
|
|
1104
1187
|
// safe; hidden in --json, non-TTY, NO_COLOR, and CI.
|
|
1105
1188
|
if (!jsonMode && !noColor && !isCI) {
|
|
@@ -1107,7 +1190,6 @@ async function main() {
|
|
|
1107
1190
|
}
|
|
1108
1191
|
// Piped stdin
|
|
1109
1192
|
if (hasPipedStdin()) {
|
|
1110
|
-
const lang = options.lang || "javascript";
|
|
1111
1193
|
const rl = (0, readline_1.createInterface)({ input: process.stdin });
|
|
1112
1194
|
const lines = [];
|
|
1113
1195
|
for await (const line of rl)
|
|
@@ -1117,11 +1199,12 @@ async function main() {
|
|
|
1117
1199
|
console.error(style(" No input received on stdin.", c.red));
|
|
1118
1200
|
process.exit(1);
|
|
1119
1201
|
}
|
|
1202
|
+
const stdinType = looksLikeUnifiedDiff(code) ? "diff" : "snippet";
|
|
1203
|
+
const stdinFiles = stdinType === "diff" ? filesFromDiff(code) : [];
|
|
1204
|
+
const lang = options.lang || detectLanguage(stdinFiles);
|
|
1120
1205
|
const startedAt = Date.now();
|
|
1121
1206
|
const spinner = createSpinner(`Uploading review request (stdin, ${lang})`);
|
|
1122
1207
|
try {
|
|
1123
|
-
const stdinType = looksLikeUnifiedDiff(code) ? "diff" : "snippet";
|
|
1124
|
-
const stdinFiles = stdinType === "diff" ? filesFromDiff(code) : [];
|
|
1125
1208
|
const result = await callAPI(code, lang, stdinType, stdinFiles, null, spinner);
|
|
1126
1209
|
spinner.stop("Reviewed stdin");
|
|
1127
1210
|
if (jsonMode) {
|
|
@@ -1161,7 +1244,7 @@ async function main() {
|
|
|
1161
1244
|
console.error(style(` ${file} is empty.`, c.yellow));
|
|
1162
1245
|
process.exit(0);
|
|
1163
1246
|
}
|
|
1164
|
-
await runReview(code, [file], file, jsonMode, failOnFindings, "file", detectLanguage([file]));
|
|
1247
|
+
await runReview(code, [file], file, jsonMode, failOnFindings, "file", detectLanguage([file]), undefined, contextOptions);
|
|
1165
1248
|
if (!jsonMode)
|
|
1166
1249
|
await maybeShowUpdateNudge();
|
|
1167
1250
|
return;
|
|
@@ -1208,7 +1291,7 @@ async function main() {
|
|
|
1208
1291
|
process.exit(0);
|
|
1209
1292
|
}
|
|
1210
1293
|
}
|
|
1211
|
-
await runReview(diff, files, label, jsonMode, failOnFindings, "diff", undefined, baseRef);
|
|
1294
|
+
await runReview(diff, files, label, jsonMode, failOnFindings, "diff", undefined, baseRef, contextOptions);
|
|
1212
1295
|
if (!jsonMode && !API_KEY) {
|
|
1213
1296
|
console.log(style(` Get ${FREE_PLAN_CREDITS_PER_MONTH} credits/month free: rmcode login`, c.dim));
|
|
1214
1297
|
console.log(style(" Auto-review every PR: rmcode install", c.dim));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@review-my-code/rmcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -38,11 +38,6 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"repository": {
|
|
42
|
-
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/review-my-code/review-my-code.git",
|
|
44
|
-
"directory": "cli"
|
|
45
|
-
},
|
|
46
41
|
"homepage": "https://review-my-code.com",
|
|
47
42
|
"author": "Alex Yang"
|
|
48
43
|
}
|