Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 74 additions & 31 deletions packages/cli/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,13 +694,32 @@ async function collectCodexAuth(): Promise<CodexTokens | null> {
* Collect AI assistant configuration
*/
async function collectAIConfig(): Promise<SetupConfig['ai']> {
// Build multiselect options dynamically from registered providers
const providers = getRegisteredProviders();
const builtinProviders = providers.filter(p => p.builtIn);
const communityProviders = providers.filter(p => !p.builtIn);

const multiselectOptions = [
...builtinProviders.map(p => ({
value: p.id,
label: p.id === 'claude' ? `${p.displayName} (Recommended)` : p.displayName,
hint:
p.id === 'claude'
? 'Anthropic Claude Code SDK'
: p.id === 'codex'
? 'OpenAI Codex SDK'
: undefined,
})),
...communityProviders.map(p => ({
value: p.id,
label: p.displayName,
hint: 'community',
})),
];

const assistants = await multiselect({
message:
'Which built-in AI assistant(s) will you use? (↑↓ navigate, space select, enter confirm)',
options: [
{ value: 'claude', label: 'Claude (Recommended)', hint: 'Anthropic Claude Code SDK' },
{ value: 'codex', label: 'Codex', hint: 'OpenAI Codex SDK' },
],
message: 'Which AI assistant(s) will you use? (↑↓ navigate, space select, enter confirm)',
options: multiselectOptions,
required: false,
});

Expand All @@ -709,8 +728,10 @@ async function collectAIConfig(): Promise<SetupConfig['ai']> {
process.exit(0);
}

let hasClaude = assistants.includes('claude');
let hasCodex = assistants.includes('codex');
// Parse selected providers
const selectedProviders = assistants;
const hasClaude = selectedProviders.includes('claude');
const hasCodex = selectedProviders.includes('codex');

// Check if selected CLI tools are installed
if (hasClaude && !isCommandAvailable('claude')) {
Expand All @@ -727,7 +748,6 @@ async function collectAIConfig(): Promise<SetupConfig['ai']> {
cancel('Please install Claude Code and run setup again.');
process.exit(0);
}
hasClaude = false;
}

if (hasCodex && !isCommandAvailable('codex')) {
Expand Down Expand Up @@ -760,7 +780,6 @@ After installing Node.js, run 'archon setup' again.`,
cancel('Please install Node.js 18+ and run setup again.');
process.exit(0);
}
hasCodex = false;
} else if (nodeVersion.major < 18) {
note(
`Node.js ${nodeVersion.major}.${nodeVersion.minor}.${nodeVersion.patch} is installed, but Codex CLI requires Node.js 18 or later.
Expand All @@ -787,7 +806,6 @@ After upgrading, run 'archon setup' again.`,
cancel('Please upgrade Node.js to 18+ and run setup again.');
process.exit(0);
}
hasCodex = false;
}
}

Expand All @@ -806,11 +824,20 @@ After upgrading, run 'archon setup' again.`,
cancel('Please install Codex CLI and run setup again.');
process.exit(0);
}
hasCodex = false;
}
}

