@@ -6,6 +6,10 @@ import {t} from '../../i18n/index.js';
66
77type SandboxModel = OdsComponents [ 'schemas' ] [ 'SandboxModel' ] ;
88type SandboxResourceProfile = OdsComponents [ 'schemas' ] [ 'SandboxResourceProfile' ] ;
9+ type SandboxState = OdsComponents [ 'schemas' ] [ 'SandboxState' ] ;
10+
11+ /** States that indicate sandbox creation has completed (success or failure) */
12+ const TERMINAL_STATES = new Set < SandboxState > ( [ 'deleted' , 'failed' , 'started' ] ) ;
913
1014/**
1115 * Command to create a new on-demand sandbox.
@@ -20,6 +24,8 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
2024 '<%= config.bin %> <%= command.id %> --realm abcd --ttl 48' ,
2125 '<%= config.bin %> <%= command.id %> --realm abcd --profile large' ,
2226 '<%= config.bin %> <%= command.id %> --realm abcd --auto-scheduled' ,
27+ '<%= config.bin %> <%= command.id %> --realm abcd --wait' ,
28+ '<%= config.bin %> <%= command.id %> --realm abcd --wait --poll-interval 15' ,
2329 '<%= config.bin %> <%= command.id %> --realm abcd --json' ,
2430 ] ;
2531
@@ -42,13 +48,31 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
4248 description : 'Enable automatic start/stop scheduling' ,
4349 default : false ,
4450 } ) ,
51+ wait : Flags . boolean ( {
52+ char : 'w' ,
53+ description : 'Wait for the sandbox to reach started or failed state before returning' ,
54+ default : false ,
55+ } ) ,
56+ 'poll-interval' : Flags . integer ( {
57+ description : 'Polling interval in seconds when using --wait' ,
58+ default : 10 ,
59+ dependsOn : [ 'wait' ] ,
60+ } ) ,
61+ timeout : Flags . integer ( {
62+ description : 'Maximum time to wait in seconds when using --wait (0 for no timeout)' ,
63+ default : 600 ,
64+ dependsOn : [ 'wait' ] ,
65+ } ) ,
4566 } ;
4667
4768 async run ( ) : Promise < SandboxModel > {
4869 const realm = this . flags . realm ;
4970 const profile = this . flags . profile as SandboxResourceProfile ;
5071 const ttl = this . flags . ttl ;
5172 const autoScheduled = this . flags [ 'auto-scheduled' ] ;
73+ const wait = this . flags . wait ;
74+ const pollInterval = this . flags [ 'poll-interval' ] ;
75+ const timeout = this . flags . timeout ;
5276
5377 this . log ( t ( 'commands.ods.create.creating' , 'Creating sandbox in realm {{realm}}...' , { realm} ) ) ;
5478 this . log ( t ( 'commands.ods.create.profile' , 'Profile: {{profile}}' , { profile} ) ) ;
@@ -74,11 +98,16 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
7498 ) ;
7599 }
76100
77- const sandbox = result . data . data ;
101+ let sandbox = result . data . data ;
78102
79103 this . log ( '' ) ;
80104 this . log ( t ( 'commands.ods.create.success' , 'Sandbox created successfully!' ) ) ;
81105
106+ if ( wait && sandbox . id ) {
107+ this . log ( '' ) ;
108+ sandbox = await this . waitForSandbox ( sandbox . id , pollInterval , timeout ) ;
109+ }
110+
82111 if ( this . jsonEnabled ( ) ) {
83112 return sandbox ;
84113 }
@@ -115,4 +144,98 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
115144
116145 ux . stdout ( ui . toString ( ) ) ;
117146 }
147+
148+ /**
149+ * Sleep for a given number of milliseconds.
150+ */
151+ private async sleep ( ms : number ) : Promise < void > {
152+ await new Promise ( ( resolve ) => {
153+ setTimeout ( resolve , ms ) ;
154+ } ) ;
155+ }
156+
157+ /**
158+ * Polls for sandbox status until it reaches a terminal state.
159+ * @param sandboxId - The sandbox ID to poll
160+ * @param pollIntervalSeconds - Interval between polls in seconds
161+ * @param timeoutSeconds - Maximum time to wait (0 for no timeout)
162+ * @returns The final sandbox state
163+ */
164+ private async waitForSandbox (
165+ sandboxId : string ,
166+ pollIntervalSeconds : number ,
167+ timeoutSeconds : number ,
168+ ) : Promise < SandboxModel > {
169+ const startTime = Date . now ( ) ;
170+ const pollIntervalMs = pollIntervalSeconds * 1000 ;
171+ const timeoutMs = timeoutSeconds * 1000 ;
172+ let lastState : SandboxState | undefined ;
173+
174+ this . log ( t ( 'commands.ods.create.waiting' , 'Waiting for sandbox to be ready...' ) ) ;
175+
176+ while ( true ) {
177+ // Check for timeout
178+ if ( timeoutSeconds > 0 && Date . now ( ) - startTime > timeoutMs ) {
179+ this . error (
180+ t ( 'commands.ods.create.timeout' , 'Timeout waiting for sandbox after {{seconds}} seconds' , {
181+ seconds : String ( timeoutSeconds ) ,
182+ } ) ,
183+ ) ;
184+ }
185+
186+ // eslint-disable-next-line no-await-in-loop
187+ const result = await this . odsClient . GET ( '/sandboxes/{sandboxId}' , {
188+ params : {
189+ path : { sandboxId} ,
190+ } ,
191+ } ) ;
192+
193+ if ( ! result . data ?. data ) {
194+ this . error (
195+ t ( 'commands.ods.create.pollError' , 'Failed to fetch sandbox status: {{message}}' , {
196+ message : result . response ?. statusText || 'Unknown error' ,
197+ } ) ,
198+ ) ;
199+ }
200+
201+ const sandbox = result . data . data ;
202+ const currentState = sandbox . state as SandboxState ;
203+
204+ // Log state changes
205+ if ( currentState !== lastState ) {
206+ const elapsed = Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ;
207+ this . log (
208+ t ( 'commands.ods.create.stateChange' , '[{{elapsed}}s] State: {{state}}' , {
209+ elapsed : String ( elapsed ) ,
210+ state : currentState || 'unknown' ,
211+ } ) ,
212+ ) ;
213+ lastState = currentState ;
214+ }
215+
216+ // Check for terminal states
217+ if ( currentState && TERMINAL_STATES . has ( currentState ) ) {
218+ switch ( currentState ) {
219+ case 'deleted' : {
220+ this . error ( t ( 'commands.ods.create.deleted' , 'Sandbox was deleted' ) ) ;
221+ break ;
222+ }
223+ case 'failed' : {
224+ this . error ( t ( 'commands.ods.create.failed' , 'Sandbox creation failed' ) ) ;
225+ break ;
226+ }
227+ case 'started' : {
228+ this . log ( '' ) ;
229+ this . log ( t ( 'commands.ods.create.ready' , 'Sandbox is now ready!' ) ) ;
230+ break ;
231+ }
232+ }
233+ return sandbox ;
234+ }
235+
236+ // Wait before next poll
237+ // eslint-disable-next-line no-await-in-loop
238+ await this . sleep ( pollIntervalMs ) ;
239+ }
240+ }
118241}
0 commit comments