/** * Client configuration wizard (TUI) with auto-discovery and multi-server support */ import { Box, render, Text } from 'ink'; import TextInput from 'ink-text-input'; import React, { useState } from 'react'; import { TCPClient } from '../protocol/socket.js'; import { MessageType } from '../protocol/types.js'; import type { DeviceInfo } from '../types/common.js'; import { getConfigPaths, saveClientConfig } from './manager.js'; import type { ClientConfig, DeviceConfig, ServerConnection } from './schema.js'; interface WizardState { step: | 'clientId' | 'serverName' | 'serverAddress' | 'serverPort' | 'discovering' | 'selectDevices' | 'addAnother' | 'done'; config: Partial; currentServer?: Partial; discoveredDevices?: DeviceInfo[]; discoveryError?: string; } function ClientConfigWizard() { const [state, setState] = useState({ step: 'clientId', config: { servers: [], }, }); const [input, setInput] = useState(''); const handleSubmit = async (value: string) => { const newState = { ...state }; switch (state.step) { case 'clientId': newState.config.clientId = value; newState.step = 'serverName'; break; case 'serverName': newState.currentServer = { name: value, devices: [], }; newState.step = 'serverAddress'; break; case 'serverAddress': if (newState.currentServer) { newState.currentServer.address = value; } newState.step = 'serverPort'; break; case 'serverPort': if (newState.currentServer) { newState.currentServer.port = Number.parseInt(value, 10) || 3240; } // Start auto-discovery newState.step = 'discovering'; setState(newState); setInput(''); await discoverDevices(newState); return; case 'selectDevices': if (value === 'done') { // Add current server to config if ( newState.currentServer && newState.currentServer.name && newState.currentServer.address && newState.currentServer.port ) { const servers = newState.config.servers || []; servers.push(newState.currentServer as ServerConnection); newState.config.servers = servers; newState.currentServer = undefined; newState.discoveredDevices = undefined; newState.step = 'addAnother'; } } else if (value) { // Add device by index const index = Number.parseInt(value, 10); if ( !Number.isNaN(index) && newState.discoveredDevices && index >= 0 && index < newState.discoveredDevices.length ) { const selectedDevice = newState.discoveredDevices[index]; if (selectedDevice) { const device: DeviceConfig = { path: selectedDevice.path, vendorId: selectedDevice.vendorId, productId: selectedDevice.productId, description: selectedDevice.description, }; if (newState.currentServer) { newState.currentServer.devices = [ ...(newState.currentServer.devices || []), device, ]; } } } } break; case 'addAnother': if (value.toLowerCase() === 'y' || value.toLowerCase() === 'yes') { newState.step = 'serverName'; } else { // Done adding servers, save config newState.step = 'done'; try { await saveClientConfig(newState.config as ClientConfig); console.log(`\nConfiguration saved to ${getConfigPaths().client}`); process.exit(0); } catch (error) { console.error(`\nFailed to save config: ${error}`); process.exit(1); } } break; } setState(newState); setInput(''); }; const discoverDevices = async (currentState: WizardState) => { const server = currentState.currentServer; if (!server || !server.address || !server.port || !currentState.config.clientId) { setState({ ...currentState, discoveryError: 'Missing server information', step: 'selectDevices', }); return; } try { // Connect to server const client = new TCPClient(server.address, server.port); const discoveredDevices = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { client.disconnect(); reject(new Error('Discovery timeout')); }, 5000); client.onMessage((message) => { if (message.type === MessageType.DEVICE_LIST_RESPONSE) { clearTimeout(timeout); client.disconnect(); resolve(message.devices); } }); client.onConnectionState((connected) => { if (connected) { // Send device list request client.send({ type: MessageType.DEVICE_LIST_REQUEST, timestamp: Date.now(), clientId: currentState.config.clientId!, }); } }); client.connect().catch(reject); }); setState({ ...currentState, discoveredDevices, step: 'selectDevices', }); } catch (error) { setState({ ...currentState, discoveryError: `Discovery failed: ${error}`, discoveredDevices: [], step: 'selectDevices', }); } }; return ( USB/IP Supervisor - Client Configuration {state.step === 'clientId' && ( <> Client ID (unique identifier): )} {state.step === 'serverName' && ( <> Server name (e.g., "office-server", "lab-server"): )} {state.step === 'serverAddress' && ( <> Server address (IP or hostname): )} {state.step === 'serverPort' && ( <> Server port (default: 3240): )} {state.step === 'discovering' && ( <> 🔍 Discovering devices from {state.currentServer?.name}... )} {state.step === 'selectDevices' && ( <> {state.discoveryError && ( <> ❌ {state.discoveryError} You can still configure devices manually )} Available devices from {state.currentServer?.name}: {state.discoveredDevices && state.discoveredDevices.length > 0 ? ( state.discoveredDevices.map((d, i) => ( [{i}] {d.vendorId}:{d.productId} - {d.description} ({d.path}) )) ) : ( No devices discovered )} Enter index to select device, or 'done' to finish this server {state.currentServer?.devices && state.currentServer.devices.length > 0 && ( <> Selected devices: {state.currentServer.devices.map((d, i) => ( - {d.vendorId}:{d.productId} - {d.description} ))} )} )} {state.step === 'addAnother' && ( <> ✓ Server "{state.config.servers?.[state.config.servers.length - 1]?.name}" configured Add another server? (y/n): )} ); } export async function runClientConfigWizard(): Promise { render(); }