Skip to content

fix: prevent infinite rebuild loop in dev-watch mode#6361

Merged
0xbbjoker merged 6 commits intodevelopfrom
fix/dev-watch-rebuild-loop
Jan 15, 2026
Merged

fix: prevent infinite rebuild loop in dev-watch mode#6361
0xbbjoker merged 6 commits intodevelopfrom
fix/dev-watch-rebuild-loop

Conversation

@0xbbjoker
Copy link
Collaborator

@0xbbjoker 0xbbjoker commented Jan 13, 2026

Summary

  • Fixed infinite rebuild loop in bun run dev caused by generate-version.ts writing to src/version.ts on every build
  • The watcher was detecting these changes and triggering rebuilds endlessly

Changes

  • scripts/dev-watch.js: Added ignoredFiles Set to skip watching version.ts and version.js files
  • packages/cli/src/scripts/generate-version.ts: Made version generation smart - only rewrites file if version/name/description actually changed
  • Removed unused variables (stdoutData, stderrData, buildOutput) from dev-watch.js

Root Cause

  1. generate-version.ts writes BUILD_TIME = '${new Date().toISOString()}' which changes every build
  2. The file watcher sees changes in cli/src/version.ts
  3. This triggers a rebuild, which generates a new version.ts with a new timestamp
  4. Infinite loop

Test plan

  • Run bun run dev from monorepo root
  • Verify initial startup completes without looping
  • Touch a source file (e.g., packages/core/src/runtime.ts)
  • Verify single rebuild occurs (no loop)
  • Verify version.ts is up to date message appears on subsequent builds

🤖 Generated with Claude Code


Note

Prevents watch-mode rebuild loops caused by auto-generated version files.

  • packages/cli/src/scripts/generate-version.ts: Adds idempotent generation (extracts existing CLI_VERSION/NAME/DESCRIPTION, compares, and skips write if unchanged), uses JSON.stringify for safe string literals, and introduces small FS helpers.
  • scripts/dev-watch.js: Ignores auto-generated version.ts/version.js via shouldIgnoreFile, forwards build stdout/stderr directly (removes unused buffers), and logs ignored files; preserves TypeScript-only watching with debounce.

Written by Cursor Bugbot for commit 6d45870. This will update automatically on new commits. Configure here.

Greptile Overview

Greptile Summary

This PR fixes an infinite rebuild loop in dev-watch mode caused by the auto-generated version.ts file changing on every build. The fix involves two approaches:

  1. Smart version generation (generate-version.ts): Only rewrites version.ts if version/name/description actually changed, not on every build. This means BUILD_TIME becomes stale in dev mode (intentional tradeoff).

  2. Watcher ignore list (dev-watch.js): Added ignore logic to skip watching version.ts and version.js files.

Critical Issue Found

The watcher ignore implementation is too broad and will incorrectly ignore legitimate files. It uses path.basename(filename) to match filenames, which means ANY file named version.ts in ANY subdirectory will be ignored, including:

  • packages/server/src/api/system/version.ts (the version API endpoint)

This breaks hot reload for legitimate source files that happen to be named version.ts.

Root Cause Analysis

The original problem was that generate-version.ts writes BUILD_TIME = '${new Date().toISOString()}' which changes every build, triggering the watcher. The fix addresses this but introduces a new bug in the ignore logic.

Additional Changes

  • Removed unused variables (stdoutData, stderrData, buildOutput) - good cleanup

Confidence Score: 2/5

  • This PR contains a critical bug that will break hot reload for legitimate files
  • Score reflects a critical logic error in the file ignore implementation that will cause the watcher to ignore legitimate source files (packages/server/src/api/system/version.ts). While the smart version generation logic is sound, the overly broad ignore pattern undermines the PR's goal of fixing the development workflow.
  • Pay close attention to scripts/dev-watch.js - the shouldIgnoreFile function needs fixing before merge

Important Files Changed

File Analysis

Filename Score Overview
scripts/dev-watch.js 2/5 Added file ignore logic to prevent rebuild loops, but the implementation is too broad and will incorrectly ignore legitimate files like packages/server/src/api/system/version.ts. Also removed unused variables (good cleanup).
packages/cli/src/scripts/generate-version.ts 4/5 Added smart version comparison to prevent unnecessary file writes. Core logic is sound but lacks input sanitization for special characters in package.json fields. BUILD_TIME will become stale in dev mode (intentional tradeoff).

Sequence Diagram

