11import { execSync , spawnSync } from "node:child_process" ;
2- import { existsSync , readFileSync , rmSync , statSync , writeFileSync } from "node:fs" ;
2+ import { existsSync , readFileSync , rmSync , statSync } from "node:fs" ;
33import { createRequire } from "node:module" ;
44import { homedir } from "node:os" ;
55import { join } from "node:path" ;
@@ -21,9 +21,11 @@ import { Database } from "@magic-context/core/shared/sqlite";
2121import { ensureTuiPluginEntry } from "@magic-context/core/shared/tui-config" ;
2222import { parse , stringify } from "comment-json" ;
2323import { isDevPathPluginEntry , matchesPluginEntry } from "../adapters/opencode" ;
24+ import { writeFileAtomic } from "../lib/atomic-write" ;
2425import { collectDiagnostics } from "../lib/diagnostics-opencode" ;
2526import { checkLocalEmbeddingRuntime } from "../lib/embedding-runtime" ;
2627import { bundleIssueReport } from "../lib/logs-opencode" ;
28+ import { migrateExperimentalPinKeyFilesForDoctor } from "../lib/migrate-experimental-doctor" ;
2729import { isOpenCodeInstalled } from "../lib/opencode-helpers" ;
2830import { detectConfigPaths , getMagicContextLogPath } from "../lib/paths" ;
2931import { confirm , intro , log , outro , selectOne , spinner , text } from "../lib/prompts" ;
@@ -800,45 +802,7 @@ export async function runDoctor(
800802 fixed ++ ;
801803 }
802804
803- // Migrate experimental.pin_key_files → dreamer.pin_key_files.
804- // Same story as user_memories — feature graduated to stable under
805- // dreamer config. Existing `enabled: true` values are preserved so
806- // we do not silently disable the feature for users who opted in.
807- if ( experimental && "pin_key_files" in experimental ) {
808- const dreamer = ( mcConfig . dreamer as Record < string , unknown > | undefined ) ?? { } ;
809- const oldPKF = experimental . pin_key_files ;
810- const existingPKF = dreamer . pin_key_files ;
811- if ( existingPKF === undefined ) {
812- dreamer . pin_key_files = oldPKF ;
813- } else if (
814- typeof oldPKF === "object" &&
815- oldPKF !== null &&
816- typeof existingPKF === "object" &&
817- existingPKF !== null
818- ) {
819- // Merge field-by-field so sub-fields like `token_budget`
820- // and `min_reads` from the old block are not silently lost
821- // when a user has partially graduated.
822- const merged = {
823- ...( oldPKF as Record < string , unknown > ) ,
824- ...( existingPKF as Record < string , unknown > ) ,
825- } ;
826- dreamer . pin_key_files = merged ;
827- } else if ( typeof oldPKF === "object" && oldPKF !== null ) {
828- // Old block is a proper object but new block is a malformed
829- // primitive. Coerce and merge so sub-fields like
830- // `token_budget` / `min_reads` are not dropped.
831- const coerced : Record < string , unknown > = {
832- ...( oldPKF as Record < string , unknown > ) ,
833- enabled : Boolean ( existingPKF ) ,
834- } ;
835- dreamer . pin_key_files = coerced ;
836- log . warn (
837- `Coerced malformed dreamer.pin_key_files (${ typeof existingPKF } ) to object form while merging sub-fields from experimental.pin_key_files` ,
838- ) ;
839- }
840- mcConfig . dreamer = dreamer ;
841- delete experimental . pin_key_files ;
805+ if ( experimental && migrateExperimentalPinKeyFilesForDoctor ( mcConfig ) ) {
842806 mcChanged = true ;
843807 log . success (
844808 "Migrated experimental.pin_key_files → dreamer.pin_key_files (preserved user enabled state)" ,
@@ -910,7 +874,7 @@ export async function runDoctor(
910874 }
911875
912876 if ( mcChanged ) {
913- writeFileSync ( paths . magicContextConfig , `${ stringify ( mcConfig , null , 2 ) } \n` ) ;
877+ writeFileAtomic ( paths . magicContextConfig , `${ stringify ( mcConfig , null , 2 ) } \n` ) ;
914878 }
915879 } catch {
916880 log . warn ( "Could not migrate deprecated config keys in magic-context.jsonc" ) ;
@@ -982,7 +946,7 @@ export async function runDoctor(
982946 rawPlugins [ existingIdx ] = PLUGIN_ENTRY_WITH_VERSION ;
983947 }
984948 config . plugin = rawPlugins ;
985- writeFileSync ( paths . opencodeConfig , `${ stringify ( config , null , 2 ) } \n` ) ;
949+ writeFileAtomic ( paths . opencodeConfig , `${ stringify ( config , null , 2 ) } \n` ) ;
986950 pass (
987951 `Upgraded plugin entry in ${ configName } : ${ oldEntryStr } → ${ PLUGIN_ENTRY_WITH_VERSION } ` ,
988952 ) ;
@@ -994,7 +958,7 @@ export async function runDoctor(
994958 // tuple/options entry the user already had.
995959 rawPlugins . push ( PLUGIN_ENTRY_WITH_VERSION ) ;
996960 config . plugin = rawPlugins ;
997- writeFileSync ( paths . opencodeConfig , `${ stringify ( config , null , 2 ) } \n` ) ;
961+ writeFileAtomic ( paths . opencodeConfig , `${ stringify ( config , null , 2 ) } \n` ) ;
998962 pass ( `Added plugin to ${ configName } ` ) ;
999963 fixed ++ ;
1000964 }
@@ -1071,7 +1035,7 @@ export async function runDoctor(
10711035 tuiRawPlugins [ tuiIdx ] = PLUGIN_ENTRY_WITH_VERSION ;
10721036 }
10731037 tuiConfig . plugin = tuiRawPlugins ;
1074- writeFileSync ( paths . tuiConfig , `${ stringify ( tuiConfig , null , 2 ) } \n` ) ;
1038+ writeFileAtomic ( paths . tuiConfig , `${ stringify ( tuiConfig , null , 2 ) } \n` ) ;
10751039 pass ( `Upgraded TUI plugin: ${ tuiEntryStr } → ${ PLUGIN_ENTRY_WITH_VERSION } ` ) ;
10761040 fixed ++ ;
10771041 } else {
0 commit comments