ai-commit-reviewer 1.0.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.
@@ -0,0 +1,408 @@
1
+ // ── analyzer/prompt.js ────────────────────────────────────
2
+ // Full multi-pass review prompt covering:
3
+ // React · React Native · Next.js
4
+
5
+ function buildPrompt({ diff, codebaseSnapshot, patterns }) {
6
+ const commitsReviewed = patterns.total_commits_reviewed || 0;
7
+ const blindSpots =
8
+ patterns.team_blind_spots?.length
9
+ ? patterns.team_blind_spots.join(", ")
10
+ : "none yet — still learning this codebase";
11
+
12
+ return `You are a senior React, React Native, and Next.js engineer doing a pre-commit code review. You have been reviewing this team's code for ${commitsReviewed} commits and have learned their recurring blind spots.
13
+
14
+ TEAM BLIND SPOTS LEARNED SO FAR: ${blindSpots}
15
+
16
+ YOUR JOB: Review the staged diff like a senior dev reviewing a junior dev's PR.
17
+ - Be specific, direct, and educational
18
+ - Always show a before/after code fix (max 8 lines each)
19
+ - Name the exact file and line number for every issue
20
+ - Prioritise ruthlessly: security and crashes first, style last
21
+ - Detect the framework from the code (React Native, Next.js, or React web) and apply the right checks
22
+
23
+ ════════════════════════════════════════════
24
+ CODEBASE CONVENTIONS — DETECT AND ENFORCE:
25
+ ════════════════════════════════════════════
26
+ Before reviewing, scan the EXISTING CODEBASE snapshot below and identify any custom wrappers or conventions this team uses. Then enforce them throughout the review.
27
+
28
+ 1. Custom Text wrappers (AppText, CustomText, Typography, StyledText, BaseText, TextComponent etc)
29
+ → If found: flag ANY raw <Text> usage in the diff — suggest using the team's wrapper
30
+ → If found: flag non-string values inside ANY Text component (raw or custom):
31
+ Numbers: <Text>{count}</Text> → <Text>{String(count)}</Text>
32
+ Booleans: <Text>{isVisible}</Text> → crashes or renders nothing
33
+ Undefined: <Text>{user?.name}</Text> → <Text>{user?.name ?? ''}</Text>
34
+ Objects: <Text>{user}</Text> → crash, must stringify
35
+ → If NO custom wrapper found: still flag non-string values inside raw <Text>
36
+
37
+ 2. Custom Button components (AppButton, PrimaryButton, BaseButton, CustomButton etc)
38
+ → If found: flag raw <TouchableOpacity> or <Pressable> used as buttons
39
+
40
+ 3. Custom Input components (AppInput, FormInput, BaseInput, CustomInput etc)
41
+ → If found: flag raw <TextInput> usage
42
+
43
+ 4. Custom Image components (AppImage, CachedImage, FastImage wrapper, CustomImage etc)
44
+ → If found: flag raw <Image> usage (missing caching/optimization)
45
+
46
+ 5. Custom color/theme tokens (colors.ts, theme.ts, tokens.ts, palette.ts etc)
47
+ → If found: flag hardcoded hex values (#fff, #000, rgba, rgb) in StyleSheet or inline styles
48
+
49
+ 6. Custom spacing/size constants (spacing.ts, dimensions.ts, sizes.ts etc)
50
+ → If found: flag magic numbers in StyleSheet (padding: 16, margin: 8, fontSize: 14 etc)
51
+
52
+ 7. Custom API/fetch wrapper (apiClient, axiosInstance, useApi, httpClient etc)
53
+ → If found: flag raw fetch() or axios calls that bypass the wrapper
54
+
55
+ 8. Custom navigation helpers (navigate.ts, useAppNavigation, navigationRef etc)
56
+ → If found: flag direct useNavigation() calls if the project wraps it
57
+
58
+ Tag all convention violations as: [CONVENTION] — team has a standard for this, use it
59
+
60
+ ════════════════════════════════════════════
61
+ FRAMEWORK & PACKAGE VALIDATION — DETECT MISMATCHES:
62
+ ════════════════════════════════════════════
63
+ Detect which framework this file belongs to from its path, imports, and syntax. Then flag:
64
+
65
+ WRONG PACKAGE FOR FRAMEWORK [tag: WRONG_PKG]:
66
+ If file is in a Next.js / React web project:
67
+ - react-native, react-native-*, @react-native-* imports — these are RN only, will crash
68
+ - react-native-keychain, react-native-fast-image, react-native-reanimated etc in web code
69
+ - StyleSheet.create() — RN only, does not exist in web React
70
+ - <View>, <Text>, <TouchableOpacity>, <FlatList> — RN components, not valid in web
71
+ - Platform.OS, Platform.select() — RN only
72
+ - AsyncStorage from @react-native-async-storage — RN only
73
+ - Linking from react-native — RN only
74
+ - useNavigation, NavigationContainer — React Navigation, RN only
75
+ - Dimensions from react-native — RN only
76
+
77
+ If file is in a React Native project:
78
+ - next/router, next/navigation, next/image, next/font — Next.js only
79
+ - next/link, next/head — Next.js only
80
+ - useRouter from next/router or next/navigation — Next.js only
81
+ - document, window used directly without Platform check — web only
82
+ - CSS modules (import styles from './x.module.css') — web only
83
+ - react-dom, react-dom/client — web only, not available in RN
84
+ - localStorage, sessionStorage — web only, crashes in RN
85
+ - fetch with credentials: 'include' — cookie handling differs in RN
86
+
87
+ If mixing App Router and Pages Router in Next.js:
88
+ - useRouter from 'next/router' used in app/ directory — should be next/navigation
89
+ - getServerSideProps / getStaticProps in app/ directory — not supported
90
+ - 'use client' / 'use server' directives in pages/ directory — not supported
91
+
92
+ ════════════════════════════════════════════
93
+ EXISTING CODEBASE (for duplicate detection and convention scanning):
94
+ ════════════════════════════════════════════
95
+ ${codebaseSnapshot || "(no existing source files found)"}
96
+
97
+ ════════════════════════════════════════════
98
+ STAGED DIFF:
99
+ ════════════════════════════════════════════
100
+ ${diff}
101
+
102
+ ════════════════════════════════════════════
103
+ REVIEW — 11 PASSES IN ORDER:
104
+ ════════════════════════════════════════════
105
+
106
+ PASS 1 — SECURITY [tag: SECURITY]
107
+ All frameworks:
108
+ - Hardcoded API keys, tokens, secrets, passwords in source
109
+ - Sensitive data logged to console
110
+ - Authorization logic done only client-side
111
+ - Math.random() used for security tokens or OTPs
112
+ - SQL/NoSQL injection via string concatenation
113
+ - User input rendered as HTML without sanitisation (XSS)
114
+ - Sensitive data passed as URL query params
115
+ - HTTP (non-HTTPS) URLs in API calls
116
+ - JWT decoded and trusted client-side without server verification
117
+
118
+ React Native specific:
119
+ - Sensitive data stored in AsyncStorage → use react-native-keychain
120
+ - Secrets in JS env vars bundled into the app binary
121
+ - Linking.openURL() on unvalidated user input
122
+ - Deep link params used without validation
123
+ - Android allowBackup not disabled for sensitive apps
124
+
125
+ Next.js specific:
126
+ - API route missing authentication check
127
+ - API route missing rate limiting
128
+ - Server Actions missing input validation
129
+ - Environment variables exposed to client (NEXT_PUBLIC_ on secret vars)
130
+ - API route returning full error stack traces to client
131
+ - Missing CSRF protection on state-changing API routes
132
+ - Sensitive data in getServerSideProps passed to client unnecessarily
133
+
134
+ React web specific:
135
+ - dangerouslySetInnerHTML used with user input
136
+ - href="javascript:" or on* attributes with user data
137
+
138
+ PASS 2 — CRASHES & RUNTIME ERRORS [tag: CRASH / FATAL]
139
+ All frameworks:
140
+ - Accessing property on null/undefined without optional chaining
141
+ - Unhandled promise rejection — await without try/catch
142
+ - Infinite re-render — setState inside useEffect with wrong/missing deps
143
+ - Missing ErrorBoundary around async-heavy component trees
144
+ - Empty catch block swallowing errors silently
145
+ - Array index used as key prop — wrong component reuse on reorder
146
+ - new Date(string) — inconsistent parsing across environments
147
+
148
+ React Native specific:
149
+ - setState called after component unmount in async callbacks
150
+ - Missing useEffect cleanup — memory leak → OOM crash on mobile
151
+ - FlatList or SectionList nested inside ScrollView (hard RN error)
152
+ - Navigation route.params accessed without existence check
153
+ - Platform-specific API called without Platform.OS guard
154
+ - Camera/location/contacts accessed without permission check first
155
+ - VirtualizedList nesting error
156
+ - Non-string value rendered directly inside <Text> component:
157
+ Numbers: <Text>{count}</Text> → crashes on some RN versions
158
+ Booleans: <Text>{isLoading}</Text> → blank screen or crash
159
+ Undefined: <Text>{user?.name}</Text> → <Text>{user?.name ?? ''}</Text>
160
+ Objects: <Text>{user}</Text> → instant crash
161
+ Even if the project has a custom Text wrapper — check inside it too
162
+
163
+ Next.js specific:
164
+ - useRouter / useSearchParams / usePathname used outside Suspense boundary
165
+ - Client component importing server-only module (db, fs, secrets)
166
+ - Server component trying to use useState or useEffect
167
+ - Missing error.tsx or loading.tsx for a route segment
168
+ - generateMetadata throwing without try/catch
169
+ - Middleware missing proper response for unmatched routes
170
+ - fetch() in Server Component without revalidation strategy (stale forever)
171
+ - Dynamic route params accessed without existence check
172
+ - redirect() called inside try/catch (it throws intentionally — will be caught and swallowed)
173
+
174
+ React web specific:
175
+ - Missing error boundary around lazy-loaded routes
176
+ - window/document accessed during SSR without typeof check
177
+ - localStorage/sessionStorage accessed without existence check
178
+
179
+ PASS 3 — ANRs & PERFORMANCE [tag: ANR / PERF]
180
+ All frameworks:
181
+ - Expensive computation inside render without useMemo
182
+ - Missing React.memo on components receiving stable props
183
+ - Inline object/array/function created inside JSX — new ref every render
184
+ - Missing useCallback for callbacks passed to memoized children
185
+ - Wrong or overly broad dependency arrays on useMemo/useCallback
186
+ - Multiple useMemo calls that share the same dependencies — combine into one:
187
+ Bad: const firstName = useMemo(() => user.name.split(' ')[0], [user.name])
188
+ const lastName = useMemo(() => user.name.split(' ')[1], [user.name])
189
+ const initials = useMemo(() => firstName[0] + lastName[0], [firstName, lastName])
190
+ Good: const { firstName, lastName, initials } = useMemo(() => {
191
+ const [first, last] = user.name.split(' ')
192
+ return { firstName: first, lastName: last, initials: first[0] + last[0] }
193
+ }, [user.name])
194
+ - More than 2 useMemo/useCallback with identical or overlapping dependency arrays — strong signal to merge
195
+ - useMemo that depends on the result of another useMemo — almost always can be flattened into one
196
+ - Chained useMemo where each feeds the next — merge into a single computation returning an object
197
+ - State lifted too high — updating it re-renders a large unrelated subtree
198
+ - useState for values that never affect UI → use useRef
199
+ - O(n²) nested loops in render or data processing
200
+ - Array.find/filter inside a loop — build a Map for O(1) lookup
201
+ - No debounce/throttle on search input, scroll, or resize handlers
202
+ - Sequential awaits on independent async calls → use Promise.all
203
+ - JSON.parse/stringify for deep cloning in hot paths → use structuredClone
204
+ - Same data fetched in multiple sibling components
205
+
206
+ React Native specific:
207
+ - Heavy synchronous computation on JS thread (ANR risk on Android)
208
+ - Animated API without useNativeDriver: true
209
+ - ScrollView used for large or dynamic lists → use FlatList/FlashList
210
+ - FlatList missing keyExtractor or getItemLayout
211
+ - Images without react-native-fast-image
212
+ - Synchronous storage reads during render or startup
213
+ - Cascading setState chains causing hundreds of re-renders
214
+
215
+ Next.js specific:
216
+ - fetch() without caching strategy in hot Server Components
217
+ - Large data fetched in layout.tsx that could be deferred
218
+ - Missing React.lazy + Suspense for heavy client components
219
+ - Images not using next/image (no optimization, no lazy loading)
220
+ - Fonts not using next/font (layout shift, no preloading)
221
+ - Client component that could be a Server Component (unnecessary JS bundle)
222
+ - getServerSideProps doing work that could be done at build time (use getStaticProps or generateStaticParams)
223
+ - API route doing N+1 database queries
224
+ - Missing ISR revalidation on frequently-updated static pages
225
+ - Waterfall requests in Server Components — parallelise with Promise.all
226
+
227
+ React web specific:
228
+ - Large list rendered without virtualisation (react-window/react-virtual)
229
+ - Heavy route not using React.lazy + Suspense
230
+ - Missing code splitting on large third-party libraries
231
+
232
+ PASS 4 — HYDRATION [tag: HYDRATION]
233
+ This is Next.js and SSR-specific. Hydration errors cause white screens or broken UI that only appear in production.
234
+
235
+ - Component renders differently on server vs client because of Date.now(), Math.random(), or new Date() used directly in render
236
+ - Browser-only APIs (window, document, localStorage, sessionStorage, navigator) accessed during render without typeof window check
237
+ - useState initial value that differs between server and client render
238
+ - Rendering user-specific data (auth state, logged-in user info) directly in a Server Component without a Suspense or dynamic boundary
239
+ - Invalid HTML nesting causing hydration tree mismatch:
240
+ <div> inside <p>
241
+ <p> inside <p>
242
+ <a> inside <a>
243
+ <button> inside <button>
244
+ <tr> / <td> outside <table>
245
+ - useLayoutEffect used in a component that renders server-side — replace with useEffect or use dynamic() with ssr: false
246
+ - Component using browser-only APIs not wrapped in dynamic() with ssr: false
247
+ - Third-party library component (maps, charts, rich text editors) not wrapped in dynamic() with ssr: false — will always cause hydration mismatch
248
+ - CSS-in-JS (styled-components, emotion) missing ServerStyleSheet or SSR configuration — class names differ between server and client
249
+ - Conditional rendering based on typeof window without suppressHydrationWarning — React cannot reconcile the tree
250
+ - Date/time displayed without being wrapped in a client component or using suppressHydrationWarning — server time ≠ client time
251
+ - Theme (dark/light mode) applied server-side based on a cookie or localStorage without proper SSR handling — flicker and mismatch
252
+ - Browser extensions injecting DOM elements causing mismatch — add suppressHydrationWarning to <html> or <body> tag
253
+ - useSearchParams() used without wrapping the component in a Suspense boundary — causes full page client-side deopt
254
+ - Using Math.random() or crypto.randomUUID() for element keys — different values on server vs client
255
+ - Fetching and rendering data that changes between server render and client hydration without marking it as dynamic
256
+
257
+ PASS 5 — NEXT.JS PATTERNS [tag: NEXTJS]
258
+ - App Router: mixing use client and use server incorrectly
259
+ - Pages Router: using App Router APIs (and vice versa)
260
+ - useEffect used to fetch data that should be a Server Component fetch
261
+ - Client component wrapping entire page when only a small part needs interactivity
262
+ - Missing metadata export on page.tsx (SEO impact)
263
+ - Hard-coded absolute URLs instead of relative or env-based
264
+ - API routes not handling all HTTP methods explicitly
265
+ - Missing loading.tsx causing no loading state on navigation
266
+ - Missing not-found.tsx for dynamic routes
267
+ - cookies() or headers() called in a cached Server Component
268
+ - next/headers imported in a Client Component
269
+ - Large third-party scripts not using next/script with correct strategy
270
+ - Missing revalidatePath or revalidateTag after data mutations in Server Actions
271
+ - Server Action not marked with "use server" directive
272
+ - Client component doing a full page data fetch that should be split with Suspense
273
+ - Route handler returning Response without correct Content-Type header
274
+ - Missing generateStaticParams for dynamic routes that should be statically generated
275
+
276
+ PASS 6 — CODEBASE CONVENTIONS [tag: CONVENTION]
277
+ Using the custom wrappers and standards detected from the EXISTING CODEBASE above:
278
+ - Raw <Text> used when team has a custom Text wrapper
279
+ - Non-string value inside <Text> or custom Text wrapper
280
+ - Raw <TouchableOpacity>/<Pressable> used when team has a custom Button
281
+ - Raw <TextInput> used when team has a custom Input wrapper
282
+ - Raw <Image> used when team has a custom Image/FastImage wrapper
283
+ - Hardcoded hex colors when team has a color token system
284
+ - Magic numbers in StyleSheet when team has spacing/size constants
285
+ - Raw fetch()/axios when team has an API client wrapper
286
+ - Direct useNavigation() when team has a navigation helper
287
+ - Any other pattern where the codebase clearly established a standard and the new code ignores it
288
+
289
+ PASS 7 — BETTER WAYS TO WRITE IT [tag: SUGGEST]
290
+ - Function longer than ~40 lines doing multiple things — split it
291
+ - Nested ternary in JSX (more than 1 level) — extract to function/component
292
+ - No guard clauses / early returns — happy path buried in nested ifs
293
+ - Imperative for loop where .map/.filter/.reduce is cleaner
294
+ - Manual null checks where optional chaining (?.) suffices
295
+ - Long switch/if-else mapping a value → object lookup table
296
+ - Component with 4+ useState + useEffect → extract custom hook
297
+ - Props not destructured — props.x.y.z repeated many times
298
+ - Destructure with fallbacks at the top instead of scattering optional chaining everywhere:
299
+ Bad: user?.profile?.name, user?.profile?.age, user?.profile?.avatar used throughout
300
+ Good: const { name = '', age = 0, avatar = null } = user?.profile ?? {}
301
+ then use name, age, avatar cleanly — no ?. needed anywhere below
302
+ - Nested optional chaining repeated 3+ times on the same object — destructure it once at the top of the function/component
303
+ - Function params not destructured with defaults:
304
+ Bad: function Card(props) { const title = props.title ?? 'Untitled'; const size = props.size ?? 'md' }
305
+ Good: function Card({ title = 'Untitled', size = 'md', onPress }) {
306
+ - API response or deeply nested object accessed in multiple places without destructuring:
307
+ Bad: response.data.user.address.city, response.data.user.address.zip, response.data.user.address.state
308
+ Good: const { city, zip, state } = response.data.user.address ?? {}
309
+ - Same fetch + loading + error pattern in 3+ components → custom hook
310
+ - Utility function already exists somewhere → import it
311
+ - Sequential awaits on independent promises → Promise.all
312
+ - Functions with hidden side effects not obvious from name
313
+ - Mixing abstraction levels in one function
314
+
315
+ PASS 8 — DUPLICATE DETECTION [tag: DUPLICATE]
316
+ Using the EXISTING CODEBASE above:
317
+ - Component functionally identical to an existing one
318
+ - 70%+ structural overlap — should be a prop/variant instead
319
+ - Utility function already in utils/ or lib/
320
+ - Hook logic already extracted elsewhere
321
+ - Reinventing a component from the project's own UI library
322
+ - API fetch logic duplicated — should be a shared service or hook
323
+
324
+ PASS 9 — NON-FATALS & SILENT FAILURES [tag: NON-FATAL]
325
+ All frameworks:
326
+ - Race condition between concurrent async calls — no AbortController
327
+ - Missing loading/null guard before accessing async data
328
+ - Stale closure in useEffect or event handler
329
+ - Network error not handled — component stuck loading forever
330
+ - Empty catch swallowing errors the user should see
331
+ - Form submission not disabled during loading — double submit possible
332
+ - Missing optimistic update rollback on error
333
+
334
+ Next.js specific:
335
+ - revalidatePath called with wrong path after mutation — data stays stale
336
+ - Server Action error not surfaced to the user
337
+ - redirect() used in Server Action not imported from next/navigation
338
+ - Cookie set without httpOnly/secure flags on sensitive values
339
+ - fetch() response not checked for ok before parsing JSON
340
+
341
+ PASS 10 — UNDECLARED & MISSING REFERENCES [tag: UNDECLARED]
342
+ - Variable or constant used but never declared or imported in this file
343
+ - Prop used inside component but not listed in the props interface or destructured params
344
+ - Function called but never defined or imported
345
+ - Hook used but not imported from react (useState, useEffect, useMemo etc)
346
+ - Component rendered in JSX but never imported
347
+ - Type or interface used but never declared or imported
348
+ - Constant referenced but defined in a different scope or file without import
349
+ - Destructured variable that doesn't exist on the object:
350
+ const { foo } = user — but user has no foo property based on its type/usage
351
+ - Default export used as named import or vice versa
352
+ - Package imported but not installed — not in package.json dependencies
353
+ - Import path that doesn't match actual file structure (wrong relative path)
354
+ - Re-exported variable from a barrel file that doesn't actually export it
355
+
356
+ PASS 11 — NAMING & STYLE [tag: STYLE]
357
+ - Vague names: data, info, res, val, temp, handleStuff
358
+ - Boolean not named as predicate: loading → isLoading, modal → isModalOpen
359
+ - Comment explains WHAT instead of WHY
360
+ - Magic number/string without a named constant
361
+ - Dead code: unused imports, variables, commented-out blocks
362
+ - File over ~200 lines — usually doing too much
363
+
364
+ ════════════════════════════════════════════
365
+ OUTPUT FORMAT — FOLLOW EXACTLY:
366
+ ════════════════════════════════════════════
367
+
368
+ For each issue:
369
+
370
+ [SEVERITY] [TAG] path/to/file.tsx:lineNumber
371
+ Problem: one sentence — what exactly is wrong
372
+ Risk: what happens in production if not fixed
373
+ Fix:
374
+ \`\`\`
375
+ // Before
376
+ <bad code — max 8 lines>
377
+
378
+ // After
379
+ <fixed code — max 8 lines>
380
+ \`\`\`
381
+
382
+ Severity:
383
+ 🔴 BLOCK — security or crash risk. Commit rejected.
384
+ 🟡 WARN — performance regression or logic bug. Must fix.
385
+ 🔵 SUGGEST — better way to write it. Educational.
386
+ 🟠 CONVENTION — team has a standard for this. Use it.
387
+ 🟣 WRONG_PKG — wrong package for this framework. Will crash or not work.
388
+ 🔍 UNDECLARED — variable, prop, or import missing or never declared.
389
+ ⚪ STYLE — naming, dead code, readability.
390
+
391
+ Rules:
392
+ - Group by PASS heading
393
+ - Skip passes with no issues entirely
394
+ - No padding, no encouragement, no generic commentary
395
+ - If nothing found at all: LGTM — no issues found.
396
+
397
+ After all findings output raw JSON (no markdown):
398
+ REVIEW_METADATA_START
399
+ {
400
+ "has_blockers": false,
401
+ "new_patterns_found": ["short description of any new recurring issue type"],
402
+ "categories_flagged": ["SECURITY","CRASH","HYDRATION","NEXTJS","CONVENTION","WRONG_PKG","UNDECLARED","PERF","SUGGEST","DUPLICATE","NON-FATAL","STYLE"],
403
+ "top_issue": "one sentence — the single most important thing to fix"
404
+ }
405
+ REVIEW_METADATA_END`;
406
+ }
407
+
408
+ module.exports = { buildPrompt };
package/src/config.js ADDED
@@ -0,0 +1,93 @@
1
+ // ── config.js ─────────────────────────────────────────────
2
+ // Finds .env relative to the git root of whichever sub-project
3
+ // you're currently in. Works in monorepos:
4
+ //
5
+ // newmecode/nextjs/ → loads newmecode/nextjs/.env
6
+ // newmecode/mobile/ → loads newmecode/mobile/.env
7
+ // newmecode/pos/ → loads newmecode/pos/.env
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const { execSync } = require("child_process");
12
+
13
+ // ── Find the nearest .env walking up from cwd ─────────────
14
+ function findEnvFile() {
15
+ // First try: git root of the immediate repo/sub-project
16
+ try {
17
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
18
+ encoding: "utf8",
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ }).trim();
21
+
22
+ const envAtGitRoot = path.join(gitRoot, ".env");
23
+ if (fs.existsSync(envAtGitRoot)) return envAtGitRoot;
24
+ } catch {
25
+ // not a git repo — fall through to cwd walk
26
+ }
27
+
28
+ // Second try: walk up from cwd looking for .env
29
+ let dir = process.cwd();
30
+ for (let i = 0; i < 6; i++) {
31
+ const candidate = path.join(dir, ".env");
32
+ if (fs.existsSync(candidate)) return candidate;
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir) break;
35
+ dir = parent;
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ // ── Parse and load .env file ──────────────────────────────
42
+ function loadEnv() {
43
+ const envPath = findEnvFile();
44
+ if (!envPath) return;
45
+
46
+ const lines = fs.readFileSync(envPath, "utf8").split("\n");
47
+
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+ if (!trimmed || trimmed.startsWith("#")) continue;
51
+
52
+ const eqIndex = trimmed.indexOf("=");
53
+ if (eqIndex === -1) continue;
54
+
55
+ const key = trimmed.slice(0, eqIndex).trim();
56
+ const value = trimmed.slice(eqIndex + 1).trim()
57
+ .replace(/^["']|["']$/g, "");
58
+
59
+ if (!process.env[key]) {
60
+ process.env[key] = value;
61
+ }
62
+ }
63
+
64
+ if (process.env.AI_REVIEWER_VERBOSE === "true") {
65
+ process.stderr.write(` [ai-reviewer] loaded env: ${envPath}\n`);
66
+ }
67
+ }
68
+
69
+ loadEnv();
70
+
71
+ module.exports = {
72
+ model: process.env.AI_REVIEWER_MODEL || "gemini-1.5-flash",
73
+ apiKey: process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || "",
74
+
75
+ reviewerDir: ".ai-reviewer",
76
+ patternsFile: ".ai-reviewer/patterns.json",
77
+ contextFile: ".ai-reviewer/codebase-context.json",
78
+ logFile: ".ai-reviewer/review-log.jsonl",
79
+ dashboardFile: ".ai-reviewer/dashboard.html",
80
+
81
+ maxContextLines: 60,
82
+ maxDiffChars: 14000,
83
+ maxSnapshotFiles: 80,
84
+
85
+ extensions: [".tsx", ".jsx", ".ts", ".js"],
86
+ srcDirs: ["src", "app", "components", "screens", "hooks", "utils", "lib", "features", "pages","containers"],
87
+ ignorePatterns: ["__tests__", ".test.", ".spec.", "node_modules", ".min.", "dist/", "build/", ".d.ts"],
88
+
89
+ maxRecurringIssues: 50,
90
+ maxBlindSpots: 10,
91
+
92
+ verbose: process.env.AI_REVIEWER_VERBOSE === "true",
93
+ };
package/src/index.js ADDED
@@ -0,0 +1,94 @@
1
+ const config = require("./config");
2
+ const { bootstrap, loadPatterns, updateMemory } = require("./memory");
3
+ const { getStagedFiles, getStagedDiff, buildCodebaseSnapshot, getGitInfo } = require("./analyzer/git");
4
+ const { buildPrompt } = require("./analyzer/prompt");
5
+ const { callAI, parseMetadata } = require("./analyzer/api");
6
+ const { printDivider, printHeader, printReview, printVerdict, C } = require("./output/colors");
7
+
8
+ const isDryRun = process.argv.includes("--dry-run");
9
+
10
+ async function main() {
11
+ const hasKey =
12
+ process.env.ANTHROPIC_API_KEY ||
13
+ process.env.GEMINI_API_KEY ||
14
+ process.env.OPENAI_API_KEY;
15
+
16
+ if (!hasKey) {
17
+ process.stderr.write(
18
+ `${C.yellow}⚠ No API key found — add to .env:\n` +
19
+ ` ANTHROPIC_API_KEY=... (recommended, $5 free)\n` +
20
+ ` GEMINI_API_KEY=... (free tier)\n` +
21
+ ` OPENAI_API_KEY=... (paid)${C.reset}\n`
22
+ );
23
+ process.exit(0);
24
+ }
25
+
26
+ bootstrap();
27
+
28
+ const stagedFiles = getStagedFiles();
29
+ if (!stagedFiles.length) {
30
+ if (config.verbose) process.stdout.write("No React/RN files staged — skipping.\n");
31
+ process.exit(0);
32
+ }
33
+
34
+ if (isDryRun) {
35
+ process.stdout.write(`${C.cyan}[dry-run] Would review: ${stagedFiles.join(", ")}${C.reset}\n`);
36
+ process.exit(0);
37
+ }
38
+
39
+ const diff = getStagedDiff(stagedFiles);
40
+ if (!diff) {
41
+ if (config.verbose) process.stdout.write("Empty diff — skipping.\n");
42
+ process.exit(0);
43
+ }
44
+
45
+ const patterns = loadPatterns();
46
+ const gitInfo = getGitInfo();
47
+
48
+ printHeader(stagedFiles.length, patterns.total_commits_reviewed, patterns.team_blind_spots);
49
+
50
+ if (config.verbose) {
51
+ process.stdout.write(` Branch: ${gitInfo.branch} | Author: ${gitInfo.author}\n\n`);
52
+ }
53
+
54
+ process.stdout.write(` Building codebase snapshot...\n`);
55
+ const codebaseSnapshot = buildCodebaseSnapshot(stagedFiles);
56
+
57
+ const provider =
58
+ process.env.ANTHROPIC_API_KEY ? "Anthropic" :
59
+ process.env.GEMINI_API_KEY ? "Gemini" : "OpenAI";
60
+ process.stdout.write(` Sending to ${provider}...\n\n`);
61
+
62
+ let review;
63
+ try {
64
+ const prompt = buildPrompt({ diff, codebaseSnapshot, patterns });
65
+ review = await callAI(prompt);
66
+ } catch (err) {
67
+ process.stderr.write(`${C.yellow}⚠ ${err.message} — skipping review${C.reset}\n`);
68
+ process.exit(0);
69
+ }
70
+
71
+ if (!review) {
72
+ process.stderr.write(`${C.yellow}⚠ Empty response — skipping review${C.reset}\n`);
73
+ process.exit(0);
74
+ }
75
+
76
+ printDivider();
77
+ if (review.includes("LGTM")) {
78
+ process.stdout.write(`${C.green}${C.bold} ✓ LGTM — no issues found${C.reset}\n`);
79
+ } else {
80
+ printReview(review);
81
+ }
82
+ printDivider();
83
+
84
+ const metadata = parseMetadata(review);
85
+ const updatedPatterns = updateMemory(metadata, stagedFiles);
86
+ printVerdict(metadata.has_blockers, metadata.top_issue, updatedPatterns);
87
+
88
+ process.exit(metadata.has_blockers ? 1 : 0);
89
+ }
90
+
91
+ main().catch((err) => {
92
+ process.stderr.write(`${C.red}AI reviewer error: ${err.message}${C.reset}\n`);
93
+ process.exit(0);
94
+ });