sequenceDiagram
    participant DevWatch as dev-watch.js
    participant Builder as Build Process
    participant GenVer as generate-version.ts
    participant FS as File System
    participant Watcher as File Watcher

    Note over DevWatch: Initial Startup
    DevWatch->>Builder: Start CLI build
    Builder->>GenVer: Execute generate-version.ts
    GenVer->>FS: Read package.json
    FS-->>GenVer: version, name, description
    GenVer->>FS: Check existing version.ts
    alt version.ts exists with same version info
        GenVer->>GenVer: Skip write (no change)
        Note over GenVer: Prevents rebuild loop
    else version.ts missing or different
        GenVer->>FS: Write new version.ts
        Note over GenVer: Includes BUILD_TIME (stale in dev)
    end
    Builder-->>DevWatch: Build complete
    DevWatch->>Watcher: Start watching source directories
    
    Note over Watcher: User modifies source file
    Watcher->>Watcher: Detect file change
    Watcher->>Watcher: Check shouldIgnoreFile(filename)
    alt filename basename is "version.ts" or "version.js"
        Watcher->>Watcher: Ignore (too broad - BUG!)
        Note over Watcher: Also ignores api/system/version.ts
    else Other TypeScript file
        Watcher->>DevWatch: Trigger rebuild
        DevWatch->>Builder: Rebuild CLI
        Builder->>GenVer: Execute generate-version.ts
        Note over GenVer: Smart comparison prevents<br/>unnecessary writes
    end
Loading

The dev-watch script was causing an infinite rebuild loop because:
1. generate-version.ts writes to src/version.ts on every build
2. version.ts contains BUILD_TIME which changes every build
3. The watcher sees this change in cli/src and triggers another rebuild

This fix addresses the issue in two ways:
1. dev-watch.js: Added ignoredFiles Set to skip watching version.ts/version.js
2. generate-version.ts: Made smart - only rewrites if version/name/description changed

Also removed unused variables (stdoutData, stderrData, buildOutput) from dev-watch.js.

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

coderabbitai bot commented Jan 13, 2026

Important

Review skipped

Auto 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.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/dev-watch-rebuild-loop

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.

@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @0xbbjoker's task in 1m 20s —— View job


PR Review: #6361 - Fix infinite rebuild loop in dev-watch mode

  • Analyze PR changes and context
  • Review scripts/dev-watch.js changes
  • Review packages/cli/src/scripts/generate-version.ts changes
  • Check for security issues
  • Check for testing requirements
  • Provide review feedback

✅ Approved

Good fix for a real problem. The approach is sound and well-documented.


Summary

Aspect Status
Security ✅ No issues
TypeScript ✅ Proper types, no any
Tests ⚠️ See note below
Patterns ✅ Follows existing codebase patterns
Tools ✅ Uses bun correctly

Changes Reviewed

generate-version.ts - Smart version detection:

  • ✅ Only writes when version/name/description actually change
  • ✅ Properly ignores BUILD_TIME in comparison (since it always changes)
  • ✅ Good use of TypeScript interface VersionInfo
  • ✅ Defensive error handling with readFileOrNull

dev-watch.js - Watcher filtering:

  • ignoredFiles Set to skip version.ts/version.js
  • shouldIgnoreFile helper checks basename
  • ✅ Logs ignored files for visibility
  • ✅ Removed unused stdoutData/stderrData/buildOutput variables (good cleanup)

💡 SUGGESTION: Test coverage

The existing test file scripts/__tests__/dev-watch.test.ts tests the watcher script behavior but doesn't specifically test the new shouldIgnoreFile function or the ignoredFiles Set.

Consider adding tests like:

test('shouldIgnoreFile ignores version files', () => {
  const devWatchContent = readFileSync(DEV_SCRIPT, 'utf-8');
  expect(devWatchContent).toContain('ignoredFiles');
  expect(devWatchContent).toContain("'version.ts'");
  expect(devWatchContent).toContain("'version.js'");
  expect(devWatchContent).toContain('shouldIgnoreFile');
});

This is not blocking since:

  1. The code is straightforward and manually verified per the test plan
  2. The existing tests already validate the overall watcher behavior
  3. Build scripts often rely on manual integration testing

Minor Observations (Non-blocking)

  1. Line 103 in generate-version.ts: BUILD_TIME is still written on every actual write. This is fine since the file is only written when version info changes, but worth noting the timestamp will be stale if version hasn't changed.

  2. Regex in extractExistingVersionInfo: The regex '([^']+)' works but could fail if the description contains an escaped single quote. Current CLI description is "CLI for elizaOS" which is safe.


