@@ -14,6 +14,18 @@ type RelinkManagedNpmRootResult = {
1414 skipped : number ;
1515} ;
1616
17+ type OpenClawPeerLinkAuditIssue = {
18+ packageName : string ;
19+ packageDir : string ;
20+ reason : string ;
21+ } ;
22+
23+ type AuditManagedNpmRootResult = {
24+ checked : number ;
25+ broken : number ;
26+ issues : OpenClawPeerLinkAuditIssue [ ] ;
27+ } ;
28+
1729type OpenClawPeerLinkResult = "linked" | "skipped" | "unchanged" ;
1830
1931function readStringRecord ( value : unknown ) : Record < string , string > {
@@ -89,6 +101,63 @@ async function safeRealpath(filePath: string): Promise<string | null> {
89101 }
90102}
91103
104+ function managedPackageNameFromDir ( params : { npmRoot : string ; packageDir : string } ) : string {
105+ return path
106+ . relative ( path . join ( params . npmRoot , "node_modules" ) , params . packageDir )
107+ . split ( path . sep )
108+ . join ( "/" ) ;
109+ }
110+
111+ async function auditOpenClawPeerDependency ( params : {
112+ hostRoot : string ;
113+ npmRoot : string ;
114+ packageDir : string ;
115+ } ) : Promise < OpenClawPeerLinkAuditIssue | null > {
116+ const packageName = managedPackageNameFromDir ( {
117+ npmRoot : params . npmRoot ,
118+ packageDir : params . packageDir ,
119+ } ) ;
120+ const nodeModulesDir = path . join ( params . packageDir , "node_modules" ) ;
121+ try {
122+ const existing = await fs . lstat ( nodeModulesDir ) ;
123+ if ( ! existing . isDirectory ( ) || existing . isSymbolicLink ( ) ) {
124+ return {
125+ packageName,
126+ packageDir : params . packageDir ,
127+ reason : `${ nodeModulesDir } is not a real directory` ,
128+ } ;
129+ }
130+ } catch ( error ) {
131+ if ( ( error as NodeJS . ErrnoException ) . code === "ENOENT" ) {
132+ return {
133+ packageName,
134+ packageDir : params . packageDir ,
135+ reason : `missing ${ path . join ( nodeModulesDir , "openclaw" ) } ` ,
136+ } ;
137+ }
138+ throw error ;
139+ }
140+
141+ const linkPath = path . join ( nodeModulesDir , "openclaw" ) ;
142+ const currentTarget = await safeRealpath ( linkPath ) ;
143+ if ( ! currentTarget ) {
144+ return {
145+ packageName,
146+ packageDir : params . packageDir ,
147+ reason : `missing ${ linkPath } ` ,
148+ } ;
149+ }
150+ const expectedTarget = ( await safeRealpath ( params . hostRoot ) ) ?? params . hostRoot ;
151+ if ( currentTarget !== expectedTarget ) {
152+ return {
153+ packageName,
154+ packageDir : params . packageDir ,
155+ reason : `${ linkPath } points to ${ currentTarget } instead of ${ expectedTarget } ` ,
156+ } ;
157+ }
158+ return null ;
159+ }
160+
92161async function ensureRealNodeModulesDir ( params : {
93162 installedDir : string ;
94163 logger : PluginPeerLinkLogger ;
@@ -222,3 +291,35 @@ export async function relinkOpenClawPeerDependenciesInManagedNpmRoot(params: {
222291 }
223292 return { checked, attempted, repaired, skipped } ;
224293}
294+
295+ export async function auditOpenClawPeerDependenciesInManagedNpmRoot ( params : {
296+ npmRoot : string ;
297+ } ) : Promise < AuditManagedNpmRootResult > {
298+ const hostRoot = resolveOpenClawPackageRootSync ( {
299+ argv1 : process . argv [ 1 ] ,
300+ moduleUrl : import . meta. url ,
301+ cwd : process . cwd ( ) ,
302+ } ) ;
303+ if ( ! hostRoot ) {
304+ return { checked : 0 , broken : 0 , issues : [ ] } ;
305+ }
306+
307+ let checked = 0 ;
308+ const issues : OpenClawPeerLinkAuditIssue [ ] = [ ] ;
309+ for ( const packageDir of await listManagedNpmRootPackageDirs ( params . npmRoot ) ) {
310+ const peerDependencies = await readPackagePeerDependencies ( packageDir ) ;
311+ if ( ! Object . hasOwn ( peerDependencies , "openclaw" ) ) {
312+ continue ;
313+ }
314+ checked += 1 ;
315+ const issue = await auditOpenClawPeerDependency ( {
316+ hostRoot,
317+ npmRoot : params . npmRoot ,
318+ packageDir,
319+ } ) ;
320+ if ( issue ) {
321+ issues . push ( issue ) ;
322+ }
323+ }
324+ return { checked, broken : issues . length , issues } ;
325+ }
0 commit comments