1- import { spawn , type ChildProcessWithoutNullStreams } from "node:child_process" ;
1+ import { spawn , type ChildProcess } from "node:child_process" ;
22import { existsSync , readdirSync , statSync } from "node:fs" ;
33import path from "node:path" ;
44import { fileURLToPath } from "node:url" ;
@@ -22,7 +22,7 @@ interface RunTaskRejected {
2222type RunTaskResult = RunTaskStarted | RunTaskRejected ;
2323
2424let mainWindow : BrowserWindow | undefined ;
25- let currentTask : ChildProcessWithoutNullStreams | undefined ;
25+ let currentTask : ChildProcess | undefined ;
2626let currentRepo : string | undefined ;
2727
2828const __filename = fileURLToPath ( import . meta. url ) ;
@@ -66,7 +66,7 @@ function registerIpc(): void {
6666 ipcMain . handle ( "task:run" , ( _event , input : RunTaskInput ) : RunTaskResult => {
6767 if ( currentTask ) return { error : "A task is already running." } ;
6868 const repo = input . repo . trim ( ) ;
69- const task = input . task . trim ( ) ;
69+ const task = normalizeTaskForCli ( input . task ) ;
7070 if ( ! repo ) return { error : "Select a repository first." } ;
7171 if ( ! task ) return { error : "Enter a task first." } ;
7272 if ( ! existsSync ( repo ) ) return { error : `Repository does not exist: ${ repo } ` } ;
@@ -76,23 +76,36 @@ function registerIpc(): void {
7676 currentRepo = repo ;
7777 currentTask = spawn ( command , args , {
7878 cwd : repo ,
79- shell : false ,
79+ env : process . env ,
80+ shell : process . platform === "win32" ,
8081 windowsHide : true
8182 } ) ;
8283
83- currentTask . stdout . on ( "data" , ( chunk : Buffer ) => {
84+ mainWindow ?. webContents . send ( "task:output" , { stream : "system" , text : `Starting: ${ formatCommand ( command , args ) } \n` } ) ;
85+
86+ currentTask . stdout ?. on ( "data" , ( chunk : Buffer ) => {
8487 mainWindow ?. webContents . send ( "task:output" , { stream : "stdout" , text : chunk . toString ( "utf8" ) } ) ;
8588 } ) ;
8689
87- currentTask . stderr . on ( "data" , ( chunk : Buffer ) => {
90+ currentTask . stderr ? .on ( "data" , ( chunk : Buffer ) => {
8891 mainWindow ?. webContents . send ( "task:output" , { stream : "stderr" , text : chunk . toString ( "utf8" ) } ) ;
8992 } ) ;
9093
94+ let finished = false ;
9195 currentTask . once ( "error" , ( error ) => {
96+ finished = true ;
97+ currentTask = undefined ;
9298 mainWindow ?. webContents . send ( "task:output" , { stream : "stderr" , text : `${ error . message } \n` } ) ;
99+ mainWindow ?. webContents . send ( "task:exit" , {
100+ code : null ,
101+ signal : "spawn-error" ,
102+ error : error . message
103+ } ) ;
93104 } ) ;
94105
95106 currentTask . once ( "close" , ( code , signal ) => {
107+ if ( finished ) return ;
108+ finished = true ;
96109 const reportPath = currentRepo ? findLatestReport ( currentRepo ) : undefined ;
97110 currentTask = undefined ;
98111 mainWindow ?. webContents . send ( "task:exit" , {
@@ -140,6 +153,18 @@ function findLatestReport(repo: string): string | undefined {
140153 return candidates [ 0 ] ?. file ;
141154}
142155
156+ function formatCommand ( command : string , args : string [ ] ) : string {
157+ return [ command , ...args ] . map ( quoteArg ) . join ( " " ) ;
158+ }
159+
160+ function normalizeTaskForCli ( task : string ) : string {
161+ return task . trim ( ) . replace ( / \s + / gu, " " ) ;
162+ }
163+
164+ function quoteArg ( value : string ) : string {
165+ return / [ \s " ] / u. test ( value ) ? `"${ value . replaceAll ( '"' , '\\"' ) . replaceAll ( "\n" , "\\n" ) } "` : value ;
166+ }
167+
143168registerIpc ( ) ;
144169
145170app
0 commit comments