Verdict

The fix correctly addresses the infinite loop by:

  1. Defense in depth: Both the generator (skip writes) and watcher (ignore files) work together
  2. Root cause fixed: Not just ignoring symptoms but preventing unnecessary writes
  3. Good documentation: Comments explain why the code exists

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +490 to +493
function shouldIgnoreFile(filename) {
if (!filename) return true;
return ignoredFiles.has(path.basename(filename));
}
Copy link

Choose a reason for hiding this comment

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

The shouldIgnoreFile function uses path.basename(filename) which will ignore any file named version.ts or version.js in any subdirectory. This is too broad and causes a critical bug:

The watcher monitors packages/server/src, and there's a legitimate file at packages/server/src/api/system/version.ts (the version API endpoint). When this file changes, the watcher callback receives filename = 'api/system/version.ts', and path.basename('api/system/version.ts') returns 'version.ts', which matches the ignored set. This means changes to the server's version endpoint will not trigger a rebuild.

Fix: Only ignore version.ts/version.js at the root level of watched directories:

Suggested change
function shouldIgnoreFile(filename) {
if (!filename) return true;
return ignoredFiles.has(path.basename(filename));
}
function shouldIgnoreFile(filename) {
if (!filename) return true;
const basename = path.basename(filename);
// Only ignore version files at root level (not in subdirectories like api/system/version.ts)
return ignoredFiles.has(basename) && !filename.includes(path.sep);
}

This ensures only version.ts (not api/system/version.ts) is ignored.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/dev-watch.js
Line: 490:493

Comment:
The `shouldIgnoreFile` function uses `path.basename(filename)` which will ignore **any file named `version.ts` or `version.js` in any subdirectory**. This is too broad and causes a critical bug:

The watcher monitors `packages/server/src`, and there's a legitimate file at `packages/server/src/api/system/version.ts` (the version API endpoint). When this file changes, the watcher callback receives `filename = 'api/system/version.ts'`, and `path.basename('api/system/version.ts')` returns `'version.ts'`, which matches the ignored set. This means **changes to the server's version endpoint will not trigger a rebuild**.

**Fix**: Only ignore `version.ts`/`version.js` at the root level of watched directories:

```suggestion
function shouldIgnoreFile(filename) {
  if (!filename) return true;
  const basename = path.basename(filename);
  // Only ignore version files at root level (not in subdirectories like api/system/version.ts)
  return ignoredFiles.has(basename) && !filename.includes(path.sep);
}
```

This ensures only `version.ts` (not `api/system/version.ts`) is ignored.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +98 to +100
export const CLI_VERSION = '${versionInfo.version}';
export const CLI_NAME = '${versionInfo.name}';
export const CLI_DESCRIPTION = '${versionInfo.description}';
Copy link

Choose a reason for hiding this comment

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

The generated content uses template string interpolation without escaping special characters. If package.json contains a single quote in the name or description fields (e.g., "eliza's CLI"), it will:

  1. Generate syntactically invalid TypeScript (unescaped quote breaks the string)
  2. Cause the regex in extractExistingVersionInfo to fail, triggering unnecessary rewrites

While unlikely given the current package.json format, consider escaping single quotes for robustness:

Suggested change
export const CLI_VERSION = '${versionInfo.version}';
export const CLI_NAME = '${versionInfo.name}';
export const CLI_DESCRIPTION = '${versionInfo.description}';
export const CLI_VERSION = '${versionInfo.version.replace(/'/g, "\\'")}';
export const CLI_NAME = '${versionInfo.name.replace(/'/g, "\\'")}';
export const CLI_DESCRIPTION = '${versionInfo.description.replace(/'/g, "\\'")}';

Alternatively, use double quotes in the generated string literals to avoid the issue entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/scripts/generate-version.ts
Line: 98:100

Comment:
The generated content uses template string interpolation without escaping special characters. If `package.json` contains a single quote in the `name` or `description` fields (e.g., `"eliza's CLI"`), it will:
1. Generate syntactically invalid TypeScript (unescaped quote breaks the string)
2. Cause the regex in `extractExistingVersionInfo` to fail, triggering unnecessary rewrites

While unlikely given the current package.json format, consider escaping single quotes for robustness:

