@@ -6,16 +6,17 @@ import { loadAgentConfig, saveAgentConfig, getConfigDir, ensureConfigDir } from
66import { discoverSSHKeys } from '../ssh' ;
77import type { SSHKeyInfo } from '../shared/client-types' ;
88
9- type Step = 'welcome' | 'auth' | 'github' | 'ssh' | 'tailscale' | 'complete' ;
9+ type Step = 'welcome' | 'auth' | 'github' | 'ssh' | 'tailscale' | 'network' | ' complete';
1010
11- const STEPS : Step [ ] = [ 'welcome' , 'auth' , 'github' , 'ssh' , 'tailscale' , 'complete' ] ;
11+ const STEPS : Step [ ] = [ 'welcome' , 'auth' , 'github' , 'ssh' , 'tailscale' , 'network' , ' complete'] ;
1212
1313interface WizardState {
1414 authToken : string ;
1515 authTokenGenerated : boolean ;
1616 githubToken : string ;
1717 selectedSSHKeys : string [ ] ;
1818 tailscaleAuthKey : string ;
19+ bindHost : string ;
1920}
2021
2122function WelcomeStep ( { onNext } : { onNext : ( ) => void } ) {
@@ -38,6 +39,7 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
3839 < Text > • Git access (GitHub token)</ Text >
3940 < Text > • SSH keys for workspaces</ Text >
4041 < Text > • Tailscale networking</ Text >
42+ < Text > • Network bind host</ Text >
4143 </ Box >
4244 < Box marginTop = { 1 } >
4345 < Text color = "gray" > Press Enter to continue...</ Text >
@@ -246,6 +248,133 @@ function SSHKeySelectStep({
246248 ) ;
247249}
248250
251+ type BindHostOption = '0.0.0.0' | '127.0.0.1' | 'custom' ;
252+
253+ const BIND_HOST_OPTIONS : { value : BindHostOption ; label : string ; description : string } [ ] = [
254+ {
255+ value : '0.0.0.0' ,
256+ label : 'All interfaces (0.0.0.0)' ,
257+ description : 'Accessible from other devices on the network' ,
258+ } ,
259+ {
260+ value : '127.0.0.1' ,
261+ label : 'Localhost only (127.0.0.1)' ,
262+ description : 'Only accessible from this machine' ,
263+ } ,
264+ { value : 'custom' , label : 'Custom' , description : 'Enter a custom hostname or IP' } ,
265+ ] ;
266+
267+ function NetworkStep ( {
268+ value,
269+ onChange,
270+ onNext,
271+ onBack,
272+ } : {
273+ value : string ;
274+ onChange : ( value : string ) => void ;
275+ onNext : ( ) => void ;
276+ onBack : ( ) => void ;
277+ } ) {
278+ const currentOption : BindHostOption =
279+ value === '0.0.0.0' ? '0.0.0.0' : value === '127.0.0.1' ? '127.0.0.1' : 'custom' ;
280+ const isCustom = currentOption === 'custom' ;
281+ const [ highlighted , setHighlighted ] = useState (
282+ Math . max (
283+ 0 ,
284+ BIND_HOST_OPTIONS . findIndex ( ( o ) => o . value === currentOption )
285+ )
286+ ) ;
287+ const [ editingCustom , setEditingCustom ] = useState ( false ) ;
288+ const [ customValue , setCustomValue ] = useState ( isCustom ? value : '' ) ;
289+
290+ useInput ( ( input , key ) => {
291+ if ( editingCustom ) {
292+ if ( key . return ) {
293+ if ( customValue . trim ( ) ) {
294+ onChange ( customValue . trim ( ) ) ;
295+ }
296+ setEditingCustom ( false ) ;
297+ } else if ( key . escape ) {
298+ setEditingCustom ( false ) ;
299+ }
300+ return ;
301+ }
302+
303+ if ( key . upArrow ) {
304+ setHighlighted ( ( h ) => Math . max ( 0 , h - 1 ) ) ;
305+ } else if ( key . downArrow ) {
306+ setHighlighted ( ( h ) => Math . min ( BIND_HOST_OPTIONS . length - 1 , h + 1 ) ) ;
307+ } else if ( input === ' ' || key . return ) {
308+ const selected = BIND_HOST_OPTIONS [ highlighted ] ;
309+ if ( selected . value === 'custom' ) {
310+ setEditingCustom ( true ) ;
311+ } else {
312+ onChange ( selected . value ) ;
313+ if ( key . return ) {
314+ onNext ( ) ;
315+ return ;
316+ }
317+ }
318+ } else if ( key . escape ) {
319+ onBack ( ) ;
320+ }
321+ } ) ;
322+
323+ return (
324+ < Box flexDirection = "column" gap = { 1 } >
325+ < Text bold > Bind Host</ Text >
326+ < Text color = "gray" >
327+ Choose which network interface the agent listens on. Use localhost to restrict access to
328+ this machine only.
329+ </ Text >
330+ < Box flexDirection = "column" marginTop = { 1 } >
331+ { BIND_HOST_OPTIONS . map ( ( option , index ) => (
332+ < Box key = { option . value } >
333+ < Text color = { highlighted === index ? 'cyan' : undefined } >
334+ < Text
335+ color = {
336+ option . value === currentOption || ( option . value === 'custom' && isCustom )
337+ ? 'green'
338+ : 'gray'
339+ }
340+ >
341+ { option . value === currentOption || ( option . value === 'custom' && isCustom )
342+ ? '(*) '
343+ : '( ) ' }
344+ </ Text >
345+ < Text > { option . label } </ Text >
346+ < Text color = "gray" > — { option . description } </ Text >
347+ </ Text >
348+ </ Box >
349+ ) ) }
350+ </ Box >
351+ { editingCustom && (
352+ < Box marginTop = { 1 } >
353+ < Text > Host: </ Text >
354+ < TextInput
355+ value = { customValue }
356+ onChange = { setCustomValue }
357+ placeholder = "e.g. 192.168.1.100"
358+ />
359+ </ Box >
360+ ) }
361+ { isCustom && ! editingCustom && value && (
362+ < Box marginTop = { 1 } >
363+ < Text > Current: </ Text >
364+ < Text color = "cyan" > { value } </ Text >
365+ </ Box >
366+ ) }
367+ < Box marginTop = { 1 } >
368+ < Text color = "gray" >
369+ { editingCustom
370+ ? 'Enter to confirm, Esc to cancel'
371+ : 'Space to select, Enter to continue, Esc to go back' }
372+ </ Text >
373+ </ Box >
374+ </ Box >
375+ ) ;
376+ }
377+
249378function CompleteStep ( { state, onFinish } : { state : WizardState ; onFinish : ( ) => void } ) {
250379 useInput ( ( _input , key ) => {
251380 if ( key . return ) {
@@ -259,6 +388,8 @@ function CompleteStep({ state, onFinish }: { state: WizardState; onFinish: () =>
259388 if ( state . selectedSSHKeys . length > 0 )
260389 configured . push ( `${ state . selectedSSHKeys . length } SSH key(s)` ) ;
261390 if ( state . tailscaleAuthKey ) configured . push ( 'Tailscale' ) ;
391+ if ( state . bindHost && state . bindHost !== '0.0.0.0' )
392+ configured . push ( `Bind host: ${ state . bindHost } ` ) ;
262393
263394 return (
264395 < Box flexDirection = "column" gap = { 1 } >
@@ -310,6 +441,7 @@ function SetupWizard() {
310441 githubToken : '' ,
311442 selectedSSHKeys : [ ] ,
312443 tailscaleAuthKey : '' ,
444+ bindHost : '0.0.0.0' ,
313445 } ) ;
314446 const [ saving , setSaving ] = useState ( false ) ;
315447
@@ -331,6 +463,7 @@ function SetupWizard() {
331463 githubToken : config . agents ?. github ?. token || '' ,
332464 selectedSSHKeys : config . ssh ?. global . copy || [ ] ,
333465 tailscaleAuthKey : config . tailscale ?. authKey || '' ,
466+ bindHost : config . host || '0.0.0.0' ,
334467 } ) ) ;
335468 } ;
336469 loadExisting ( ) . catch ( ( ) => { } ) ;
@@ -404,6 +537,10 @@ function SetupWizard() {
404537 } ;
405538 }
406539
540+ if ( state . bindHost ) {
541+ config . host = state . bindHost ;
542+ }
543+
407544 await saveAgentConfig ( config , configDir ) ;
408545 } catch {
409546 // Ignore save errors - user can reconfigure later
@@ -472,6 +609,14 @@ function SetupWizard() {
472609 optional
473610 />
474611 ) }
612+ { step === 'network' && (
613+ < NetworkStep
614+ value = { state . bindHost }
615+ onChange = { ( v ) => setState ( ( s ) => ( { ...s , bindHost : v } ) ) }
616+ onNext = { nextStep }
617+ onBack = { prevStep }
618+ />
619+ ) }
475620 { step === 'complete' && (
476621 < CompleteStep
477622 state = { state }
0 commit comments