diff --git a/changelog.md b/changelog.md index be4f786..43fd270 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-12-11 - 6.2.3 - fix(watcher.node) +Improve handling of temporary files from atomic editor writes in Node watcher + +- Detect temporary files produced by atomic editor saves and attempt to map them to the real target file instead of silently skipping the event +- Add getTempFileTarget() to extract the real file path from temp filenames (supports patterns like file.ts.tmp.PID.TIMESTAMP and generic .tmp.*) +- When a temp-file event is seen, queue a corresponding event for the resolved real file after a short delay (50ms) to allow rename/replace to complete +- Add logging around temp file detection and real-file checks to aid debugging + ## 2025-12-11 - 6.2.2 - fix(watcher.node) Defer events during initial scan, track full event sequences, and harden watcher shutdown diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index dd88051..fefad67 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartwatch', - version: '6.2.2', + version: '6.2.3', description: 'A cross-runtime file watcher with glob pattern support for Node.js, Deno, and Bun.' } diff --git a/ts/watchers/watcher.node.ts b/ts/watchers/watcher.node.ts index fe92e52..92bec1f 100644 --- a/ts/watchers/watcher.node.ts +++ b/ts/watchers/watcher.node.ts @@ -227,6 +227,35 @@ export class NodeWatcher implements IWatcher { return false; } + /** + * Extract the real file path from a temporary file path + * Used to detect atomic writes where only the temp file event is emitted + * + * Patterns: + * - Claude Code: file.ts.tmp.PID.TIMESTAMP -> file.ts + * - Vim swap: .file.ts.swp -> file.ts (but we don't handle this case) + */ + private getTempFileTarget(tempFilePath: string): string | null { + const basename = path.basename(tempFilePath); + + // Claude Code pattern: file.ts.tmp.PID.TIMESTAMP + // Match: anything.tmp.digits.digits + const claudeMatch = basename.match(/^(.+)\.tmp\.\d+\.\d+$/); + if (claudeMatch) { + const realBasename = claudeMatch[1]; + return path.join(path.dirname(tempFilePath), realBasename); + } + + // Generic .tmp. pattern: file.ts.tmp.something -> file.ts + const tmpMatch = basename.match(/^(.+)\.tmp\.[^.]+$/); + if (tmpMatch) { + const realBasename = tmpMatch[1]; + return path.join(path.dirname(tempFilePath), realBasename); + } + + return null; + } + get isWatching(): boolean { return this._isWatching; } @@ -410,14 +439,26 @@ export class NodeWatcher implements IWatcher { const fullPath = path.join(basePath, filename); - // Skip temporary files - but ONLY pure temp files, not the target of atomic writes - // Atomic writes: editor writes to file.tmp.xxx then renames to file - // We need to detect the final file, so only skip files that ARE temp files - // and haven't been renamed to the real file yet + // Handle temporary files from atomic writes (Claude Code, editors, etc.) + // Pattern: editor writes to file.tmp.xxx then renames to file + // Problem: fs.watch on Linux may ONLY emit event for the temp file, not the target! + // Solution: When we see a temp file event, also check the corresponding real file if (this.isTemporaryFile(fullPath)) { - // For temp files, we still want to track if they get renamed TO a real file - // The 'rename' event fires for both source and target, so we'll catch the real file - console.log(`[smartwatch] Skipping temp file event: ${filename}`); + console.log(`[smartwatch] Detected temp file event: ${filename}`); + + // Extract the real file path from the temp file path + // Pattern: file.ts.tmp.PID.TIMESTAMP -> file.ts + const realFilePath = this.getTempFileTarget(fullPath); + if (realFilePath) { + console.log(`[smartwatch] Checking corresponding real file: ${realFilePath}`); + // Queue an event for the REAL file - this is the actual file that changed + // Use a short delay to let the rename complete + setTimeout(() => { + if (this._isWatching) { + this.handleFsEvent(basePath, path.relative(basePath, realFilePath), 'change'); + } + }, 50); + } return; }