```suggestion
export const CLI_VERSION = '${versionInfo.version.replace(/'/g, "\\'")}';
export const CLI_NAME = '${versionInfo.name.replace(/'/g, "\\'")}';
export const CLI_DESCRIPTION = '${versionInfo.description.replace(/'/g, "\\'")}';
```

Alternatively, use double quotes in the generated string literals to avoid the issue entirely.

How can I resolve this? If you propose a fix, please make it concise.

@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @0xbbjoker's task in 1m 30s —— View job


PR Review: #6361 - Fix infinite rebuild loop in dev-watch mode

  • Analyze PR changes and context
  • Review scripts/dev-watch.js changes
  • Review packages/cli/src/scripts/generate-version.ts changes
  • Check for security issues
  • Check for testing requirements
  • Provide review feedback

❌ Changes Requested

The fix correctly addresses the root cause of the infinite loop, but the implementation has a critical bug that will break hot reload for legitimate files.


Summary

Aspect Status
Security ✅ No issues
TypeScript ✅ Proper types, no any
Tests ⚠️ No tests for new functions
Patterns ✅ Follows existing codebase patterns
Tools ✅ Uses bun correctly

❌ CRITICAL: shouldIgnoreFile is too broad

File: scripts/dev-watch.js:490-493

The shouldIgnoreFile function uses path.basename(filename) which ignores any file named version.ts in any subdirectory. This is a bug:

function shouldIgnoreFile(filename) {
  if (!filename) return true;
  return ignoredFiles.has(path.basename(filename));
}

Impact: There's a legitimate file at packages/server/src/api/system/version.ts (the version API endpoint). When this file changes:

  1. The watcher receives filename = 'api/system/version.ts'
  2. path.basename('api/system/version.ts') returns 'version.ts'
  3. This matches the ignored set → Changes to the server version endpoint won't trigger rebuilds

See inline comment for fix.


✅ Good: Smart version generation

File: packages/cli/src/scripts/generate-version.ts

The smart version detection logic is solid:

  • Only writes when version/name/description actually change
  • Properly ignores BUILD_TIME in comparison (since it always changes)
  • Good use of TypeScript interface VersionInfo
  • Defensive error handling with readFileOrNull

💡 SUGGESTION: Escape single quotes in template

File: packages/cli/src/scripts/generate-version.ts:98-100

The template uses single quotes without escaping. Low risk but could break if package.json ever contains quotes. See inline comment for suggestion.


Verdict

The fix correctly identifies and addresses the root cause (avoiding unnecessary version.ts writes). However, the watcher ignore logic has a bug that will break hot reload for the server's version endpoint. Please fix the shouldIgnoreFile function before merging.

