1414 */
1515
1616import { existsSync as nodeExistsSync } from 'fs' ;
17- import { spawn } from 'node:child_process' ;
17+ import { ChildProcess , spawn } from 'node:child_process' ;
1818import { Stats } from 'node:fs' ;
1919import { stat } from 'node:fs/promises' ;
20+ import { createServer } from 'node:net' ;
2021
2122/**
2223 * An error thrown when a command fails to execute.
2324 */
2425export class CommandError extends Error {
2526 constructor (
2627 message : string ,
27- public readonly stdout : string ,
28- public readonly stderr : string ,
28+ public readonly logs : string [ ] ,
2929 public readonly code : number | null ,
3030 ) {
3131 super ( message ) ;
@@ -67,15 +67,39 @@ export interface Host {
6767 cwd ?: string ;
6868 env ?: Record < string , string > ;
6969 } ,
70- ) : Promise < { stdout : string ; stderr : string } > ;
70+ ) : Promise < { logs : string [ ] } > ;
71+
72+ /**
73+ * Spawns a long-running child process and returns the `ChildProcess` object.
74+ * @param command The command to run.
75+ * @param args The arguments to pass to the command.
76+ * @param options Options for the child process.
77+ * @returns The spawned `ChildProcess` instance.
78+ */
79+ spawn (
80+ command : string ,
81+ args : readonly string [ ] ,
82+ options ?: {
83+ stdio ?: 'pipe' | 'ignore' ;
84+ cwd ?: string ;
85+ env ?: Record < string , string > ;
86+ } ,
87+ ) : ChildProcess ;
88+
89+ /**
90+ * Finds an available TCP port on the system.
91+ */
92+ getAvailablePort ( ) : Promise < number > ;
7193}
7294
7395/**
7496 * A concrete implementation of the `Host` interface that runs on a local workspace.
7597 */
7698export const LocalWorkspaceHost : Host = {
7799 stat,
100+
78101 existsSync : nodeExistsSync ,
102+
79103 runCommand : async (
80104 command : string ,
81105 args : readonly string [ ] ,
@@ -85,7 +109,7 @@ export const LocalWorkspaceHost: Host = {
85109 cwd ?: string ;
86110 env ?: Record < string , string > ;
87111 } = { } ,
88- ) : Promise < { stdout : string ; stderr : string } > => {
112+ ) : Promise < { logs : string [ ] } > => {
89113 const signal = options . timeout ? AbortSignal . timeout ( options . timeout ) : undefined ;
90114
91115 return new Promise ( ( resolve , reject ) => {
@@ -100,30 +124,74 @@ export const LocalWorkspaceHost: Host = {
100124 } ,
101125 } ) ;
102126
103- let stdout = '' ;
104- childProcess . stdout ?. on ( 'data' , ( data ) => ( stdout += data . toString ( ) ) ) ;
105-
106- let stderr = '' ;
107- childProcess . stderr ?. on ( 'data' , ( data ) => ( stderr += data . toString ( ) ) ) ;
127+ const logs : string [ ] = [ ] ;
128+ childProcess . stdout ?. on ( 'data' , ( data ) => logs . push ( data . toString ( ) ) ) ;
129+ childProcess . stderr ?. on ( 'data' , ( data ) => logs . push ( data . toString ( ) ) ) ;
108130
109131 childProcess . on ( 'close' , ( code ) => {
110132 if ( code === 0 ) {
111- resolve ( { stdout , stderr } ) ;
133+ resolve ( { logs } ) ;
112134 } else {
113135 const message = `Process exited with code ${ code } .` ;
114- reject ( new CommandError ( message , stdout , stderr , code ) ) ;
136+ reject ( new CommandError ( message , logs , code ) ) ;
115137 }
116138 } ) ;
117139
118140 childProcess . on ( 'error' , ( err ) => {
119141 if ( err . name === 'AbortError' ) {
120142 const message = `Process timed out.` ;
121- reject ( new CommandError ( message , stdout , stderr , null ) ) ;
143+ reject ( new CommandError ( message , logs , null ) ) ;
122144
123145 return ;
124146 }
125147 const message = `Process failed with error: ${ err . message } ` ;
126- reject ( new CommandError ( message , stdout , stderr , null ) ) ;
148+ reject ( new CommandError ( message , logs , null ) ) ;
149+ } ) ;
150+ } ) ;
151+ } ,
152+
153+ spawn (
154+ command : string ,
155+ args : readonly string [ ] ,
156+ options : {
157+ stdio ?: 'pipe' | 'ignore' ;
158+ cwd ?: string ;
159+ env ?: Record < string , string > ;
160+ } = { } ,
161+ ) : ChildProcess {
162+ return spawn ( command , args , {
163+ shell : false ,
164+ stdio : options . stdio ?? 'pipe' ,
165+ cwd : options . cwd ,
166+ env : {
167+ ...process . env ,
168+ ...options . env ,
169+ } ,
170+ } ) ;
171+ } ,
172+
173+ getAvailablePort ( ) : Promise < number > {
174+ return new Promise ( ( resolve , reject ) => {
175+ // Create a new temporary server from Node's net library.
176+ const server = createServer ( ) ;
177+
178+ server . once ( 'error' , ( err : unknown ) => {
179+ reject ( err ) ;
180+ } ) ;
181+
182+ // Listen on port 0 to let the OS assign an available port.
183+ server . listen ( 0 , ( ) => {
184+ const address = server . address ( ) ;
185+
186+ // Ensure address is an object with a port property.
187+ if ( address && typeof address === 'object' ) {
188+ const port = address . port ;
189+
190+ server . close ( ) ;
191+ resolve ( port ) ;
192+ } else {
193+ reject ( new Error ( 'Unable to retrieve address information from server.' ) ) ;
194+ }
127195 } ) ;
128196 } ) ;
129197 } ,
0 commit comments