22
33import { execFileSync } from "node:child_process" ;
44import { readFileSync } from "node:fs" ;
5+ import { basename } from "node:path" ;
56import { pathToFileURL } from "node:url" ;
67
78type PackageJson = {
@@ -40,7 +41,6 @@ const CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>
4041const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw" ;
4142const MAX_CALVER_DISTANCE_DAYS = 2 ;
4243const REQUIRED_PACKED_PATHS = [ "dist/control-ui/index.html" ] ;
43- const NPM_COMMAND = process . platform === "win32" ? "npm.cmd" : "npm" ;
4444
4545function normalizeRepoUrl ( value : unknown ) : string {
4646 if ( typeof value !== "string" ) {
@@ -288,15 +288,31 @@ function loadPackageJson(): PackageJson {
288288 return JSON . parse ( readFileSync ( "package.json" , "utf8" ) ) as PackageJson ;
289289}
290290
291+ function isNpmExecPath ( value : string ) : boolean {
292+ return / ^ n p m (?: - c l i ) ? (?: \. (?: c ? j s | c m d | e x e ) ) ? $ / . test ( basename ( value ) . toLowerCase ( ) ) ;
293+ }
294+
295+ export function resolveNpmCommandInvocation (
296+ params : {
297+ npmExecPath ?: string ;
298+ nodeExecPath ?: string ;
299+ platform ?: NodeJS . Platform ;
300+ } = { } ,
301+ ) : { command : string ; args : string [ ] } {
302+ const npmExecPath = params . npmExecPath ?? process . env . npm_execpath ;
303+ const nodeExecPath = params . nodeExecPath ?? process . execPath ;
304+ const npmCommand = ( params . platform ?? process . platform ) === "win32" ? "npm.cmd" : "npm" ;
305+
306+ if ( typeof npmExecPath === "string" && npmExecPath . length > 0 && isNpmExecPath ( npmExecPath ) ) {
307+ return { command : nodeExecPath , args : [ npmExecPath ] } ;
308+ }
309+
310+ return { command : npmCommand , args : [ ] } ;
311+ }
312+
291313function runNpmCommand ( args : string [ ] ) : string {
292- const npmExecPath = process . env . npm_execpath ;
293- if ( typeof npmExecPath === "string" && npmExecPath . length > 0 ) {
294- return execFileSync ( process . execPath , [ npmExecPath , ...args ] , {
295- encoding : "utf8" ,
296- stdio : [ "ignore" , "pipe" , "pipe" ] ,
297- } ) ;
298- }
299- return execFileSync ( NPM_COMMAND , args , {
314+ const invocation = resolveNpmCommandInvocation ( ) ;
315+ return execFileSync ( invocation . command , [ ...invocation . args , ...args ] , {
300316 encoding : "utf8" ,
301317 stdio : [ "ignore" , "pipe" , "pipe" ] ,
302318 } ) ;
@@ -312,16 +328,16 @@ type NpmPackResult = {
312328} ;
313329
314330type ExecFailure = Error & {
315- stderr ?: string | Buffer ;
316- stdout ?: string | Buffer ;
331+ stderr ?: string | Uint8Array ;
332+ stdout ?: string | Uint8Array ;
317333} ;
318334
319- function toTrimmedUtf8 ( value : string | Buffer | undefined ) : string {
335+ function toTrimmedUtf8 ( value : string | Uint8Array | undefined ) : string {
320336 if ( typeof value === "string" ) {
321337 return value . trim ( ) ;
322338 }
323- if ( Buffer . isBuffer ( value ) ) {
324- return value . toString ( "utf8" ) . trim ( ) ;
339+ if ( value instanceof Uint8Array ) {
340+ return new TextDecoder ( ) . decode ( value ) . trim ( ) ;
325341 }
326342 return "";
327343}
@@ -343,6 +359,32 @@ function describeExecFailure(error: unknown): string {
343359 return details . join ( " | " ) ;
344360}
345361
362+ export function parseNpmPackJsonOutput ( stdout : string ) : NpmPackResult [ ] | null {
363+ const trimmed = stdout . trim ( ) ;
364+ if ( ! trimmed ) {
365+ return null ;
366+ }
367+
368+ const candidates = [ trimmed ] ;
369+ const trailingArrayStart = trimmed . lastIndexOf ( "\n[" ) ;
370+ if ( trailingArrayStart !== - 1 ) {
371+ candidates . push ( trimmed . slice ( trailingArrayStart + 1 ) . trim ( ) ) ;
372+ }
373+
374+ for ( const candidate of candidates ) {
375+ try {
376+ const parsed = JSON . parse ( candidate ) as unknown ;
377+ if ( Array . isArray ( parsed ) ) {
378+ return parsed as NpmPackResult [ ] ;
379+ }
380+ } catch {
381+ // Try the next candidate. npm lifecycle output can prepend non-JSON logs.
382+ }
383+ }
384+
385+ return null ;
386+ }
387+
346388function collectPackedTarballErrors ( ) : string [ ] {
347389 const errors : string [ ] = [ ] ;
348390 let stdout = "" ;
@@ -356,15 +398,11 @@ function collectPackedTarballErrors(): string[] {
356398 return errors ;
357399 }
358400
359- let parsed : unknown ;
360- try {
361- parsed = JSON . parse ( stdout ) ;
362- } catch {
401+ const packResults = parseNpmPackJsonOutput ( stdout ) ;
402+ if ( ! packResults ) {
363403 errors . push ( "Failed to parse JSON output from `npm pack --json --dry-run`." ) ;
364404 return errors ;
365405 }
366-
367- const packResults = Array . isArray ( parsed ) ? ( parsed as NpmPackResult [ ] ) : [ ] ;
368406 const firstResult = packResults [ 0 ] ;
369407 if ( ! firstResult || ! Array . isArray ( firstResult . files ) ) {
370408 errors . push ( "`npm pack --json --dry-run` did not return a files list to validate." ) ;
0 commit comments