Skip to content

feat(cli): Add --watch flag for automatic re-packing on file changes#1466

Open
PAMulligan wants to merge 14 commits intoyamadashy:mainfrom
PMDevSolutions:feat/file-watch-option
Open

feat(cli): Add --watch flag for automatic re-packing on file changes#1466
PAMulligan wants to merge 14 commits intoyamadashy:mainfrom
PMDevSolutions:feat/file-watch-option

Conversation

@PAMulligan
Copy link
Copy Markdown

@PAMulligan PAMulligan commented Apr 13, 2026

Closes #1429

Adds a --watch / -w flag that watches input files for changes and automatically re-packs when files are modified, created, or deleted.

How it works

  1. Runs a normal pack on startup
  2. Sets up a chokidar file watcher on the files discovered during file search (safeFilePaths)
  3. On file change events (change, add, unlink), debounces for 300ms then re-runs the pack
  4. Logs a "Rebuilt at HH:MM:SS" timestamp after each rebuild
  5. Gracefully shuts down on Ctrl+C (SIGINT/SIGTERM)

Usage

# Watch all files in current directory
repomix --watch
repomix -w

# Combine with other options
repomix -w --include "src/**/*.ts"
repomix -w -o output.xml --style xml

Conflict validation

--watch cannot be combined with:

  • --remote (watch mode only works with local directories)
  • --stdout (watch mode writes to a file)
  • --stdin (watch mode discovers files automatically)

Clear error messages are shown when conflicting flags are used.

Implementation details

  • Uses chokidar for cross-platform file watching with awaitWriteFinish for stability
  • Follows the existing action pattern (remoteAction, defaultAction) with dependency injection via deps parameter for testability
  • Lazy-loads chokidar via dynamic import() so the dependency is only loaded when --watch is used
  • 10 new tests (7 watch behavior + 3 conflict validation), all using fake timers and AbortSignal for deterministic testing

Checklist

  • Run npm run test (1116 passed, 0 failures)
  • Run npm run lint (0 errors, only pre-existing warnings)

Open with Devin

@PAMulligan PAMulligan requested a review from yamadashy as a code owner April 13, 2026 19:13
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: feee50ec-850c-46dc-8cce-4b572b51163d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a CLI watch mode that runs an initial pack, starts a chokidar watcher over resolved directories, triggers debounced (300ms) serialized rebuilds on file add/change/unlink, logs rebuild timestamps, enforces mutual-exclusion rules, supports graceful shutdown via AbortSignal or SIGINT/SIGTERM, and includes tests and README docs.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added Watch Mode section and examples (repomix --watch, -w --include "src/**/*.ts"); documents 300ms debounce, rebuild timestamps, and Ctrl+C exit.
Dependencies
package.json
Added runtime dependency chokidar (^5.0.0) for file system watching.
Watch Action Implementation
src/cli/actions/watchAction.ts
New module exporting WatchDeps and runWatchAction. Loads migrations/config, runs initial pack, creates chokidar watcher (ignoreInitial, awaitWriteFinish, ignored includes output), schedules 300ms debounced rebuilds, serializes rebuilds (prevent concurrent runs, single pending), logs rebuild timestamps, reports errors without exiting, and performs graceful shutdown on AbortSignal or SIGINT/SIGTERM.
CLI Integration
src/cli/cliRun.ts
Added -w, --watch option with semantic suggestions (monitor, live, auto), validates mutual exclusions (--remote, --stdout, --stdin, --split-output, or positional remote URL), and dispatches to runWatchAction when set.
Type Definitions
src/cli/types.ts
Extended CliOptions with optional watch?: boolean.
Test Suite
tests/cli/actions/watchAction.test.ts
New Vitest tests mocking packager, config, spinner/reporter, migrations, and watcher. Verifies CLI conflict errors, initial pack, watcher setup and ignored patterns, 300ms debounce/coalescing, rebuild serialization, “Rebuilt at …” logging, and watcher close on abort.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant CliRun as cliRun
    participant WatchAction as runWatchAction
    participant Chokidar as Chokidar
    participant Packager as Packager
    participant Reporter as Reporter

    User->>CliRun: run(['path'], cwd, {watch: true})
    CliRun->>CliRun: Validate --watch compatibility
    CliRun->>WatchAction: runWatchAction(directories, cwd, options)
    WatchAction->>WatchAction: Load config & migrations
    WatchAction->>Packager: pack() initial run
    Packager->>Reporter: Report initial results
    WatchAction->>Chokidar: Create watcher (ignoreInitial, awaitWriteFinish, ignored)
    Chokidar->>WatchAction: File add/change/unlink events
    WatchAction->>WatchAction: Schedule debounced rebuild (300ms)
    WatchAction->>Packager: pack() after debounce
    Packager->>Reporter: Report rebuild results
    Reporter->>User: Log "Rebuilt at ..."
    User->>WatchAction: Signal abort (Ctrl+C / AbortSignal)
    WatchAction->>Chokidar: Close watcher
    WatchAction->>User: Exit gracefully
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • yamadashy
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a --watch flag for automatic re-packing on file changes.
Description check ✅ Passed The description is comprehensive, covering implementation details, usage examples, conflict validation, and test results; however, it partially deviates from the template by including extensive custom sections rather than strictly following the template structure.
Linked Issues check ✅ Passed The PR fully addresses issue #1429 by implementing a --watch flag that monitors files and triggers automatic rebuilds with 300ms debounce, graceful shutdown, and conflict validation as requested.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the --watch feature: CLI option addition, watch action implementation, dependency injection for testability, chokidar integration, and comprehensive tests with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new watch mode to Repomix, enabling automatic re-packing upon file changes using the chokidar library. It includes the implementation of a watch action with debouncing, CLI integration with conflict validation, and comprehensive tests. Review feedback identifies a critical bug where file resolution fails when multiple directories are watched and notes that the current implementation fails to detect newly created files. Additionally, concerns were raised regarding the use of global process event listeners, which could cause issues when the tool is used as a library.

