@@ -354,11 +354,48 @@ def menu_start_server():
354354 console .print (f"\n [red]Error: { e } [/]" )
355355
356356
357- async def _run_server (server : DarkCodeServer ):
358- """Run the server until interrupted."""
357+ async def _run_server (server : DarkCodeServer , show_status : bool = True ):
358+ """Run the server until interrupted with live status display."""
359+ import time
360+ from datetime import timedelta
361+
359362 await server .start ()
363+ start_time = time .time ()
364+
360365 try :
361- await asyncio .Future () # Run forever
366+ if show_status :
367+ # Create a live status display that updates periodically
368+ status_table = Table (show_header = False , box = None , padding = (0 , 1 ))
369+ status_table .add_column ("" , style = "green" , width = 2 )
370+ status_table .add_column ("" , style = "dim" )
371+
372+ with Live (console = console , refresh_per_second = 1 , transient = False ) as live :
373+ while True :
374+ # Calculate uptime
375+ uptime_secs = int (time .time () - start_time )
376+ uptime = str (timedelta (seconds = uptime_secs ))
377+
378+ # Get session count from server
379+ session_count = len (server .sessions ) if hasattr (server , 'sessions' ) else 0
380+ state = server .state .value if hasattr (server , 'state' ) else "running"
381+
382+ # Build status display
383+ status_text = Text ()
384+ status_text .append ("[*] " , style = "bold green" )
385+ status_text .append ("DARKCODE SERVER RUNNING" , style = "bold green" )
386+ status_text .append (" | " , style = "dim" )
387+ status_text .append (f"{ uptime } " , style = "cyan" )
388+ status_text .append (" | " , style = "dim" )
389+ status_text .append (f"{ session_count } session{ 's' if session_count != 1 else '' } " , style = "yellow" )
390+ status_text .append (" | " , style = "dim" )
391+ status_text .append (f"{ state } " , style = "magenta" )
392+ status_text .append (" | " , style = "dim" )
393+ status_text .append ("Ctrl+C to stop" , style = "dim italic" )
394+
395+ live .update (status_text )
396+ await asyncio .sleep (1 )
397+ else :
398+ await asyncio .Future () # Run forever without status
362399 finally :
363400 await server .stop ()
364401
@@ -656,86 +693,70 @@ def main(ctx, version, classic):
656693 return
657694
658695 if ctx .invoked_subcommand is None :
659- show_banner ()
660-
661696 if classic :
662697 # Use old menu if explicitly requested
698+ show_banner ()
663699 interactive_menu ()
664700 else :
665- # Default to modern prompt_toolkit dialogs with dropdowns
701+ # Default to arrow-key navigation menu using prompt_toolkit
666702 try :
667703 from darkcode_server .prompt_ui import run_interactive_menu
668704 result = run_interactive_menu ()
669705 if result :
670- action , mode = result
706+ action , data = result
671707 if action == "start" :
672- # Start server with selected mode
708+ # data is a dict with mode, port, working_dir, no_web, save
673709 config = ServerConfig .load ()
674- if mode == "ssh" :
675- config .local_only = True
676- else :
677- config .local_only = False
678- # Call the start command logic
679- ctx .invoke (start , local_only = config .local_only , no_banner = True )
680- elif action == "status" :
681- menu_status ()
682- elif action == "qr" :
683- menu_qr_code ()
684- elif action == "guest" :
685- menu_guest_codes ()
686- elif action == "guest_create" :
687- from darkcode_server .prompt_ui import show_guest_create_dialog
688- guest_data = show_guest_create_dialog ()
689- if guest_data :
690- from darkcode_server .security import GuestAccessManager
691- config = ServerConfig .load ()
692- guest_mgr = GuestAccessManager (config .config_dir / "guests.db" )
693- result = guest_mgr .create_guest_code (** guest_data )
694- console .print (f"\n [green]Guest code created:[/] [bold]{ result ['code' ]} [/]" )
695- Prompt .ask ("\n [dim]Press Enter to continue[/]" )
696- elif action == "guest_list" :
697- from click .testing import CliRunner
698- runner = CliRunner ()
699- runner .invoke (guest_list , [], standalone_mode = False )
700- Prompt .ask ("\n [dim]Press Enter to continue[/]" )
701- elif action == "guest_revoke" :
702- code = Prompt .ask ("[cyan]Code to revoke[/]" )
703- from click .testing import CliRunner
704- runner = CliRunner ()
705- runner .invoke (guest_revoke , [code ], standalone_mode = False )
706- Prompt .ask ("\n [dim]Press Enter to continue[/]" )
707- elif action == "guest_qr" :
708- code = Prompt .ask ("[cyan]Code for QR[/]" )
709- from click .testing import CliRunner
710- runner = CliRunner ()
711- runner .invoke (guest_qr , [code ], standalone_mode = False )
712- Prompt .ask ("\n [dim]Press Enter to continue[/]" )
713- elif action == "config" :
714- menu_config ()
715- elif action == "security" :
716- menu_security ()
710+ local_only = data .get ("mode" ) == "ssh"
711+ port = data .get ("port" , config .port )
712+ working_dir = data .get ("working_dir" , str (config .working_dir ))
713+ no_web = data .get ("no_web" , False )
714+ if data .get ("save" ):
715+ config .port = port
716+ config .working_dir = Path (working_dir )
717+ config .save ()
718+ ctx .invoke (start , port = port , working_dir = working_dir , local_only = local_only , no_web = no_web , no_banner = True )
719+ elif action == "daemon_foreground" :
720+ ctx .invoke (daemon , detach = False )
721+ elif action == "daemon_background" :
722+ ctx .invoke (daemon , detach = True )
723+ elif action == "daemon_stop" :
724+ ctx .invoke (stop )
717725 elif action == "setup" :
718726 setup_wizard_menu ()
719- elif action == "install_tailscale" :
720- prompt_install_tailscale ()
727+ elif action == "install" :
728+ ctx .invoke (install )
729+ elif action == "uninstall" :
730+ ctx .invoke (uninstall )
731+ elif action == "rotate_token" :
732+ ctx .invoke (rotate_token )
733+ elif action == "client_cert" :
734+ device_id = data
735+ ctx .invoke (client_cert , device_id = device_id )
721736 except ImportError as e :
722- console .print (f"[yellow]Interactive dialogs require prompt_toolkit . Falling back to classic menu.[/]" )
737+ console .print (f"[yellow]prompt_toolkit not installed . Falling back to classic menu.[/]" )
723738 console .print (f"[dim]Install with: pip install prompt_toolkit[/]" )
739+ show_banner ()
724740 interactive_menu ()
725741 except Exception as e :
726- console .print (f"[yellow]Dialog error: { e } . Falling back to classic menu.[/]" )
742+ import traceback
743+ console .print (f"[yellow]Menu error: { e } [/]" )
744+ console .print (f"[dim]{ traceback .format_exc ()} [/]" )
745+ show_banner ()
727746 interactive_menu ()
728747
729748
730749@main .command ()
731750@click .option ("--port" , "-p" , type = int , envvar = "DARKCODE_PORT" , help = "Server port (default: 3100, env: DARKCODE_PORT)" )
732751@click .option ("--token" , "-t" , envvar = "DARKCODE_TOKEN" , help = "Auth token (env: DARKCODE_TOKEN)" )
733- @click .option ("--working-dir" , "-d" , type = click .Path (exists = True ), envvar = "DARKCODE_WORKING_DIR" , help = "Working directory (env: DARKCODE_WORKING_DIR)" )
752+ @click .option ("--working-dir" , "-d" , type = click .Path (exists = True ), envvar = "DARKCODE_WORKING_DIR" , help = "Working directory for Claude (env: DARKCODE_WORKING_DIR)" )
753+ @click .option ("--browse-dir" , "-b" , type = click .Path (exists = True ), envvar = "DARKCODE_BROWSE_DIR" , help = "Default directory for app file browser (env: DARKCODE_BROWSE_DIR)" )
734754@click .option ("--name" , "-n" , envvar = "DARKCODE_SERVER_NAME" , help = "Server display name" )
735755@click .option ("--local-only" , "-l" , is_flag = True , envvar = "DARKCODE_LOCAL_ONLY" , help = "Only accept localhost connections (use with SSH tunnel)" )
736756@click .option ("--no-banner" , is_flag = True , help = "Skip banner animation" )
757+ @click .option ("--no-web" , is_flag = True , envvar = "DARKCODE_NO_WEB" , help = "Disable web admin dashboard" )
737758@click .option ("--save" , "-s" , is_flag = True , help = "Save options to config file" )
738- def start (port , token , working_dir , name , local_only , no_banner , save ):
759+ def start (port , token , working_dir , browse_dir , name , local_only , no_banner , no_web , save ):
739760 """Start the DarkCode server.
740761
741762 CONNECTION MODES:
@@ -782,10 +803,14 @@ def start(port, token, working_dir, name, local_only, no_banner, save):
782803 config .token = token
783804 if working_dir :
784805 config .working_dir = Path (working_dir )
806+ if browse_dir :
807+ config .browse_dir = Path (browse_dir )
785808 if name :
786809 config .server_name = name
787810 if local_only :
788811 config .local_only = True
812+ if no_web :
813+ config .web_admin_disabled = True
789814
790815 # Auto-save on first run or if requested
791816 if first_run or save :
@@ -843,6 +868,24 @@ def start(port, token, working_dir, name, local_only, no_banner, save):
843868 ))
844869
845870 console .print (f"\n [green]Server listening on { config .bind_host } :{ config .port } [/]" )
871+
872+ # Show admin URL and PIN (unless disabled)
873+ if not config .web_admin_disabled :
874+ local_ips = config .get_local_ips ()
875+ local_ip = local_ips [0 ]["address" ] if local_ips else "127.0.0.1"
876+ protocol = "https" if config .tls_enabled else "http"
877+ console .print (f"[cyan]Web Admin:[/] { protocol } ://{ local_ip } :{ config .port } /admin" )
878+
879+ # Get and display web PIN
880+ try :
881+ from darkcode_server .web_admin import WebAdminHandler
882+ web_pin = WebAdminHandler .get_web_pin ()
883+ console .print (f"[cyan]Web PIN:[/] [bold yellow]{ web_pin } [/]" )
884+ except ImportError :
885+ pass
886+ else :
887+ console .print ("[dim]Web Admin: disabled (--no-web)[/]" )
888+
846889 console .print ("[dim]Press Ctrl+C to stop[/]\n " )
847890
848891 server = DarkCodeServer (config )
0 commit comments