@@ -5,7 +5,6 @@ import { homedir } from "node:os";
55import { join } from "node:path" ;
66import { loadPluginConfig } from "@magic-context/core/config" ;
77import { substituteConfigVariables } from "@magic-context/core/config/variable" ;
8-
98import {
109 type EmbeddingProbeOutcome ,
1110 probeEmbeddingEndpoint ,
@@ -17,6 +16,7 @@ import { getMagicContextStorageDir } from "@magic-context/core/shared/data-path"
1716import { Database } from "@magic-context/core/shared/sqlite" ;
1817import { ensureTuiPluginEntry } from "@magic-context/core/shared/tui-config" ;
1918import { parse , stringify } from "comment-json" ;
19+
2020import { isDevPathPluginEntry , matchesPluginEntry } from "../adapters/opencode" ;
2121import { writeFileAtomic } from "../lib/atomic-write" ;
2222import { migrateConfigLocationsForCli } from "../lib/config-location-migration" ;
@@ -37,6 +37,11 @@ import {
3737} from "../lib/opencode-plugin-cache" ;
3838import { detectConfigPaths , getMagicContextLogPath } from "../lib/paths" ;
3939import { confirm , intro , log , outro , selectOne , spinner , text } from "../lib/prompts" ;
40+ import {
41+ sanitizeDiagnosticEndpoint ,
42+ sanitizeDiagnosticText ,
43+ sanitizePathString ,
44+ } from "../lib/redaction" ;
4045import { runV22BackfillCommands , type V22BackfillCommandArgs } from "../lib/v22-backfill-commands" ;
4146import { clearPluginCache } from "./doctor-opencode-cache" ;
4247
@@ -140,6 +145,40 @@ function getSelfVersion(): string {
140145 return "0.0.0" ;
141146}
142147
148+ export function isPinnedOpenCodePluginSpecifier ( specifier : string ) : boolean {
149+ if ( specifier === PLUGIN_NAME || specifier === PLUGIN_ENTRY_WITH_VERSION ) return false ;
150+ return specifier . startsWith ( `${ PLUGIN_NAME } @` ) ;
151+ }
152+
153+ export function getUserNpmrcPath ( ) : string {
154+ const custom = process . env . NPM_CONFIG_USERCONFIG ?. trim ( ) ;
155+ if ( custom ) return custom ;
156+ const home = process . env . HOME ?. trim ( ) ;
157+ return join ( home || homedir ( ) , ".npmrc" ) ;
158+ }
159+
160+ export function collectNpmReleaseAgeWarnings ( ) : string [ ] {
161+ const ageWarnings : string [ ] = [ ] ;
162+ const npmrcPath = getUserNpmrcPath ( ) ;
163+ if ( ! existsSync ( npmrcPath ) ) return ageWarnings ;
164+ try {
165+ const npmrc = readFileSync ( npmrcPath , "utf-8" ) ;
166+ for ( const line of npmrc . split ( "\n" ) ) {
167+ const trimmed = line . trim ( ) ;
168+ if ( trimmed . startsWith ( "#" ) || trimmed . startsWith ( ";" ) ) continue ;
169+ const [ key ] = trimmed . split ( "=" ) . map ( ( s ) => s . trim ( ) ) ;
170+ if ( key === "min-release-age" || key === "before" ) {
171+ ageWarnings . push (
172+ `${ sanitizePathString ( npmrcPath ) } has '${ sanitizeDiagnosticText ( trimmed ) } '` ,
173+ ) ;
174+ }
175+ }
176+ } catch {
177+ // Can't read .npmrc — skip.
178+ }
179+ return ageWarnings ;
180+ }
181+
143182/** Compare semver-like strings. Returns -1 if a<b, 0 if equal, 1 if a>b. */
144183function compareVersions ( a : string , b : string ) : number {
145184 const pa = a . split ( / [ . - ] / ) . map ( ( s ) => Number . parseInt ( s , 10 ) ) ;
@@ -417,7 +456,9 @@ async function checkEmbeddingConfig(
417456
418457 // Run the live probe.
419458 const probeSpinner = spinner ( ) ;
420- probeSpinner . start ( `Testing embedding endpoint ${ endpoint } (model: ${ model } )` ) ;
459+ probeSpinner . start (
460+ `Testing embedding endpoint ${ sanitizeDiagnosticEndpoint ( endpoint ) } (model: ${ sanitizeDiagnosticText ( model ) } )` ,
461+ ) ;
421462
422463 let outcome : EmbeddingProbeOutcome ;
423464 try {
@@ -431,7 +472,9 @@ async function checkEmbeddingConfig(
431472 } ) ;
432473 } catch ( error ) {
433474 probeSpinner . stop ( "Embedding probe failed unexpectedly" ) ;
434- log . error ( `Probe threw: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
475+ log . error (
476+ `Probe threw: ${ sanitizeDiagnosticText ( error instanceof Error ? error . message : String ( error ) ) } ` ,
477+ ) ;
435478 return { issues : localIssues + 1 } ;
436479 }
437480
@@ -447,11 +490,11 @@ async function checkEmbeddingConfig(
447490 log . error (
448491 `Embedding endpoint rejected credentials (${ outcome . status } ) — check api_key / env var` ,
449492 ) ;
450- if ( outcome . preview ) log . info ( ` ${ outcome . preview } ` ) ;
493+ if ( outcome . preview ) log . info ( ` ${ sanitizeDiagnosticText ( outcome . preview ) } ` ) ;
451494 return { issues : localIssues + 1 } ;
452495 case "endpoint_unsupported" :
453496 log . error ( `Embedding endpoint does not support embeddings (${ outcome . status } )` ) ;
454- if ( outcome . preview ) log . info ( ` ${ outcome . preview } ` ) ;
497+ if ( outcome . preview ) log . info ( ` ${ sanitizeDiagnosticText ( outcome . preview ) } ` ) ;
455498 log . info (
456499 " Common causes: endpoint points at a chat-completion route (should be the provider base, e.g. '.../v1'), or the provider doesn't offer an embeddings API" ,
457500 ) ;
@@ -461,19 +504,21 @@ async function checkEmbeddingConfig(
461504 return { issues : localIssues + 1 } ;
462505 case "http_error" :
463506 log . error ( `Embedding endpoint returned ${ outcome . status } ` ) ;
464- if ( outcome . preview ) log . info ( ` ${ outcome . preview } ` ) ;
507+ if ( outcome . preview ) log . info ( ` ${ sanitizeDiagnosticText ( outcome . preview ) } ` ) ;
465508 return { issues : localIssues + 1 } ;
466509 case "timeout" :
467510 log . warn (
468511 `Embedding endpoint did not respond within ${ outcome . timeoutMs } ms — check endpoint URL and network` ,
469512 ) ;
470513 return { issues : localIssues + 1 } ;
471514 case "network_error" :
472- log . error ( `Could not reach embedding endpoint: ${ outcome . message } ` ) ;
515+ log . error (
516+ `Could not reach embedding endpoint: ${ sanitizeDiagnosticText ( outcome . message ) } ` ,
517+ ) ;
473518 return { issues : localIssues + 1 } ;
474519 case "invalid_scheme" :
475520 log . error (
476- `Embedding endpoint must start with http:// or https://: ${ outcome . endpoint } ` ,
521+ `Embedding endpoint must start with http:// or https://: ${ sanitizeDiagnosticEndpoint ( outcome . endpoint ) } ` ,
477522 ) ;
478523 return { issues : localIssues + 1 } ;
479524 }
@@ -873,10 +918,7 @@ export async function runDoctor(
873918 if ( isDevPathPluginEntry ( oldEntry ) ) {
874919 pass ( `Plugin registered in ${ configName } (dev path: ${ oldEntryStr } )` ) ;
875920 } else {
876- const isPinned =
877- oldEntryStr !== PLUGIN_NAME &&
878- oldEntryStr !== PLUGIN_ENTRY_WITH_VERSION &&
879- / ^ @ c o r t e x k i t \/ o p e n c o d e - m a g i c - c o n t e x t @ \d / . test ( oldEntryStr ) ;
921+ const isPinned = isPinnedOpenCodePluginSpecifier ( oldEntryStr ) ;
880922
881923 if ( isPinned && ! options . force ) {
882924 // Warn but don't change — user intentionally pinned
@@ -967,10 +1009,7 @@ export async function runDoctor(
9671009 if ( isDevPathPluginEntry ( tuiEntry ) ) {
9681010 pass ( `TUI sidebar plugin configured (dev path: ${ tuiEntryStr } )` ) ;
9691011 } else {
970- const tuiPinned =
971- tuiEntryStr !== PLUGIN_NAME &&
972- tuiEntryStr !== PLUGIN_ENTRY_WITH_VERSION &&
973- / ^ @ c o r t e x k i t \/ o p e n c o d e - m a g i c - c o n t e x t @ \d / . test ( tuiEntryStr ) ;
1012+ const tuiPinned = isPinnedOpenCodePluginSpecifier ( tuiEntryStr ) ;
9741013 if ( tuiPinned && ! options . force ) {
9751014 warn (
9761015 `TUI plugin pinned to ${ tuiEntryStr } — use 'doctor --force' to upgrade` ,
@@ -1117,7 +1156,14 @@ export async function runDoctor(
11171156 ) ;
11181157 } else if ( cacheResult . action === "error" ) {
11191158 warn ( `Could not clear plugin cache: ${ cacheResult . error } ` ) ;
1120- log . info ( ` Manually delete: ${ cacheResult . path } ` ) ;
1159+ if ( cacheResult . clearedPaths && cacheResult . clearedPaths . length > 0 ) {
1160+ log . info ( ` Cleared roots: ${ cacheResult . clearedPaths . join ( ", " ) } ` ) ;
1161+ }
1162+ if ( cacheResult . failedPaths && cacheResult . failedPaths . length > 0 ) {
1163+ log . info ( ` Failed roots: ${ cacheResult . failedPaths . join ( ", " ) } ` ) ;
1164+ } else {
1165+ log . info ( ` Manually delete: ${ cacheResult . path } ` ) ;
1166+ }
11211167 issues ++ ;
11221168 } else {
11231169 pass ( "Plugin cache clean (no cached version found)" ) ;
@@ -1129,23 +1175,7 @@ export async function runDoctor(
11291175 // npx and the auto-update checker uses npm install, neither of which read
11301176 // bunfig.
11311177 {
1132- const ageWarnings : string [ ] = [ ] ;
1133- const npmrcPath = join ( homedir ( ) , ".npmrc" ) ;
1134- if ( existsSync ( npmrcPath ) ) {
1135- try {
1136- const npmrc = readFileSync ( npmrcPath , "utf-8" ) ;
1137- for ( const line of npmrc . split ( "\n" ) ) {
1138- const trimmed = line . trim ( ) ;
1139- if ( trimmed . startsWith ( "#" ) || trimmed . startsWith ( ";" ) ) continue ;
1140- const [ key ] = trimmed . split ( "=" ) . map ( ( s ) => s . trim ( ) ) ;
1141- if ( key === "min-release-age" || key === "before" ) {
1142- ageWarnings . push ( `~/.npmrc has '${ trimmed } '` ) ;
1143- }
1144- }
1145- } catch {
1146- // Can't read .npmrc — skip
1147- }
1148- }
1178+ const ageWarnings = collectNpmReleaseAgeWarnings ( ) ;
11491179
11501180 if ( ageWarnings . length > 0 ) {
11511181 log . warn (
0 commit comments