Comment thread src/cli/actions/watchAction.ts Outdated
Comment on lines +79 to +87
// Resolve safeFilePaths against the first target path for watching
const rootDir = targetPaths[0];
const absoluteWatchPaths = packResult.safeFilePaths.map((filePath) => path.resolve(rootDir, filePath));

// Set up chokidar watcher
const watcher = resolvedDeps.watch(absoluteWatchPaths, {
ignoreInitial: true,
awaitWriteFinish: { stabilityThreshold: 100 },
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of the file watcher has two significant issues:

  1. Multiple Directories Bug: It resolves all file paths against targetPaths[0]. If multiple directories are provided as positional arguments (e.g., repomix dir1 dir2), files belonging to dir2 will be incorrectly resolved relative to dir1, causing the watcher to fail for those files.
  2. New File Detection: By watching a fixed list of files (safeFilePaths) discovered during the initial pack, the watcher cannot detect newly created files. This contradicts the PR description which states that it handles created files.

Watching the targetPaths (directories) directly solves both issues. It ensures all roots are covered and allows Chokidar to detect new files within those directories. Note that we should ignore the output file to prevent an infinite rebuild loop.

  // Set up chokidar watcher on target directories to catch new files and handle multiple roots
  const watcher = resolvedDeps.watch(targetPaths, {
    ignoreInitial: true,
    awaitWriteFinish: { stabilityThreshold: 100 },
    ignored: config.output.filePath ? path.resolve(cwd, config.output.filePath) : undefined,
  });

Comment on lines +127 to +136
} else {
process.on('SIGINT', async () => {
await cleanup();
process.exit(0);
});
process.on('SIGTERM', async () => {
await cleanup();
process.exit(0);
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When runWatchAction is used as a library (which is a supported use case for Repomix), attaching global listeners to process.on('SIGINT') and process.on('SIGTERM') that call process.exit(0) is problematic. It will terminate the entire host process when the user intended to only stop the watch action.

Additionally, if this function is called multiple times in a long-running process (e.g., during tests or in a wrapper), it will leak event listeners on the process object. Consider only attaching these listeners if no AbortSignal is provided and ensure they are cleaned up properly.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/actions/watchAction.ts`:
- Around line 79-87: The watcher is using the static packResult.safeFilePaths
snapshot and resolving each entry against targetPaths[0], which prevents
observing newly created files and mis-roots when multiple input directories are
provided; change the watch input to use the selected watch roots/patterns (e.g.
the original roots/selection used to compute safeFilePaths or targetPaths array
itself) instead of packResult.safeFilePaths, ensure you pass all targetPaths
(not just targetPaths[0]) or their corresponding glob roots into
resolvedDeps.watch, and add the generated output path to the watch ignore list
so rebuilds don’t loop (keep existing options like ignoreInitial and
awaitWriteFinish).

In `@src/cli/cliRun.ts`:
- Around line 242-253: The watch-mode validation misses positional remote URLs,
so extend the existing check in runCli (the block that tests options.watch and
options.remote/stdout/stdin) to also scan parsed positional arguments (e.g.,
options._ or the parsed positional array) for remote URLs and reject them;
implement a small predicate (isRemoteUrl) that matches common remote forms
(https?:// or git@) and if any positional matches, throw the same
RepomixError('--watch cannot be used with --remote. Watch mode only works with
local directories.').
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 550a0ece-5160-4c8b-8e30-c7fc04aa1228

📥 Commits

Reviewing files that changed from the base of the PR and between 4c356f7 and c4f35fa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • README.md
  • package.json
  • src/cli/actions/watchAction.ts
  • src/cli/cliRun.ts
  • src/cli/types.ts
  • tests/cli/actions/watchAction.test.ts

Comment thread src/cli/actions/watchAction.ts Outdated
Comment thread src/cli/cliRun.ts
Comment on lines +242 to +253
// Validate --watch conflicts early, before log level changes can suppress error messages
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject --watch with positional remote URLs too.

This only guards options.remote. repomix -w https://github.com/... still falls through to the explicit-URL auto-detection later in runCli, so watch mode is silently ignored instead of producing the documented conflict error.

Suggested fix
   // Validate --watch conflicts early, before log level changes can suppress error messages
   if (options.watch) {
-    if (options.remote) {
+    const positionalRemote = directories.length === 1 && isExplicitRemoteUrl(directories[0]);
+    if (options.remote || positionalRemote) {
       throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
     }
     if (options.stdout) {
       throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/cliRun.ts` around lines 242 - 253, The watch-mode validation misses
positional remote URLs, so extend the existing check in runCli (the block that
tests options.watch and options.remote/stdout/stdin) to also scan parsed
positional arguments (e.g., options._ or the parsed positional array) for remote
URLs and reject them; implement a small predicate (isRemoteUrl) that matches
common remote forms (https?:// or git@) and if any positional matches, throw the
same RepomixError('--watch cannot be used with --remote. Watch mode only works
with local directories.').

@PAMulligan
Copy link
Copy Markdown
Author

Thanks for the thorough reviews! All valid points. I'm working on a follow-up commit to address these:

  1. Watch target directories instead of individual files (gemini-code-assist + devin) — Switching from watching safeFilePaths to watching targetPaths directly. This fixes both the multi-directory resolution bug and enables new file detection. Will add ignored for the output file to prevent infinite rebuild loops.

  2. Concurrent rebuild race condition (devin) — Adding an isRebuilding guard with a pendingRebuild flag so at most one pack runs at a time, with changes during a pack queued for a follow-up rebuild.

  3. Global process event listener cleanup (gemini-code-assist) — Refactoring to only attach SIGINT/SIGTERM listeners when no AbortSignal is provided, and properly removing them on cleanup to avoid listener leaks in library usage.

Will push the fixes shortly.

Paul Mulligan added 2 commits April 13, 2026 15:23
- Watch directories instead of individual files to detect new files
- Ignore output file path to prevent infinite rebuild loops
- Add rebuild guard to prevent concurrent packs with pending queue
- Use named signal handlers with cleanup to avoid listener leaks
- Remove process.exit() calls for library compatibility
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment thread src/cli/actions/watchAction.ts Outdated
Comment thread src/cli/cliRun.ts
Comment on lines +243 to +261
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Watch mode silently ignores --skill-generate instead of rejecting the incompatible combination

The --watch conflict validation in src/cli/cliRun.ts:243-261 checks for --remote, --stdout, --stdin, and --split-output, but does not check for --skill-generate. If a user runs repomix --watch --skill-generate myskill, the config's skillGenerate is set via mergeConfigs, but watchAction.ts:106 calls pack(targetPaths, config, handleProgress) without passing PackOptions.skillDir. The pack function at src/core/packager.ts:171 checks config.skillGenerate !== undefined && options.skillDir — since options.skillDir is undefined, it silently falls through to normal output generation. The user gets regular packed output instead of skill output with no error or warning.

Suggested change
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
// Validate --watch conflicts early, before log level changes can suppress error messages
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (options.skillGenerate !== undefined) {
throw new RepomixError('--watch cannot be used with --skill-generate. Watch mode does not support skill generation.');
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@PAMulligan
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]

This comment was marked as resolved.

- Add early-return guard before any await to prevent race conditions
- Skip chokidar import when mock watch function is provided
- Extend ignored patterns to include defaultIgnoreList and custom patterns
- Add shuttingDown flag to prevent pendingRebuild during shutdown
- Reorder cleanup to close watcher before resolving keepAlive
- Update test assertions for array-based ignore patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment thread src/cli/actions/watchAction.ts Outdated
Comment on lines +194 to +211
if (resolvedDeps.signal) {
resolvedDeps.signal.addEventListener('abort', () => {
cleanup();
});
} else {
process.on('SIGINT', onSigint);
process.on('SIGTERM', onSigterm);
}

// Keep alive — wait until signal is aborted (in tests) or cleanup resolves the promise
if (resolvedDeps.signal) {
await new Promise<void>((resolve) => {
if (resolvedDeps.signal?.aborted) {
resolve();
return;
}
resolvedDeps.signal?.addEventListener('abort', () => resolve());
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Signal-path cleanup races with keep-alive resolve, allowing runWatchAction to return before watcher is closed

When using an AbortSignal (the signal path), two separate abort event listeners are registered: one at line 195 that calls cleanup() without awaiting it, and another at line 210 that immediately calls resolve(). When abort fires, both listeners execute: cleanup() starts its async work (including await watcher.close() at line 183), but the second listener resolves the keep-alive promise immediately. This means runWatchAction() returns before watcher.close() finishes, violating the stated invariant in the comment at lines 181-182: "Close watcher before resolving keepAlive so callers don't see runWatchAction() resolve until shutdown is truly finished." In tests, the mock close resolves synchronously so this is masked, but the semantic guarantee is broken and in real usage with a real AbortSignal, resources may not be fully cleaned up when the function returns.

How the two listeners interact on abort

Listener 1 (line 195–197): cleanup() is called but not awaited — starts async teardown.
Listener 2 (line 210): resolve() is called synchronously — resolves the keep-alive promise.

The keep-alive promise at line 205 resolves before cleanup() finishes because watcher.close() is still pending. Meanwhile, keepAliveResolve is null in the signal path (it's only set at line 214 in the non-signal branch), so cleanup()'s keepAliveResolve?.() at line 184 is a no-op.

Suggested change
if (resolvedDeps.signal) {
resolvedDeps.signal.addEventListener('abort', () => {
cleanup();
});
} else {
process.on('SIGINT', onSigint);
process.on('SIGTERM', onSigterm);
}
// Keep alive — wait until signal is aborted (in tests) or cleanup resolves the promise
if (resolvedDeps.signal) {
await new Promise<void>((resolve) => {
if (resolvedDeps.signal?.aborted) {
resolve();
return;
}
resolvedDeps.signal?.addEventListener('abort', () => resolve());
});
// Keep alive — wait until signal is aborted (in tests) or cleanup resolves the promise
if (resolvedDeps.signal) {
await new Promise<void>((resolve) => {
if (resolvedDeps.signal?.aborted) {
cleanup().then(resolve);
return;
}
resolvedDeps.signal?.addEventListener('abort', () => {
cleanup().then(resolve);
});
});
} else {
process.on('SIGINT', onSigint);
process.on('SIGTERM', onSigterm);
await new Promise<void>((resolve) => {
keepAliveResolve = resolve;
});
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@PAMulligan
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/cli/actions/watchAction.ts (2)

132-165: ⚠️ Potential issue | 🟠 Major

scheduleRebuild() can still start work after shutdown begins.

Line 175 flips shuttingDown, but Line 132 never checks it before arming a fresh timer, and Line 136 never re-checks it before runPack(). A late chokidar event while watcher.close() is draining can still kick off one more rebuild after Ctrl+C.

Suggested guard
 const scheduleRebuild = () => {
+  if (shuttingDown) {
+    return;
+  }
   if (debounceTimer !== null) {
     clearTimeout(debounceTimer);
   }
   debounceTimer = setTimeout(async () => {
     debounceTimer = null;
+    if (shuttingDown) {
+      return;
+    }
 
     if (isRebuilding) {
       pendingRebuild = true;
       return;
     }
@@
   const cleanup = async () => {
     shuttingDown = true;
     if (debounceTimer !== null) {
       clearTimeout(debounceTimer);
+      debounceTimer = null;
     }

Also applies to: 174-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 132 - 165, scheduleRebuild() can
still kick off a rebuild after shutdown; add guards to avoid arming or executing
work when shuttingDown is true: before creating/setting debounceTimer in
scheduleRebuild() return early if shuttingDown, and inside the setTimeout
callback re-check shuttingDown (and return without setting isRebuilding or
calling runPack) to prevent starting work while watcher.close() is draining;
ensure pendingRebuild is cleared when shuttingDown and that existing timers are
cleared during shutdown so no late timers fire and call runPack().

174-185: ⚠️ Potential issue | 🟠 Major

Abort-driven shutdown can still leak the watcher and return early.

If the signal flips after startup begins but before Line 195 registers the listener, that late handler never fires and the Line 206 fast-path returns without calling cleanup(), leaving the watcher open. Even in the non-racy path, this branch waits for the abort event rather than the async cleanup, so runWatchAction() can resolve before Line 183 finishes watcher.close(). Please funnel both branches through one named { once: true } abort handler / cleanup promise and await that before returning.

Run this to confirm the late-listener behavior and inspect the current shutdown branch. Expect lateListenerFired to be false.

#!/bin/bash
set -euo pipefail

echo "Current shutdown logic:"
sed -n '174,211p' src/cli/actions/watchAction.ts

echo
echo "AbortSignal late-listener check:"
node <<'NODE'
const controller = new AbortController();
controller.abort();

let lateListenerFired = false;
controller.signal.addEventListener('abort', () => {
  lateListenerFired = true;
});

setImmediate(() => {
  console.log(JSON.stringify({
    aborted: controller.signal.aborted,
    lateListenerFired
  }, null, 2));
});
NODE

Also applies to: 194-211

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 174 - 185, The shutdown logic
can miss racey aborts and resolve before watcher.close() completes; refactor
runWatchAction to funnel all shutdown paths through a single named abort handler
that runs cleanup(), register the AbortSignal listener with { once: true } (so
late handlers fire deterministically), remove the duplicated fast-path that
resolves without awaiting cleanup, and ensure the function awaits the cleanup
promise (which should call watcher.close(), removeListener for
onSigint/onSigterm, set shuttingDown and invoke keepAliveResolve) before
returning; use the existing cleanup function name and call sites (cleanup,
watcher.close, onSigint, onSigterm, keepAliveResolve, runWatchAction) to locate
and replace the early-return branch so all exits await the same cleanup promise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cli/actions/watchAction.ts`:
- Around line 132-165: scheduleRebuild() can still kick off a rebuild after
shutdown; add guards to avoid arming or executing work when shuttingDown is
true: before creating/setting debounceTimer in scheduleRebuild() return early if
shuttingDown, and inside the setTimeout callback re-check shuttingDown (and
return without setting isRebuilding or calling runPack) to prevent starting work
while watcher.close() is draining; ensure pendingRebuild is cleared when
shuttingDown and that existing timers are cleared during shutdown so no late
timers fire and call runPack().
- Around line 174-185: The shutdown logic can miss racey aborts and resolve
before watcher.close() completes; refactor runWatchAction to funnel all shutdown
paths through a single named abort handler that runs cleanup(), register the
AbortSignal listener with { once: true } (so late handlers fire
deterministically), remove the duplicated fast-path that resolves without
awaiting cleanup, and ensure the function awaits the cleanup promise (which
should call watcher.close(), removeListener for onSigint/onSigterm, set
shuttingDown and invoke keepAliveResolve) before returning; use the existing
cleanup function name and call sites (cleanup, watcher.close, onSigint,
onSigterm, keepAliveResolve, runWatchAction) to locate and replace the
early-return branch so all exits await the same cleanup promise.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f6117b2-6442-4f1a-9d6f-9096e3729592

📥 Commits

Reviewing files that changed from the base of the PR and between 7b68348 and 8b2365c.

📒 Files selected for processing (2)
  • src/cli/actions/watchAction.ts
  • tests/cli/actions/watchAction.test.ts

- Add shuttingDown guard at top of scheduleRebuild() to prevent new timers
- Re-check shuttingDown inside timer callback before calling runPack()
- Add cleanupStarted flag to prevent multiple cleanup() invocations
- Add cleanupDone flag to handle race where cleanup finishes before promise
- Register abort listener with { once: true } to avoid duplicate calls
- Check signal.aborted after registering listener for late-abort race
- Both signal and SIGINT/SIGTERM paths now await full cleanup completion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@PAMulligan
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/actions/watchAction.ts`:
- Around line 141-174: The shutdown logic must wait for any in-flight rebuild
started in the debounce timer (where runPack, reportResults and logger calls
occur) and always resolve the keep-alive via cleanupResolve; to fix it, create a
promise (e.g., activeRebuildPromise / activeRebuildResolve) that is assigned
when the debounce timer sets isRebuilding and cleared in the finally block after
runPack completes, update cleanup() to await watcher.close() and then await
activeRebuildPromise (if present) before resolving, and wrap the watcher.close()
+ activeRebuild await in a try-finally so cleanupResolve?.() is always called
even if watcher.close() rejects; ensure you still flip
pendingRebuild/isRebuilding and call scheduleRebuild() as before inside the
debounce finally block.
- Around line 120-124: The FSWatcher created by resolvedDeps.watch (assigned to
watcher) lacks an 'error' event handler; add watcher.on('error', handler) that
catches watcher errors (e.g., EMFILE, EACCES) and routes them into your existing
graceful shutdown logic (call the same shutdown/cleanup function you use for
SIGINT/SIGTERM or invoke the shutdown sequence used elsewhere) so the process
doesn't crash on unhandled 'error' events; ensure the handler logs the error via
the same logger used nearby and performs the same cleanup/exit path as other
shutdown triggers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3826b4e-55c8-4e97-8e1a-753639580e2c

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2365c and ce3cbfe.

📒 Files selected for processing (1)
  • src/cli/actions/watchAction.ts

Comment on lines +120 to +124
const watcher = resolvedDeps.watch(targetPaths, {
ignoreInitial: true,
awaitWriteFinish: { stabilityThreshold: 100 },
ignored: watchIgnorePatterns,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/cli/actions/watchAction.ts | head -200

Repository: yamadashy/repomix

Length of output: 8259


🏁 Script executed:

cat -n src/cli/actions/watchAction.ts | sed -n '200,250p'

Repository: yamadashy/repomix

Length of output: 1283


🏁 Script executed:

# Check if there are any existing error handlers on the watcher elsewhere
grep -n "watcher\.on" src/cli/actions/watchAction.ts

Repository: yamadashy/repomix

Length of output: 192


🌐 Web query:

chokidar FSWatcher error event handling EventEmitter

💡 Result:

In chokidar, the FSWatcher class extends Node.js EventEmitter and emits an 'error' event when significant filesystem errors occur during watching. This includes errors like EMFILE (too many open files), UNKNOWN, EPERM, EACCES (unless ignorePermissionErrors is true), and others excluding common ignorable ones like ENOENT and ENOTDIR. To handle these errors and prevent unhandled error crashes, always attach an 'error' event listener: const watcher = chokidar.watch('path/to/watch', { ignorePermissionErrors: true // Optional: suppress permission errors }); watcher.on('error', (error) => { console.error('FS Watcher error:', error); // Handle gracefully, e.g., log, retry, or close watcher // watcher.close; }); The internal _handleError method decides whether to emit the error: - Ignores ENOENT, ENOTDIR - Ignores EPERM/EACCES if ignorePermissionErrors: true - Emits 'error' event for other significant errors - Common in large directories (e.g., node_modules) causing EMFILE Without a listener, errors can crash the process with "Emitted 'error' event on FSWatcher instance". Best practice: always listen for 'error' events, especially in production or watch mode. Example from official docs: watcher.on('error', (error) => log(Watcher error: ${error}));

Citations:


Add error event handler to watcher.

FSWatcher extends Node.js EventEmitter and emits error events when filesystem monitoring fails (e.g., EMFILE, EACCES, EPERM). Without a listener, these errors crash the process with "Emitted 'error' event on FSWatcher instance". Hook error events into graceful shutdown to prevent watch mode from crashing on filesystem errors.

Suggested fix
  watcher.on('change', scheduleRebuild);
  watcher.on('add', scheduleRebuild);
  watcher.on('unlink', scheduleRebuild);
+  watcher.on('error', (error) => {
+    logger.error('Watcher failed:', error);
+    void cleanup();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 120 - 124, The FSWatcher created
by resolvedDeps.watch (assigned to watcher) lacks an 'error' event handler; add
watcher.on('error', handler) that catches watcher errors (e.g., EMFILE, EACCES)
and routes them into your existing graceful shutdown logic (call the same
shutdown/cleanup function you use for SIGINT/SIGTERM or invoke the shutdown
sequence used elsewhere) so the process doesn't crash on unhandled 'error'
events; ensure the handler logs the error via the same logger used nearby and
performs the same cleanup/exit path as other shutdown triggers.

Comment thread src/cli/actions/watchAction.ts
…utdown

- Add error event handler to chokidar watcher to prevent uncaught
  exceptions when filesystem monitoring fails (EMFILE, EACCES, EPERM)
- Track active rebuild promise and await it in cleanup() so shutdown
  doesn't complete while a pack operation is still running
- Wrap cleanup body in try-finally to always settle the keep-alive
  promise, even if watcher.close() or the rebuild throws

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@PAMulligan
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread src/cli/cliRun.ts
Comment on lines +243 to +261
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing conflict check: --watch --skill-generate silently ignores skill generation

When a user passes --watch --skill-generate, the watch action runs but --skill-generate is silently ignored. The buildCliConfig at src/cli/actions/defaultAction.ts:341 correctly adds skillGenerate to the config, and mergeConfigs includes it in the merged config. However, runPack in watchAction.ts:41 calls pack(targetPaths, config, handleProgress) without passing any PackOptions (specifically skillDir). In pack() at src/core/packager.ts:171, the skill generation path requires config.skillGenerate !== undefined && options.skillDir, which is false because skillDir is never set. The result is regular output (repomix-output.xml) instead of skill directory output, with no error or warning to the user. The validation block at src/cli/cliRun.ts:243-261 checks for --remote, --stdout, --stdin, and --split-output conflicts but misses --skill-generate.

Suggested change
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
if (options.watch) {
if (options.remote) {
throw new RepomixError('--watch cannot be used with --remote. Watch mode only works with local directories.');
}
if (options.stdout) {
throw new RepomixError('--watch cannot be used with --stdout. Watch mode writes to a file.');
}
if (options.stdin) {
throw new RepomixError('--watch cannot be used with --stdin. Watch mode discovers files automatically.');
}
if (options.splitOutput) {
throw new RepomixError(
'--watch cannot be used with --split-output. Watch mode does not yet support split output files.',
);
}
if (options.skillGenerate !== undefined) {
throw new RepomixError(
'--watch cannot be used with --skill-generate. Watch mode does not support skill generation.',
);
}
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
throw new RepomixError('--watch cannot be used with remote URLs. Watch mode only works with local directories.');
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/cli/actions/watchAction.ts (1)

212-219: ⚠️ Potential issue | 🟠 Major

Still drain the active rebuild if watcher.close() fails.

If watcher.close() rejects on Line 215, control jumps straight to finally and skips Lines 217-219. That lets runWatchAction() resolve before the in-flight rebuild finishes, and the rejected cleanup() promise is then fire-and-forget from the signal/abort handlers.

Suggested fix
     try {
-      // Close watcher before resolving so callers don't see
-      // runWatchAction() resolve until shutdown is truly finished
-      await watcher.close();
-      // Wait for any in-flight rebuild to complete before resolving
+      try {
+        // Close watcher before resolving so callers don't see
+        // runWatchAction() resolve until shutdown is truly finished
+        await watcher.close();
+      } catch (error) {
+        logger.error('Failed to close file watcher:', error);
+      }
+      // Wait for any in-flight rebuild to complete even if close() failed
       if (activeRebuildPromise) {
         await activeRebuildPromise;
       }
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 212 - 219, If watcher.close()
rejects, we must still await any in-flight activeRebuildPromise before letting
runWatchAction() propagate the error; wrap the await watcher.close() call in a
try/catch (or use Promise.allSettled semantics) so that on error you still await
activeRebuildPromise (if set) and then rethrow the original error so cleanup()
and signal/abort handlers don't leave the cleanup promise fire-and-forget;
update the block around watcher.close(), activeRebuildPromise, runWatchAction(),
and cleanup() to ensure activeRebuildPromise is always drained even when
watcher.close() fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cli/actions/watchAction.ts`:
- Around line 212-219: If watcher.close() rejects, we must still await any
in-flight activeRebuildPromise before letting runWatchAction() propagate the
error; wrap the await watcher.close() call in a try/catch (or use
Promise.allSettled semantics) so that on error you still await
activeRebuildPromise (if set) and then rethrow the original error so cleanup()
and signal/abort handlers don't leave the cleanup promise fire-and-forget;
update the block around watcher.close(), activeRebuildPromise, runWatchAction(),
and cleanup() to ensure activeRebuildPromise is always drained even when
watcher.close() fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d970fec0-7764-49e8-b591-06773ca468fa

📥 Commits

Reviewing files that changed from the base of the PR and between ce3cbfe and 4048975.

📒 Files selected for processing (1)
  • src/cli/actions/watchAction.ts

Use separate try/catch blocks so that activeRebuildPromise is always
awaited before resolving, regardless of whether watcher.close() throws.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@PAMulligan
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/cli/actions/watchAction.ts (2)

233-238: Prefix cleanup() calls with void to silence floating-promise lint rules.

The signal handlers call cleanup() without awaiting the returned Promise. While functionally correct (the keep-alive promise waits for cleanupResolve), this may trigger @typescript-eslint/no-floating-promises or similar lint rules.

♻️ Suggested fix
 const onSigint = () => {
-  cleanup();
+  void cleanup();
 };
 const onSigterm = () => {
-  cleanup();
+  void cleanup();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 233 - 238, The signal handlers
onSigint and onSigterm call cleanup() but leave its returned Promise unhandled;
prefix those calls with void (e.g., void cleanup()) to explicitly ignore the
Promise and satisfy `@typescript-eslint/no-floating-promises` linting while
keeping the existing behavior of cleanup and the keep-alive promise resolution.

127-129: Consider triggering cleanup for critical watcher errors.

The error handler prevents uncaught exceptions, which is good. However, for critical errors like EMFILE (too many open files) or EACCES, the watcher may be in a broken state and continuing watch mode is not meaningful. Consider calling cleanup on error so the user knows watch mode has stopped.

♻️ Optional enhancement
   // Handle watcher errors (EMFILE, EACCES, EPERM, etc.) to prevent uncaught exceptions
   watcher.on('error', (error) => {
     logger.error('File watcher error:', error);
+    void cleanup();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/actions/watchAction.ts` around lines 127 - 129, The file watcher
error handler (watcher.on('error', (error) => { logger.error(...) })) should
detect critical OS errors (e.g., error.code === 'EMFILE' or 'EACCES') and
trigger the existing cleanup routine so watch mode stops and the user is
informed; update the watcher.on('error') handler to log the error as before,
then if the error.code matches critical list call cleanup() (or the cleanup
function used in this module) and surface a clear message/exit flow so the
process doesn't keep running in a broken watch state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/cli/actions/watchAction.ts`:
- Around line 233-238: The signal handlers onSigint and onSigterm call cleanup()
but leave its returned Promise unhandled; prefix those calls with void (e.g.,
void cleanup()) to explicitly ignore the Promise and satisfy
`@typescript-eslint/no-floating-promises` linting while keeping the existing
behavior of cleanup and the keep-alive promise resolution.
- Around line 127-129: The file watcher error handler (watcher.on('error',
(error) => { logger.error(...) })) should detect critical OS errors (e.g.,
error.code === 'EMFILE' or 'EACCES') and trigger the existing cleanup routine so
watch mode stops and the user is informed; update the watcher.on('error')
handler to log the error as before, then if the error.code matches critical list
call cleanup() (or the cleanup function used in this module) and surface a clear
message/exit flow so the process doesn't keep running in a broken watch state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e6fb424-81b5-474a-8c51-adac849a0b8d

📥 Commits

Reviewing files that changed from the base of the PR and between ce3cbfe and 87a7a0f.

📒 Files selected for processing (1)
  • src/cli/actions/watchAction.ts

@PAMulligan
Copy link
Copy Markdown
Author

Summary of changes across all review rounds

Hi @yamadashy — this PR adds a --watch / -w flag (closes #1429) that watches input files and automatically re-packs on changes. Here's a summary of everything addressed across review feedback from CodeRabbit, Gemini Code Assist, and Devin:

Core feature (commits 1–6)

  • Added chokidar dependency, CLI option definition/routing, debounced file watching, conflict validation (--remote, --stdout, --stdin), and README docs.

Review round 1 — Gemini + CodeRabbit + Devin

  • Watch directories instead of individual files — switched from watching safeFilePaths to watching targetPaths directly so new files are detected and multiple directories are covered.
  • Reject --watch with positional remote URLs — added guard for repomix -w https://....
  • Block --watch with --split-output and --skill-generate — added conflict validation for these flags.
  • Fix double Ctrl+C exit — resolved duplicate signal handler registration.
  • Global process event listener cleanup — refactored SIGINT/SIGTERM listeners with proper cleanup.
  • Concurrent rebuild race condition — added isRebuilding/pendingRebuild guard.

Review round 2 — CodeRabbit

  • Early abort guard — short-circuit runWatchAction if the AbortSignal is already aborted before starting work.
  • Apply packer's ignore rules to chokidar — extended the ignored option to include default ignore patterns (.git, node_modules, etc.) and custom patterns from config, not just the output file.
  • Shutdown-aware rebuild draining — added shuttingDown flag checks in scheduleRebuild() and its finally block to prevent post-shutdown rebuilds.

Review round 3 — CodeRabbit

  • Unified shutdown path — funneled both AbortSignal and SIGINT/SIGTERM through a shared cleanup promise with { once: true } abort handler.
  • scheduleRebuild() shutdown guards — added checks before arming the timer and before calling runPack().

Review round 4 — CodeRabbit

  • Watcher error handler — added watcher.on('error', ...) to prevent uncaught exceptions from crashing the process.
  • Await in-flight rebuild on shutdowncleanup() now tracks and awaits activeRebuildPromise so shutdown waits for any running pack to complete.
  • Always settle keep-alive promise — wrapped cleanup in try/finally so cleanupResolve() is always called even if watcher.close() throws.

Review round 5 — CodeRabbit

  • Independent error handling in cleanup — separated watcher.close() and await activeRebuildPromise into individual try/catch blocks so a failure in one doesn't skip the other.

All tests pass (npm test — 1116 passed) and lint is clean (npm run lint — 0 errors). The remaining two CodeRabbit comments are minor suggestions (prefixing void on fire-and-forget cleanup calls, and optionally triggering cleanup on critical watcher errors) which I'm happy to address if you'd like.

Ready for your review when you get a chance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

file watch option

1 participant