@@ -44,6 +44,8 @@ interface CliArgs {
4444 provider ? : string
4545 /** Append to system prompt (added after base instructions) */
4646 systemPrompt ? : string
47+ /** Launch interactive first-time setup wizard */
48+ setup ? : boolean
4749}
4850
4951function parseCliArgs ( argv : string [ ] ) : CliArgs {
@@ -118,6 +120,10 @@ function parseCliArgs(argv: string[]): CliArgs {
118120 args . systemPrompt = argv [ i + 1 ]
119121 i ++
120122 break
123+ case 'setup' :
124+ case '--setup' :
125+ args . setup = true
126+ break
121127 default :
122128 break
123129 }
@@ -174,6 +180,7 @@ Options:
174180 --thinking Enable extended thinking mode
175181 --thinking-budget <n> Extended thinking budget in tokens (default: 1024)
176182 --system-prompt <text> Append additional system prompt text
183+ --setup Launch interactive first-time setup wizard
177184` ) ;
178185 process . exit ( 0 ) ;
179186}
@@ -600,8 +607,155 @@ if (cliArgs.model || process.argv.includes('--model')) {
600607 process . exit ( 0 ) ;
601608}
602609
610+ // --setup: interactive first-time setup wizard
611+ if ( cliArgs . setup || process . argv . includes ( 'setup' ) ) {
612+ const { readFileSync , writeFileSync } = await import ( 'node:fs' ) ;
613+ const { homedir } = await import ( 'node:os' ) ;
614+ const { join } = await import ( 'node:path' ) ;
615+ const readline = await import ( 'node:readline' ) ;
616+ const stdin = process . stdin ;
617+ const stdout = process . stdout ;
618+
619+ const settingsPath = join ( homedir ( ) , '.coder' , 'settings.json' ) ;
620+ let settings : any = { } ;
621+ try {
622+ settings = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
623+ } catch { }
624+ settings . model_list = settings . model_list ?? [ ] ;
625+
626+ console . log ( '\n🔧 Coder Agent — First Time Setup\n' ) ;
627+
628+ // ── Step 1: Theme ──────────────────────────────────────────────────────────
629+ {
630+ const rl = readline . createInterface ( { input : stdin , output : stdout } ) ;
631+ const theme = await new Promise < string > ( resolve => {
632+ rl . question ( 'Theme (dark/light) [dark]: ' , answer => {
633+ const trimmed = answer . trim ( ) . toLowerCase ( ) ;
634+ resolve ( trimmed === 'light' ? 'light' : 'dark' ) ;
635+ } ) ;
636+ } ) ;
637+ settings . theme = theme ;
638+ console . log ( ` Theme: ${ theme } \n` ) ;
639+ rl . close ( ) ;
640+ }
641+
642+ // ── Step 2: max_tokens ─────────────────────────────────────────────────────
643+ {
644+ const rl = readline . createInterface ( { input : stdin , output : stdout } ) ;
645+ const maxTokens = await new Promise < number > ( resolve => {
646+ rl . question ( 'Max output tokens [32768]: ' , answer => {
647+ const trimmed = answer . trim ( ) ;
648+ resolve ( trimmed ? parseInt ( trimmed , 10 ) || 32768 : 32768 ) ;
649+ } ) ;
650+ } ) ;
651+ settings . max_tokens = maxTokens ;
652+ console . log ( ` Max tokens: ${ maxTokens } \n` ) ;
653+ rl . close ( ) ;
654+ }
655+
656+ // ── Step 3: Provider + Model ───────────────────────────────────────────────
657+ const modelList : Array < any > = settings.model_list;
658+
659+ if (modelList.length === 0) {
660+ console . log ( 'No providers configured. Let us create one.\n' ) ;
661+ const rl = readline . createInterface ( { input : stdin , output : stdout } ) ;
662+
663+ const name = await new Promise < string > ( resolve => {
664+ rl . question ( 'Provider name (e.g. deepseek): ' , resolve ) ;
665+ } ) ;
666+ const url = await new Promise < string > ( resolve => {
667+ rl . question ( 'Base URL (e.g. https://api.deepseek.com/anthropic): ' , resolve ) ;
668+ } ) ;
669+ const key = await new Promise < string > ( resolve => {
670+ rl . question ( 'API key: ' , resolve ) ;
671+ } ) ;
672+ const proxy = await new Promise < string > ( resolve => {
673+ rl . question ( 'Proxy URL (or Enter to skip): ' , resolve ) ;
674+ } ) ;
675+ rl . close ( ) ;
676+
677+ const providerName = name . trim ( ) ;
678+ const providerUrl = url . trim ( ) || undefined ;
679+ const providerKey = key . trim ( ) || `YOUR_${ providerName . toUpperCase ( ) } _API_KEY` ;
680+ const providerProxy = proxy . trim ( ) || null ;
681+
682+ const newProvider = {
683+ provider : providerName ,
684+ model : [ ] ,
685+ base_url : providerUrl ,
686+ auth_token_env : providerKey ,
687+ proxy : providerProxy ,
688+ price : { input : 0 , output : 0 , currency : 'USD' , unit : '1M tokens' } ,
689+ } ;
690+ modelList . push ( newProvider ) ;
691+ console . log ( ` Provider created: ${ providerName } \n` ) ;
692+ } else {
693+ console . log ( `Found ${ modelList . length } existing provider(s).` ) ;
694+ const rl = readline . createInterface ( { input : stdin , output : stdout } ) ;
695+ const skipModels = await new Promise < string > ( resolve => {
696+ rl . question ( 'Skip provider configuration? (y/N): ' , resolve ) ;
697+ } ) ;
698+ rl . close ( ) ;
699+ if ( skipModels . trim ( ) . toLowerCase ( ) === 'y' ) {
700+ console . log ( ' Skipping provider setup.\n' ) ;
701+ } else {
702+ // Show existing providers
703+ for ( let i = 0 ; i < modelList . length ; i ++ ) {
704+ console . log ( ` [${ i + 1 } ] ${ modelList [ i ] . provider } — ${ modelList [ i ] . model ?. join ( ', ' ) || 'no models' } ` ) ;
705+ }
706+ console . log ( ` [${ modelList . length + 1 } ] Add new provider` ) ;
707+ const rl2 = readline . createInterface ( { input : stdin , output : stdout } ) ;
708+ const choice = await new Promise < string > ( resolve => {
709+ rl2 . question ( '\nSelect provider to configure: ' , resolve ) ;
710+ } ) ;
711+ rl2 . close ( ) ;
712+
713+ const idx = parseInt ( choice . trim ( ) , 10 ) - 1 ;
714+ if ( idx >= 0 && idx < modelList . length ) {
715+ const selected = modelList [ idx ] ;
716+ console . log ( ` Configuring: ${ selected . provider } \n` ) ;
717+ const rl3 = readline . createInterface ( { input : stdin , output : stdout } ) ;
718+ const newModels = await new Promise < string > ( resolve => {
719+ rl3 . question ( 'Model IDs (comma-separated): ' , resolve ) ;
720+ } ) ;
721+ rl3 . close ( ) ;
722+ selected . model = newModels . split ( ',' ) . map ( m => m . trim ( ) ) . filter ( Boolean ) ;
723+ settings . default_model = `${ selected . provider } /${ selected . model [ 0 ] } ` ;
724+ console . log ( ` Updated models for ${ selected . provider } \n` ) ;
725+ } else if ( idx === modelList . length ) {
726+ console . log ( ' Adding new provider...\n' ) ;
727+ // Quick add (same flow as above)
728+ const rl3 = readline . createInterface ( { input : stdin , output : stdout } ) ;
729+ const pName = await new Promise < string > ( resolve => rl3 . question ( 'Provider name: ' , resolve ) ) ;
730+ const pUrl = await new Promise < string > ( resolve => rl3 . question ( 'Base URL: ' , resolve ) ) ;
731+ const pKey = await new Promise < string > ( resolve => rl3 . question ( 'API key: ' , resolve ) ) ;
732+ const pProxy = await new Promise < string > ( resolve => rl3 . question ( 'Proxy URL (or Enter to skip): ' , resolve ) ) ;
733+ rl3 . close ( ) ;
734+ modelList . push ( {
735+ provider : pName . trim ( ) ,
736+ model : [ ] ,
737+ base_url : pUrl . trim ( ) || undefined ,
738+ auth_token_env : pKey . trim ( ) || `YOUR_${ pName . trim ( ) . toUpperCase ( ) } _API_KEY` ,
739+ proxy : pProxy . trim ( ) || null ,
740+ price : { input : 0 , output : 0 , currency : 'USD' , unit : '1M tokens' } ,
741+ } ) ;
742+ console . log ( ` Provider added: ${ pName } \n` ) ;
743+ }
744+ }
745+ }
746+
747+ // ── Save and exit ──────────────────────────────────────────────────────────
748+ settings . model_list = modelList ;
749+ writeFileSync ( settingsPath , JSON . stringify ( settings , null , 2 ) ) ;
750+
751+ console . log ( '✅ Setup complete! Settings saved to ~/.coder/settings.json\n' ) ;
752+ console . log ( 'Starting Coder Agent...\n' ) ;
753+
754+ // Fall through to TUI init
755+ }
756+
603757// TTY check — skip for non-interactive flags handled above
604- const isNonInteractive = cliArgs . help || cliArgs . version || cliArgs . print || cliArgs . model || process . argv . includes ( '--model' ) || process . argv . includes ( '-m' )
758+ const isNonInteractive = cliArgs . help || cliArgs . version || cliArgs . print || cliArgs . model || cliArgs . setup || process . argv . includes ( '--model' ) || process . argv . includes ( '-m' ) || process . argv . includes ( 'setup ')
605759if ( isNonInteractive ) {
606760 // Exit gracefully — the --model handler above uses readline callback to exit
607761 if ( ! cliArgs . model && ! process . argv . includes ( '--model' ) && ! process . argv . includes ( '-m' ) ) {
0 commit comments