@rootplatform/review-my-pr 1.0.0

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 ADDED
@@ -0,0 +1,164 @@
1
+ # review-my-pr
2
+
3
+ CLI tool that generates a code review prompt from your git diff and copies it to the clipboard. Paste it into any LLM chat to get an instant code review.
4
+
5
+
6
+ ## Usage
7
+
8
+ Run from any git repository on a feature branch:
9
+
10
+ ```bash
11
+ # Auto-detects main/master as the base branch
12
+ review-my-pr
13
+
14
+ # Specify a base branch
15
+ review-my-pr --base develop
16
+
17
+ # Exclude additional file patterns
18
+ review-my-pr --exclude "*.generated.ts" --exclude "docs/**"
19
+ ```
20
+
21
+ ### Options
22
+
23
+ | Flag | Description |
24
+ |---|---|
25
+ | `-b, --base <branch>` | Base branch to diff against. Auto-detects `origin/main`, `origin/master`, `main`, or `master` if not specified. |
26
+ | `-e, --exclude <pattern>` | Additional git pathspec exclusion pattern. Can be repeated for multiple patterns. |
27
+ | `-V, --version` | Show version number |
28
+ | `-h, --help` | Show help |
29
+
30
+ ### Example Output
31
+
32
+ ```
33
+ Generating diff from origin/master...
34
+ ✅ Diff generated successfully
35
+ ✅ Found 2 changed file(s) (65 lines)
36
+
37
+ ✅ Review prompt copied to clipboard!
38
+ Just paste it into your chat with Cmd+V
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Create a `.review-my-pr/` directory in your repository root to customize the review prompt. The directory contains two files:
44
+
45
+ ```
46
+ .review-my-pr/
47
+ prompt.md # The review prompt template
48
+ config.json # Category definitions and settings
49
+ ```
50
+
51
+ If this directory is not present, a sensible default prompt is used.
52
+
53
+ ### `prompt.md` - Prompt Template
54
+
55
+ Use these placeholders in your template and they will be replaced with actual values:
56
+
57
+ | Placeholder | Description |
58
+ |---|---|
59
+ | `{{DIFF}}` | The full git diff content |
60
+ | `{{BASE_BRANCH}}` | The base branch name (e.g. `origin/master`) |
61
+ | `{{CHANGED_FILES}}` | List of all changed files |
62
+ | `{{SRC_FILES}}` | Source files only (excludes tests, mocks, fixtures) |
63
+ | `{{TEST_FILES}}` | Test/spec files only |
64
+ | `{{MIGRATION_FILES}}` | Database migration files |
65
+ | `{{HTTP_FILES}}` | HTTP layer files |
66
+ | `{{PR_TITLE}}` | PR title (requires `gh` CLI) |
67
+ | `{{PR_BODY}}` | PR description body (requires `gh` CLI) |
68
+ | `{{PR_METADATA}}` | Formatted PR title + description block |
69
+ | `{{CATEGORY.NAME}}` | Custom category defined in `config.json` (see below) |
70
+
71
+ If `{{DIFF}}` is not present in your template, the diff will be appended automatically at the end.
72
+
73
+ ### `config.json` - Custom File Categories
74
+
75
+ Define custom file categories to group changed files by regex patterns. Reference them in `prompt.md` with `{{CATEGORY.NAME}}`.
76
+
77
+ ```json
78
+ {
79
+ "categories": {
80
+ "SRC_FILES": {
81
+ "exclude": "\\.(test|spec)\\.(ts|tsx|js|jsx)$|__mocks__|fixtures"
82
+ },
83
+ "TEST_FILES": {
84
+ "pattern": "\\.(test|spec)\\.(ts|tsx|js|jsx)$"
85
+ },
86
+ "MIGRATION_FILES": {
87
+ "pattern": "migrations/\\d+_.*\\.(ts|js)$"
88
+ },
89
+ "SCHEMAS": {
90
+ "pattern": "\\.graphql$|\\.schema\\.ts$"
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ | Property | Description |
97
+ |---|---|
98
+ | `pattern` | Regex to include matching files. If omitted, all files are included (before `exclude` is applied). |
99
+ | `exclude` | Regex to exclude matching files. Applied after `pattern`. |
100
+
101
+ Categories are independent -- a file can appear in multiple categories.
102
+
103
+ ### Example
104
+
105
+ `.review-my-pr/config.json`:
106
+
107
+ ```json
108
+ {
109
+ "categories": {
110
+ "SRC_FILES": { "exclude": "\\.(test|spec)\\.(ts|tsx|js|jsx)$|__mocks__|fixtures" },
111
+ "TEST_FILES": { "pattern": "\\.(test|spec)\\.(ts|tsx|js|jsx)$" }
112
+ }
113
+ }
114
+ ```
115
+
116
+ `.review-my-pr/prompt.md`:
117
+
118
+ ```markdown
119
+ You are an expert code reviewer for our TypeScript backend.
120
+
121
+ ## Context
122
+ - Stack: TypeScript, Node.js, PostgreSQL
123
+ - Focus: bugs, security, performance, test coverage
124
+
125
+ ## Source Files
126
+ {{CATEGORY.SRC_FILES}}
127
+
128
+ ## Test Files
129
+ {{CATEGORY.TEST_FILES}}
130
+
131
+ ## Diff
132
+
133
+ \`\`\`diff
134
+ {{DIFF}}
135
+ \`\`\`
136
+ ```
137
+
138
+ The built-in placeholders (`{{SRC_FILES}}`, `{{TEST_FILES}}`, etc.) still work and use the default categorization patterns. Custom categories via `{{CATEGORY.*}}` give you full control over the regex patterns.
139
+
140
+ ## Default Exclusions
141
+
142
+ The following file patterns are always excluded from the diff:
143
+
144
+ - `**/package-lock.json`
145
+ - `**/yarn.lock`
146
+ - `**/pnpm-lock.yaml`
147
+ - `**/test-results/**`
148
+ - `**/playwright-report/**`
149
+
150
+ Use `--exclude` to add more patterns on top of these.
151
+
152
+ ## PR Metadata
153
+
154
+ If you have the [GitHub CLI](https://cli.github.com/) (`gh`) installed and your branch has an open PR, the tool will automatically include the PR title and description in the prompt.
155
+
156
+ ## Development
157
+
158
+ ```bash
159
+ git clone <repo-url>
160
+ cd review-my-pr
161
+ npm install
162
+ npm run build
163
+ npm link # makes `review-my-pr` available globally
164
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runReview } from "./review.js";
4
+ const program = new Command();
5
+ program
6
+ .name("review-my-pr")
7
+ .description("Generate a code review prompt from your git diff and copy it to the clipboard")
8
+ .version("1.0.0")
9
+ .option("-b, --base <branch>", "base branch to diff against (default: auto-detect main/master)")
10
+ .option("-e, --exclude <pattern>", "additional git pathspec exclusion (repeatable)", (val, acc) => [...acc, val], [])
11
+ .action(async (options) => {
12
+ await runReview({
13
+ base: options.base,
14
+ exclude: options.exclude,
15
+ });
16
+ });
17
+ program.parse();
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,cAAc,CAAC;KACpB,WAAW,CACV,+EAA+E,CAChF;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,MAAM,CAAC,qBAAqB,EAAE,gEAAgE,CAAC;KAC/F,MAAM,CACL,yBAAyB,EACzB,gDAAgD,EAChD,CAAC,GAAW,EAAE,GAAa,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,CAAC,EAC7C,EAAc,CACf;KACA,MAAM,CAAC,KAAK,EAAE,OAA8C,EAAE,EAAE;IAC/D,MAAM,SAAS,CAAC;QACd,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function copyToClipboard(text: string): Promise<void>;
2
+ export declare function getPasteHint(): string;
@@ -0,0 +1,10 @@
1
+ import clipboardy from "clipboardy";
2
+ export async function copyToClipboard(text) {
3
+ await clipboardy.write(text);
4
+ }
5
+ export function getPasteHint() {
6
+ return process.platform === "darwin"
7
+ ? "Just paste it into your chat with Cmd+V"
8
+ : "Just paste it into your chat with Ctrl+V";
9
+ }
10
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/lib/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,MAAM,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAClC,CAAC,CAAC,yCAAyC;QAC3C,CAAC,CAAC,0CAA0C,CAAC;AACjD,CAAC"}
@@ -0,0 +1,27 @@
1
+ export declare function isGitRepo(): boolean;
2
+ export declare function getRepoRoot(): string;
3
+ export declare function detectBaseBranch(explicit?: string): string;
4
+ export declare function ensureBranchUpToDate(baseBranch: string): void;
5
+ export interface DiffResult {
6
+ content: string;
7
+ changedFiles: string[];
8
+ lineCount: number;
9
+ }
10
+ export declare function getDiff(baseBranch: string, extraExclusions?: string[]): DiffResult;
11
+ export interface CategorizedFiles {
12
+ source: string[];
13
+ test: string[];
14
+ migration: string[];
15
+ http: string[];
16
+ }
17
+ export declare function categorizeFiles(files: string[]): CategorizedFiles;
18
+ export interface CategoryDefinition {
19
+ pattern?: string;
20
+ exclude?: string;
21
+ }
22
+ export declare function categorizeFilesWithConfig(files: string[], categories: Record<string, CategoryDefinition>): Record<string, string[]>;
23
+ export interface PrMetadata {
24
+ title: string;
25
+ body: string;
26
+ }
27
+ export declare function getPrMetadata(): PrMetadata | null;
@@ -0,0 +1,127 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ export function isGitRepo() {
3
+ try {
4
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function getRepoRoot() {
12
+ return execSync("git rev-parse --show-toplevel", { stdio: "pipe" })
13
+ .toString()
14
+ .trim();
15
+ }
16
+ function branchExists(branch) {
17
+ try {
18
+ execSync(`git rev-parse --verify ${branch}`, { stdio: "pipe" });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ export function detectBaseBranch(explicit) {
26
+ if (explicit) {
27
+ if (!branchExists(explicit)) {
28
+ throw new Error(`Branch '${explicit}' does not exist`);
29
+ }
30
+ return explicit;
31
+ }
32
+ const candidates = ["origin/main", "origin/master", "main", "master"];
33
+ for (const branch of candidates) {
34
+ if (branchExists(branch)) {
35
+ return branch;
36
+ }
37
+ }
38
+ throw new Error("Could not detect a base branch. No main/master branch found.\n" +
39
+ "Use --base <branch> to specify one explicitly.");
40
+ }
41
+ export function ensureBranchUpToDate(baseBranch) {
42
+ const behindCount = execFileSync("git", ["rev-list", "--count", `HEAD..${baseBranch}`], { stdio: "pipe" })
43
+ .toString()
44
+ .trim();
45
+ if (parseInt(behindCount, 10) > 0) {
46
+ throw new Error(`Your branch is behind ${baseBranch} by ${behindCount} commit(s). ` +
47
+ `Please update your branch and try again.`);
48
+ }
49
+ }
50
+ const DEFAULT_EXCLUSIONS = [
51
+ ":(exclude)**/package-lock.json",
52
+ ":(exclude)**/yarn.lock",
53
+ ":(exclude)**/pnpm-lock.yaml",
54
+ ":(exclude)**/test-results/**",
55
+ ":(exclude)**/playwright-report/**",
56
+ ];
57
+ export function getDiff(baseBranch, extraExclusions = []) {
58
+ const exclusions = [
59
+ ...DEFAULT_EXCLUSIONS,
60
+ ...extraExclusions.map((e) => `:(exclude)${e}`),
61
+ ];
62
+ const pathspec = ["--", ".", ...exclusions];
63
+ const content = execFileSync("git", ["diff", baseBranch, ...pathspec], {
64
+ stdio: "pipe",
65
+ maxBuffer: 50 * 1024 * 1024,
66
+ })
67
+ .toString()
68
+ .trim();
69
+ const changedFiles = execFileSync("git", ["diff", "--name-only", baseBranch, ...pathspec], { stdio: "pipe", maxBuffer: 10 * 1024 * 1024 })
70
+ .toString()
71
+ .trim()
72
+ .split("\n")
73
+ .filter(Boolean);
74
+ const lineCount = content ? content.split("\n").length : 0;
75
+ return { content, changedFiles, lineCount };
76
+ }
77
+ export function categorizeFiles(files) {
78
+ const testPattern = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
79
+ const mockPattern = /__mocks__|fixtures/;
80
+ const migrationPattern = /migrations\/\d+_.*\.(ts|js)$/;
81
+ const httpPattern = /\/http\//;
82
+ const test = files.filter((f) => testPattern.test(f));
83
+ const migration = files.filter((f) => migrationPattern.test(f));
84
+ const http = files.filter((f) => httpPattern.test(f));
85
+ const source = files.filter((f) => !testPattern.test(f) && !mockPattern.test(f));
86
+ return { source, test, migration, http };
87
+ }
88
+ export function categorizeFilesWithConfig(files, categories) {
89
+ const result = {};
90
+ for (const [name, def] of Object.entries(categories)) {
91
+ const includeRe = def.pattern ? new RegExp(def.pattern) : null;
92
+ const excludeRe = def.exclude ? new RegExp(def.exclude) : null;
93
+ result[name] = files.filter((f) => {
94
+ const included = includeRe ? includeRe.test(f) : true;
95
+ const excluded = excludeRe ? excludeRe.test(f) : false;
96
+ return included && !excluded;
97
+ });
98
+ }
99
+ return result;
100
+ }
101
+ export function getPrMetadata() {
102
+ try {
103
+ execSync("which gh", { stdio: "pipe" });
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ try {
109
+ const title = execSync("gh pr view --json title -q .title", {
110
+ stdio: "pipe",
111
+ })
112
+ .toString()
113
+ .trim();
114
+ const body = execSync("gh pr view --json body -q .body", {
115
+ stdio: "pipe",
116
+ })
117
+ .toString()
118
+ .trim();
119
+ if (!title)
120
+ return null;
121
+ return { title, body };
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CategorizedFiles, CategoryDefinition, PrMetadata } from "./git.js";
2
+ export interface ReviewConfig {
3
+ categories?: Record<string, CategoryDefinition>;
4
+ }
5
+ export interface PromptContext {
6
+ baseBranch: string;
7
+ diffContent: string;
8
+ changedFiles: string[];
9
+ categorizedFiles: CategorizedFiles;
10
+ customCategories?: Record<string, string[]>;
11
+ prMetadata: PrMetadata | null;
12
+ }
13
+ export declare function hasCustomPrompt(repoRoot: string): boolean;
14
+ export declare function loadTemplate(repoRoot: string): string;
15
+ export declare function loadConfig(repoRoot: string): ReviewConfig;
16
+ export declare function buildPrompt(template: string, context: PromptContext): string;
@@ -0,0 +1,164 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const CONFIG_DIR = ".review-my-pr";
4
+ const PROMPT_FILE = "prompt.md";
5
+ const CONFIG_FILE = "config.json";
6
+ const DEFAULT_PROMPT = `You are an expert code reviewer. Below is a git diff comparing a feature branch to {{BASE_BRANCH}}.
7
+
8
+ {{PR_METADATA}}
9
+ ## Your Task
10
+
11
+ ### 0. High-Level Summary
12
+ In 2-3 sentences, describe:
13
+ - **Product impact**: What does this change deliver for users or customers?
14
+ - **Engineering approach**: Key patterns or architectural decisions.
15
+
16
+ ### 1. Critical Review Focus
17
+ Prioritize your review on these areas (in order of importance):
18
+
19
+ **🐛 Bugs & Logic Errors** (HIGHEST PRIORITY)
20
+ - Off-by-one errors, null/undefined handling, race conditions
21
+ - Incorrect conditional logic, missing edge cases
22
+ - Type mismatches, incorrect API contract usage
23
+ - Transaction handling issues (missing rollback, deadlocks)
24
+
25
+ **💥 Breaking Changes** (CRITICAL)
26
+ - API contract changes (request/response shape changes)
27
+ - Database schema changes without proper migrations
28
+ - Backward compatibility issues
29
+
30
+ **🔒 Security Issues**
31
+ - Authentication/authorization bypasses
32
+ - Injection vulnerabilities (SQL, command, etc.)
33
+ - Sensitive data exposure in logs or responses
34
+ - Missing access control checks
35
+
36
+ **⚡ Performance Regressions**
37
+ - N+1 query patterns
38
+ - Missing database indexes on new columns
39
+ - Unbounded queries without pagination
40
+ - Unnecessary computation in hot paths
41
+
42
+ **🧪 Test Coverage Gaps**
43
+ - New code paths without tests
44
+ - Modified logic without updated tests
45
+ - Missing error case coverage
46
+
47
+ ### 2. Review Guidelines
48
+ - **Only report issues you're confident about (>80% certainty)**
49
+ - **Don't flag stylistic preferences** if the code follows existing patterns in the codebase
50
+ - **If you see an unfamiliar pattern**, assume it may be a project convention before flagging
51
+ - **Focus on functional correctness** over cosmetic improvements
52
+ - **Skip categories with no relevant changes** - don't force issues where none exist
53
+
54
+ ### 3. Issue Format
55
+ For each real issue found:
56
+
57
+ 📍 \`path/to/file.ts:line-range\`
58
+ **[🔴 Critical | 🟠 Major | 🟡 Minor | 🔵 Enhancement]** Brief title
59
+
60
+ → **Problem**: What's wrong and why it matters
61
+ → **Fix**: Specific suggestion or code snippet
62
+
63
+ **Confidence**: 🔴 Certain / 🟡 Likely / 🟢 Suggestion
64
+
65
+ ### 4. Output Structure
66
+
67
+ \`\`\`
68
+ ## Summary
69
+ [2-3 sentence overview]
70
+
71
+ ## Prioritized Issues
72
+
73
+ ### 🔴 Critical
74
+ [Issues that must be fixed before merge - bugs, security, breaking changes]
75
+
76
+ ### 🟠 Major
77
+ [Issues that should be fixed - logic problems, missing error handling]
78
+
79
+ ### 🟡 Minor
80
+ [Issues worth addressing - code clarity, minor optimizations]
81
+
82
+ ### 🔵 Enhancements
83
+ [Nice-to-haves - suggestions for improvement]
84
+
85
+ ## ✅ Highlights
86
+ [Positive findings, well-implemented patterns, good practices observed]
87
+ \`\`\`
88
+
89
+ ## Changed Files
90
+ {{CHANGED_FILES}}
91
+
92
+ ## Diff
93
+
94
+ \`\`\`diff
95
+ {{DIFF}}
96
+ \`\`\`
97
+
98
+ ---
99
+
100
+ **CRITICAL INSTRUCTION**: Your primary job is catching bugs and breaking changes. Be thorough but avoid false positives. If something looks unusual but might be intentional, note it as a question rather than an issue.`;
101
+ export function hasCustomPrompt(repoRoot) {
102
+ return existsSync(join(repoRoot, CONFIG_DIR, PROMPT_FILE));
103
+ }
104
+ export function loadTemplate(repoRoot) {
105
+ const promptPath = join(repoRoot, CONFIG_DIR, PROMPT_FILE);
106
+ if (existsSync(promptPath)) {
107
+ return readFileSync(promptPath, "utf-8");
108
+ }
109
+ return DEFAULT_PROMPT;
110
+ }
111
+ export function loadConfig(repoRoot) {
112
+ const configPath = join(repoRoot, CONFIG_DIR, CONFIG_FILE);
113
+ if (!existsSync(configPath)) {
114
+ return {};
115
+ }
116
+ try {
117
+ return JSON.parse(readFileSync(configPath, "utf-8"));
118
+ }
119
+ catch {
120
+ return {};
121
+ }
122
+ }
123
+ function formatFileList(files) {
124
+ if (files.length === 0)
125
+ return "_None_";
126
+ return files.map((f) => `- ${f}`).join("\n");
127
+ }
128
+ function buildPrMetadataBlock(meta) {
129
+ if (!meta)
130
+ return "";
131
+ return `## PR Metadata
132
+ **Title**: ${meta.title}
133
+ **Description**: ${meta.body}
134
+
135
+ `;
136
+ }
137
+ export function buildPrompt(template, context) {
138
+ const { baseBranch, diffContent, changedFiles, categorizedFiles, customCategories, prMetadata, } = context;
139
+ const diffBlock = diffContent;
140
+ const fencedDiff = "```diff\n" + diffContent + "\n```";
141
+ let prompt = template;
142
+ prompt = prompt.replaceAll("{{BASE_BRANCH}}", baseBranch);
143
+ prompt = prompt.replaceAll("{{CHANGED_FILES}}", formatFileList(changedFiles));
144
+ prompt = prompt.replaceAll("{{SRC_FILES}}", formatFileList(categorizedFiles.source));
145
+ prompt = prompt.replaceAll("{{TEST_FILES}}", formatFileList(categorizedFiles.test));
146
+ prompt = prompt.replaceAll("{{MIGRATION_FILES}}", formatFileList(categorizedFiles.migration));
147
+ prompt = prompt.replaceAll("{{HTTP_FILES}}", formatFileList(categorizedFiles.http));
148
+ prompt = prompt.replaceAll("{{PR_TITLE}}", prMetadata?.title ?? "");
149
+ prompt = prompt.replaceAll("{{PR_BODY}}", prMetadata?.body ?? "");
150
+ prompt = prompt.replaceAll("{{PR_METADATA}}", buildPrMetadataBlock(prMetadata));
151
+ if (customCategories) {
152
+ for (const [name, files] of Object.entries(customCategories)) {
153
+ prompt = prompt.replaceAll(`{{CATEGORY.${name}}}`, formatFileList(files));
154
+ }
155
+ }
156
+ if (prompt.includes("{{DIFF}}")) {
157
+ prompt = prompt.replaceAll("{{DIFF}}", diffBlock);
158
+ }
159
+ else {
160
+ prompt += "\n\n## Diff\n\n" + fencedDiff;
161
+ }
162
+ return prompt;
163
+ }
164
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/lib/prompt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export interface ReviewOptions {
2
+ base?: string;
3
+ exclude?: string[];
4
+ }
5
+ export declare function runReview(options: ReviewOptions): Promise<void>;
package/dist/review.js ADDED
@@ -0,0 +1,67 @@
1
+ import { isGitRepo, getRepoRoot, detectBaseBranch, ensureBranchUpToDate, getDiff, categorizeFiles, categorizeFilesWithConfig, getPrMetadata, } from "./lib/git.js";
2
+ import { loadTemplate, loadConfig, buildPrompt, hasCustomPrompt } from "./lib/prompt.js";
3
+ import { copyToClipboard, getPasteHint } from "./lib/clipboard.js";
4
+ export async function runReview(options) {
5
+ if (!isGitRepo()) {
6
+ console.error("❌ Not a git repository. Run this command from inside a git repo.");
7
+ process.exit(1);
8
+ }
9
+ const repoRoot = getRepoRoot();
10
+ let baseBranch;
11
+ try {
12
+ baseBranch = detectBaseBranch(options.base);
13
+ }
14
+ catch (err) {
15
+ console.error(`❌ ${err.message}`);
16
+ process.exit(1);
17
+ }
18
+ try {
19
+ ensureBranchUpToDate(baseBranch);
20
+ }
21
+ catch (err) {
22
+ console.error(`❌ ${err.message}`);
23
+ process.exit(1);
24
+ }
25
+ console.log(`Generating diff from ${baseBranch}...`);
26
+ const diff = getDiff(baseBranch, options.exclude);
27
+ if (!diff.content) {
28
+ console.error(`❌ No changes found compared to ${baseBranch}`);
29
+ process.exit(1);
30
+ }
31
+ console.log("✅ Diff generated successfully");
32
+ console.log(`✅ Found ${diff.changedFiles.length} changed file(s) (${diff.lineCount} lines)`);
33
+ console.log("");
34
+ if (diff.lineCount > 2000) {
35
+ console.log(`⚠️ Large diff (${diff.lineCount} lines). Consider reviewing in chunks for better results.`);
36
+ console.log("");
37
+ }
38
+ const categorized = categorizeFiles(diff.changedFiles);
39
+ const prMetadata = getPrMetadata();
40
+ const template = loadTemplate(repoRoot);
41
+ const config = loadConfig(repoRoot);
42
+ const isCustom = hasCustomPrompt(repoRoot);
43
+ const customCategories = config.categories
44
+ ? categorizeFilesWithConfig(diff.changedFiles, config.categories)
45
+ : undefined;
46
+ const prompt = buildPrompt(template, {
47
+ baseBranch,
48
+ diffContent: diff.content,
49
+ changedFiles: diff.changedFiles,
50
+ categorizedFiles: categorized,
51
+ customCategories,
52
+ prMetadata,
53
+ });
54
+ try {
55
+ await copyToClipboard(prompt);
56
+ if (isCustom) {
57
+ console.log("📋 Using custom prompt from .review-my-pr/prompt.md");
58
+ }
59
+ console.log("✅ Review prompt copied to clipboard!");
60
+ console.log(getPasteHint());
61
+ }
62
+ catch {
63
+ console.error("❌ Failed to copy to clipboard. Outputting prompt to stdout:\n");
64
+ console.log(prompt);
65
+ }
66
+ }
67
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}