/** * Server configuration wizard (TUI) */ import { Box, render, Text } from 'ink'; import TextInput from 'ink-text-input'; import React, { useEffect, useState } from 'react'; import { listLocalDevices } from '../usbip/commands.js'; import { getConfigPaths, saveServerConfig } from './manager.js'; import type { ServerConfig, ServerDeviceConfig } from './schema.js'; interface WizardState { step: 'address' | 'port' | 'list' | 'devices' | 'done'; config: Partial; availableDevices?: Array<{ path: string; vendorId: string; productId: string; description: string; }>; } function ServerConfigWizard() { const [state, setState] = useState({ step: 'address', config: { listenAddress: '0.0.0.0', listenPort: 3240, devices: [], }, }); const [input, setInput] = useState(''); // Load available devices when we reach the list step useEffect(() => { if (state.step === 'list') { listLocalDevices().then((devices) => { setState({ ...state, availableDevices: devices, step: 'devices', }); }); } }, [state.step]); const handleSubmit = async (value: string) => { const newState = { ...state }; switch (state.step) { case 'address': newState.config.listenAddress = value || '0.0.0.0'; newState.step = 'port'; break; case 'port': newState.config.listenPort = Number.parseInt(value, 10) || 3240; newState.step = 'list'; break; case 'devices': if (value === 'done') { newState.step = 'done'; try { await saveServerConfig(newState.config as ServerConfig); console.log(`\nConfiguration saved to ${getConfigPaths().server}`); process.exit(0); } catch (error) { console.error(`\nFailed to save config: ${error}`); process.exit(1); } } else if (value) { // Parse device: path or index:clientId1,clientId2 const parts = value.split(':'); const availableDevices = newState.availableDevices || []; // Check if it's an index const firstPart = parts[0]; if (!firstPart) break; const index = Number.parseInt(firstPart, 10); let selectedDevice; if (!Number.isNaN(index) && index >= 0 && index < availableDevices.length) { selectedDevice = availableDevices[index]; } else { // Treat as path selectedDevice = availableDevices.find((d) => d.path === firstPart); } if (selectedDevice) { const device: ServerDeviceConfig = { path: selectedDevice.path, vendorId: selectedDevice.vendorId, productId: selectedDevice.productId, description: selectedDevice.description, allowedClients: parts[1] ? parts[1].split(',') : [], }; newState.config.devices = [...(newState.config.devices || []), device]; } } break; } setState(newState); setInput(''); }; return ( USB/IP Supervisor - Server Configuration {state.step === 'address' && ( <> Listen Address (default: 0.0.0.0): )} {state.step === 'port' && ( <> Listen Port (default: 3240): )} {state.step === 'list' && ( <> Loading available USB devices... )} {state.step === 'devices' && ( <> Available USB devices: Select by index number and optionally specify allowed clients {state.availableDevices?.map((d, i) => ( [{i}] {d.vendorId}:{d.productId} - {d.description} ({d.path}) )) || No devices found} Format: index[:clientId1,clientId2] Example: 0:client1,client2 (or just 0 for all clients) Type 'done' when finished {state.config.devices && state.config.devices.length > 0 && ( <> Configured devices: {state.config.devices.map((d) => ( - {d.vendorId}:{d.productId} - {d.description} {d.allowedClients.length > 0 && ` (clients: ${d.allowedClients.join(', ')})`} ))} )} )} ); } export async function runServerConfigWizard(): Promise { render(); }