Comment on lines +490 to +493
function shouldIgnoreFile(filename) {
if (!filename) return true;
return ignoredFiles.has(path.basename(filename));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL BUG: This function ignores any file named version.ts in any subdirectory, not just the auto-generated one at cli/src/version.ts.

Impact: packages/server/src/api/system/version.ts (the version API endpoint) will never trigger rebuilds when changed.

Repro:

  • shouldIgnoreFile('api/system/version.ts') returns true because path.basename() returns 'version.ts'
Suggested change
function shouldIgnoreFile(filename) {
if (!filename) return true;
return ignoredFiles.has(path.basename(filename));
}
function shouldIgnoreFile(filename) {
if (!filename) return true;
// Only ignore version files at root level (no directory separators)
// This ignores 'version.ts' but NOT 'api/system/version.ts'
return ignoredFiles.has(filename);
}

This fix works because the watcher callback receives the relative path from the watched directory. For root-level files, filename is just 'version.ts'. For nested files, filename is 'api/system/version.ts'.

Comment on lines +98 to +100
export const CLI_VERSION = '${versionInfo.version}';
export const CLI_NAME = '${versionInfo.name}';
export const CLI_DESCRIPTION = '${versionInfo.description}';
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 SUGGESTION: If package.json ever contains a single quote in the description (e.g., "Eliza's CLI"), this would generate invalid TypeScript:

export const CLI_DESCRIPTION = 'Eliza's CLI';  // Syntax error!

Low risk since current package.json doesn't have quotes, but for robustness consider:

Suggested change
export const CLI_VERSION = '${versionInfo.version}';
export const CLI_NAME = '${versionInfo.name}';
export const CLI_DESCRIPTION = '${versionInfo.description}';
export const CLI_VERSION = ${JSON.stringify(versionInfo.version)};
export const CLI_NAME = ${JSON.stringify(versionInfo.name)};
export const CLI_DESCRIPTION = ${JSON.stringify(versionInfo.description)};

This produces the same output for simple strings and handles edge cases safely.

0xbbjoker and others added 2 commits January 13, 2026 17:08
The shouldIgnoreFile function was using path.basename() which would
ignore ANY file named version.ts in any subdirectory, including
legitimate files like packages/server/src/api/system/version.ts.

Fixed by checking the filename directly without basename extraction.
The watcher callback provides the relative path from the watched
directory root, so root-level 'version.ts' will match but nested
'api/system/version.ts' will not.

Addresses review comment: #6361 (comment)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed template string interpolation to use JSON.stringify() for all
string values. This safely handles special characters like quotes and
backslashes that could break the generated TypeScript syntax.

Also updated extractExistingVersionInfo regex to support both single
and double quoted strings for backwards compatibility.

Addresses review comment: #6361 (comment)

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

claude bot commented Jan 13, 2026

Claude finished @0xbbjoker's task in 49s —— View job


PR Review: #6361 - Fix infinite rebuild loop in dev-watch mode

  • Analyze PR changes and context
  • Review updated changes (new commits)
  • Verify previous feedback was addressed
  • Provide final review feedback

✅ Approved

Both issues from the previous review have been properly addressed.


Summary

Aspect Status
Security ✅ No issues
TypeScript ✅ Proper types
Tests ⚠️ Manual testing only (acceptable for build scripts)
Patterns ✅ Follows codebase patterns
Tools ✅ Uses bun correctly

Fixes Verified

1. ✅ shouldIgnoreFile now correctly scoped (scripts/dev-watch.js:490-495)

Previous issue: Used path.basename() which would ignore ALL files named version.ts in any directory (including packages/server/src/api/system/version.ts).

Fix: Now checks exact filename match:

return ignoredFiles.has(filename);

This works because the watcher callback receives relative paths:

  • packages/cli/src/version.ts → callback receives 'version.ts'ignored
  • packages/server/src/api/system/version.ts → callback receives 'api/system/version.ts'not ignored

2. ✅ Safe string escaping (packages/cli/src/scripts/generate-version.ts:100-102)

Previous issue: Single quotes without escaping could break if package.json contained quotes.

Fix: Now uses JSON.stringify():

export const CLI_VERSION = ${JSON.stringify(versionInfo.version)};
export const CLI_NAME = ${JSON.stringify(versionInfo.name)};
export const CLI_DESCRIPTION = ${JSON.stringify(versionInfo.description)};

Implementation Quality

The fix correctly addresses the root cause through defense in depth:

  1. Smart generation - generate-version.ts only writes when version/name/description change (ignores BUILD_TIME in comparison)
  2. Watcher filtering - dev-watch.js ignores the auto-generated file even if it does change

Good documentation in comments explaining why the ignore logic exists.


@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @0xbbjoker's task in 1m 10s —— View job


PR Review: #6361 - Fix infinite rebuild loop in dev-watch mode

  • Analyze PR changes and context
  • Review updated changes (commits since last review)
  • Verify previous review feedback was addressed
  • Provide final review feedback

✅ Approved

Both issues from the previous review have been properly addressed.


Summary

Aspect Status
Security ✅ No issues
TypeScript ✅ Proper types
Tests ⚠️ Manual testing only (acceptable for build scripts)
Patterns ✅ Follows codebase patterns
Tools ✅ Uses bun correctly

Fixes Verified

1. ✅ shouldIgnoreFile now correctly scoped (scripts/dev-watch.js:490-495)

Previous issue: Used path.basename() which would ignore ALL files named version.ts in any directory (including packages/server/src/api/system/version.ts).

Fix applied in commit 08ca69f:

function shouldIgnoreFile(filename) {
  if (!filename) return true;
  // Only ignore version files at root level (no directory separators)
  // This ignores 'version.ts' but NOT 'api/system/version.ts'
  return ignoredFiles.has(filename);
}

Verification: The watcher watches packages/server/src with recursive: true. The callback receives:

  • 'version.ts' for root-level files → ignored
  • 'api/system/version.ts' for nested files → not ignored

2. ✅ Safe string escaping (packages/cli/src/scripts/generate-version.ts:100-102)

Previous issue: Single quotes without escaping could break if package.json contained quotes.

Fix applied in commit 71c737c:

export const CLI_VERSION = ${JSON.stringify(versionInfo.version)};
export const CLI_NAME = ${JSON.stringify(versionInfo.name)};
export const CLI_DESCRIPTION = ${JSON.stringify(versionInfo.description)};

Implementation Quality

The fix correctly addresses the root cause through defense in depth:

  1. Smart generation - generate-version.ts only writes when version/name/description actually change (ignores BUILD_TIME in comparison)
  2. Watcher filtering - dev-watch.js ignores the auto-generated file even if it does change

Good documentation in comments explaining why the ignore logic exists.


The previous regex [^"']+ would stop at apostrophes, causing incorrect
extraction for values like "It's awesome" (would extract just "It").

Updated to use a proper JSON string pattern that handles:
- Apostrophes (unescaped in JSON strings)
- Escaped double quotes (\")
- Other escaped characters (\\, \n, etc.)

Addresses review comment: #6361 (comment)

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

claude bot commented Jan 13, 2026

Claude finished @0xbbjoker's task in 1m 4s —— View job


PR Review: #6361 - Fix infinite rebuild loop in dev-watch mode

  • Analyze PR changes and context
  • Review latest changes (new commits since last review)
  • Verify previous feedback was addressed
  • Check for any remaining issues
  • Provide final review feedback

✅ Approved

All previous issues have been properly addressed. The fix is ready to merge.


Summary

Aspect Status
Security ✅ No issues
TypeScript ✅ Proper types
Tests ⚠️ Manual testing only (acceptable for build scripts)
Patterns ✅ Follows codebase patterns
Tools ✅ Uses bun correctly

Issues Fixed (All Verified ✅)

1. ✅ shouldIgnoreFile now correctly scoped (scripts/dev-watch.js:490-495)

Previous issue: Used path.basename() which would ignore ALL files named version.ts in any directory.

Fix applied:

function shouldIgnoreFile(filename) {
  if (!filename) return true;
  // Only ignore version files at root level (no directory separators)
  return ignoredFiles.has(filename);
}

Verification: The watcher watches directories with { recursive: true }, and the callback receives relative paths from the watched root:

  • packages/cli/src/version.ts → callback receives 'version.ts'ignored
  • packages/server/src/api/system/version.ts → callback receives 'api/system/version.ts'not ignored

2. ✅ Safe string escaping (packages/cli/src/scripts/generate-version.ts:106-108)

Previous issue: Single quotes without escaping could break if package.json contained quotes.

Fix applied:

export const CLI_VERSION = ${JSON.stringify(versionInfo.version)};
export const CLI_NAME = ${JSON.stringify(versionInfo.name)};
export const CLI_DESCRIPTION = ${JSON.stringify(versionInfo.description)};

3. ✅ Regex handles JSON-stringified values (generate-version.ts:33-34)

Previous issue: Regex pattern [^"']+ would fail for values with apostrophes.

Fix applied:

const jsonStringPattern = (name: string) =>
  new RegExp(`export const ${name} = "((?:[^"\\\\]|\\\\.)*)"`);

This pattern correctly handles:

  • Normal strings: "hello" → extracts hello
  • Escaped quotes: "say \"hi\"" → extracts say \"hi\"
  • Apostrophes: "it's good" → extracts it's good

Implementation Quality

The fix uses defense in depth:

  1. Smart generation - generate-version.ts only writes when version/name/description change (ignores BUILD_TIME)
  2. Watcher filtering - dev-watch.js ignores auto-generated files even if they change

Good documentation in comments explaining why the ignore logic exists.


existingInfo.version === newInfo.version &&
existingInfo.name === newInfo.name &&
existingInfo.description === newInfo.description
);
Copy link

Choose a reason for hiding this comment

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

JSON escape mismatch causes failed version comparison

Medium Severity

The extractExistingVersionInfo function extracts values from a file written with JSON.stringify(), which escapes special characters (quotes, backslashes, etc.). However, versionInfoMatches compares these extracted JSON-escaped strings directly against raw values from package.json. For example, a description containing "quoted" would be stored as \"quoted\" in the file, but compared against the unescaped "quoted" from package.json. This mismatch causes the comparison to always fail when metadata contains escapable characters, defeating the optimization and potentially reintroducing the infinite rebuild loop.

Additional Locations (1)

Fix in Cursor Fix in Web

@0xbbjoker 0xbbjoker merged commit bdfa7f0 into develop Jan 15, 2026
19 checks passed
@0xbbjoker 0xbbjoker deleted the fix/dev-watch-rebuild-loop branch January 15, 2026 13:51
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.

3 participants