@contextrail/code-review-agent 0.1.1 → 0.1.2-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.
@@ -1,7 +1,8 @@
1
- import { access, mkdir, writeFile } from 'node:fs/promises';
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { gitDiffProvider } from './git-diff-provider.js';
4
- import { filterDiffInputs, filterFiles } from './filtering.js';
4
+ import { DEFAULT_EXCLUDE_PATTERNS } from '../config/defaults.js';
5
+ import { filterDiffInputs, filterFiles, parseGitignoreContent } from './filtering.js';
5
6
  export { triagePr } from './triage.js';
6
7
  export const isDiffInputs = (inputs) => inputs.mode === 'diff';
7
8
  export const isFileListInputs = (inputs) => inputs.mode === 'file-list';
@@ -45,6 +46,43 @@ const validateFileList = async (files, basePath) => {
45
46
  const normalizeFiles = (files) => {
46
47
  return files.map((file) => file.trim()).filter(Boolean);
47
48
  };
49
+ const mergeFilteringWithRootGitignore = async (filtering, repoRoot) => {
50
+ if (!repoRoot) {
51
+ return filtering;
52
+ }
53
+ const gitignorePath = path.join(repoRoot, '.gitignore');
54
+ try {
55
+ const gitignoreContents = await readFile(gitignorePath, 'utf-8');
56
+ if (typeof gitignoreContents !== 'string' || gitignoreContents.length === 0) {
57
+ return filtering;
58
+ }
59
+ const parsed = parseGitignoreContent(gitignoreContents);
60
+ if (parsed.excludePatterns.length === 0 && parsed.includePatterns.length === 0) {
61
+ return filtering;
62
+ }
63
+ const excludePatterns = filtering?.excludePatterns
64
+ ? [...filtering.excludePatterns, ...parsed.excludePatterns]
65
+ : [...DEFAULT_EXCLUDE_PATTERNS, ...parsed.excludePatterns];
66
+ const includePatterns = [...(filtering?.includePatterns ?? []), ...parsed.includePatterns];
67
+ return {
68
+ ...filtering,
69
+ excludePatterns,
70
+ includePatterns,
71
+ };
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ const code = typeof error === 'object' && error !== null && 'code' in error
76
+ ? String(error.code)
77
+ : undefined;
78
+ const isMissingFileMessage = message.toLowerCase().includes('no such file') || message.toLowerCase().includes('file not found');
79
+ // Missing .gitignore is expected in many repos; keep defaults without failing.
80
+ if (code === 'ENOENT' || isMissingFileMessage) {
81
+ return filtering;
82
+ }
83
+ throw new Error(`Failed to read root .gitignore at "${gitignorePath}": ${message}`);
84
+ }
85
+ };
48
86
  export const buildReviewInputs = async (options, deps) => {
49
87
  if (options.mode === 'diff') {
50
88
  const diffProvider = deps?.diffProvider ?? gitDiffProvider;
@@ -53,8 +91,9 @@ export const buildReviewInputs = async (options, deps) => {
53
91
  from: options.from,
54
92
  to: options.to,
55
93
  });
94
+ const mergedFiltering = await mergeFilteringWithRootGitignore(options.filtering, options.repoPath);
56
95
  const normalizedFiles = normalizeFiles(result.files);
57
- const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs, options.filtering);
96
+ const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs, mergedFiltering);
58
97
  if (files.length === 0) {
59
98
  throw new Error('Diff mode produced no files to review after filtering.');
60
99
  }
@@ -76,8 +115,9 @@ export const buildReviewInputs = async (options, deps) => {
76
115
  }
77
116
  return inputs;
78
117
  }
118
+ const mergedFiltering = await mergeFilteringWithRootGitignore(options.filtering, options.basePath);
79
119
  const normalizedFiles = normalizeFiles(options.files);
80
- const files = filterFiles(normalizedFiles, options.filtering);
120
+ const files = filterFiles(normalizedFiles, mergedFiltering);
81
121
  if (files.length === 0) {
82
122
  throw new Error('File list produced no files to review after filtering.');
83
123
  }
@@ -108,15 +148,21 @@ export const buildReviewInputs = async (options, deps) => {
108
148
  return inputs;
109
149
  };
110
150
  export const persistReviewInputs = async (inputs, outputDir) => {
111
- await mkdir(outputDir, { recursive: true });
112
- const filesPayload = JSON.stringify({ mode: inputs.mode, files: inputs.files }, null, 2);
113
- await writeFile(path.join(outputDir, 'files.json'), filesPayload);
114
- if (inputs.mode !== 'diff') {
115
- return;
151
+ try {
152
+ await mkdir(outputDir, { recursive: true });
153
+ const filesPayload = JSON.stringify({ mode: inputs.mode, files: inputs.files }, null, 2);
154
+ await writeFile(path.join(outputDir, 'files.json'), filesPayload);
155
+ if (inputs.mode !== 'diff') {
156
+ return;
157
+ }
158
+ for (const [filePath, diff] of Object.entries(inputs.diffs)) {
159
+ const diffPath = path.join(outputDir, 'diff', `${filePath}.diff`);
160
+ await mkdir(path.dirname(diffPath), { recursive: true });
161
+ await writeFile(diffPath, diff);
162
+ }
116
163
  }
117
- for (const [filePath, diff] of Object.entries(inputs.diffs)) {
118
- const diffPath = path.join(outputDir, 'diff', `${filePath}.diff`);
119
- await mkdir(path.dirname(diffPath), { recursive: true });
120
- await writeFile(diffPath, diff);
164
+ catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ throw new Error(`Failed to persist review inputs to "${outputDir}": ${message}`);
121
167
  }
122
168
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextrail/code-review-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-alpha.1",
4
4
  "description": "CLI tool for orchestrating ContextRail-powered code reviews",
5
5
  "homepage": "https://contextrail.app",
6
6
  "repository": {