if (!hasClaude && !hasCodex) {
// Recalculate after user chose to skip
const finalHasClaude = hasClaude && isCommandAvailable('claude');
const finalHasCodex = hasCodex && isCommandAvailable('codex');

// Check if any provider selected (built-in or community)
const hasAnyBuiltIn = finalHasClaude || finalHasCodex;
const hasCommunityProvider = selectedProviders.some(
id => !getRegisteredProviders().find(p => p.id === id)?.builtIn
);
Comment on lines +836 to +838
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getRegisteredProviders() function is called repeatedly within the some loop, which is inefficient. Since the providers variable is already available from line 698, it should be reused. Additionally, pre-calculating the list of selected community providers here will simplify the defaultAssistant logic later.

Suggested change
const hasCommunityProvider = selectedProviders.some(
id => !getRegisteredProviders().find(p => p.id === id)?.builtIn
);
const communitySelected = selectedProviders.filter(id =>
providers.find(p => p.id === id && !p.builtIn)
);
const hasCommunityProvider = communitySelected.length > 0;


if (!hasAnyBuiltIn && !hasCommunityProvider) {
log.warning('No AI assistant selected. You can add one later by running `archon setup` again.');
return {
claude: false,
Expand All @@ -825,32 +852,41 @@ After upgrading, run 'archon setup' again.`,
let claudeBinaryPath: string | undefined;
let codexTokens: CodexTokens | undefined;

// Collect Claude auth if selected
if (hasClaude) {
// Collect Claude auth if selected (built-in requires auth)
if (finalHasClaude) {
const claudeAuth = await collectClaudeAuth();
claudeAuthType = claudeAuth.authType;
claudeApiKey = claudeAuth.apiKey;
claudeOauthToken = claudeAuth.oauthToken;
claudeBinaryPath = await collectClaudeBinaryPath();
}

// Collect Codex auth if selected
if (hasCodex) {
// Collect Codex auth if selected (built-in requires auth)
if (finalHasCodex) {
const tokens = await collectCodexAuth();
codexTokens = tokens ?? undefined;
}

// Determine default assistant — use the registry, but keep setup/auth flows built-in only.
// Default to first registered built-in provider rather than hardcoding 'claude'.
let defaultAssistant = getRegisteredProviders().find(p => p.builtIn)?.id ?? 'claude';
// Community providers (e.g., Pi) manage their own auth externally — no collection needed here

if (hasClaude && hasCodex) {
const providerChoices = getRegisteredProviders()
.filter(p => p.builtIn)
.map(p => ({
value: p.id,
label: p.id === 'claude' ? `${p.displayName} (Recommended)` : p.displayName,
}));
// Determine default assistant
const builtinSelected = selectedProviders.filter(id =>
getRegisteredProviders().find(p => p.id === id && p.builtIn)
);
Comment on lines +873 to +875
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The builtinSelected list currently includes providers that the user might have opted to skip (e.g., if the binary was missing and they chose to continue without it). This can lead to a skipped provider being set as the default assistant or appearing in the selection prompt. It should be filtered based on finalHasClaude and finalHasCodex to reflect only available providers.

Suggested change
const builtinSelected = selectedProviders.filter(id =>
getRegisteredProviders().find(p => p.id === id && p.builtIn)
);
const activeBuiltins = [];
if (finalHasClaude) activeBuiltins.push('claude');
if (finalHasCodex) activeBuiltins.push('codex');


let defaultAssistant: string;
if (builtinSelected.length > 1) {
// Multiple built-in providers: prompt user to choose default
const providerChoices: { value: string; label: string }[] = [];
for (const id of builtinSelected) {
const p = getRegisteredProviders().find(p => p.id === id);
if (p) {
Comment on lines +878 to +883
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the filtered activeBuiltins list and the existing providers variable to ensure only available providers are considered for the default assistant selection.

  let defaultAssistant: string;
  if (activeBuiltins.length > 1) {
    // Multiple built-in providers: prompt user to choose default
    const providerChoices: { value: string; label: string }[] = [];
    for (const id of activeBuiltins) {
      const p = providers.find(p => p.id === id);
      if (p) {

providerChoices.push({
value: p.id,
label: p.id === 'claude' ? `${p.displayName} (Recommended)` : p.displayName,
});
}
}

const defaultChoice = await select({
message: 'Which should be the default AI assistant?',
Expand All @@ -863,17 +899,24 @@ After upgrading, run 'archon setup' again.`,
}

defaultAssistant = defaultChoice;
} else if (hasCodex && !hasClaude) {
defaultAssistant = 'codex';
} else if (builtinSelected.length === 1) {
// Single built-in provider selected
defaultAssistant = builtinSelected[0];
} else if (selectedProviders.length > 0) {
// Only community providers selected — use first one
defaultAssistant = selectedProviders[0];
} else {
// Fallback to first available built-in
defaultAssistant = getRegisteredProviders().find(p => p.builtIn)?.id ?? 'claude';
}
Comment on lines +902 to 911
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for determining the defaultAssistant should prioritize active built-in providers and then fall back to selected community providers. The current implementation might pick a skipped built-in provider if it was the first one in selectedProviders. Using activeBuiltins and communitySelected ensures a valid configuration.

Suggested change
} else if (builtinSelected.length === 1) {
// Single built-in provider selected
defaultAssistant = builtinSelected[0];
} else if (selectedProviders.length > 0) {
// Only community providers selected — use first one
defaultAssistant = selectedProviders[0];
} else {
// Fallback to first available built-in
defaultAssistant = getRegisteredProviders().find(p => p.builtIn)?.id ?? 'claude';
}
} else if (activeBuiltins.length === 1) {
// Single built-in provider selected
defaultAssistant = activeBuiltins[0];
} else if (communitySelected.length > 0) {
// Only community providers selected — use first one
defaultAssistant = communitySelected[0];
} else {
// Fallback to first available built-in
defaultAssistant = providers.find(p => p.builtIn)?.id ?? 'claude';
}


return {
claude: hasClaude,
claude: finalHasClaude,
claudeAuthType,
claudeApiKey,
claudeOauthToken,
...(claudeBinaryPath !== undefined ? { claudeBinaryPath } : {}),
codex: hasCodex,
codex: finalHasCodex,
codexTokens,
defaultAssistant,
};
Expand Down