@@ -5,6 +5,7 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce";
55import { note } from "../../packages/terminal-core/src/note.js" ;
66import { formatCliCommand } from "../cli/command-format.js" ;
77import type { OpenClawConfig } from "../config/types.openclaw.js" ;
8+ import type { HealthFinding , HealthRepairEffect } from "../flows/health-checks.js" ;
89import { saveJsonFile } from "../infra/json-file.js" ;
910import { tryReadJsonSync } from "../infra/json-files.js" ;
1011import type { BundledPluginSource } from "../plugins/bundled-sources.js" ;
@@ -18,6 +19,7 @@ import { hasRetainedManagedNpmInstallMarker } from "../plugins/managed-npm-reten
1819import { listManagedPluginNpmRootsSync } from "../plugins/npm-project-roots.js" ;
1920import {
2021 auditOpenClawPeerDependenciesInManagedNpmRoot ,
22+ type OpenClawPeerLinkAuditIssue ,
2123 relinkOpenClawPeerDependenciesInManagedNpmRoot ,
2224} from "../plugins/plugin-peer-link.js" ;
2325import { refreshPluginRegistry } from "../plugins/plugin-registry.js" ;
@@ -34,6 +36,8 @@ import {
3436 type PluginRegistryInstallMigrationParams ,
3537} from "./doctor/shared/plugin-registry-migration.js" ;
3638
39+ const PLUGIN_REGISTRY_CHECK_ID = "core/doctor/plugin-registry" ;
40+
3741type PluginRegistryDoctorRepairParams = Omit < PluginRegistryInstallMigrationParams , "config" > &
3842 InstalledPluginIndexRecordStoreOptions & {
3943 config : OpenClawConfig ;
@@ -53,6 +57,31 @@ type PluginRegistryDoctorNoteLogger = {
5357 warn : ( message : string ) => void ;
5458} ;
5559
60+ export type PluginRegistryHealthIssue =
61+ | {
62+ kind : "registry-missing-or-stale" ;
63+ path : string ;
64+ }
65+ | {
66+ kind : "stale-managed-npm-bundled-plugin" ;
67+ pluginId : string ;
68+ packageName : string ;
69+ packageDir : string ;
70+ npmRoot : string ;
71+ version ?: string ;
72+ }
73+ | {
74+ kind : "stale-local-bundled-plugin-install-record" ;
75+ pluginId : string ;
76+ stalePath : string ;
77+ }
78+ | {
79+ kind : "managed-npm-openclaw-peer-link" ;
80+ packageName : string ;
81+ packageDir : string ;
82+ reason : string ;
83+ } ;
84+
5685function readJsonObject ( filePath : string ) : Record < string , unknown > | null {
5786 const parsed = tryReadJsonSync ( filePath ) ;
5887 return isRecord ( parsed ) ? parsed : null ;
@@ -390,6 +419,137 @@ async function loadInstallRecordsWithoutPluginIds(
390419 return records ;
391420}
392421
422+ async function listManagedNpmOpenClawPeerLinkIssues (
423+ params : PluginRegistryDoctorRepairParams ,
424+ ) : Promise < OpenClawPeerLinkAuditIssue [ ] > {
425+ const audits = await Promise . all (
426+ listManagedPluginNpmRoots ( params ) . map ( ( npmRoot ) =>
427+ auditOpenClawPeerDependenciesInManagedNpmRoot ( { npmRoot } ) ,
428+ ) ,
429+ ) ;
430+ return audits . flatMap ( ( audit ) => audit . issues ) ;
431+ }
432+
433+ export async function detectPluginRegistryHealthIssues (
434+ params : PluginRegistryDoctorRepairParams ,
435+ ) : Promise < PluginRegistryHealthIssue [ ] > {
436+ const preflight = preflightPluginRegistryInstallMigration ( params ) ;
437+ const issues : PluginRegistryHealthIssue [ ] = [ ] ;
438+ if ( preflight . action === "migrate" ) {
439+ issues . push ( {
440+ kind : "registry-missing-or-stale" ,
441+ path : preflight . filePath ,
442+ } ) ;
443+ }
444+ for ( const plugin of listStaleManagedNpmBundledPlugins ( params ) ) {
445+ issues . push ( {
446+ kind : "stale-managed-npm-bundled-plugin" ,
447+ pluginId : plugin . pluginId ,
448+ packageName : plugin . packageName ,
449+ packageDir : plugin . packageDir ,
450+ npmRoot : plugin . npmRoot ,
451+ ...( plugin . version ? { version : plugin . version } : { } ) ,
452+ } ) ;
453+ }
454+ for ( const record of await listStaleLocalBundledPluginInstallRecordShadows ( params ) ) {
455+ issues . push ( {
456+ kind : "stale-local-bundled-plugin-install-record" ,
457+ pluginId : record . pluginId ,
458+ stalePath : record . stalePath ,
459+ } ) ;
460+ }
461+ for ( const issue of await listManagedNpmOpenClawPeerLinkIssues ( params ) ) {
462+ issues . push ( {
463+ kind : "managed-npm-openclaw-peer-link" ,
464+ packageName : issue . packageName ,
465+ packageDir : issue . packageDir ,
466+ reason : issue . reason ,
467+ } ) ;
468+ }
469+ return issues ;
470+ }
471+
472+ export function pluginRegistryIssueToHealthFinding (
473+ issue : PluginRegistryHealthIssue ,
474+ ) : HealthFinding {
475+ switch ( issue . kind ) {
476+ case "registry-missing-or-stale" :
477+ return {
478+ checkId : PLUGIN_REGISTRY_CHECK_ID ,
479+ severity : "warning" ,
480+ message : "Persisted plugin registry is missing or stale." ,
481+ path : issue . path ,
482+ fixHint : "Run `openclaw doctor --fix` to rebuild the plugin registry from enabled plugins." ,
483+ } ;
484+ case "stale-managed-npm-bundled-plugin" :
485+ return {
486+ checkId : PLUGIN_REGISTRY_CHECK_ID ,
487+ severity : "warning" ,
488+ message : `Managed npm package ${ issue . packageName } ${
489+ issue . version ? `@${ issue . version } ` : ""
490+ } shadows bundled plugin ${ issue . pluginId } .`,
491+ path : issue . packageDir ,
492+ target : issue . pluginId ,
493+ fixHint :
494+ "Run `openclaw doctor --fix` to remove stale managed npm packages and rebuild the plugin registry." ,
495+ } ;
496+ case "stale-local-bundled-plugin-install-record" :
497+ return {
498+ checkId : PLUGIN_REGISTRY_CHECK_ID ,
499+ severity : "warning" ,
500+ message : `Local install record for bundled plugin ${ issue . pluginId } points at a stale path.` ,
501+ path : issue . stalePath ,
502+ target : issue . pluginId ,
503+ fixHint :
504+ "Run `openclaw doctor --fix` to remove stale local install records and rebuild the plugin registry." ,
505+ } ;
506+ case "managed-npm-openclaw-peer-link" :
507+ return {
508+ checkId : PLUGIN_REGISTRY_CHECK_ID ,
509+ severity : "warning" ,
510+ message : `Managed npm package ${ issue . packageName } has a broken OpenClaw peer link: ${ issue . reason } .` ,
511+ path : issue . packageDir ,
512+ target : issue . packageName ,
513+ fixHint : "Run `openclaw doctor --fix` to relink managed npm plugin packages." ,
514+ } ;
515+ }
516+ }
517+
518+ export function pluginRegistryIssueToRepairEffect (
519+ issue : PluginRegistryHealthIssue ,
520+ ) : HealthRepairEffect {
521+ switch ( issue . kind ) {
522+ case "registry-missing-or-stale" :
523+ return {
524+ kind : "state" ,
525+ action : "would-rebuild-plugin-registry" ,
526+ target : issue . path ,
527+ dryRunSafe : false ,
528+ } ;
529+ case "stale-managed-npm-bundled-plugin" :
530+ return {
531+ kind : "package" ,
532+ action : "would-remove-stale-managed-npm-bundled-plugin" ,
533+ target : issue . packageDir ,
534+ dryRunSafe : false ,
535+ } ;
536+ case "stale-local-bundled-plugin-install-record" :
537+ return {
538+ kind : "state" ,
539+ action : "would-remove-stale-local-bundled-plugin-install-record" ,
540+ target : issue . pluginId ,
541+ dryRunSafe : false ,
542+ } ;
543+ case "managed-npm-openclaw-peer-link" :
544+ return {
545+ kind : "package" ,
546+ action : "would-relink-managed-npm-openclaw-peer" ,
547+ target : issue . packageDir ,
548+ dryRunSafe : false ,
549+ } ;
550+ }
551+ }
552+
393553/**
394554 * Runs plugin registry doctor repairs and refreshes the persisted plugin index when needed.
395555 *
0 commit comments