Skip to content

fix(config): load legacy Windows antigravity config so account switching settings are applied#406

Closed
ndycode wants to merge 1 commit intoNoeFabris:mainfrom
ndycode:fix/issue-405-windows-config
Closed

fix(config): load legacy Windows antigravity config so account switching settings are applied#406
ndycode wants to merge 1 commit intoNoeFabris:mainfrom
ndycode:fix/issue-405-windows-config

Conversation

@ndycode
Copy link

@ndycode ndycode commented Feb 8, 2026

Fixes #405.

What was happening

On Windows, we only read config from ~/.config/opencode/antigravity.json.

If a user still had config in the legacy path %APPDATA%\opencode\antigravity.json, their settings were effectively ignored, and defaults were used instead (scheduling_mode: cache_first, etc). That can look like getting stuck on the first rate-limited account for a long time before switching.

What changed

  • Added legacy Windows config handling in the loader.
  • If legacy config exists and new config doesn't, we migrate:
    • %APPDATA%\opencode\antigravity.json -> current config path.
  • If migration fails, we still fall back to reading the legacy file.
  • Added regression tests for migration and loading migrated settings.

Files changed

  • src/plugin/config/loader.ts
  • src/plugin/config/loader.test.ts

Verification

  • npm run typecheck
  • npm run build
  • npm test (full suite) run 3 times:
    • 26 test files passed
    • 839 tests passed
    • 25 todo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 8, 2026

Walkthrough

This PR adds Windows legacy config migration functionality to the config loader. It introduces two new functions: getLegacyWindowsConfigDir() to determine the legacy %APPDATA%/opencode path, and resolveUserConfigPath() to resolve the user config path and handle migration of legacy Windows configs to the new location with automatic fallback on failure. The getUserConfigPath() function is modified to delegate to resolveUserConfigPath(). A comprehensive test suite verifies migration behavior, including legacy file removal, migrated config path resolution, and loading of legacy antigravity.json settings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding legacy Windows config migration to ensure antigravity settings are loaded, directly addressing issue #405.
Description check ✅ Passed The description is well-related to the changeset, explaining the problem (legacy Windows config being ignored), the solution (migration logic), and listing the modified files.
Linked Issues check ✅ Passed The PR fully addresses issue #405 by implementing Windows legacy config migration to ensure user settings (scheduling_mode, account_selection_strategy, etc.) are properly loaded instead of using defaults.
Out of Scope Changes check ✅ Passed All changes in loader.ts and loader.test.ts are directly scoped to implementing Windows legacy config migration as specified in issue #405, with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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


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
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: 1

🤖 Fix all issues with AI agents
In `@src/plugin/config/loader.ts`:
- Around line 68-87: The migration block mishandles a failing unlinkSync: if
copyFileSync succeeds but unlinkSync throws, the outer catch logs a full
migration failure and returns legacyPath even though newPath contains the
migrated config; fix by wrapping unlinkSync in its own try/catch (inside the
catch that handles renameSync failure), so on unlink errors you call log.warn
with a message like "Copied config but failed to remove legacy file, leaving
both files" including legacyPath/newPath/error, and still return newPath; keep
the existing outer catch for true migration failures (e.g., copyFileSync errors)
so renameSync, copyFileSync, unlinkSync are referenced accordingly and the
function returns newPath when the config was successfully copied.
🧹 Nitpick comments (4)
src/plugin/config/loader.ts (2)

56-61: getUserConfigPath() now has file-system side effects (mkdir, rename, copy, unlink) — consider documenting this.

Previously callers could assume getUserConfigPath() was a pure path lookup. It now performs I/O on first call when a legacy config is detected. A JSDoc note on getUserConfigPath (or at least on resolveUserConfigPath) would help future maintainers.


284-287: Exposing internals via __testExports in production code.

This is a common pragmatic pattern, but it ships migration internals to production consumers. If the module bundler supports it, a cleaner alternative is using @visibleForTesting-style annotations or re-exporting only in the test environment. Low priority given the limited surface area.

src/plugin/config/loader.test.ts (2)

