1313 * See the License for the specific language governing permissions and
1414 * limitations under the License.
1515 */
16- import { exec , spawn , type ExecException } from 'node:child_process' ;
17- import { promisify } from 'node:util' ;
16+ import { spawn } from 'node:child_process' ;
1817import { SfError } from '@salesforce/core' ;
1918import { Messages } from '@salesforce/core' ;
2019import { type PythonVersionInfo } from './pythonChecker.js' ;
2120import { type PipPackageInfo } from './pipChecker.js' ;
2221import { type DatacodeBinaryInfo } from './datacodeBinaryChecker.js' ;
23-
24- const execAsync = promisify ( exec ) ;
22+ import { spawnAsync , type SpawnError } from './spawnHelper.js' ;
2523
2624Messages . importMessagesDirectoryFromMetaUrl ( import . meta. url ) ;
2725const messages = Messages . loadMessages ( '@salesforce/plugin-data-code-extension' , 'datacodeBinaryExecutor' ) ;
@@ -89,11 +87,9 @@ export class DatacodeBinaryExecutor {
8987 codeType : 'script' | 'function' ,
9088 packageDir : string
9189 ) : Promise < DatacodeInitExecutionResult > {
92- const command = `datacustomcode init --code-type ${ codeType } ${ packageDir } ` ;
93-
9490 try {
95- const { stdout, stderr } = await execAsync ( command , {
96- timeout : 30_000 , // 30 second timeout
91+ const { stdout, stderr } = await spawnAsync ( 'datacustomcode' , [ 'init' , '--code-type' , codeType , packageDir ] , {
92+ timeout : 30_000 ,
9793 } ) ;
9894
9995 // Parse created files from output if available
@@ -111,8 +107,8 @@ export class DatacodeBinaryExecutor {
111107 projectPath : packageDir ,
112108 } ;
113109 } catch ( error ) {
114- const execError = error as ExecException & { stderr ?: string } ;
115- const binaryOutput = execError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
110+ const spawnError = error as SpawnError ;
111+ const binaryOutput = spawnError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
116112 throw new SfError (
117113 messages . getMessage ( 'error.initExecutionFailed' , [ packageDir , binaryOutput ] ) ,
118114 'InitExecutionFailed' ,
@@ -138,30 +134,26 @@ export class DatacodeBinaryExecutor {
138134 noRequirements : boolean = false ,
139135 configFile ?: string
140136 ) : Promise < DatacodeScanExecutionResult > {
141- // Build the command with optional flags
142- let command = 'datacustomcode scan' ;
137+ const args = [ 'scan' ] ;
143138
144- // Add boolean flags FIRST (before positional argument)
145139 if ( dryRun ) {
146- command += ' --dry-run';
140+ args . push ( ' --dry-run') ;
147141 }
148142
149143 if ( noRequirements ) {
150- command += ' --no-requirements';
144+ args . push ( ' --no-requirements') ;
151145 }
152146
153147 if ( configFile ) {
154- command += ` --config " ${ configFile } "` ;
148+ args . push ( ' --config' , configFile ) ;
155149 }
156150
157- // Add entrypoint as positional argument LAST (with proper quoting for paths with spaces)
158- const configPath = config ?? 'payload/config.json' ;
159- command += ` "${ configPath } "` ;
151+ args . push ( config ?? 'payload/config.json' ) ;
160152
161153 try {
162- const { stdout, stderr } = await execAsync ( command , {
154+ const { stdout, stderr } = await spawnAsync ( 'datacustomcode' , args , {
163155 cwd : workingDir ,
164- timeout : 60_000 , // 60 second timeout (longer than init's 30 seconds)
156+ timeout : 60_000 ,
165157 } ) ;
166158
167159 // Parse scan results from output
@@ -197,8 +189,8 @@ export class DatacodeBinaryExecutor {
197189 filesScanned : filesScanned . length > 0 ? filesScanned : undefined ,
198190 } ;
199191 } catch ( error ) {
200- const execError = error as ExecException & { stderr ?: string } ;
201- const binaryOutput = execError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
192+ const spawnError = error as SpawnError ;
193+ const binaryOutput = spawnError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
202194 throw new SfError (
203195 messages . getMessage ( 'error.scanExecutionFailed' , [ workingDir , binaryOutput ] ) ,
204196 'ScanExecutionFailed' ,
@@ -216,20 +208,17 @@ export class DatacodeBinaryExecutor {
216208 * @throws SfError if execution fails
217209 */
218210 public static async executeBinaryZip ( packageDir : string , network ?: string ) : Promise < DatacodeZipExecutionResult > {
219- // Build the command with optional network flag
220- let command = 'datacustomcode zip' ;
211+ const args = [ 'zip' ] ;
221212
222- // Add network flag if provided (before positional argument)
223213 if ( network ) {
224- command += ` --network " ${ network } "` ;
214+ args . push ( ' --network' , network ) ;
225215 }
226216
227- // Add package directory as positional argument (with proper quoting for paths with spaces)
228- command += ` "${ packageDir } "` ;
217+ args . push ( packageDir ) ;
229218
230219 try {
231- const { stdout, stderr } = await execAsync ( command , {
232- timeout : 120_000 , // 120 second timeout (zipping can take time for large packages)
220+ const { stdout, stderr } = await spawnAsync ( 'datacustomcode' , args , {
221+ timeout : 120_000 ,
233222 } ) ;
234223
235224 // Parse archive path from output
@@ -264,8 +253,8 @@ export class DatacodeBinaryExecutor {
264253 archiveSize,
265254 } ;
266255 } catch ( error ) {
267- const execError = error as ExecException & { stderr ?: string } ;
268- const binaryOutput = execError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
256+ const spawnError = error as SpawnError ;
257+ const binaryOutput = spawnError . stderr ?. trim ( ) ?? ( error instanceof Error ? error . message : String ( error ) ) ;
269258 throw new SfError (
270259 messages . getMessage ( 'error.zipExecutionFailed' , [ packageDir , binaryOutput ] ) ,
271260 'ZipExecutionFailed' ,
@@ -432,23 +421,21 @@ export class DatacodeBinaryExecutor {
432421 configFile ?: string ,
433422 dependencies ?: string
434423 ) : Promise < DatacodeRunExecutionResult > {
435- // Build the command — flags before the positional argument
436- let command = 'datacustomcode run' ;
437- command += ` --sf-cli-org "${ targetOrg } "` ;
424+ const args = [ 'run' , '--sf-cli-org' , targetOrg ] ;
438425
439426 if ( configFile ) {
440- command += ` --config-file " ${ configFile } "` ;
427+ args . push ( ' --config-file' , configFile ) ;
441428 }
442429
443430 if ( dependencies ) {
444- command += ` --dependencies " ${ dependencies } "` ;
431+ args . push ( ' --dependencies' , dependencies ) ;
445432 }
446433
447- command += ` " ${ packageDir } "` ;
434+ args . push ( packageDir ) ;
448435
449436 try {
450- const { stdout, stderr } = await execAsync ( command , {
451- timeout : 300_000 , // 5 minute timeout
437+ const { stdout, stderr } = await spawnAsync ( 'datacustomcode' , args , {
438+ timeout : 300_000 ,
452439 } ) ;
453440
454441 // Parse status from output
@@ -474,8 +461,8 @@ export class DatacodeBinaryExecutor {
474461 output,
475462 } ;
476463 } catch ( error ) {
477- const execError = error as ExecException & { stderr ?: string } ;
478- const errorMessage = execError . message ?? String ( error ) ;
464+ const spawnError = error as SpawnError ;
465+ const errorMessage = spawnError . message ?? String ( error ) ;
479466
480467 if ( errorMessage . includes ( 'Authentication failed' ) || errorMessage . includes ( 'Invalid credentials' ) ) {
481468 throw new SfError (
@@ -488,7 +475,7 @@ export class DatacodeBinaryExecutor {
488475 // Surface the binary's stderr directly so any runtime error is shown as-is.
489476 // File-existence checks for entrypoint and config-file are already handled by
490477 // the CLI flag layer (exists: true), so those patterns are not matched here.
491- const binaryOutput = execError . stderr ?. trim ( ) ?? errorMessage ;
478+ const binaryOutput = spawnError . stderr ?. trim ( ) ?? errorMessage ;
492479 throw new SfError (
493480 messages . getMessage ( 'error.runExecutionFailed' , [ binaryOutput ] ) ,
494481 'RunExecutionFailed' ,
0 commit comments