@review-my-code/rmcode 0.1.0-alpha.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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const readline_1 = require("readline");
8
+ const API_URL = process.env.RMC_API_URL || "https://review-my-code.com";
9
+ const VERSION = "0.1.0";
10
+ // ── Colors (no dependencies) ──────────────────────────────────────────
11
+ const c = {
12
+ reset: "\x1b[0m",
13
+ bold: "\x1b[1m",
14
+ dim: "\x1b[2m",
15
+ red: "\x1b[31m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ blue: "\x1b[34m",
19
+ magenta: "\x1b[35m",
20
+ cyan: "\x1b[36m",
21
+ gray: "\x1b[90m",
22
+ white: "\x1b[97m",
23
+ bgRed: "\x1b[41m",
24
+ bgYellow: "\x1b[43m",
25
+ bgGreen: "\x1b[42m",
26
+ bgBlue: "\x1b[44m",
27
+ };
28
+ const isCI = process.env.CI === "true";
29
+ const noColor = process.env.NO_COLOR === "1" || !process.stdout.isTTY;
30
+ function style(text, ...codes) {
31
+ if (noColor)
32
+ return text;
33
+ return codes.join("") + text + c.reset;
34
+ }
35
+ // ── Severity styling ──────────────────────────────────────────────────
36
+ function severityBadge(severity) {
37
+ const s = severity.toLowerCase();
38
+ if (s === "critical" || s === "security")
39
+ return style(` ${severity.toUpperCase()} `, c.bold, c.bgRed, c.white);
40
+ if (s === "bug" || s === "high")
41
+ return style(` ${severity.toUpperCase()} `, c.bold, c.red);
42
+ if (s === "perf" || s === "medium")
43
+ return style(` ${severity.toUpperCase()} `, c.bold, c.yellow);
44
+ return style(` ${severity.toUpperCase()} `, c.dim);
45
+ }
46
+ // ── Git helpers ───────────────────────────────────────────────────────
47
+ function gitDiff(staged) {
48
+ try {
49
+ const flag = staged ? "--cached" : "";
50
+ const diff = (0, child_process_1.execSync)(`git diff ${flag}`, { encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 });
51
+ return diff.trim() || null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function gitStagedFiles() {
58
+ try {
59
+ return (0, child_process_1.execSync)("git diff --cached --name-only", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ function gitUnstagedFiles() {
66
+ try {
67
+ return (0, child_process_1.execSync)("git diff --name-only", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ function isGitRepo() {
74
+ try {
75
+ (0, child_process_1.execSync)("git rev-parse --is-inside-work-tree", { encoding: "utf-8", stdio: "pipe" });
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ function gitRepoName() {
83
+ try {
84
+ const remote = (0, child_process_1.execSync)("git remote get-url origin", {
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;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ const API_KEY = process.env.RMC_API_KEY || null;
101
+ async function callAuthenticatedAPI(code, language, type, files) {
102
+ const repo = gitRepoName();
103
+ const resp = await fetch(`${API_URL}/api/reviews/cli`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "X-API-Key": API_KEY,
108
+ },
109
+ body: JSON.stringify({ type, value: code, language, files, repo }),
110
+ });
111
+ return resp.json();
112
+ }
113
+ async function callAnonymousAPI(code, language) {
114
+ const repo = gitRepoName();
115
+ const resp = await fetch(`${API_URL}/api/reviews/anonymous`, {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({ type: "snippet", value: code, language, repo }),
119
+ });
120
+ const result = (await resp.json());
121
+ // Handle 202 polling
122
+ if (resp.status === 202 && result.data?.accessToken) {
123
+ return pollForResults(result.data.accessToken, result.meta);
124
+ }
125
+ return result;
126
+ }
127
+ async function pollForResults(accessToken, initialMeta) {
128
+ const deadline = Date.now() + 60_000;
129
+ while (Date.now() < deadline) {
130
+ await new Promise((r) => setTimeout(r, 2000));
131
+ const resp = await fetch(`${API_URL}/api/reviews/anonymous/${accessToken}`);
132
+ const result = (await resp.json());
133
+ if (result.data?.status === "completed" || result.data?.status === "failed") {
134
+ if (initialMeta && !result.meta)
135
+ result.meta = initialMeta;
136
+ return result;
137
+ }
138
+ }
139
+ return { success: false, error: "Review timed out after 60 seconds." };
140
+ }
141
+ function callAPI(code, language, type = "snippet", files = []) {
142
+ if (API_KEY) {
143
+ return callAuthenticatedAPI(code, language, type, files);
144
+ }
145
+ return callAnonymousAPI(code, language);
146
+ }
147
+ // ── Language detection ────────────────────────────────────────────────
148
+ const EXT_TO_LANG = {
149
+ ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", ".jsx": "javascript",
150
+ ".py": "python", ".java": "java", ".go": "go", ".rs": "rust",
151
+ ".c": "c", ".cpp": "cpp", ".h": "c", ".cs": "csharp",
152
+ ".rb": "ruby", ".php": "php", ".swift": "swift", ".kt": "kotlin",
153
+ ".scala": "scala", ".lua": "lua", ".dart": "dart",
154
+ ".v": "verilog", ".sv": "verilog", ".vhd": "vhdl",
155
+ ".glsl": "glsl", ".hlsl": "hlsl", ".wgsl": "wgsl",
156
+ ".jl": "julia", ".r": "r", ".ex": "elixir", ".erl": "erlang",
157
+ ".hs": "haskell", ".ml": "ocaml", ".fs": "fsharp",
158
+ ".sh": "bash", ".zig": "zig", ".sol": "solidity",
159
+ };
160
+ function detectLanguage(files) {
161
+ for (const f of files) {
162
+ const ext = (0, path_1.extname)(f).toLowerCase();
163
+ if (EXT_TO_LANG[ext])
164
+ return EXT_TO_LANG[ext];
165
+ }
166
+ return "javascript";
167
+ }
168
+ // ── Output formatting ─────────────────────────────────────────────────
169
+ function printFindings(findings, jsonMode) {
170
+ if (jsonMode) {
171
+ console.log(JSON.stringify(findings, null, 2));
172
+ return;
173
+ }
174
+ if (findings.length === 0) {
175
+ console.log(`\n ${style("No issues found.", c.green, c.bold)} Your code looks good.\n`);
176
+ return;
177
+ }
178
+ const counts = {};
179
+ for (const f of findings) {
180
+ counts[f.severity] = (counts[f.severity] || 0) + 1;
181
+ }
182
+ const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(" · ");
183
+ console.log(`\n ${style(`${findings.length} finding${findings.length !== 1 ? "s" : ""}`, c.bold, c.white)} · ${summary}\n`);
184
+ for (const f of findings) {
185
+ const location = f.filePath ? `${style(f.filePath, c.cyan)}${f.line ? `:${f.line}` : ""}` : "";
186
+ console.log(` ${severityBadge(f.severity)} ${style(f.title, c.bold, c.white)}`);
187
+ if (location)
188
+ console.log(` ${style("at", c.dim)} ${location}`);
189
+ console.log(` ${style(f.explanation, c.gray)}`);
190
+ if (f.codeContext) {
191
+ console.log(` ${style(f.codeContext, c.dim)}`);
192
+ }
193
+ if (f.suggestedCode) {
194
+ console.log(` ${style("fix:", c.green)} ${style(f.suggestedCode, c.green)}`);
195
+ }
196
+ console.log();
197
+ }
198
+ }
199
+ function printMeta(meta) {
200
+ if (!meta)
201
+ return;
202
+ const parts = [];
203
+ if (meta.reviewer)
204
+ parts.push(meta.reviewer.charAt(0).toUpperCase() + meta.reviewer.slice(1));
205
+ if (meta.plan) {
206
+ const planLabel = meta.plan.charAt(0).toUpperCase() + meta.plan.slice(1);
207
+ if (meta.creditsRemaining != null) {
208
+ parts.push(`${planLabel} (${meta.creditsRemaining} credits remaining)`);
209
+ }
210
+ else {
211
+ parts.push(planLabel);
212
+ }
213
+ }
214
+ if (meta.quotaUsed) {
215
+ parts.push(`Free review (${meta.quotaUsed} today)`);
216
+ }
217
+ if (parts.length > 0) {
218
+ console.log(` ${style(parts.join(" · "), c.dim)}`);
219
+ }
220
+ if (meta.message) {
221
+ console.log(` ${style(meta.message, c.yellow)}`);
222
+ }
223
+ }
224
+ // ── Spinner ───────────────────────────────────────────────────────────
225
+ function createSpinner(msg) {
226
+ if (noColor || isCI) {
227
+ process.stderr.write(` ${msg}...\n`);
228
+ return { stop: (final) => { if (final)
229
+ process.stderr.write(` ${final}\n`); } };
230
+ }
231
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
232
+ let i = 0;
233
+ const interval = setInterval(() => {
234
+ process.stderr.write(`\r ${style(frames[i++ % frames.length], c.green)} ${msg}`);
235
+ }, 80);
236
+ return {
237
+ stop(finalMsg) {
238
+ clearInterval(interval);
239
+ process.stderr.write(`\r ${style("✓", c.green)} ${finalMsg || msg}${"".padEnd(20)}\n`);
240
+ },
241
+ };
242
+ }
243
+ // ── Commands ──────────────────────────────────────────────────────────
244
+ async function reviewDiff(staged, jsonMode) {
245
+ if (!isGitRepo()) {
246
+ console.error(style(" Not a git repository.", c.red));
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})`);
293
+ try {
294
+ const result = await callAPI(content, lang, "file", [filePath]);
295
+ spinner.stop(`Reviewed ${(0, path_1.basename)(filePath)}`);
296
+ if (!result.success) {
297
+ console.error(style(` ${result.error || "Review failed."}`, c.red));
298
+ if (result.meta?.message) {
299
+ console.log(` ${style(result.meta.message, c.yellow)}`);
300
+ }
301
+ process.exit(1);
302
+ }
303
+ if (!jsonMode)
304
+ printMeta(result.meta);
305
+ if (result.data?.findings?.findings) {
306
+ printFindings(result.data.findings.findings, jsonMode);
307
+ }
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
+ if (!result.success) {
330
+ console.error(style(` ${result.error || "Review failed."}`, c.red));
331
+ if (result.meta?.message) {
332
+ console.log(` ${style(result.meta.message, c.yellow)}`);
333
+ }
334
+ process.exit(1);
335
+ }
336
+ if (!jsonMode)
337
+ printMeta(result.meta);
338
+ if (result.data?.findings?.findings) {
339
+ printFindings(result.data.findings.findings, jsonMode);
340
+ }
341
+ }
342
+ catch (err) {
343
+ spinner.stop("Review failed");
344
+ console.error(style(` ${err.message}`, c.red));
345
+ process.exit(1);
346
+ }
347
+ }
348
+ // ── Help & Version ────────────────────────────────────────────────────
349
+ function printHelp() {
350
+ console.log(`
351
+ ${style("rmcode", c.bold, c.green)} — AI code review from your terminal
352
+
353
+ ${style("USAGE", c.bold)}
354
+
355
+ ${style("rmcode", c.cyan)} Review unstaged changes
356
+ ${style("rmcode --staged", c.cyan)} Review staged changes
357
+ ${style("rmcode file.ts", c.cyan)} Review a specific file
358
+ ${style("cat file.ts | rmcode", c.cyan)} Review from stdin
359
+ ${style("rmcode login", c.cyan)} Set up your API key
360
+ ${style("rmcode install", c.cyan)} Install the GitHub App for PR reviews
361
+
362
+ ${style("OPTIONS", c.bold)}
363
+
364
+ --staged Review staged (git add) changes only
365
+ --deep Deep multi-pass review (coming soon)
366
+ --json Output findings as JSON (for agents/CI)
367
+ --lang <language> Language hint for stdin input
368
+ --help, -h Show this help
369
+ --version, -v Show version
370
+
371
+ ${style("EXAMPLES", c.bold)}
372
+
373
+ ${style("# Review your work before committing", c.dim)}
374
+ rmcode --staged
375
+
376
+ ${style("# Let an AI agent review its own code", c.dim)}
377
+ rmcode --staged --json
378
+
379
+ ${style("# Review a file directly", c.dim)}
380
+ rmcode src/auth/login.ts
381
+
382
+ ${style("# Pipe from another command", c.dim)}
383
+ git diff main | rmcode --lang typescript
384
+
385
+ ${style("# Set up authenticated reviews (30/month free)", c.dim)}
386
+ rmcode login
387
+
388
+ ${style("# Auto-review every PR on GitHub", c.dim)}
389
+ rmcode install
390
+
391
+ ${style("https://review-my-code.com", c.dim)}
392
+ `);
393
+ }
394
+ // ── Version check ─────────────────────────────────────────────────────
395
+ async function checkForUpdate() {
396
+ try {
397
+ const resp = await fetch("https://registry.npmjs.org/rmcode/latest", {
398
+ signal: AbortSignal.timeout(3000),
399
+ });
400
+ if (!resp.ok)
401
+ return;
402
+ const data = await resp.json();
403
+ const latest = data.version;
404
+ if (latest && latest !== VERSION) {
405
+ console.log(style(`\n Update available: ${VERSION} → ${latest}`, c.yellow));
406
+ console.log(style(` Run: npm install -g rmcode\n`, c.dim));
407
+ }
408
+ }
409
+ catch {
410
+ // Silent fail — don't block the review
411
+ }
412
+ }
413
+ // ── Main ──────────────────────────────────────────────────────────────
414
+ async function main() {
415
+ const args = process.argv.slice(2);
416
+ const flags = new Set(args.filter((a) => a.startsWith("-")));
417
+ const positional = args.filter((a) => !a.startsWith("-"));
418
+ if (flags.has("--help") || flags.has("-h")) {
419
+ printHelp();
420
+ return;
421
+ }
422
+ if (flags.has("--version") || flags.has("-v")) {
423
+ console.log(`rmcode v${VERSION}`);
424
+ return;
425
+ }
426
+ const jsonMode = flags.has("--json");
427
+ const staged = flags.has("--staged");
428
+ // rmc install
429
+ if (positional[0] === "install") {
430
+ console.log(`\n ${style("Install the GitHub App for automatic PR reviews:", c.bold)}\n`);
431
+ console.log(` ${style("https://github.com/apps/rmcode-ai", c.cyan, c.bold)}\n`);
432
+ console.log(` ${style("Once installed, RMCode AI reviews every PR automatically.", c.dim)}\n`);
433
+ return;
434
+ }
435
+ // rmc login
436
+ if (positional[0] === "login") {
437
+ const url = `${API_URL}/api-keys`;
438
+ console.log(`\n ${style("Opening your browser to create an API key...", c.bold)}\n`);
439
+ try {
440
+ const openCmd = process.platform === "darwin"
441
+ ? "open"
442
+ : process.platform === "win32"
443
+ ? "start"
444
+ : "xdg-open";
445
+ (0, child_process_1.execSync)(`${openCmd} ${url}`, { stdio: "ignore" });
446
+ }
447
+ catch {
448
+ console.log(` ${style("Could not open browser. Visit this URL:", c.yellow)}`);
449
+ }
450
+ console.log(` ${style(url, c.cyan, c.bold)}\n`);
451
+ console.log(` ${style("Then set your API key:", c.dim)}`);
452
+ console.log(` ${style("export RMC_API_KEY=rmc_k_...", c.green)}\n`);
453
+ return;
454
+ }
455
+ // --deep stub
456
+ if (flags.has("--deep")) {
457
+ console.log(`\n ${style("Deep review is coming soon.", c.yellow)}`);
458
+ console.log(` ${style("Scout review is available now — just run: rmcode --staged", c.dim)}\n`);
459
+ return;
460
+ }
461
+ console.log(`\n ${style("rmcode", c.green, c.bold)} ${style(`v${VERSION}`, c.dim)}`);
462
+ // rmc file.ts
463
+ if (positional.length > 0 && (0, fs_1.existsSync)((0, path_1.resolve)(positional[0]))) {
464
+ await reviewFile(positional[0], jsonMode);
465
+ return;
466
+ }
467
+ // Piped stdin
468
+ if (!process.stdin.isTTY) {
469
+ const langIdx = args.indexOf("--lang");
470
+ const lang = langIdx >= 0 && args[langIdx + 1] ? args[langIdx + 1] : "javascript";
471
+ await reviewStdin(lang, jsonMode);
472
+ return;
473
+ }
474
+ // Default: review git diff
475
+ await reviewDiff(staged, jsonMode);
476
+ if (!jsonMode && !API_KEY) {
477
+ console.log(style(" Get 30 reviews/month free: rmcode login", c.dim));
478
+ console.log(style(" Auto-review every PR: rmcode install", c.dim));
479
+ console.log();
480
+ }
481
+ // Non-blocking update check after review completes
482
+ await checkForUpdate();
483
+ }
484
+ main().catch((err) => {
485
+ console.error(style(` Fatal: ${err.message}`, c.red));
486
+ process.exit(1);
487
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@review-my-code/rmcode",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "AI code review from your terminal. Catches logic errors, null risks, security holes, and broken error handling.",
5
+ "keywords": [
6
+ "code-review",
7
+ "ai",
8
+ "linter",
9
+ "security",
10
+ "static-analysis",
11
+ "github",
12
+ "cli",
13
+ "developer-tools"
14
+ ],
15
+ "bin": {
16
+ "rmcode": "dist/cli.js",
17
+ "rmc": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsx src/cli.ts",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "dependencies": {},
28
+ "devDependencies": {
29
+ "typescript": "^5.4.0",
30
+ "@types/node": "^22.0.0",
31
+ "tsx": "^4.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/review-my-code/review-my-code"
40
+ },
41
+ "homepage": "https://review-my-code.com",
42
+ "author": "Alex Yang"
43
+ }