1- import { execFileSync } from "node:child_process" ;
1+ import { spawnSync } from "node:child_process" ;
22import type { Dirent } from "node:fs" ;
33import { access , readFile , readdir , stat , writeFile } from "node:fs/promises" ;
44import { homedir , hostname } from "node:os" ;
@@ -167,6 +167,14 @@ function redactJsonBuffer(raw: Buffer): Buffer {
167167 }
168168}
169169
170+ function parseJsonBuffer < T > ( raw : Buffer ) : T | null {
171+ try {
172+ return JSON . parse ( raw . toString ( "utf8" ) ) as T ;
173+ } catch {
174+ return null ;
175+ }
176+ }
177+
170178// ---------------------------------------------------------------------------
171179// Artifact collection
172180// ---------------------------------------------------------------------------
@@ -229,19 +237,109 @@ async function listFilesRecursive(directoryPath: string): Promise<string[]> {
229237 return output ;
230238}
231239
240+ function runCommand (
241+ binaryPath : string ,
242+ args : string [ ] ,
243+ ) : {
244+ binaryPath : string ;
245+ args : string [ ] ;
246+ ok : boolean ;
247+ status : number | null ;
248+ signal : NodeJS . Signals | null ;
249+ stdout : string | null ;
250+ stderr : string | null ;
251+ error : string | null ;
252+ } {
253+ const result = spawnSync ( binaryPath , args , {
254+ encoding : "utf8" ,
255+ timeout : 5000 ,
256+ } ) ;
257+
258+ const stdout = result . stdout . trim ( ) ;
259+ const stderr = result . stderr . trim ( ) ;
260+
261+ return {
262+ binaryPath,
263+ args,
264+ ok : result . status === 0 && ! result . error ,
265+ status : result . status ,
266+ signal : result . signal ,
267+ stdout : stdout . length > 0 ? stdout : null ,
268+ stderr : stderr . length > 0 ? stderr : null ,
269+ error : result . error ? String ( result . error . message ) : null ,
270+ } ;
271+ }
272+
232273function readMacOsProductVersion ( ) : string | null {
233274 if ( process . platform !== "darwin" ) {
234275 return null ;
235276 }
236277
237- try {
238- const output = execFileSync ( "sw_vers" , [ "-productVersion" ] , {
239- encoding : "utf8" ,
240- } ) . trim ( ) ;
241- return output . length > 0 ? output : null ;
242- } catch {
278+ const result = runCommand ( "/usr/bin/sw_vers" , [ "-productVersion" ] ) ;
279+ return result . ok ? result . stdout : null ;
280+ }
281+
282+ function buildMachineSummary ( runtimeConfig : DesktopRuntimeConfig ) : object {
283+ const rosettaCheck =
284+ process . platform === "darwin"
285+ ? runCommand ( "/usr/sbin/sysctl" , [ "-n" , "sysctl.proc_translated" ] )
286+ : null ;
287+
288+ const unameMachine =
289+ process . platform === "darwin" ? runCommand ( "/usr/bin/uname" , [ "-m" ] ) : null ;
290+
291+ return {
292+ buildInfo : runtimeConfig . buildInfo ,
293+ hostName : hostname ( ) ,
294+ platform : process . platform ,
295+ arch : process . arch ,
296+ osVersion : readMacOsProductVersion ( ) ,
297+ processVersions : process . versions ,
298+ executablePath : app . getPath ( "exe" ) ,
299+ processExecPath : process . execPath ,
300+ resourcesPath : process . resourcesPath ,
301+ isPackaged : app . isPackaged ,
302+ rosetta : rosettaCheck
303+ ? {
304+ translated :
305+ rosettaCheck . ok && rosettaCheck . stdout !== null
306+ ? rosettaCheck . stdout === "1"
307+ : null ,
308+ command : rosettaCheck ,
309+ }
310+ : null ,
311+ uname : unameMachine ,
312+ appPaths : {
313+ userData : app . getPath ( "userData" ) ,
314+ logs : app . getPath ( "logs" ) ,
315+ crashDumps : app . getPath ( "crashDumps" ) ,
316+ nexuHome : runtimeConfig . paths . nexuHome ,
317+ } ,
318+ } ;
319+ }
320+
321+ function buildAppSigningSummary ( ) : object | null {
322+ if ( process . platform !== "darwin" ) {
243323 return null ;
244324 }
325+
326+ const appExecutablePath = app . getPath ( "exe" ) ;
327+
328+ return {
329+ executablePath : appExecutablePath ,
330+ codesign : runCommand ( "/usr/bin/codesign" , [
331+ "-dv" ,
332+ "--verbose=4" ,
333+ appExecutablePath ,
334+ ] ) ,
335+ spctl : runCommand ( "/usr/sbin/spctl" , [
336+ "--assess" ,
337+ "--type" ,
338+ "execute" ,
339+ "-vv" ,
340+ appExecutablePath ,
341+ ] ) ,
342+ } ;
245343}
246344
247345function getTimestampSlug ( ) : string {
@@ -265,6 +363,7 @@ async function collectArtifacts(
265363 const included : string [ ] = [ ] ;
266364 const missing : string [ ] = [ ] ;
267365 const warnings : string [ ] = [ ] ;
366+ let desktopDiagnosticsSummary : unknown = null ;
268367
269368 const additionalArtifacts = {
270369 startupHealth : null as CollectedFileMetadata | null ,
@@ -305,14 +404,59 @@ async function collectArtifacts(
305404 }
306405
307406 // Desktop diagnostics snapshot
308- await addFile (
407+ const desktopDiagnosticsMetadata = await addFile (
309408 "diagnostics/desktop-diagnostics.json" ,
310409 getDesktopDiagnosticsFilePath ( ) ,
311410 {
312411 redact : true ,
313412 } ,
314413 ) ;
315414
415+ if ( desktopDiagnosticsMetadata ) {
416+ const desktopDiagnosticsFile = await tryReadFile (
417+ getDesktopDiagnosticsFilePath ( ) ,
418+ ) ;
419+ const parsedDiagnostics = desktopDiagnosticsFile
420+ ? parseJsonBuffer < {
421+ startupProbe ?: {
422+ preloadSeen ?: boolean ;
423+ rendererSeen ?: boolean ;
424+ entries ?: Array < {
425+ source ?: string ;
426+ stage ?: string ;
427+ status ?: string ;
428+ detail ?: string | null ;
429+ at ?: string ;
430+ } > ;
431+ } ;
432+ renderer ?: {
433+ didFinishLoad ?: boolean ;
434+ lastError ?: string | null ;
435+ processGone ?: {
436+ seen ?: boolean ;
437+ reason ?: string | null ;
438+ exitCode ?: number | null ;
439+ at ?: string | null ;
440+ } ;
441+ } ;
442+ coldStart ?: {
443+ status ?: string ;
444+ step ?: string | null ;
445+ error ?: string | null ;
446+ } ;
447+ } > ( desktopDiagnosticsFile . data )
448+ : null ;
449+
450+ if ( parsedDiagnostics ) {
451+ desktopDiagnosticsSummary = {
452+ sourceArchivePath : desktopDiagnosticsMetadata . archivePath ,
453+ coldStart : parsedDiagnostics . coldStart ?? null ,
454+ renderer : parsedDiagnostics . renderer ?? null ,
455+ startupProbe : parsedDiagnostics . startupProbe ?? null ,
456+ } ;
457+ }
458+ }
459+
316460 // Main process logs
317461 const logsDir = resolve ( app . getPath ( "userData" ) , "logs" ) ;
318462 await addFile ( "logs/cold-start.log" , resolve ( logsDir , "cold-start.log" ) , {
@@ -440,6 +584,8 @@ async function collectArtifacts(
440584
441585 // Environment summary (safe metadata only)
442586 const envSummary = buildEnvironmentSummary ( runtimeConfig ) ;
587+ const machineSummary = buildMachineSummary ( runtimeConfig ) ;
588+ const appSigningSummary = buildAppSigningSummary ( ) ;
443589 const now = new Date ( ) ;
444590 entries . push ( {
445591 name : `${ archiveRoot } /summary/environment-summary.json` ,
@@ -448,6 +594,37 @@ async function collectArtifacts(
448594 } ) ;
449595 included . push ( "summary/environment-summary.json" ) ;
450596
597+ entries . push ( {
598+ name : `${ archiveRoot } /summary/machine-info.json` ,
599+ data : Buffer . from ( `${ JSON . stringify ( machineSummary , null , 2 ) } \n` , "utf8" ) ,
600+ modTime : now ,
601+ } ) ;
602+ included . push ( "summary/machine-info.json" ) ;
603+
604+ if ( appSigningSummary ) {
605+ entries . push ( {
606+ name : `${ archiveRoot } /summary/app-signing.json` ,
607+ data : Buffer . from (
608+ `${ JSON . stringify ( appSigningSummary , null , 2 ) } \n` ,
609+ "utf8" ,
610+ ) ,
611+ modTime : now ,
612+ } ) ;
613+ included . push ( "summary/app-signing.json" ) ;
614+ }
615+
616+ if ( desktopDiagnosticsSummary ) {
617+ entries . push ( {
618+ name : `${ archiveRoot } /summary/startup-probe-summary.json` ,
619+ data : Buffer . from (
620+ `${ JSON . stringify ( desktopDiagnosticsSummary , null , 2 ) } \n` ,
621+ "utf8" ,
622+ ) ,
623+ modTime : now ,
624+ } ) ;
625+ included . push ( "summary/startup-probe-summary.json" ) ;
626+ }
627+
451628 const extraArtifactsSummary = {
452629 startupHealth : additionalArtifacts . startupHealth ,
453630 openclawLogs : additionalArtifacts . openclawLogs ,
@@ -465,6 +642,12 @@ async function collectArtifacts(
465642 } ) ;
466643 included . push ( "summary/additional-artifacts.json" ) ;
467644
645+ if ( missing . length > 0 ) {
646+ warnings . push ( `${ missing . length } file(s) were not found and were skipped.` ) ;
647+ }
648+
649+ included . push ( "summary/manifest.json" ) ;
650+
468651 // Manifest
469652 const manifest = {
470653 exportedAt : now . toISOString ( ) ,
@@ -482,10 +665,6 @@ async function collectArtifacts(
482665 modTime : now ,
483666 } ) ;
484667
485- if ( missing . length > 0 ) {
486- warnings . push ( `${ missing . length } file(s) were not found and were skipped.` ) ;
487- }
488-
489668 return { entries, warnings } ;
490669}
491670
0 commit comments