@review-my-code/rmcode 0.1.3 → 0.1.4

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +105 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,7 +21,7 @@ rmcode --staged # review staged changes
21
21
  rmcode --unstaged # review unstaged changes
22
22
  rmcode --all # review uncommitted tracked changes and safe text files
23
23
  rmcode src/auth.ts # review one file
24
- git diff main | rmcode --lang typescript
24
+ git diff main | rmcode # review a custom diff; language is detected from filenames
25
25
  rmcode --json --fail-on-findings
26
26
  ```
27
27
 
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.3";
12
+ const VERSION = "0.1.4";
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 = git(["remote", "get-url", "origin"]);
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
- "--pr=0",
523
+ `--pr=${contextOptions.prNumber ?? 0}`,
494
524
  `--diff-file=${diffPath}`,
495
525
  `--output-bundle=${bundlePath}`,
496
- `--source-repo=${process.env.RMC_SOURCE_REPO || gitRepoName() || "local/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> Language hint for stdin input
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 --lang typescript
900
+ git diff abc123..def456 | rmcode
867
901
 
868
902
  ${style("# Review one file", c.dim)}
869
903
  rmcode src/auth.ts
@@ -879,6 +913,9 @@ ${header}
879
913
  RMC_API_KEY API key for authenticated reviews (get one: rmcode login)
880
914
  RMC_API_URL API endpoint (default: https://review-my-code.com)
881
915
  RMC_APP_URL App URL for login (default: https://review-my-code.com)
916
+ RMC_PR_TITLE Optional PR title hint for repository-context reviews
917
+ RMC_PR_NUMBER Optional PR number hint for CI/benchmark parity
918
+ RMC_SOURCE_REPO Optional owner/repo hint when no origin remote exists
882
919
 
883
920
  ${style("PRIVACY", c.bold)}
884
921
 
@@ -892,7 +929,8 @@ ${header}
892
929
  }
893
930
  // ── Self-update ───────────────────────────────────────────────────────
894
931
  const PACKAGE_NAME = "@review-my-code/rmcode";
895
- const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@review-my-code%2Frmcode/latest";
932
+ const REGISTRY_LATEST_URL = process.env.RMC_REGISTRY_LATEST_URL ||
933
+ "https://registry.npmjs.org/@review-my-code%2Frmcode/latest";
896
934
  /** Resolved real path of the running entry script, for install detection. */
897
935
  function entryScriptPath() {
898
936
  const binPath = process.argv[1] || "";
@@ -1010,15 +1048,60 @@ async function maybeShowUpdateNudge() {
1010
1048
  /* silent */
1011
1049
  }
1012
1050
  }
1051
+ async function enforceLatestCliForReview(jsonMode) {
1052
+ let latest = null;
1053
+ try {
1054
+ latest = await fetchLatestVersion();
1055
+ }
1056
+ catch {
1057
+ return;
1058
+ }
1059
+ if (!latest || compareVersions(latest, VERSION) <= 0)
1060
+ return;
1061
+ const update = updateCommandFor(entryScriptPath());
1062
+ const command = [update.cmd, ...update.args].join(" ");
1063
+ const error = `Update required: rmcode v${latest} is available, but this is v${VERSION}. Run \`${command}\` before reviewing.`;
1064
+ if (jsonMode) {
1065
+ console.log(JSON.stringify({
1066
+ success: false,
1067
+ error,
1068
+ code: "RMC-1006",
1069
+ meta: {
1070
+ currentVersion: VERSION,
1071
+ latestVersion: latest,
1072
+ updateCommand: command,
1073
+ },
1074
+ }));
1075
+ }
1076
+ else {
1077
+ console.error(style(`\n ${error}\n`, c.yellow));
1078
+ }
1079
+ process.exit(1);
1080
+ }
1013
1081
  function parseArgs(args) {
1014
1082
  const flags = new Set();
1015
1083
  const options = {};
1016
1084
  const positional = [];
1085
+ const optionAliases = {
1086
+ "--lang": "lang",
1087
+ "--title": "title",
1088
+ "--pr-title": "title",
1089
+ "--pr-number": "prNumber",
1090
+ "--source-repo": "sourceRepo",
1091
+ };
1017
1092
  for (let i = 0; i < args.length; i++) {
1018
1093
  const arg = args[i];
1019
- if (arg === "--lang") {
1020
- options.lang = args[i + 1];
1021
- i++;
1094
+ const eq = arg.indexOf("=");
1095
+ const rawOption = eq > 0 ? arg.slice(0, eq) : arg;
1096
+ const optionName = optionAliases[rawOption];
1097
+ if (optionName) {
1098
+ if (eq > 0) {
1099
+ options[optionName] = arg.slice(eq + 1);
1100
+ }
1101
+ else {
1102
+ options[optionName] = args[i + 1];
1103
+ i++;
1104
+ }
1022
1105
  }
1023
1106
  else if (arg.startsWith("-")) {
1024
1107
  flags.add(arg);
@@ -1095,11 +1178,13 @@ async function main() {
1095
1178
  }
1096
1179
  const jsonMode = flags.has("--json");
1097
1180
  const failOnFindings = flags.has("--fail-on-findings");
1181
+ const contextOptions = contextOptionsFromCli(options);
1098
1182
  if (positional.length > 1) {
1099
1183
  console.error(style(`\n Unknown command: ${positional[0]}`, c.red));
1100
1184
  console.error(style(` Run: rmcode help\n`, c.dim));
1101
1185
  process.exit(1);
1102
1186
  }
1187
+ await enforceLatestCliForReview(jsonMode);
1103
1188
  // Brand header for review runs — stderr so any stdout consumers stay
1104
1189
  // safe; hidden in --json, non-TTY, NO_COLOR, and CI.
1105
1190
  if (!jsonMode && !noColor && !isCI) {
@@ -1107,7 +1192,6 @@ async function main() {
1107
1192
  }
1108
1193
  // Piped stdin
1109
1194
  if (hasPipedStdin()) {
1110
- const lang = options.lang || "javascript";
1111
1195
  const rl = (0, readline_1.createInterface)({ input: process.stdin });
1112
1196
  const lines = [];
1113
1197
  for await (const line of rl)
@@ -1117,11 +1201,12 @@ async function main() {
1117
1201
  console.error(style(" No input received on stdin.", c.red));
1118
1202
  process.exit(1);
1119
1203
  }
1204
+ const stdinType = looksLikeUnifiedDiff(code) ? "diff" : "snippet";
1205
+ const stdinFiles = stdinType === "diff" ? filesFromDiff(code) : [];
1206
+ const lang = options.lang || detectLanguage(stdinFiles);
1120
1207
  const startedAt = Date.now();
1121
1208
  const spinner = createSpinner(`Uploading review request (stdin, ${lang})`);
1122
1209
  try {
1123
- const stdinType = looksLikeUnifiedDiff(code) ? "diff" : "snippet";
1124
- const stdinFiles = stdinType === "diff" ? filesFromDiff(code) : [];
1125
1210
  const result = await callAPI(code, lang, stdinType, stdinFiles, null, spinner);
1126
1211
  spinner.stop("Reviewed stdin");
1127
1212
  if (jsonMode) {
@@ -1161,7 +1246,7 @@ async function main() {
1161
1246
  console.error(style(` ${file} is empty.`, c.yellow));
1162
1247
  process.exit(0);
1163
1248
  }
1164
- await runReview(code, [file], file, jsonMode, failOnFindings, "file", detectLanguage([file]));
1249
+ await runReview(code, [file], file, jsonMode, failOnFindings, "file", detectLanguage([file]), undefined, contextOptions);
1165
1250
  if (!jsonMode)
1166
1251
  await maybeShowUpdateNudge();
1167
1252
  return;
@@ -1208,7 +1293,7 @@ async function main() {
1208
1293
  process.exit(0);
1209
1294
  }
1210
1295
  }
1211
- await runReview(diff, files, label, jsonMode, failOnFindings, "diff", undefined, baseRef);
1296
+ await runReview(diff, files, label, jsonMode, failOnFindings, "diff", undefined, baseRef, contextOptions);
1212
1297
  if (!jsonMode && !API_KEY) {
1213
1298
  console.log(style(` Get ${FREE_PLAN_CREDITS_PER_MONTH} credits/month free: rmcode login`, c.dim));
1214
1299
  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",
3
+ "version": "0.1.4",
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",