interface CloudBoundaryInvocation { command: string args: string[] } export type CloudBoundaryResult = { handled: false } | { handled: true; code: number } const CLOUD_TARGET_COMMANDS = new Set([ 'assert', 'camera', 'claim', 'close', 'debug', 'describe', 'detox', 'do', 'film', 'find', 'get', 'logs', 'maestro', 'mode', 'network', 'perf', 'permissions', 'record', 'reset', 'screenshot', 'shell', 'state', 'storage', 'timeline', 'wait', 'what-happened', ]) function firstVerb(positional: string[]): string | null { return positional.find((value) => !value.startsWith('-')) ?? null } const CLOUD_DO_VERBS = new Set([ 'tap', 'tap-id', 'tap-text', 'tap-best', 'double-tap', 'long-press', 'type', 'key', 'key-sequence', 'scroll', 'sleep', 'settle', ]) function doChainVerbs(positional: string[]): string[] { const verbs: string[] = [] let expectingVerb = true for (const argument of positional) { if (argument === '--then') { expectingVerb = true continue } if (expectingVerb && !argument.startsWith('-')) { verbs.push(argument) expectingVerb = false } } return verbs } function supportsCloudDo(positional: string[]): boolean { const verbs = doChainVerbs(positional) if (verbs.length === 0) return true return verbs.every((verb) => CLOUD_DO_VERBS.has(verb)) } function supportsCloudRead( command: string, args: string[], positional: string[], ): boolean { if (command === 'describe' || command === 'find') return true if (command === 'state') return firstVerb(positional) === null if (command === 'get') { const verb = firstVerb(positional) return verb === 'tree' || verb === 'node' || verb === 'count' || verb === 'memory' } if (command === 'wait') { const verb = firstVerb(positional) return verb === 'ready' || verb === 'selector' } if (command === 'logs') { const verb = firstVerb(positional) return ( !args.includes('--tail') && !args.includes('-f') && verb !== 'tail' && verb !== 'clear' && verb !== 'purge' ) } if (command === 'screenshot') { return !args.some((arg) => ['--area', '--id', '--no-shell', '--shell-only', '--text', '--with-frame'].includes( arg, ), ) } if (command === 'do') return supportsCloudDo(positional) if (command === 'reset') return true return false } function refuseCloudInvocation( command: string, positional: string[], ): CloudBoundaryResult { const verb = firstVerb(positional) const name = verb ? `${command} ${verb}` : command console.error(` rnx ${name} is not available for a remote simulator`) return { handled: true, code: 1 } } export async function dispatchCloudBoundary({ command, args, }: CloudBoundaryInvocation): Promise { if ((command === 'ios' || command === 'android') && args.includes('--remote')) { return { handled: false } } if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false } const [{ parseBridgeCliArgs }, cloudClient, cloudSession] = await Promise.all([ import('./ws-bridge'), import('./cloud-client'), import('./cloud-session'), ]) const parsed = parseBridgeCliArgs(args) const positionalTarget = command === 'claim' || command === 'close' ? firstVerb(parsed.positional) : null const target: Pick = positionalTarget ? { simId: positionalTarget, simIdSource: 'flag' } : parsed let session try { session = cloudClient.resolveCloudSessionForParsed(target) } catch (error) { console.error(` ${error instanceof Error ? error.message : String(error)}`) return { handled: true, code: 1 } } if (!session) return { handled: false } if (command === 'close') { if (args.includes('--all') || args.includes('--others')) { return refuseCloudInvocation(command, parsed.positional) } try { const descriptor = cloudSession.requireCloudSessionShellUpdate() if (session.service === 'box') await cloudClient.closeCloudBox(session) else await cloudClient.closeCloudSession(session) cloudSession.publishCloudSessionToShell(null, descriptor) console.log( session.service === 'box' ? ` closed: remote box ${session.boxId}` : ` closed: remote simulator ${session.simId}`, ) return { handled: true, code: 0 } } catch (error) { console.error( ` close failed: ${error instanceof Error ? error.message : String(error)}`, ) return { handled: true, code: 1 } } } // a box relays the whole bridge vocabulary to the page hosting its // simulator, which runs the same shell the local CLI drives. so there is no // subset to police: every command below reaches it through the ordinary // bridge, which createBridgeFromParsed points at the box. claim is specific // to nano. if (session.service === 'box') { if (command === 'claim') { console.error( ` rnx ${command} is not available for a box; its simulator is held by the Box page. use rnx box stop to end the box.`, ) return { handled: true, code: 1 } } return { handled: false } } if (command === 'claim') { try { const descriptor = cloudSession.requireCloudSessionShellUpdate() const claimed = await cloudClient.claimCloudSimulator(session, { force: args.includes('--force'), }) cloudSession.publishCloudSessionToShell(claimed.session, descriptor) console.log(` claimed: ${session.simId}`) console.log(` expires: ${claimed.claim.expiresAt}`) return { handled: true, code: 0 } } catch (error) { console.error( ` claim failed: ${error instanceof Error ? error.message : String(error)}`, ) return { handled: true, code: 1 } } } // these inputs use the same perform executor as an inline chain. a CPU // simulator has focused text inputs but no browser keyboard to inspect. if ( command === 'do' && supportsCloudDo(parsed.positional) && doChainVerbs(parsed.positional).some( (verb) => verb === 'type' || verb === 'key' || verb === 'key-sequence', ) ) { const { runDoChain } = await import('./commands/do-chain') return { handled: true, code: await runDoChain(args) } } return supportsCloudRead(command, args, parsed.positional) ? { handled: false } : refuseCloudInvocation(command, parsed.positional) }