diff --git a/apps/server/src/providers/opencode-provider.ts b/apps/server/src/providers/opencode-provider.ts
index 42a7045f..b54592c3 100644
--- a/apps/server/src/providers/opencode-provider.ts
+++ b/apps/server/src/providers/opencode-provider.ts
@@ -22,7 +22,18 @@ import type {
ContentBlock,
} from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
-import { type SubprocessOptions } from '@automaker/platform';
+import { type SubprocessOptions, getOpenCodeAuthIndicators } from '@automaker/platform';
+
+// =============================================================================
+// OpenCode Auth Types
+// =============================================================================
+
+export interface OpenCodeAuthStatus {
+ authenticated: boolean;
+ method: 'api_key' | 'oauth' | 'none';
+ hasOAuthToken?: boolean;
+ hasApiKey?: boolean;
+}
// =============================================================================
// OpenCode Stream Event Types
@@ -583,6 +594,48 @@ export class OpencodeProvider extends CliProvider {
return supportedFeatures.includes(feature);
}
+ // ==========================================================================
+ // Authentication
+ // ==========================================================================
+
+ /**
+ * Check authentication status for OpenCode CLI
+ *
+ * Checks for authentication via:
+ * - OAuth token in auth file
+ * - API key in auth file
+ */
+ async checkAuth(): Promise {
+ const authIndicators = await getOpenCodeAuthIndicators();
+
+ // Check for OAuth token
+ if (authIndicators.hasOAuthToken) {
+ return {
+ authenticated: true,
+ method: 'oauth',
+ hasOAuthToken: true,
+ hasApiKey: authIndicators.hasApiKey,
+ };
+ }
+
+ // Check for API key
+ if (authIndicators.hasApiKey) {
+ return {
+ authenticated: true,
+ method: 'api_key',
+ hasOAuthToken: false,
+ hasApiKey: true,
+ };
+ }
+
+ return {
+ authenticated: false,
+ method: 'none',
+ hasOAuthToken: false,
+ hasApiKey: false,
+ };
+ }
+
// ==========================================================================
// Installation Detection
// ==========================================================================
@@ -593,16 +646,21 @@ export class OpencodeProvider extends CliProvider {
* Checks if the opencode CLI is available either through:
* - Direct installation (npm global)
* - NPX (fallback on Windows)
+ * Also checks authentication status.
*/
async detectInstallation(): Promise {
this.ensureCliDetected();
const installed = await this.isInstalled();
+ const auth = await this.checkAuth();
return {
installed,
path: this.cliPath || undefined,
method: this.detectedStrategy === 'npx' ? 'npm' : 'cli',
+ authenticated: auth.authenticated,
+ hasApiKey: auth.hasApiKey,
+ hasOAuthToken: auth.hasOAuthToken,
};
}
}
diff --git a/apps/server/src/routes/setup/routes/opencode-status.ts b/apps/server/src/routes/setup/routes/opencode-status.ts
index 7e8edd5e..f474cfb1 100644
--- a/apps/server/src/routes/setup/routes/opencode-status.ts
+++ b/apps/server/src/routes/setup/routes/opencode-status.ts
@@ -12,7 +12,7 @@ import { getErrorMessage, logError } from '../common.js';
*/
export function createOpencodeStatusHandler() {
const installCommand = 'curl -fsSL https://opencode.ai/install | bash';
- const loginCommand = 'opencode auth';
+ const loginCommand = 'opencode auth login';
return async (_req: Request, res: Response): Promise => {
try {
@@ -35,11 +35,13 @@ export function createOpencodeStatusHandler() {
method: authMethod,
hasApiKey: status.hasApiKey || false,
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY || !!process.env.OPENAI_API_KEY,
- hasOAuthToken: false, // OpenCode doesn't use OAuth
+ hasOAuthToken: status.hasOAuthToken || false,
},
recommendation: status.installed
? undefined
: 'Install OpenCode CLI to use multi-provider AI models.',
+ installCommand,
+ loginCommand,
installCommands: {
macos: installCommand,
linux: installCommand,
diff --git a/apps/ui/src/components/views/setup-view.tsx b/apps/ui/src/components/views/setup-view.tsx
index 82e399ea..f3e9d1dd 100644
--- a/apps/ui/src/components/views/setup-view.tsx
+++ b/apps/ui/src/components/views/setup-view.tsx
@@ -5,10 +5,7 @@ import {
WelcomeStep,
ThemeStep,
CompleteStep,
- ClaudeSetupStep,
- CursorSetupStep,
- CodexSetupStep,
- OpencodeSetupStep,
+ ProvidersSetupStep,
GitHubSetupStep,
} from './setup-view/steps';
import { useNavigate } from '@tanstack/react-router';
@@ -17,30 +14,31 @@ const logger = createLogger('SetupView');
// Main Setup View
export function SetupView() {
- const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup } = useSetupStore();
+ const { currentStep, setCurrentStep, completeSetup } = useSetupStore();
const navigate = useNavigate();
- const steps = [
- 'welcome',
- 'theme',
- 'claude',
- 'cursor',
- 'codex',
- 'opencode',
- 'github',
- 'complete',
- ] as const;
+ // Simplified steps: welcome, theme, providers (combined), github, complete
+ const steps = ['welcome', 'theme', 'providers', 'github', 'complete'] as const;
type StepName = (typeof steps)[number];
+
const getStepName = (): StepName => {
- if (currentStep === 'claude_detect' || currentStep === 'claude_auth') return 'claude';
+ // Map old step names to new consolidated steps
if (currentStep === 'welcome') return 'welcome';
if (currentStep === 'theme') return 'theme';
- if (currentStep === 'cursor') return 'cursor';
- if (currentStep === 'codex') return 'codex';
- if (currentStep === 'opencode') return 'opencode';
+ if (
+ currentStep === 'claude_detect' ||
+ currentStep === 'claude_auth' ||
+ currentStep === 'cursor' ||
+ currentStep === 'codex' ||
+ currentStep === 'opencode' ||
+ currentStep === 'providers'
+ ) {
+ return 'providers';
+ }
if (currentStep === 'github') return 'github';
return 'complete';
};
+
const currentIndex = steps.indexOf(getStepName());
const handleNext = (from: string) => {
@@ -51,22 +49,10 @@ export function SetupView() {
setCurrentStep('theme');
break;
case 'theme':
- logger.debug('[Setup Flow] Moving to claude_detect step');
- setCurrentStep('claude_detect');
+ logger.debug('[Setup Flow] Moving to providers step');
+ setCurrentStep('providers');
break;
- case 'claude':
- logger.debug('[Setup Flow] Moving to cursor step');
- setCurrentStep('cursor');
- break;
- case 'cursor':
- logger.debug('[Setup Flow] Moving to codex step');
- setCurrentStep('codex');
- break;
- case 'codex':
- logger.debug('[Setup Flow] Moving to opencode step');
- setCurrentStep('opencode');
- break;
- case 'opencode':
+ case 'providers':
logger.debug('[Setup Flow] Moving to github step');
setCurrentStep('github');
break;
@@ -83,45 +69,15 @@ export function SetupView() {
case 'theme':
setCurrentStep('welcome');
break;
- case 'claude':
+ case 'providers':
setCurrentStep('theme');
break;
- case 'cursor':
- setCurrentStep('claude_detect');
- break;
- case 'codex':
- setCurrentStep('cursor');
- break;
- case 'opencode':
- setCurrentStep('codex');
- break;
case 'github':
- setCurrentStep('opencode');
+ setCurrentStep('providers');
break;
}
};
- const handleSkipClaude = () => {
- logger.debug('[Setup Flow] Skipping Claude setup');
- setSkipClaudeSetup(true);
- setCurrentStep('cursor');
- };
-
- const handleSkipCursor = () => {
- logger.debug('[Setup Flow] Skipping Cursor setup');
- setCurrentStep('codex');
- };
-
- const handleSkipCodex = () => {
- logger.debug('[Setup Flow] Skipping Codex setup');
- setCurrentStep('opencode');
- };
-
- const handleSkipOpencode = () => {
- logger.debug('[Setup Flow] Skipping OpenCode setup');
- setCurrentStep('github');
- };
-
const handleSkipGithub = () => {
logger.debug('[Setup Flow] Skipping GitHub setup');
setCurrentStep('complete');
@@ -160,35 +116,15 @@ export function SetupView() {
handleNext('theme')} onBack={() => handleBack('theme')} />
)}
- {(currentStep === 'claude_detect' || currentStep === 'claude_auth') && (
- handleNext('claude')}
- onBack={() => handleBack('claude')}
- onSkip={handleSkipClaude}
- />
- )}
-
- {currentStep === 'cursor' && (
- handleNext('cursor')}
- onBack={() => handleBack('cursor')}
- onSkip={handleSkipCursor}
- />
- )}
-
- {currentStep === 'codex' && (
- handleNext('codex')}
- onBack={() => handleBack('codex')}
- onSkip={handleSkipCodex}
- />
- )}
-
- {currentStep === 'opencode' && (
- handleNext('opencode')}
- onBack={() => handleBack('opencode')}
- onSkip={handleSkipOpencode}
+ {(currentStep === 'providers' ||
+ currentStep === 'claude_detect' ||
+ currentStep === 'claude_auth' ||
+ currentStep === 'cursor' ||
+ currentStep === 'codex' ||
+ currentStep === 'opencode') && (
+ handleNext('providers')}
+ onBack={() => handleBack('providers')}
/>
)}
diff --git a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
index 9637a081..8b56f49c 100644
--- a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
@@ -38,6 +38,11 @@ interface ClaudeSetupStepProps {
onSkip: () => void;
}
+interface ClaudeSetupContentProps {
+ /** Hide header and navigation for embedded use */
+ embedded?: boolean;
+}
+
type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error';
// Claude Setup Step
diff --git a/apps/ui/src/components/views/setup-view/steps/index.ts b/apps/ui/src/components/views/setup-view/steps/index.ts
index 0c25aaed..f6497647 100644
--- a/apps/ui/src/components/views/setup-view/steps/index.ts
+++ b/apps/ui/src/components/views/setup-view/steps/index.ts
@@ -2,8 +2,11 @@
export { WelcomeStep } from './welcome-step';
export { ThemeStep } from './theme-step';
export { CompleteStep } from './complete-step';
+export { ProvidersSetupStep } from './providers-setup-step';
+export { GitHubSetupStep } from './github-setup-step';
+
+// Legacy individual step exports (kept for backwards compatibility)
export { ClaudeSetupStep } from './claude-setup-step';
export { CursorSetupStep } from './cursor-setup-step';
export { CodexSetupStep } from './codex-setup-step';
export { OpencodeSetupStep } from './opencode-setup-step';
-export { GitHubSetupStep } from './github-setup-step';
diff --git a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
index a185d888..afb40b6d 100644
--- a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
@@ -96,7 +96,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
try {
// Copy login command to clipboard and show instructions
- const loginCommand = opencodeCliStatus?.loginCommand || 'opencode login';
+ const loginCommand = opencodeCliStatus?.loginCommand || 'opencode auth login';
await navigator.clipboard.writeText(loginCommand);
toast.info('Login command copied! Paste in terminal to authenticate.');
@@ -297,13 +297,13 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
- {opencodeCliStatus?.loginCommand || 'opencode login'}
+ {opencodeCliStatus?.loginCommand || 'opencode auth login'}