11import fs from "node:fs/promises" ;
22import path from "node:path" ;
3+ import { runCommandWithTimeout } from "../process/exec.js" ;
34import type { NpmSpecResolution } from "./install-source-utils.js" ;
45import type { ParsedRegistryNpmSpec } from "./npm-registry-spec.js" ;
6+ import { createSafeNpmInstallEnv } from "./safe-package-install.js" ;
57
68type ManagedNpmRootManifest = {
79 private ?: boolean ;
@@ -21,6 +23,12 @@ type ManagedNpmRootLockfile = {
2123 [ key : string ] : unknown ;
2224} ;
2325
26+ type ManagedNpmRootLogger = {
27+ warn ?: ( message : string ) => void ;
28+ } ;
29+
30+ type ManagedNpmRootRunCommand = typeof runCommandWithTimeout ;
31+
2432function isRecord ( value : unknown ) : value is Record < string , unknown > {
2533 return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
2634}
@@ -83,10 +91,105 @@ export async function upsertManagedNpmRootDependency(params: {
8391
8492export async function repairManagedNpmRootOpenClawPeer ( params : {
8593 npmRoot : string ;
94+ timeoutMs ?: number ;
95+ logger ?: ManagedNpmRootLogger ;
96+ runCommand ?: ManagedNpmRootRunCommand ;
8697} ) : Promise < boolean > {
87- let changed = false ;
88-
8998 await fs . mkdir ( params . npmRoot , { recursive : true } ) ;
99+
100+ const manifestPath = path . join ( params . npmRoot , "package.json" ) ;
101+ const manifest = await readManagedNpmRootManifest ( manifestPath ) ;
102+ const dependencies = readDependencyRecord ( manifest . dependencies ) ;
103+ const hasManifestDependency = "openclaw" in dependencies ;
104+ const hasLockDependency = await managedNpmRootLockfileHasOpenClawPeer ( params . npmRoot ) ;
105+ const hasPackageDir = await pathExists ( path . join ( params . npmRoot , "node_modules" , "openclaw" ) ) ;
106+ if ( ! hasManifestDependency && ! hasLockDependency && ! hasPackageDir ) {
107+ return false ;
108+ }
109+
110+ const command = params . runCommand ?? runCommandWithTimeout ;
111+ const npmArgs = hasManifestDependency
112+ ? [
113+ "npm" ,
114+ "uninstall" ,
115+ "--loglevel=error" ,
116+ "--ignore-scripts" ,
117+ "--no-audit" ,
118+ "--no-fund" ,
119+ "--prefix" ,
120+ "." ,
121+ "openclaw" ,
122+ ]
123+ : [
124+ "npm" ,
125+ "prune" ,
126+ "--loglevel=error" ,
127+ "--ignore-scripts" ,
128+ "--no-audit" ,
129+ "--no-fund" ,
130+ "--prefix" ,
131+ "." ,
132+ ] ;
133+ try {
134+ const result = await command ( npmArgs , {
135+ cwd : params . npmRoot ,
136+ timeoutMs : Math . max ( params . timeoutMs ?? 300_000 , 300_000 ) ,
137+ env : createSafeNpmInstallEnv ( process . env , { packageLock : true , quiet : true } ) ,
138+ } ) ;
139+ if ( result . code !== 0 ) {
140+ params . logger ?. warn ?.(
141+ `npm ${ hasManifestDependency ? "uninstall openclaw" : "prune" } failed while repairing managed npm root; falling back to direct cleanup: ${ result . stderr . trim ( ) || result . stdout . trim ( ) } ` ,
142+ ) ;
143+ }
144+ } catch ( error ) {
145+ params . logger ?. warn ?.(
146+ `npm ${ hasManifestDependency ? "uninstall openclaw" : "prune" } failed while repairing managed npm root; falling back to direct cleanup: ${ String ( error ) } ` ,
147+ ) ;
148+ }
149+
150+ await scrubManagedNpmRootOpenClawPeer ( { npmRoot : params . npmRoot } ) ;
151+ return true ;
152+ }
153+
154+ async function managedNpmRootLockfileHasOpenClawPeer ( npmRoot : string ) : Promise < boolean > {
155+ const lockPath = path . join ( npmRoot , "package-lock.json" ) ;
156+ try {
157+ const parsed = JSON . parse ( await fs . readFile ( lockPath , "utf8" ) ) as ManagedNpmRootLockfile ;
158+ if ( isRecord ( parsed . packages ) ) {
159+ const rootPackage = parsed . packages [ "" ] ;
160+ if (
161+ isRecord ( rootPackage ) &&
162+ isRecord ( rootPackage . dependencies ) &&
163+ "openclaw" in rootPackage . dependencies
164+ ) {
165+ return true ;
166+ }
167+ if ( "node_modules/openclaw" in parsed . packages ) {
168+ return true ;
169+ }
170+ }
171+ return isRecord ( parsed . dependencies ) && "openclaw" in parsed . dependencies ;
172+ } catch ( err ) {
173+ if ( ( err as NodeJS . ErrnoException ) . code === "ENOENT" ) {
174+ return false ;
175+ }
176+ throw err ;
177+ }
178+ }
179+
180+ async function pathExists ( filePath : string ) : Promise < boolean > {
181+ return await fs
182+ . lstat ( filePath )
183+ . then ( ( ) => true )
184+ . catch ( ( err : NodeJS . ErrnoException ) => {
185+ if ( err . code === "ENOENT" ) {
186+ return false ;
187+ }
188+ throw err ;
189+ } ) ;
190+ }
191+
192+ async function scrubManagedNpmRootOpenClawPeer ( params : { npmRoot : string } ) : Promise < void > {
90193 const manifestPath = path . join ( params . npmRoot , "package.json" ) ;
91194 const manifest = await readManagedNpmRootManifest ( manifestPath ) ;
92195 const dependencies = readDependencyRecord ( manifest . dependencies ) ;
@@ -97,7 +200,6 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
97200 `${ JSON . stringify ( { ...manifest , private : true , dependencies : nextDependencies } , null , 2 ) } \n` ,
98201 "utf8" ,
99202 ) ;
100- changed = true ;
101203 }
102204
103205 const lockPath = path . join ( params . npmRoot , "package-lock.json" ) ;
@@ -127,7 +229,6 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
127229 }
128230 if ( lockChanged ) {
129231 await fs . writeFile ( lockPath , `${ JSON . stringify ( parsed , null , 2 ) } \n` , "utf8" ) ;
130- changed = true ;
131232 }
132233 } catch ( err ) {
133234 if ( ( err as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
@@ -136,21 +237,12 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
136237 }
137238
138239 const openclawPackageDir = path . join ( params . npmRoot , "node_modules" , "openclaw" ) ;
139- const openclawPackageDirExists = await fs
140- . lstat ( openclawPackageDir )
141- . then ( ( ) => true )
142- . catch ( ( err : NodeJS . ErrnoException ) => {
143- if ( err . code === "ENOENT" ) {
144- return false ;
145- }
146- throw err ;
147- } ) ;
148- if ( openclawPackageDirExists ) {
240+ if ( await pathExists ( openclawPackageDir ) ) {
149241 await fs . rm ( openclawPackageDir , { recursive : true , force : true } ) ;
150- changed = true ;
151242 }
152-
153- return changed ;
243+ await fs . rm ( path . join ( params . npmRoot , "node_modules" , ".package-lock.json" ) , {
244+ force : true ,
245+ } ) ;
154246}
155247
156248export async function readManagedNpmRootInstalledDependency ( params : {
0 commit comments