-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(mcp): Add monorepo submodule support with caching and security enhancements #1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
RyderFreeman4Logos
wants to merge
6
commits into
yamadashy:main
Choose a base branch
from
RyderFreeman4Logos:feat/monorepo-submodule-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
872c1e1
feat(mcp): Add monorepo submodule support with caching
RyderFreeman4Logos 1914693
fix(mcp): Add path traversal protection for monorepo tools
RyderFreeman4Logos e400553
feat(cli): Add file lock to prevent concurrent repomix execution
RyderFreeman4Logos c612754
chore(config): Update .gitignore patterns
RyderFreeman4Logos 9f5ea93
fix(mcp): add submodule count limit to prevent resource exhaustion
RyderFreeman4Logos 1ffd4b3
fix(mcp): add schema validation for cache metadata
RyderFreeman4Logos File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import fs from 'node:fs/promises'; | ||
| import path from 'node:path'; | ||
| import { logger } from '../../shared/logger.js'; | ||
|
|
||
| const LOCK_FILENAME = '.repomix.lock'; | ||
| const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes | ||
|
|
||
| export interface LockInfo { | ||
| pid: number; | ||
| startTime: number; | ||
| cwd: string; | ||
| } | ||
|
|
||
| export class FileLockError extends Error { | ||
| constructor( | ||
| message: string, | ||
| public readonly lockPath: string, | ||
| public readonly existingLock?: LockInfo, | ||
| ) { | ||
| super(message); | ||
| this.name = 'FileLockError'; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if a process with given PID is still running. | ||
| * Uses a platform-agnostic approach by trying to send signal 0. | ||
| */ | ||
| const isProcessRunning = (pid: number): boolean => { | ||
| try { | ||
| process.kill(pid, 0); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Read and parse lock file contents. | ||
| */ | ||
| const readLockFile = async (lockPath: string): Promise<LockInfo | null> => { | ||
| try { | ||
| const content = await fs.readFile(lockPath, 'utf-8'); | ||
| const parsed = JSON.parse(content) as LockInfo; | ||
|
|
||
| if (typeof parsed.pid !== 'number' || typeof parsed.startTime !== 'number') { | ||
| return null; | ||
| } | ||
|
|
||
| return parsed; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Check if a lock is stale (process dead or lock too old). | ||
| */ | ||
| const isLockStale = (lockInfo: LockInfo): boolean => { | ||
| // Check if process is still running | ||
| if (!isProcessRunning(lockInfo.pid)) { | ||
| logger.debug(`Lock is stale: process ${lockInfo.pid} is not running`); | ||
| return true; | ||
| } | ||
|
|
||
| // Check if lock is too old (fallback for zombie processes) | ||
| const age = Date.now() - lockInfo.startTime; | ||
| if (age > STALE_THRESHOLD_MS) { | ||
| logger.debug(`Lock is stale: lock age ${age}ms exceeds threshold ${STALE_THRESHOLD_MS}ms`); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }; | ||
|
|
||
| /** | ||
| * Acquire a file lock for the specified directory. | ||
| * | ||
| * @param targetDir The directory to lock (where .repomix.lock will be created) | ||
| * @returns The path to the lock file (for cleanup) | ||
| * @throws FileLockError if lock cannot be acquired | ||
| */ | ||
| export const acquireLock = async (targetDir: string): Promise<string> => { | ||
| const lockPath = path.join(targetDir, LOCK_FILENAME); | ||
| const lockInfo: LockInfo = { | ||
| pid: process.pid, | ||
| startTime: Date.now(), | ||
| cwd: process.cwd(), | ||
| }; | ||
|
|
||
| // Check for existing lock | ||
| const existingLock = await readLockFile(lockPath); | ||
|
|
||
| if (existingLock) { | ||
| if (isLockStale(existingLock)) { | ||
| // Remove stale lock | ||
| logger.debug(`Removing stale lock file: ${lockPath}`); | ||
| try { | ||
| await fs.unlink(lockPath); | ||
| } catch { | ||
| // Ignore errors when removing stale lock | ||
| } | ||
| } else { | ||
| // Lock is held by another active process | ||
| throw new FileLockError( | ||
| `Another repomix process (PID: ${existingLock.pid}) is already running in this directory. ` + | ||
| `If you believe this is an error, remove the lock file: ${lockPath}`, | ||
| lockPath, | ||
| existingLock, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // Try to create lock file atomically | ||
| try { | ||
| const fileHandle = await fs.open(lockPath, 'wx'); | ||
| await fileHandle.writeFile(JSON.stringify(lockInfo, null, 2)); | ||
| await fileHandle.close(); | ||
| logger.debug(`Acquired lock: ${lockPath}`); | ||
| return lockPath; | ||
| } catch (error) { | ||
| if (error instanceof Error && 'code' in error && error.code === 'EEXIST') { | ||
| // Race condition: another process created the lock between our check and create | ||
| const raceLock = await readLockFile(lockPath); | ||
| throw new FileLockError( | ||
| `Another repomix process acquired the lock. If you believe this is an error, remove the lock file: ${lockPath}`, | ||
| lockPath, | ||
| raceLock ?? undefined, | ||
| ); | ||
| } | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Release a file lock. | ||
| * | ||
| * @param lockPath The path to the lock file to remove | ||
| */ | ||
| export const releaseLock = async (lockPath: string): Promise<void> => { | ||
| try { | ||
| // Verify we own the lock before releasing | ||
| const lockInfo = await readLockFile(lockPath); | ||
| if (lockInfo && lockInfo.pid === process.pid) { | ||
| await fs.unlink(lockPath); | ||
| logger.debug(`Released lock: ${lockPath}`); | ||
| } else if (lockInfo) { | ||
| logger.warn(`Lock file owned by different process (PID: ${lockInfo.pid}), not releasing`); | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { | ||
| // Lock file already removed, that's fine | ||
| logger.debug(`Lock file already removed: ${lockPath}`); | ||
| return; | ||
| } | ||
| logger.warn(`Failed to release lock: ${lockPath}`, error); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Execute a function while holding a lock on the target directory. | ||
| * | ||
| * @param targetDir The directory to lock | ||
| * @param fn The function to execute while holding the lock | ||
| * @returns The result of the function | ||
| */ | ||
| export const withLock = async <T>(targetDir: string, fn: () => Promise<T>): Promise<T> => { | ||
| const lockPath = await acquireLock(targetDir); | ||
| try { | ||
| return await fn(); | ||
| } finally { | ||
| await releaseLock(lockPath); | ||
| } | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export type { LockInfo } from './fileLock.js'; | ||
| export { acquireLock, FileLockError, releaseLock, withLock } from './fileLock.js'; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gitignore pattern may not match the actual lock file.
The pattern
**/*.repomix.lockmatches files ending in.repomix.lockwith at least one character before the dot (e.g.,foo.repomix.lock). However, the lock file created byfileLock.tsis named.repomix.lock(with a leading dot), which this pattern won't match.💡 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents