Skip to content

Commit 4bcfc85

Browse files
jimgqyuDeepSeek
andcommitted
feat: coder setup first-time configuration wizard
- Add `coder setup` CLI command with interactive wizard - Guide users through theme, max_tokens, provider setup - Save configuration to ~/.coder/settings.json and launch TUI - Fix hasProvider() to detect placeholder API keys (YOUR_*, API_KEY, NO_KEY) Co-Authored-By: DeepSeek <noreply@deepseek.com>
1 parent 4175714 commit 4bcfc85

2 files changed

Lines changed: 160 additions & 2 deletions

File tree

packages/cli/src/entry.tsx

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

4951
function 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')
605759
if (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')) {

packages/cli/src/gateway/coder-client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,11 @@ export class CoderGatewayClient extends EventEmitter implements IGatewayClient {
10711071
entry = settings.model_list.find(m => m.provider === providerName)
10721072
}
10731073
if (!entry) entry = settings.model_list[0]
1074-
if (entry?.auth_token_env) return true
1074+
// Check if auth_token_env looks like a real value (not placeholder)
1075+
const tokenValue = entry?.auth_token_env ?? '';
1076+
if (tokenValue && !tokenValue.startsWith('YOUR_') && !tokenValue.includes('API_KEY') && !tokenValue.includes('NO_KEY')) {
1077+
return true
1078+
}
10751079
}
10761080

10771081
// Legacy env check

0 commit comments

Comments
 (0)