@review-my-code/rmcode 0.1.0-alpha.1 → 0.1.1
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 +29 -0
- package/dist/cli.js +500 -247
- package/package.json +8 -3
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# RMCode CLI
|
|
2
|
+
|
|
3
|
+
Run Review My Code from your terminal.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @review-my-code/rmcode
|
|
7
|
+
rmcode login
|
|
8
|
+
export RMC_API_KEY=rmc_...
|
|
9
|
+
rmcode --staged
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The CLI sends the selected diff, file, or stdin content to RMCode for analysis.
|
|
13
|
+
Anonymous CLI reviews are disabled by default, so set `RMC_API_KEY` before
|
|
14
|
+
running a review.
|
|
15
|
+
|
|
16
|
+
## Common Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
rmcode # review branch diff from merge base
|
|
20
|
+
rmcode --staged # review staged changes
|
|
21
|
+
rmcode --unstaged # review unstaged changes
|
|
22
|
+
rmcode --all # review uncommitted tracked changes and safe text files
|
|
23
|
+
rmcode src/auth.ts # review one file
|
|
24
|
+
git diff main | rmcode --lang typescript
|
|
25
|
+
rmcode --json --fail-on-findings
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `RMC_API_URL` to point at a non-production API and `RMC_APP_URL` to point
|
|
29
|
+
`rmcode login` at a non-production web app.
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,13 @@ const fs_1 = require("fs");
|
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const readline_1 = require("readline");
|
|
8
8
|
const API_URL = process.env.RMC_API_URL || "https://review-my-code.com";
|
|
9
|
-
const
|
|
9
|
+
const APP_URL = process.env.RMC_APP_URL || "https://review-my-code.com";
|
|
10
|
+
const VERSION = "0.1.1";
|
|
11
|
+
const FREE_PLAN_CREDITS_PER_MONTH = 30;
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 290_000;
|
|
13
|
+
const POLL_TIMEOUT_MS = 10 * 60_000;
|
|
14
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
15
|
+
const MAX_UNTRACKED_FILE_BYTES = 512 * 1024;
|
|
10
16
|
// ── Colors (no dependencies) ──────────────────────────────────────────
|
|
11
17
|
const c = {
|
|
12
18
|
reset: "\x1b[0m",
|
|
@@ -15,15 +21,12 @@ const c = {
|
|
|
15
21
|
red: "\x1b[31m",
|
|
16
22
|
green: "\x1b[32m",
|
|
17
23
|
yellow: "\x1b[33m",
|
|
18
|
-
blue: "\x1b[34m",
|
|
19
|
-
magenta: "\x1b[35m",
|
|
20
24
|
cyan: "\x1b[36m",
|
|
21
25
|
gray: "\x1b[90m",
|
|
22
26
|
white: "\x1b[97m",
|
|
23
27
|
bgRed: "\x1b[41m",
|
|
24
28
|
bgYellow: "\x1b[43m",
|
|
25
29
|
bgGreen: "\x1b[42m",
|
|
26
|
-
bgBlue: "\x1b[44m",
|
|
27
30
|
};
|
|
28
31
|
const isCI = process.env.CI === "true";
|
|
29
32
|
const noColor = process.env.NO_COLOR === "1" || !process.stdout.isTTY;
|
|
@@ -44,63 +47,198 @@ function severityBadge(severity) {
|
|
|
44
47
|
return style(` ${severity.toUpperCase()} `, c.dim);
|
|
45
48
|
}
|
|
46
49
|
// ── Git helpers ───────────────────────────────────────────────────────
|
|
47
|
-
function
|
|
50
|
+
function git(args) {
|
|
51
|
+
return (0, child_process_1.execFileSync)("git", args, {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
54
|
+
}).trim();
|
|
55
|
+
}
|
|
56
|
+
function isGitRepo() {
|
|
48
57
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return diff.trim() || null;
|
|
58
|
+
git(["rev-parse", "--is-inside-work-tree"]);
|
|
59
|
+
return true;
|
|
52
60
|
}
|
|
53
61
|
catch {
|
|
54
|
-
return
|
|
62
|
+
return false;
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
|
-
function
|
|
65
|
+
function gitWorktreeRoot() {
|
|
66
|
+
return git(["rev-parse", "--show-toplevel"]);
|
|
67
|
+
}
|
|
68
|
+
function mergeBase() {
|
|
69
|
+
const refs = [];
|
|
58
70
|
try {
|
|
59
|
-
|
|
71
|
+
refs.push(git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]));
|
|
60
72
|
}
|
|
61
73
|
catch {
|
|
62
|
-
|
|
74
|
+
// Remote HEAD is not available.
|
|
75
|
+
}
|
|
76
|
+
for (const ref of [
|
|
77
|
+
...refs,
|
|
78
|
+
"origin/main",
|
|
79
|
+
"origin/master",
|
|
80
|
+
"main",
|
|
81
|
+
"master",
|
|
82
|
+
]) {
|
|
83
|
+
try {
|
|
84
|
+
git(["rev-parse", "--verify", ref]);
|
|
85
|
+
return git(["merge-base", ref, "HEAD"]);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Try the next common default branch ref.
|
|
89
|
+
}
|
|
63
90
|
}
|
|
91
|
+
throw new Error("Could not determine a base branch. Set an upstream branch, or run rmcode --staged, rmcode --all, or pipe an explicit git diff.");
|
|
64
92
|
}
|
|
65
|
-
function
|
|
93
|
+
function diffFromMergeBase() {
|
|
94
|
+
const base = mergeBase();
|
|
95
|
+
const diff = git(["diff", `${base}...HEAD`]);
|
|
96
|
+
const files = git(["diff", `${base}...HEAD`, "--name-only"])
|
|
97
|
+
.split("\n")
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
return { diff, files };
|
|
100
|
+
}
|
|
101
|
+
function diffStaged() {
|
|
102
|
+
const diff = git(["diff", "--cached"]);
|
|
103
|
+
const files = git(["diff", "--cached", "--name-only"])
|
|
104
|
+
.split("\n")
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
return { diff, files };
|
|
107
|
+
}
|
|
108
|
+
function diffUnstaged() {
|
|
109
|
+
const diff = git(["diff"]);
|
|
110
|
+
const files = git(["diff", "--name-only"]).split("\n").filter(Boolean);
|
|
111
|
+
return { diff, files };
|
|
112
|
+
}
|
|
113
|
+
function diffAll() {
|
|
114
|
+
const root = gitWorktreeRoot();
|
|
115
|
+
const trackedDiff = git(["diff", "HEAD"]);
|
|
116
|
+
const trackedFiles = git(["diff", "HEAD", "--name-only"])
|
|
117
|
+
.split("\n")
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
const untrackedFiles = git(["ls-files", "--others", "--exclude-standard"])
|
|
120
|
+
.split("\n")
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const untrackedDiffs = untrackedFiles
|
|
123
|
+
.map((file) => pseudoDiffForNewFile(file, root))
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
const diff = [trackedDiff, ...untrackedDiffs].filter(Boolean).join("\n\n");
|
|
126
|
+
const files = [...trackedFiles, ...untrackedFiles];
|
|
127
|
+
return { diff, files };
|
|
128
|
+
}
|
|
129
|
+
function pseudoDiffForNewFile(file, worktreeRoot) {
|
|
66
130
|
try {
|
|
67
|
-
|
|
131
|
+
if ((0, path_1.isAbsolute)(file))
|
|
132
|
+
return "";
|
|
133
|
+
const stat = (0, fs_1.lstatSync)(file);
|
|
134
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
135
|
+
return "";
|
|
136
|
+
if (stat.size > MAX_UNTRACKED_FILE_BYTES)
|
|
137
|
+
return "";
|
|
138
|
+
const realRoot = (0, fs_1.realpathSync)(worktreeRoot);
|
|
139
|
+
const realFile = (0, fs_1.realpathSync)(file);
|
|
140
|
+
const rel = (0, path_1.relative)(realRoot, realFile);
|
|
141
|
+
if (rel.startsWith("..") || (0, path_1.isAbsolute)(rel))
|
|
142
|
+
return "";
|
|
143
|
+
const contents = (0, fs_1.readFileSync)(realFile, "utf-8");
|
|
144
|
+
if (contents.includes("\0"))
|
|
145
|
+
return "";
|
|
146
|
+
const added = contents
|
|
147
|
+
.split("\n")
|
|
148
|
+
.map((line) => `+${line}`)
|
|
149
|
+
.join("\n");
|
|
150
|
+
return `diff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${contents.split("\n").length} @@\n${added}`;
|
|
68
151
|
}
|
|
69
152
|
catch {
|
|
70
|
-
return
|
|
153
|
+
return "";
|
|
71
154
|
}
|
|
72
155
|
}
|
|
73
|
-
function
|
|
156
|
+
function gitRepoName() {
|
|
74
157
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
158
|
+
const remote = git(["remote", "get-url", "origin"]);
|
|
159
|
+
const m = remote.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
160
|
+
return m ? m[1] : null;
|
|
77
161
|
}
|
|
78
162
|
catch {
|
|
79
|
-
return
|
|
163
|
+
return null;
|
|
80
164
|
}
|
|
81
165
|
}
|
|
82
|
-
function
|
|
166
|
+
function currentBranch() {
|
|
83
167
|
try {
|
|
84
|
-
|
|
85
|
-
encoding: "utf-8",
|
|
86
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
87
|
-
}).trim();
|
|
88
|
-
const httpsMatch = remote.match(/github\.com\/([^/]+\/[^/.]+)/);
|
|
89
|
-
if (httpsMatch)
|
|
90
|
-
return httpsMatch[1];
|
|
91
|
-
const sshMatch = remote.match(/github\.com:([^/]+\/[^/.]+)/);
|
|
92
|
-
if (sshMatch)
|
|
93
|
-
return sshMatch[1];
|
|
94
|
-
return null;
|
|
168
|
+
return git(["branch", "--show-current"]);
|
|
95
169
|
}
|
|
96
170
|
catch {
|
|
97
|
-
return
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function filesFromDiff(diff) {
|
|
175
|
+
const files = new Set();
|
|
176
|
+
const re = /^diff --git a\/.+ b\/(.+)$/gm;
|
|
177
|
+
let match;
|
|
178
|
+
while ((match = re.exec(diff)) !== null) {
|
|
179
|
+
files.add(match[1]);
|
|
180
|
+
}
|
|
181
|
+
return [...files];
|
|
182
|
+
}
|
|
183
|
+
function looksLikeUnifiedDiff(value) {
|
|
184
|
+
return value.startsWith("diff --git ") || /\n@@ -\d/.test(value);
|
|
185
|
+
}
|
|
186
|
+
function hasPipedStdin() {
|
|
187
|
+
if (process.stdin.isTTY)
|
|
188
|
+
return false;
|
|
189
|
+
try {
|
|
190
|
+
const stat = (0, fs_1.fstatSync)(0);
|
|
191
|
+
return stat.isFIFO() || stat.isFile() || stat.isSocket();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
98
195
|
}
|
|
99
196
|
}
|
|
100
197
|
const API_KEY = process.env.RMC_API_KEY || null;
|
|
198
|
+
async function fetchReviewJson(url, init) {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
201
|
+
try {
|
|
202
|
+
const resp = await fetch(url, { ...init, signal: controller.signal });
|
|
203
|
+
const text = await resp.text();
|
|
204
|
+
let body;
|
|
205
|
+
try {
|
|
206
|
+
body = text ? JSON.parse(text) : { success: false };
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
body = {
|
|
210
|
+
success: false,
|
|
211
|
+
error: `RMCode API returned a non-JSON response (${resp.status}).`,
|
|
212
|
+
code: "RMC-5000",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (!resp.ok && body.success !== false) {
|
|
216
|
+
body = {
|
|
217
|
+
...body,
|
|
218
|
+
success: false,
|
|
219
|
+
error: body.error || `RMCode API request failed (${resp.status}).`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { status: resp.status, body };
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error && error.name === "AbortError"
|
|
226
|
+
? "RMCode API request timed out."
|
|
227
|
+
: error instanceof Error
|
|
228
|
+
? error.message
|
|
229
|
+
: "RMCode API request failed.";
|
|
230
|
+
return {
|
|
231
|
+
status: 0,
|
|
232
|
+
body: { success: false, error: message, code: "RMC-5000" },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
101
239
|
async function callAuthenticatedAPI(code, language, type, files) {
|
|
102
240
|
const repo = gitRepoName();
|
|
103
|
-
const
|
|
241
|
+
const { status, body } = await fetchReviewJson(`${API_URL}/api/reviews/cli`, {
|
|
104
242
|
method: "POST",
|
|
105
243
|
headers: {
|
|
106
244
|
"Content-Type": "application/json",
|
|
@@ -108,54 +246,89 @@ async function callAuthenticatedAPI(code, language, type, files) {
|
|
|
108
246
|
},
|
|
109
247
|
body: JSON.stringify({ type, value: code, language, files, repo }),
|
|
110
248
|
});
|
|
111
|
-
|
|
249
|
+
if (status === 202 && body.data?.accessToken) {
|
|
250
|
+
return pollForResults(`${API_URL}/api/reviews/cli/${body.data.accessToken}`, body.meta, { "X-API-Key": API_KEY });
|
|
251
|
+
}
|
|
252
|
+
return body;
|
|
112
253
|
}
|
|
113
|
-
async function callAnonymousAPI(code, language) {
|
|
254
|
+
async function callAnonymousAPI(code, language, type, files) {
|
|
114
255
|
const repo = gitRepoName();
|
|
115
|
-
const
|
|
256
|
+
const { status, body } = await fetchReviewJson(`${API_URL}/api/reviews/anonymous`, {
|
|
116
257
|
method: "POST",
|
|
117
258
|
headers: { "Content-Type": "application/json" },
|
|
118
|
-
body: JSON.stringify({ type
|
|
259
|
+
body: JSON.stringify({ type, value: code, language, files, repo }),
|
|
119
260
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (resp.status === 202 && result.data?.accessToken) {
|
|
123
|
-
return pollForResults(result.data.accessToken, result.meta);
|
|
261
|
+
if (status === 202 && body.data?.accessToken) {
|
|
262
|
+
return pollForResults(`${API_URL}/api/reviews/anonymous/${body.data.accessToken}`, body.meta);
|
|
124
263
|
}
|
|
125
|
-
return
|
|
264
|
+
return body;
|
|
126
265
|
}
|
|
127
|
-
async function pollForResults(
|
|
128
|
-
const deadline = Date.now() +
|
|
266
|
+
async function pollForResults(url, initialMeta, headers) {
|
|
267
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
129
268
|
while (Date.now() < deadline) {
|
|
130
|
-
await new Promise((r) => setTimeout(r,
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
269
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
270
|
+
const { status, body } = await fetchReviewJson(url, { headers });
|
|
271
|
+
if (status >= 400 || body.success === false) {
|
|
272
|
+
if (initialMeta && !body.meta)
|
|
273
|
+
body.meta = initialMeta;
|
|
274
|
+
return body;
|
|
275
|
+
}
|
|
276
|
+
if (body.data?.status === "completed" || body.data?.status === "failed") {
|
|
277
|
+
if (initialMeta && !body.meta)
|
|
278
|
+
body.meta = initialMeta;
|
|
279
|
+
return body;
|
|
137
280
|
}
|
|
138
281
|
}
|
|
139
|
-
return { success: false, error: "Review timed out after
|
|
282
|
+
return { success: false, error: "Review timed out after 10 minutes." };
|
|
140
283
|
}
|
|
141
284
|
function callAPI(code, language, type = "snippet", files = []) {
|
|
142
|
-
if (API_KEY)
|
|
285
|
+
if (API_KEY)
|
|
143
286
|
return callAuthenticatedAPI(code, language, type, files);
|
|
144
|
-
|
|
145
|
-
|
|
287
|
+
return Promise.resolve({
|
|
288
|
+
success: false,
|
|
289
|
+
error: "API key required. Run `rmcode login`, then set RMC_API_KEY before sending code for review.",
|
|
290
|
+
code: "RMC-1002",
|
|
291
|
+
meta: {
|
|
292
|
+
plan: null,
|
|
293
|
+
reviewer: "scout",
|
|
294
|
+
message: "Anonymous CLI reviews are disabled so your credits stay tied to your RMCode account.",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
146
297
|
}
|
|
147
298
|
// ── Language detection ────────────────────────────────────────────────
|
|
148
299
|
const EXT_TO_LANG = {
|
|
149
|
-
".ts": "typescript",
|
|
150
|
-
".
|
|
151
|
-
".
|
|
152
|
-
".
|
|
153
|
-
".
|
|
154
|
-
".
|
|
155
|
-
".
|
|
156
|
-
".
|
|
157
|
-
".
|
|
158
|
-
".
|
|
300
|
+
".ts": "typescript",
|
|
301
|
+
".tsx": "typescript",
|
|
302
|
+
".js": "javascript",
|
|
303
|
+
".jsx": "javascript",
|
|
304
|
+
".py": "python",
|
|
305
|
+
".java": "java",
|
|
306
|
+
".go": "go",
|
|
307
|
+
".rs": "rust",
|
|
308
|
+
".c": "c",
|
|
309
|
+
".cpp": "cpp",
|
|
310
|
+
".h": "c",
|
|
311
|
+
".cs": "csharp",
|
|
312
|
+
".rb": "ruby",
|
|
313
|
+
".php": "php",
|
|
314
|
+
".swift": "swift",
|
|
315
|
+
".kt": "kotlin",
|
|
316
|
+
".scala": "scala",
|
|
317
|
+
".lua": "lua",
|
|
318
|
+
".dart": "dart",
|
|
319
|
+
".v": "verilog",
|
|
320
|
+
".sv": "verilog",
|
|
321
|
+
".vhd": "vhdl",
|
|
322
|
+
".sh": "bash",
|
|
323
|
+
".zig": "zig",
|
|
324
|
+
".sol": "solidity",
|
|
325
|
+
".jl": "julia",
|
|
326
|
+
".r": "r",
|
|
327
|
+
".ex": "elixir",
|
|
328
|
+
".erl": "erlang",
|
|
329
|
+
".hs": "haskell",
|
|
330
|
+
".ml": "ocaml",
|
|
331
|
+
".fs": "fsharp",
|
|
159
332
|
};
|
|
160
333
|
function detectLanguage(files) {
|
|
161
334
|
for (const f of files) {
|
|
@@ -166,33 +339,30 @@ function detectLanguage(files) {
|
|
|
166
339
|
return "javascript";
|
|
167
340
|
}
|
|
168
341
|
// ── Output formatting ─────────────────────────────────────────────────
|
|
169
|
-
function printFindings(findings
|
|
170
|
-
if (jsonMode) {
|
|
171
|
-
console.log(JSON.stringify(findings, null, 2));
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
342
|
+
function printFindings(findings) {
|
|
174
343
|
if (findings.length === 0) {
|
|
175
344
|
console.log(`\n ${style("No issues found.", c.green, c.bold)} Your code looks good.\n`);
|
|
176
345
|
return;
|
|
177
346
|
}
|
|
178
347
|
const counts = {};
|
|
179
|
-
for (const f of findings)
|
|
348
|
+
for (const f of findings)
|
|
180
349
|
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
181
|
-
|
|
182
|
-
|
|
350
|
+
const summary = Object.entries(counts)
|
|
351
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
352
|
+
.join(" · ");
|
|
183
353
|
console.log(`\n ${style(`${findings.length} finding${findings.length !== 1 ? "s" : ""}`, c.bold, c.white)} · ${summary}\n`);
|
|
184
354
|
for (const f of findings) {
|
|
185
|
-
const location = f.filePath
|
|
355
|
+
const location = f.filePath
|
|
356
|
+
? `${style(f.filePath, c.cyan)}${f.line ? `:${f.line}` : ""}`
|
|
357
|
+
: "";
|
|
186
358
|
console.log(` ${severityBadge(f.severity)} ${style(f.title, c.bold, c.white)}`);
|
|
187
359
|
if (location)
|
|
188
360
|
console.log(` ${style("at", c.dim)} ${location}`);
|
|
189
361
|
console.log(` ${style(f.explanation, c.gray)}`);
|
|
190
|
-
if (f.codeContext)
|
|
362
|
+
if (f.codeContext)
|
|
191
363
|
console.log(` ${style(f.codeContext, c.dim)}`);
|
|
192
|
-
|
|
193
|
-
if (f.suggestedCode) {
|
|
364
|
+
if (f.suggestedCode)
|
|
194
365
|
console.log(` ${style("fix:", c.green)} ${style(f.suggestedCode, c.green)}`);
|
|
195
|
-
}
|
|
196
366
|
console.log();
|
|
197
367
|
}
|
|
198
368
|
}
|
|
@@ -203,30 +373,33 @@ function printMeta(meta) {
|
|
|
203
373
|
if (meta.reviewer)
|
|
204
374
|
parts.push(meta.reviewer.charAt(0).toUpperCase() + meta.reviewer.slice(1));
|
|
205
375
|
if (meta.plan) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
else {
|
|
211
|
-
parts.push(planLabel);
|
|
212
|
-
}
|
|
376
|
+
const label = meta.plan.charAt(0).toUpperCase() + meta.plan.slice(1);
|
|
377
|
+
parts.push(meta.creditsRemaining != null
|
|
378
|
+
? `${label} (${meta.creditsRemaining} cr left)`
|
|
379
|
+
: label);
|
|
213
380
|
}
|
|
214
|
-
if (
|
|
215
|
-
parts.push(`Free review (${meta.quotaUsed} today)`);
|
|
216
|
-
}
|
|
217
|
-
if (parts.length > 0) {
|
|
381
|
+
if (parts.length > 0)
|
|
218
382
|
console.log(` ${style(parts.join(" · "), c.dim)}`);
|
|
219
|
-
|
|
220
|
-
if (meta.message) {
|
|
383
|
+
if (meta.message)
|
|
221
384
|
console.log(` ${style(meta.message, c.yellow)}`);
|
|
222
|
-
|
|
385
|
+
}
|
|
386
|
+
function printJsonResponse(result) {
|
|
387
|
+
console.log(JSON.stringify(result, null, 2));
|
|
388
|
+
}
|
|
389
|
+
function hasBlockingFindings(result) {
|
|
390
|
+
const findings = result.data?.findings?.findings || [];
|
|
391
|
+
return (findings.length > 0 || result.data?.findings?.verdict === "request_changes");
|
|
223
392
|
}
|
|
224
393
|
// ── Spinner ───────────────────────────────────────────────────────────
|
|
225
394
|
function createSpinner(msg) {
|
|
226
395
|
if (noColor || isCI) {
|
|
227
396
|
process.stderr.write(` ${msg}...\n`);
|
|
228
|
-
return {
|
|
229
|
-
|
|
397
|
+
return {
|
|
398
|
+
stop: (final) => {
|
|
399
|
+
if (final)
|
|
400
|
+
process.stderr.write(` ${final}\n`);
|
|
401
|
+
},
|
|
402
|
+
};
|
|
230
403
|
}
|
|
231
404
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
232
405
|
let i = 0;
|
|
@@ -240,104 +413,30 @@ function createSpinner(msg) {
|
|
|
240
413
|
},
|
|
241
414
|
};
|
|
242
415
|
}
|
|
243
|
-
// ──
|
|
244
|
-
async function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
const files = staged ? gitStagedFiles() : gitUnstagedFiles();
|
|
250
|
-
const diff = gitDiff(staged);
|
|
251
|
-
if (!diff) {
|
|
252
|
-
const which = staged ? "staged" : "unstaged";
|
|
253
|
-
console.error(style(` No ${which} changes found.`, c.yellow));
|
|
254
|
-
if (!staged && gitStagedFiles().length > 0) {
|
|
255
|
-
console.error(style(" Try: rmcode --staged", c.dim));
|
|
256
|
-
}
|
|
257
|
-
process.exit(0);
|
|
258
|
-
}
|
|
259
|
-
const lang = detectLanguage(files);
|
|
260
|
-
const spinner = createSpinner(`Reviewing ${files.length} file${files.length !== 1 ? "s" : ""} (${lang})`);
|
|
261
|
-
try {
|
|
262
|
-
const result = await callAPI(diff, lang, "diff", files);
|
|
263
|
-
spinner.stop(`Reviewed ${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
264
|
-
if (!result.success) {
|
|
265
|
-
console.error(style(` ${result.error || "Review failed."}`, c.red));
|
|
266
|
-
if (result.meta?.message) {
|
|
267
|
-
console.log(` ${style(result.meta.message, c.yellow)}`);
|
|
268
|
-
}
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
if (!jsonMode)
|
|
272
|
-
printMeta(result.meta);
|
|
273
|
-
if (result.data?.findings?.findings) {
|
|
274
|
-
printFindings(result.data.findings.findings, jsonMode);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch (err) {
|
|
278
|
-
spinner.stop("Review failed");
|
|
279
|
-
console.error(style(` ${err.message}`, c.red));
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async function reviewFile(filePath, jsonMode) {
|
|
284
|
-
const resolved = (0, path_1.resolve)(filePath);
|
|
285
|
-
if (!(0, fs_1.existsSync)(resolved)) {
|
|
286
|
-
console.error(style(` File not found: ${filePath}`, c.red));
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
289
|
-
const content = (0, fs_1.readFileSync)(resolved, "utf-8");
|
|
290
|
-
const ext = (0, path_1.extname)(filePath).toLowerCase();
|
|
291
|
-
const lang = EXT_TO_LANG[ext] || "javascript";
|
|
292
|
-
const spinner = createSpinner(`Reviewing ${(0, path_1.basename)(filePath)} (${lang})`);
|
|
416
|
+
// ── Review runner ─────────────────────────────────────────────────────
|
|
417
|
+
async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language) {
|
|
418
|
+
const lang = language || detectLanguage(files);
|
|
419
|
+
const spinner = createSpinner(`Reviewing ${label} (${files.length} file${files.length !== 1 ? "s" : ""}, ${lang})`);
|
|
293
420
|
try {
|
|
294
|
-
const result = await callAPI(content, lang,
|
|
295
|
-
spinner.stop(`Reviewed ${
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
if (result.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!jsonMode)
|
|
304
|
-
printMeta(result.meta);
|
|
305
|
-
if (result.data?.findings?.findings) {
|
|
306
|
-
printFindings(result.data.findings.findings, jsonMode);
|
|
421
|
+
const result = await callAPI(content, lang, type, files);
|
|
422
|
+
spinner.stop(`Reviewed ${label}`);
|
|
423
|
+
if (jsonMode) {
|
|
424
|
+
printJsonResponse(result);
|
|
425
|
+
if (!result.success)
|
|
426
|
+
process.exit(1);
|
|
427
|
+
if (failOnFindings && hasBlockingFindings(result))
|
|
428
|
+
process.exit(2);
|
|
429
|
+
return;
|
|
307
430
|
}
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
spinner.stop("Review failed");
|
|
311
|
-
console.error(style(` ${err.message}`, c.red));
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
async function reviewStdin(lang, jsonMode) {
|
|
316
|
-
const rl = (0, readline_1.createInterface)({ input: process.stdin });
|
|
317
|
-
const lines = [];
|
|
318
|
-
for await (const line of rl)
|
|
319
|
-
lines.push(line);
|
|
320
|
-
const code = lines.join("\n");
|
|
321
|
-
if (!code.trim()) {
|
|
322
|
-
console.error(style(" No input received on stdin.", c.red));
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
const spinner = createSpinner(`Reviewing stdin (${lang})`);
|
|
326
|
-
try {
|
|
327
|
-
const result = await callAPI(code, lang, "snippet", []);
|
|
328
|
-
spinner.stop("Reviewed stdin");
|
|
329
431
|
if (!result.success) {
|
|
330
432
|
console.error(style(` ${result.error || "Review failed."}`, c.red));
|
|
331
|
-
if (result.meta?.message)
|
|
433
|
+
if (result.meta?.message)
|
|
332
434
|
console.log(` ${style(result.meta.message, c.yellow)}`);
|
|
333
|
-
}
|
|
334
435
|
process.exit(1);
|
|
335
436
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
printFindings(result.data.findings.findings, jsonMode);
|
|
340
|
-
}
|
|
437
|
+
printMeta(result.meta);
|
|
438
|
+
if (result.data?.findings?.findings)
|
|
439
|
+
printFindings(result.data.findings.findings);
|
|
341
440
|
}
|
|
342
441
|
catch (err) {
|
|
343
442
|
spinner.stop("Review failed");
|
|
@@ -345,48 +444,71 @@ async function reviewStdin(lang, jsonMode) {
|
|
|
345
444
|
process.exit(1);
|
|
346
445
|
}
|
|
347
446
|
}
|
|
348
|
-
// ── Help
|
|
447
|
+
// ── Help ──────────────────────────────────────────────────────────────
|
|
349
448
|
function printHelp() {
|
|
350
449
|
console.log(`
|
|
351
|
-
${style("rmcode", c.bold, c.green)} — AI code review from your terminal
|
|
450
|
+
${style("rmcode", c.bold, c.green)} ${style(`v${VERSION}`, c.dim)} — AI code review from your terminal
|
|
352
451
|
|
|
353
|
-
${style("
|
|
452
|
+
${style("COMMANDS", c.bold)}
|
|
354
453
|
|
|
355
|
-
${style("rmcode", c.cyan)} Review
|
|
356
|
-
${style("rmcode --staged", c.cyan)} Review staged changes
|
|
357
|
-
${style("rmcode
|
|
358
|
-
${style("
|
|
454
|
+
${style("rmcode", c.cyan)} Review changes from merge base (default)
|
|
455
|
+
${style("rmcode --staged", c.cyan)} Review staged changes only
|
|
456
|
+
${style("rmcode --unstaged", c.cyan)} Review unstaged changes only
|
|
457
|
+
${style("rmcode --all", c.cyan)} Review all uncommitted changes (staged + unstaged)
|
|
458
|
+
${style("rmcode <file>", c.cyan)} Review a single local file
|
|
359
459
|
${style("rmcode login", c.cyan)} Set up your API key
|
|
360
|
-
${style("rmcode install", c.cyan)} Install the GitHub App for PR reviews
|
|
460
|
+
${style("rmcode install", c.cyan)} Install the GitHub App for automatic PR reviews
|
|
461
|
+
${style("rmcode help", c.cyan)} Show this help
|
|
361
462
|
|
|
362
463
|
${style("OPTIONS", c.bold)}
|
|
363
464
|
|
|
364
|
-
--
|
|
365
|
-
--
|
|
366
|
-
--
|
|
367
|
-
--lang <language> Language hint for stdin input
|
|
465
|
+
--json Output the full API response as JSON (for CI/agents)
|
|
466
|
+
--fail-on-findings Exit 2 when JSON output contains findings
|
|
467
|
+
--lang <language> Language hint for stdin input
|
|
368
468
|
--help, -h Show this help
|
|
369
469
|
--version, -v Show version
|
|
370
470
|
|
|
471
|
+
${style("MODES", c.bold)}
|
|
472
|
+
|
|
473
|
+
${style("Default", c.white)} Diff from merge base to HEAD — what your PR would contain
|
|
474
|
+
${style("--staged", c.white)} Only changes in the staging area (git add)
|
|
475
|
+
${style("--unstaged", c.white)} Only working directory changes not yet staged
|
|
476
|
+
${style("--all", c.white)} Everything not yet committed, including untracked text files
|
|
477
|
+
${style("file", c.white)} A single local file
|
|
478
|
+
${style("stdin", c.white)} Pipe any diff or code: ${style("git diff main | rmcode", c.dim)}
|
|
479
|
+
|
|
371
480
|
${style("EXAMPLES", c.bold)}
|
|
372
481
|
|
|
373
|
-
${style("# Review your
|
|
482
|
+
${style("# Review your branch before opening a PR", c.dim)}
|
|
483
|
+
rmcode
|
|
484
|
+
|
|
485
|
+
${style("# Review only what you're about to commit", c.dim)}
|
|
374
486
|
rmcode --staged
|
|
375
487
|
|
|
376
|
-
${style("#
|
|
377
|
-
rmcode --
|
|
488
|
+
${style("# Review a specific range", c.dim)}
|
|
489
|
+
git diff abc123..def456 | rmcode --lang typescript
|
|
378
490
|
|
|
379
|
-
${style("# Review
|
|
380
|
-
rmcode src/auth
|
|
491
|
+
${style("# Review one file", c.dim)}
|
|
492
|
+
rmcode src/auth.ts
|
|
381
493
|
|
|
382
|
-
${style("#
|
|
383
|
-
|
|
494
|
+
${style("# JSON output for CI pipeline", c.dim)}
|
|
495
|
+
rmcode --json
|
|
384
496
|
|
|
385
|
-
${style(
|
|
497
|
+
${style(`# Set up authenticated reviews (${FREE_PLAN_CREDITS_PER_MONTH} credits/month free)`, c.dim)}
|
|
386
498
|
rmcode login
|
|
387
499
|
|
|
388
|
-
|
|
389
|
-
|
|
500
|
+
${style("ENVIRONMENT", c.bold)}
|
|
501
|
+
|
|
502
|
+
RMC_API_KEY API key for authenticated reviews (get one: rmcode login)
|
|
503
|
+
RMC_API_URL API endpoint (default: https://review-my-code.com)
|
|
504
|
+
RMC_APP_URL App URL for login (default: https://review-my-code.com)
|
|
505
|
+
|
|
506
|
+
${style("PRIVACY", c.bold)}
|
|
507
|
+
|
|
508
|
+
CLI reviews run locally to collect your diff/file/stdin content, then send that
|
|
509
|
+
review content to RMCode's backend and model providers for analysis. For
|
|
510
|
+
automatic PR reviews, install the GitHub App; that path uses the repository's
|
|
511
|
+
GitHub Actions runner to produce review context.
|
|
390
512
|
|
|
391
513
|
${style("https://review-my-code.com", c.dim)}
|
|
392
514
|
`);
|
|
@@ -394,28 +516,59 @@ function printHelp() {
|
|
|
394
516
|
// ── Version check ─────────────────────────────────────────────────────
|
|
395
517
|
async function checkForUpdate() {
|
|
396
518
|
try {
|
|
397
|
-
const resp = await fetch("https://registry.npmjs.org/rmcode/latest", {
|
|
519
|
+
const resp = await fetch("https://registry.npmjs.org/@review-my-code/rmcode/latest", {
|
|
398
520
|
signal: AbortSignal.timeout(3000),
|
|
399
521
|
});
|
|
400
522
|
if (!resp.ok)
|
|
401
523
|
return;
|
|
402
|
-
const data = await resp.json();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log(style(
|
|
406
|
-
console.log(style(` Run: npm install -g rmcode\n`, c.dim));
|
|
524
|
+
const data = (await resp.json());
|
|
525
|
+
if (data.version && data.version !== VERSION) {
|
|
526
|
+
console.log(style(`\n Update available: ${VERSION} → ${data.version}`, c.yellow));
|
|
527
|
+
console.log(style(` Run: npm install -g @review-my-code/rmcode\n`, c.dim));
|
|
407
528
|
}
|
|
408
529
|
}
|
|
409
530
|
catch {
|
|
410
|
-
|
|
531
|
+
/* silent */
|
|
411
532
|
}
|
|
412
533
|
}
|
|
534
|
+
function parseArgs(args) {
|
|
535
|
+
const flags = new Set();
|
|
536
|
+
const options = {};
|
|
537
|
+
const positional = [];
|
|
538
|
+
for (let i = 0; i < args.length; i++) {
|
|
539
|
+
const arg = args[i];
|
|
540
|
+
if (arg === "--lang") {
|
|
541
|
+
options.lang = args[i + 1];
|
|
542
|
+
i++;
|
|
543
|
+
}
|
|
544
|
+
else if (arg.startsWith("-")) {
|
|
545
|
+
flags.add(arg);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
positional.push(arg);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { flags, options, positional };
|
|
552
|
+
}
|
|
553
|
+
function unknownFlags(flags) {
|
|
554
|
+
const known = new Set([
|
|
555
|
+
"--all",
|
|
556
|
+
"--fail-on-findings",
|
|
557
|
+
"--help",
|
|
558
|
+
"-h",
|
|
559
|
+
"--json",
|
|
560
|
+
"--staged",
|
|
561
|
+
"--unstaged",
|
|
562
|
+
"--version",
|
|
563
|
+
"-v",
|
|
564
|
+
]);
|
|
565
|
+
return [...flags].filter((flag) => !known.has(flag));
|
|
566
|
+
}
|
|
413
567
|
// ── Main ──────────────────────────────────────────────────────────────
|
|
414
568
|
async function main() {
|
|
415
569
|
const args = process.argv.slice(2);
|
|
416
|
-
const flags
|
|
417
|
-
|
|
418
|
-
if (flags.has("--help") || flags.has("-h")) {
|
|
570
|
+
const { flags, options, positional } = parseArgs(args);
|
|
571
|
+
if (flags.has("--help") || flags.has("-h") || positional[0] === "help") {
|
|
419
572
|
printHelp();
|
|
420
573
|
return;
|
|
421
574
|
}
|
|
@@ -423,65 +576,165 @@ async function main() {
|
|
|
423
576
|
console.log(`rmcode v${VERSION}`);
|
|
424
577
|
return;
|
|
425
578
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
579
|
+
const invalidFlags = unknownFlags(flags);
|
|
580
|
+
if (invalidFlags.length > 0) {
|
|
581
|
+
console.error(style(`\n Unknown option: ${invalidFlags[0]}`, c.red));
|
|
582
|
+
console.error(style(` Run: rmcode help\n`, c.dim));
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
429
585
|
if (positional[0] === "install") {
|
|
430
586
|
console.log(`\n ${style("Install the GitHub App for automatic PR reviews:", c.bold)}\n`);
|
|
431
587
|
console.log(` ${style("https://github.com/apps/rmcode-ai", c.cyan, c.bold)}\n`);
|
|
432
|
-
console.log(` ${style("
|
|
588
|
+
console.log(` ${style("Install from the same GitHub account linked in RMCode so PR reviews share your plan credits.", c.dim)}`);
|
|
589
|
+
console.log(` ${style("The app uses a GitHub Actions workflow in your repo for context extraction.", c.dim)}`);
|
|
590
|
+
console.log(` ${style("If that workflow cannot be created or dispatched, the PR check fails until setup is fixed.", c.dim)}\n`);
|
|
433
591
|
return;
|
|
434
592
|
}
|
|
435
|
-
// rmc login
|
|
436
593
|
if (positional[0] === "login") {
|
|
437
|
-
const url = `${
|
|
594
|
+
const url = `${APP_URL}/api-keys`;
|
|
438
595
|
console.log(`\n ${style("Opening your browser to create an API key...", c.bold)}\n`);
|
|
439
596
|
try {
|
|
440
597
|
const openCmd = process.platform === "darwin"
|
|
441
598
|
? "open"
|
|
442
599
|
: process.platform === "win32"
|
|
443
|
-
? "
|
|
600
|
+
? "cmd"
|
|
444
601
|
: "xdg-open";
|
|
445
|
-
|
|
602
|
+
const openArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
603
|
+
(0, child_process_1.execFileSync)(openCmd, openArgs, { stdio: "ignore" });
|
|
446
604
|
}
|
|
447
605
|
catch {
|
|
448
|
-
console.log(` ${style("Could not open browser. Visit
|
|
606
|
+
console.log(` ${style("Could not open browser. Visit:", c.yellow)}`);
|
|
449
607
|
}
|
|
450
608
|
console.log(` ${style(url, c.cyan, c.bold)}\n`);
|
|
451
609
|
console.log(` ${style("Then set your API key:", c.dim)}`);
|
|
452
|
-
console.log(` ${style("export RMC_API_KEY=
|
|
610
|
+
console.log(` ${style("export RMC_API_KEY=rmc_...", c.green)}\n`);
|
|
453
611
|
return;
|
|
454
612
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
console.
|
|
459
|
-
|
|
613
|
+
const jsonMode = flags.has("--json");
|
|
614
|
+
const failOnFindings = flags.has("--fail-on-findings");
|
|
615
|
+
if (positional.length > 1) {
|
|
616
|
+
console.error(style(`\n Unknown command: ${positional[0]}`, c.red));
|
|
617
|
+
console.error(style(` Run: rmcode help\n`, c.dim));
|
|
618
|
+
process.exit(1);
|
|
460
619
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (positional.length > 0 && (0, fs_1.existsSync)((0, path_1.resolve)(positional[0]))) {
|
|
464
|
-
await reviewFile(positional[0], jsonMode);
|
|
465
|
-
return;
|
|
620
|
+
if (!jsonMode) {
|
|
621
|
+
console.log(`\n ${style("rmcode", c.green, c.bold)} ${style(`v${VERSION}`, c.dim)}`);
|
|
466
622
|
}
|
|
467
623
|
// Piped stdin
|
|
468
|
-
if (
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
624
|
+
if (hasPipedStdin()) {
|
|
625
|
+
const lang = options.lang || "javascript";
|
|
626
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin });
|
|
627
|
+
const lines = [];
|
|
628
|
+
for await (const line of rl)
|
|
629
|
+
lines.push(line);
|
|
630
|
+
const code = lines.join("\n");
|
|
631
|
+
if (!code.trim()) {
|
|
632
|
+
console.error(style(" No input received on stdin.", c.red));
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const spinner = createSpinner(`Reviewing stdin (${lang})`);
|
|
636
|
+
try {
|
|
637
|
+
const stdinType = looksLikeUnifiedDiff(code) ? "diff" : "snippet";
|
|
638
|
+
const stdinFiles = stdinType === "diff" ? filesFromDiff(code) : [];
|
|
639
|
+
const result = await callAPI(code, lang, stdinType, stdinFiles);
|
|
640
|
+
spinner.stop("Reviewed stdin");
|
|
641
|
+
if (jsonMode) {
|
|
642
|
+
printJsonResponse(result);
|
|
643
|
+
if (!result.success)
|
|
644
|
+
process.exit(1);
|
|
645
|
+
if (failOnFindings && hasBlockingFindings(result))
|
|
646
|
+
process.exit(2);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (!result.success) {
|
|
650
|
+
console.error(style(` ${result.error || "Review failed."}`, c.red));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
printMeta(result.meta);
|
|
654
|
+
if (result.data?.findings?.findings)
|
|
655
|
+
printFindings(result.data.findings.findings);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
spinner.stop("Review failed");
|
|
659
|
+
console.error(style(` ${err.message}`, c.red));
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
472
662
|
return;
|
|
473
663
|
}
|
|
474
|
-
|
|
475
|
-
|
|
664
|
+
if (positional.length === 1) {
|
|
665
|
+
const file = positional[0];
|
|
666
|
+
let code;
|
|
667
|
+
try {
|
|
668
|
+
code = (0, fs_1.readFileSync)(file, "utf-8");
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
console.error(style(` Could not read ${file}: ${err.message}`, c.red));
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
if (!code.trim()) {
|
|
675
|
+
console.error(style(` ${file} is empty.`, c.yellow));
|
|
676
|
+
process.exit(0);
|
|
677
|
+
}
|
|
678
|
+
await runReview(code, [file], file, jsonMode, failOnFindings, "file", detectLanguage([file]));
|
|
679
|
+
if (!jsonMode)
|
|
680
|
+
await checkForUpdate();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// Git modes
|
|
684
|
+
if (!isGitRepo()) {
|
|
685
|
+
console.error(style(" Not a git repository. Run from inside a git repo.", c.red));
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
const branch = currentBranch();
|
|
689
|
+
let diff;
|
|
690
|
+
let files;
|
|
691
|
+
let label;
|
|
692
|
+
if (flags.has("--staged")) {
|
|
693
|
+
({ diff, files } = diffStaged());
|
|
694
|
+
label = "staged changes";
|
|
695
|
+
if (!diff) {
|
|
696
|
+
console.error(style(" No staged changes. Stage files with: git add <files>", c.yellow));
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (flags.has("--unstaged")) {
|
|
701
|
+
({ diff, files } = diffUnstaged());
|
|
702
|
+
label = "unstaged changes";
|
|
703
|
+
if (!diff) {
|
|
704
|
+
console.error(style(" No unstaged changes.", c.yellow));
|
|
705
|
+
process.exit(0);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else if (flags.has("--all")) {
|
|
709
|
+
({ diff, files } = diffAll());
|
|
710
|
+
label = "all uncommitted changes";
|
|
711
|
+
if (!diff) {
|
|
712
|
+
console.error(style(" No uncommitted changes.", c.yellow));
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
({ diff, files } = diffFromMergeBase());
|
|
718
|
+
label = `branch ${style(branch, c.cyan)} vs base`;
|
|
719
|
+
if (!diff) {
|
|
720
|
+
console.error(style(" No changes from merge base. Your branch matches main.", c.yellow));
|
|
721
|
+
process.exit(0);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
await runReview(diff, files, label, jsonMode, failOnFindings);
|
|
476
725
|
if (!jsonMode && !API_KEY) {
|
|
477
|
-
console.log(style(
|
|
726
|
+
console.log(style(` Get ${FREE_PLAN_CREDITS_PER_MONTH} credits/month free: rmcode login`, c.dim));
|
|
478
727
|
console.log(style(" Auto-review every PR: rmcode install", c.dim));
|
|
479
728
|
console.log();
|
|
480
729
|
}
|
|
481
|
-
|
|
482
|
-
|
|
730
|
+
if (!jsonMode)
|
|
731
|
+
await checkForUpdate();
|
|
483
732
|
}
|
|
484
|
-
main()
|
|
733
|
+
main()
|
|
734
|
+
.then(() => {
|
|
735
|
+
process.exit(process.exitCode ?? 0);
|
|
736
|
+
})
|
|
737
|
+
.catch((err) => {
|
|
485
738
|
console.error(style(` Fatal: ${err.message}`, c.red));
|
|
486
739
|
process.exit(1);
|
|
487
740
|
});
|
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.1",
|
|
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",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"rmc": "dist/cli.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc",
|
|
@@ -34,9 +35,13 @@
|
|
|
34
35
|
"node": ">=18"
|
|
35
36
|
},
|
|
36
37
|
"license": "MIT",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
37
41
|
"repository": {
|
|
38
42
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/review-my-code/review-my-code"
|
|
43
|
+
"url": "git+https://github.com/review-my-code/review-my-code.git",
|
|
44
|
+
"directory": "cli"
|
|
40
45
|
},
|
|
41
46
|
"homepage": "https://review-my-code.com",
|
|
42
47
|
"author": "Alex Yang"
|