11import childProcess from 'node:child_process'
2- import { printDebug , printError } from './executionUtils.ts'
2+ import { printDebug , printError , printWarning } from './executionUtils.ts'
3+
4+ // Git operations share a single index lock, so they must not run concurrently.
5+ let gitMutex = Promise . resolve < unknown > ( undefined )
36
47interface CommandOptions {
58 cwd ?: string
@@ -12,6 +15,7 @@ interface CommandBuilder {
1215 withCurrentWorkingDirectory ( newCurrentWorkingDirectory : string ) : CommandBuilder
1316 withLogs ( ) : CommandBuilder
1417 run ( ) : string
18+ runAsync ( ) : Promise < string >
1519}
1620
1721/**
@@ -31,6 +35,7 @@ interface CommandBuilder {
3135export function command ( ...templateArguments : [ TemplateStringsArray , ...any [ ] ] ) : CommandBuilder {
3236 const [ commandName , ...commandArguments ] = parseCommandTemplateArguments ( ...templateArguments )
3337
38+ const formattedCommand = `${ commandName } ${ commandArguments . join ( ' ' ) } `
3439 let input = ''
3540 let env : Record < string , string > | undefined
3641 const extraOptions : CommandOptions = { }
@@ -57,7 +62,6 @@ export function command(...templateArguments: [TemplateStringsArray, ...any[]]):
5762 } ,
5863
5964 run ( ) : string {
60- const formattedCommand = `${ commandName } ${ commandArguments . join ( ' ' ) } `
6165 printDebug ( `Running command: ${ formattedCommand } ` )
6266
6367 const commandResult = childProcess . spawnSync ( commandName , commandArguments , {
@@ -68,16 +72,9 @@ export function command(...templateArguments: [TemplateStringsArray, ...any[]]):
6872 } )
6973
7074 if ( commandResult . status !== 0 ) {
71- const formattedStderr = commandResult . stderr ? `\n---- stderr: ----\n${ commandResult . stderr } \n----` : ''
72- const formattedStdout = commandResult . stdout ? `\n---- stdout: ----\n${ commandResult . stdout } \n----` : ''
73- const exitCause =
74- commandResult . signal !== null
75- ? ` due to signal ${ commandResult . signal } `
76- : commandResult . status !== null
77- ? ` with exit status ${ commandResult . status } `
78- : ''
79- throw new Error ( `Command failed${ exitCause } : ${ formattedCommand } ${ formattedStderr } ${ formattedStdout } ` , {
80- cause : commandResult . error ,
75+ throw buildCommandError ( {
76+ formattedCommand,
77+ ...commandResult ,
8178 } )
8279 }
8380
@@ -87,9 +84,86 @@ export function command(...templateArguments: [TemplateStringsArray, ...any[]]):
8784
8885 return commandResult . stdout
8986 } ,
87+
88+ runAsync ( ) : Promise < string > {
89+ if ( extraOptions . stdio === 'inherit' ) {
90+ printWarning (
91+ `runAsync() ignores withLogs() for command: ${ formattedCommand } (running multiple commands concurrently would produce interleaved output)`
92+ )
93+ }
94+ printDebug ( `Running command: ${ formattedCommand } ` )
95+
96+ const run = ( ) =>
97+ new Promise < string > ( ( resolve , reject ) => {
98+ const child = childProcess . spawn ( commandName , commandArguments , {
99+ env : { ...process . env , ...env } ,
100+ ...extraOptions ,
101+ stdio : 'pipe' ,
102+ } )
103+
104+ let stdout = ''
105+ let stderr = ''
106+
107+ child . stdout . on ( 'data' , ( chunk : Buffer ) => {
108+ stdout += chunk . toString ( )
109+ } )
110+ child . stderr . on ( 'data' , ( chunk : Buffer ) => {
111+ stderr += chunk . toString ( )
112+ } )
113+
114+ child . on ( 'error' , ( error ) => {
115+ reject ( buildCommandError ( { formattedCommand, status : null , signal : null , stdout, stderr, error } ) )
116+ } )
117+ child . on ( 'close' , ( status , signal ) => {
118+ if ( status !== 0 ) {
119+ reject ( buildCommandError ( { formattedCommand, status, signal, stdout, stderr } ) )
120+ } else {
121+ resolve ( stdout )
122+ }
123+ } )
124+ } )
125+
126+ if ( commandName === 'git' ) {
127+ const result = gitMutex . then ( run )
128+ gitMutex = result . then (
129+ ( ) => {
130+ // ignore result
131+ } ,
132+ ( ) => {
133+ // ignore exception
134+ }
135+ )
136+ return result
137+ }
138+
139+ return run ( )
140+ } ,
90141 }
91142}
92143
144+ function buildCommandError ( {
145+ formattedCommand,
146+ status,
147+ signal,
148+ stdout,
149+ stderr,
150+ error,
151+ } : {
152+ formattedCommand : string
153+ status : number | null
154+ signal : NodeJS . Signals | null
155+ stdout : string
156+ stderr : string
157+ error ?: Error
158+ } ) : Error {
159+ const formattedStderr = stderr ? `\n---- stderr: ----\n${ stderr } \n----` : ''
160+ const formattedStdout = stdout ? `\n---- stdout: ----\n${ stdout } \n----` : ''
161+ const exitCause = signal !== null ? ` due to signal ${ signal } ` : status !== null ? ` with exit status ${ status } ` : ''
162+ return new Error ( `Command failed${ exitCause } : ${ formattedCommand } ${ formattedStderr } ${ formattedStdout } ` , {
163+ cause : error ,
164+ } )
165+ }
166+
93167/**
94168 * Parse template values passed to the `command` template tag, and return a list of arguments to run
95169 * the command.
0 commit comments