53-82: Consider adding a test for the migration failure fallback path.

The current tests cover the happy path (successful migration) and end-to-end settings loading, which is great. However, the fallback behavior in resolveUserConfigPath (Lines 79–87 of loader.ts) — where migration fails and the function returns legacyPath — is untested. A test that makes the destination directory non-writable (or uses a read-only mock) would increase confidence that the fallback works correctly and that user settings are still loaded from the legacy location.


69-72: Clarify the two-step test approach with a brief comment.

Line 70 explicitly triggers migration with "win32", then Line 72 calls loadConfig(tempRoot) which internally calls resolveUserConfigPath() with process.platform. On non-Windows CI this means the migration gate is bypassed (returns newPath directly since the platform isn't "win32"), which only works because the file was already migrated by Line 70. A one-line comment would prevent confusion for future readers.

Suggested clarification
-    // Force legacy -> new path migration even when tests run on non-Windows CI.
+    // Force legacy -> new path migration even when tests run on non-Windows CI.
+    // loadConfig() below will find the already-migrated file at OPENCODE_CONFIG_DIR.
     __testExports.resolveUserConfigPath("win32");
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 28f46c2 and 27bfc60.

📒 Files selected for processing (2)
  • src/plugin/config/loader.test.ts
  • src/plugin/config/loader.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/plugin/config/loader.test.ts (1)
src/plugin/config/loader.ts (2)
  • __testExports (285-287)
  • loadConfig (237-259)
src/plugin/config/loader.ts (2)
src/plugin/storage.ts (1)
  • getConfigDir (328-328)
src/plugin/config/index.ts (1)
  • getUserConfigPath (25-25)
🔇 Additional comments (4)
src/plugin/config/loader.ts (2)

12-14: LGTM on the updated imports.

All newly imported symbols (copyFileSync, mkdirSync, renameSync, unlinkSync, dirname) are used in the new migration logic.


40-48: LGTM on legacy directory resolution.

Correctly mirrors the %APPDATA% fallback pattern and isolates the legacy path logic.

src/plugin/config/loader.test.ts (2)

12-36: Good environment isolation with proper save/restore of env vars and temp directory cleanup.

The beforeEach/afterEach pattern correctly handles the undefined case for env vars (deleting instead of setting to undefined string) and uses rmSync with force: true for cleanup.


38-51: Migration test is solid — covers the happy path well.

Verifies the resolved path, confirms the new file was created, and asserts the legacy file was removed.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +68 to +87
try {
mkdirSync(dirname(newPath), { recursive: true });
try {
renameSync(legacyPath, newPath);
log.info("Migrated Windows config via rename", { from: legacyPath, to: newPath });
} catch {
copyFileSync(legacyPath, newPath);
unlinkSync(legacyPath);
log.info("Migrated Windows config via copy+delete", { from: legacyPath, to: newPath });
}
return newPath;
} catch (error) {
// Fallback to legacy path if migration failed, so user settings are still respected.
log.warn("Failed to migrate legacy Windows config, falling back to legacy path", {
legacyPath,
newPath,
error: String(error),
});
return legacyPath;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

If unlinkSync fails after a successful copyFileSync, the log is misleading and the return value is suboptimal.

When copyFileSync (Line 74) succeeds but unlinkSync (Line 75) throws, the exception propagates to the outer catch (Line 79), which logs "Failed to migrate" and returns legacyPath. At that point newPath already contains the correct config, so:

  1. The warning is misleading — migration partially succeeded (copy worked, delete didn't).
  2. Returning legacyPath is harmless this run (same content), but on the next run both files exist and newPath wins (Line 64), so the user silently flips paths between invocations.

Consider catching unlinkSync failures separately so you can still return newPath and log a more accurate warning about the leftover legacy file.

Suggested fix
     } catch {
       copyFileSync(legacyPath, newPath);
-      unlinkSync(legacyPath);
-      log.info("Migrated Windows config via copy+delete", { from: legacyPath, to: newPath });
+      try {
+        unlinkSync(legacyPath);
+      } catch (unlinkError) {
+        log.warn("Migrated config but could not remove legacy file", {
+          legacyPath,
+          error: String(unlinkError),
+        });
+      }
+      log.info("Migrated Windows config via copy+delete", { from: legacyPath, to: newPath });
     }
🤖 Prompt for AI Agents
In `@src/plugin/config/loader.ts` around lines 68 - 87, The migration block
mishandles a failing unlinkSync: if copyFileSync succeeds but unlinkSync throws,
the outer catch logs a full migration failure and returns legacyPath even though
newPath contains the migrated config; fix by wrapping unlinkSync in its own
try/catch (inside the catch that handles renameSync failure), so on unlink
errors you call log.warn with a message like "Copied config but failed to remove
legacy file, leaving both files" including legacyPath/newPath/error, and still
return newPath; keep the existing outer catch for true migration failures (e.g.,
copyFileSync errors) so renameSync, copyFileSync, unlinkSync are referenced
accordingly and the function returns newPath when the config was successfully
copied.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 8, 2026

Greptile Overview

Greptile Summary

This PR fixes a configuration loading issue on Windows where legacy config settings in %APPDATA%\opencode\antigravity.json were being ignored after the config path was changed to ~/.config/opencode/antigravity.json.

Key Changes:

  • Added automatic migration from legacy Windows path to current config path
  • Migration attempts rename first (fast), then falls back to copy+delete if needed
  • If migration fails, falls back to reading legacy path so user settings are still respected
  • Added comprehensive tests covering both migration scenarios and settings persistence
  • Platform-specific logic only activates on Windows systems

Implementation Quality:

  • Clean separation of concerns with dedicated helper functions
  • Proper error handling with fallback to legacy path on migration failure
  • Tests properly isolate environment variables and filesystem operations
  • Migration is idempotent (only runs when legacy exists and new doesn't)

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-designed with proper error handling, comprehensive tests, and smart fallback logic. The migration only runs on Windows when needed, and if it fails, the system falls back to reading the legacy path so users never lose their settings. Tests verify both migration and settings persistence.
  • No files require special attention

Important Files Changed

Filename Overview
src/plugin/config/loader.ts Added legacy Windows config migration logic with proper fallback handling for %APPDATA% path
src/plugin/config/loader.test.ts New test file covering migration scenarios and settings persistence from legacy config

Sequence Diagram

sequenceDiagram
    participant User
    participant getUserConfigPath
    participant resolveUserConfigPath
    participant FS as File System
    participant Logger

    User->>getUserConfigPath: Request config path
    getUserConfigPath->>resolveUserConfigPath: Call with process.platform
    
    alt Platform is not win32
        resolveUserConfigPath-->>getUserConfigPath: Return new path only
    else Platform is win32
        resolveUserConfigPath->>FS: Check if legacy config exists
        resolveUserConfigPath->>FS: Check if new config exists
        
        alt Legacy exists AND new doesn't exist
            resolveUserConfigPath->>FS: Create new config directory
            
            alt Try rename first
                resolveUserConfigPath->>FS: renameSync(legacy, new)
                resolveUserConfigPath->>Logger: Log migration via rename
                resolveUserConfigPath-->>getUserConfigPath: Return new path
            else Rename fails
                resolveUserConfigPath->>FS: copyFileSync(legacy, new)
                resolveUserConfigPath->>FS: unlinkSync(legacy)
                resolveUserConfigPath->>Logger: Log migration via copy+delete
                resolveUserConfigPath-->>getUserConfigPath: Return new path
            end
        else Migration fails completely
            resolveUserConfigPath->>Logger: Warn about migration failure
            resolveUserConfigPath-->>getUserConfigPath: Return legacy path (fallback)
        else Legacy doesn't exist OR new already exists
            resolveUserConfigPath-->>getUserConfigPath: Return new path
        end
    end
    
    getUserConfigPath-->>User: Return resolved path
Loading

@NoeFabris
Copy link
Owner

This doesn't solve issue #405 and the plugin already contains the logic that migrates the config file from the legacy path to the new one.

@NoeFabris NoeFabris closed this Feb 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants