mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
Implement multi-environment authentication for Roo Code Cloud
This commit is contained in:
parent
53e6419fab
commit
6b46b46a0a
7 changed files with 720 additions and 59 deletions
238
MULTI_ENVIRONMENT_AUTH_IMPLEMENTATION.md
Normal file
238
MULTI_ENVIRONMENT_AUTH_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
# Multi-Environment Authentication Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds support for being logged into multiple Roo Code Cloud environments simultaneously. Previously, the system only supported authentication to a single environment at a time. Now users can authenticate to multiple environments (e.g., production, staging, development) and switch between them seamlessly.
|
||||
|
||||
## Architecture Changes
|
||||
|
||||
### 1. CloudService Refactoring
|
||||
|
||||
**Before**: Single singleton instance managing one authentication session
|
||||
**After**: Multiple instances managed by environment ID, with an active environment concept
|
||||
|
||||
Key changes:
|
||||
- `CloudService` now manages multiple instances via `_instances` map
|
||||
- Each instance is associated with a specific `CloudEnvironment`
|
||||
- Active environment concept allows switching context without losing other sessions
|
||||
- Environment-specific method variants (e.g., `loginToEnvironment()`, `logoutFromEnvironment()`)
|
||||
|
||||
### 2. AuthService Updates
|
||||
|
||||
**Before**: Single credential storage with fixed keys
|
||||
**After**: Environment-specific credential storage with dynamic keys
|
||||
|
||||
Key changes:
|
||||
- Constructor now requires `CloudEnvironment` parameter
|
||||
- Credential storage keys are environment-specific: `clerk-auth-credentials-{environmentId}`
|
||||
- State keys are environment-specific: `clerk-auth-state-{environmentId}`
|
||||
- Login URLs include environment parameter for proper callback handling
|
||||
- Logging includes environment ID for better debugging
|
||||
|
||||
### 3. New EnvironmentManager Service
|
||||
|
||||
A new service that orchestrates multiple CloudService instances:
|
||||
- Loads environment configuration from VS Code settings
|
||||
- Initializes CloudService instances for each configured environment
|
||||
- Provides high-level environment management operations
|
||||
- Handles environment switching and authentication coordination
|
||||
|
||||
## Configuration
|
||||
|
||||
### VS Code Settings
|
||||
|
||||
Add environment configuration to your VS Code settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"rooCode.cloud": {
|
||||
"environments": [
|
||||
{
|
||||
"id": "production",
|
||||
"name": "Roo Code Cloud (Production)"
|
||||
},
|
||||
{
|
||||
"id": "staging",
|
||||
"name": "Roo Code Cloud (Staging)",
|
||||
"apiUrl": "https://staging-api.roocode.com",
|
||||
"clerkBaseUrl": "https://staging-clerk.roocode.com"
|
||||
},
|
||||
{
|
||||
"id": "development",
|
||||
"name": "Roo Code Cloud (Development)",
|
||||
"apiUrl": "https://dev-api.roocode.com",
|
||||
"clerkBaseUrl": "https://dev-clerk.roocode.com"
|
||||
}
|
||||
],
|
||||
"defaultEnvironment": "production"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no environments are configured, the system defaults to a single production environment.
|
||||
|
||||
## New Message Types
|
||||
|
||||
### WebviewMessage (from UI to extension)
|
||||
|
||||
- `rooCloudSignInToEnvironment` - Sign in to specific environment
|
||||
- `rooCloudSignOutFromEnvironment` - Sign out from specific environment
|
||||
- `rooCloudSetActiveEnvironment` - Switch active environment
|
||||
- `rooCloudGetEnvironments` - Request list of all environments
|
||||
- `rooCloudGetAuthenticatedEnvironments` - Request list of authenticated environments
|
||||
- `rooCloudLogoutFromAllEnvironments` - Sign out from all environments
|
||||
|
||||
### ExtensionMessage (from extension to UI)
|
||||
|
||||
- `cloudEnvironments` - List of all configured environments
|
||||
- `cloudAuthenticatedEnvironments` - List of authenticated environments
|
||||
- `cloudActiveEnvironment` - Currently active environment
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Backend Usage
|
||||
|
||||
```typescript
|
||||
// Initialize environment manager
|
||||
const environmentManager = await EnvironmentManager.createInstance(context, callbacks)
|
||||
|
||||
// Get all environments
|
||||
const environments = environmentManager.getEnvironments()
|
||||
|
||||
// Get authenticated environments
|
||||
const authenticatedEnvs = environmentManager.getAuthenticatedEnvironments()
|
||||
|
||||
// Login to specific environment
|
||||
await environmentManager.loginToEnvironment('staging')
|
||||
|
||||
// Switch active environment
|
||||
environmentManager.setActiveEnvironment('development')
|
||||
|
||||
// Check authentication status
|
||||
const isAuthenticated = environmentManager.isAuthenticated('production')
|
||||
|
||||
// Get user info for specific environment
|
||||
const userInfo = environmentManager.getUserInfo('staging')
|
||||
```
|
||||
|
||||
### Frontend Usage
|
||||
|
||||
```typescript
|
||||
// Sign in to specific environment
|
||||
vscode.postMessage({
|
||||
type: "rooCloudSignInToEnvironment",
|
||||
environmentId: "staging"
|
||||
})
|
||||
|
||||
// Sign out from specific environment
|
||||
vscode.postMessage({
|
||||
type: "rooCloudSignOutFromEnvironment",
|
||||
environmentId: "production"
|
||||
})
|
||||
|
||||
// Switch active environment
|
||||
vscode.postMessage({
|
||||
type: "rooCloudSetActiveEnvironment",
|
||||
environmentId: "development"
|
||||
})
|
||||
|
||||
// Get list of environments
|
||||
vscode.postMessage({ type: "rooCloudGetEnvironments" })
|
||||
|
||||
// Sign out from all environments
|
||||
vscode.postMessage({ type: "rooCloudLogoutFromAllEnvironments" })
|
||||
```
|
||||
|
||||
## Data Storage
|
||||
|
||||
### Credential Storage
|
||||
|
||||
Each environment stores its credentials separately:
|
||||
- Production: `clerk-auth-credentials-production`
|
||||
- Staging: `clerk-auth-credentials-staging`
|
||||
- Development: `clerk-auth-credentials-development`
|
||||
|
||||
### State Storage
|
||||
|
||||
Each environment stores its auth state separately:
|
||||
- Production: `clerk-auth-state-production`
|
||||
- Staging: `clerk-auth-state-staging`
|
||||
- Development: `clerk-auth-state-development`
|
||||
|
||||
## Error Handling
|
||||
|
||||
The implementation includes comprehensive error handling:
|
||||
- Environment-specific error logging with environment ID prefixes
|
||||
- Graceful handling of missing environments
|
||||
- Proper cleanup on environment removal
|
||||
- Fallback to default environment on active environment removal
|
||||
|
||||
## Migration
|
||||
|
||||
### Existing Users
|
||||
|
||||
Existing users with single-environment authentication will:
|
||||
1. Have their existing credentials migrated to the default "production" environment
|
||||
2. Continue to work without any configuration changes
|
||||
3. Can optionally configure additional environments as needed
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
The original `CloudService.instance` getter still works and returns the active environment's instance, ensuring existing code continues to function.
|
||||
|
||||
## UI Integration
|
||||
|
||||
The account view in the webview needs to be updated to:
|
||||
1. Display available environments
|
||||
2. Show authentication status per environment
|
||||
3. Provide environment-specific sign in/out buttons
|
||||
4. Allow switching between active environments
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Each environment maintains separate authentication sessions
|
||||
- Credentials are stored using VS Code's secure secret storage
|
||||
- Environment-specific state prevents cross-environment data leakage
|
||||
- Proper logout procedures clear environment-specific data
|
||||
|
||||
## Testing
|
||||
|
||||
To test the multi-environment authentication:
|
||||
|
||||
1. Configure multiple environments in VS Code settings
|
||||
2. Sign in to different environments
|
||||
3. Verify separate credential storage
|
||||
4. Test environment switching
|
||||
5. Verify proper logout behavior
|
||||
6. Test error scenarios (invalid environments, network issues)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Environment not found**: Check VS Code settings configuration
|
||||
2. **Authentication failures**: Verify API URLs and environment configuration
|
||||
3. **Credential conflicts**: Clear existing credentials and re-authenticate
|
||||
4. **State inconsistencies**: Use "Sign out from all environments" to reset
|
||||
|
||||
### Debug Logging
|
||||
|
||||
All operations include environment-specific logging:
|
||||
```
|
||||
[CloudService:production] Successfully authenticated
|
||||
[auth:staging] Transitioned to active-session state
|
||||
[EnvironmentManager] Set active environment to: development
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements:
|
||||
- Environment-specific user preferences
|
||||
- Cross-environment data synchronization options
|
||||
- Environment health monitoring
|
||||
- Bulk operations across environments
|
||||
- Environment templates for common configurations
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This implementation maintains backward compatibility while adding powerful multi-environment capabilities. The modular design allows for easy extension and maintenance, with clear separation of concerns between authentication, environment management, and UI coordination.
|
||||
|
|
@ -17,6 +17,13 @@ export interface AuthServiceEvents {
|
|||
"user-info": [data: { userInfo: CloudUserInfo }]
|
||||
}
|
||||
|
||||
export interface CloudEnvironment {
|
||||
id: string
|
||||
name: string
|
||||
apiUrl?: string
|
||||
clerkBaseUrl?: string
|
||||
}
|
||||
|
||||
const authCredentialsSchema = z.object({
|
||||
clientToken: z.string().min(1, "Client token cannot be empty"),
|
||||
sessionId: z.string().min(1, "Session ID cannot be empty"),
|
||||
|
|
@ -24,8 +31,8 @@ const authCredentialsSchema = z.object({
|
|||
|
||||
type AuthCredentials = z.infer<typeof authCredentialsSchema>
|
||||
|
||||
const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials"
|
||||
const AUTH_STATE_KEY = "clerk-auth-state"
|
||||
const AUTH_CREDENTIALS_KEY_PREFIX = "clerk-auth-credentials"
|
||||
const AUTH_STATE_KEY_PREFIX = "clerk-auth-state"
|
||||
|
||||
type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
|
||||
|
||||
|
|
@ -85,6 +92,7 @@ class InvalidClientTokenError extends Error {
|
|||
}
|
||||
|
||||
export class AuthService extends EventEmitter<AuthServiceEvents> {
|
||||
private environment: CloudEnvironment
|
||||
private context: vscode.ExtensionContext
|
||||
private timer: RefreshTimer
|
||||
private state: AuthState = "initializing"
|
||||
|
|
@ -94,9 +102,10 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
private sessionToken: string | null = null
|
||||
private userInfo: CloudUserInfo | null = null
|
||||
|
||||
constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
|
||||
constructor(environment: CloudEnvironment, context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
|
||||
super()
|
||||
|
||||
this.environment = environment
|
||||
this.context = context
|
||||
this.log = log || console.log
|
||||
|
||||
|
|
@ -111,6 +120,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
})
|
||||
}
|
||||
|
||||
private getCredentialsKey(): string {
|
||||
return `${AUTH_CREDENTIALS_KEY_PREFIX}-${this.environment.id}`
|
||||
}
|
||||
|
||||
private getStateKey(): string {
|
||||
return `${AUTH_STATE_KEY_PREFIX}-${this.environment.id}`
|
||||
}
|
||||
|
||||
private async handleCredentialsChange(): Promise<void> {
|
||||
try {
|
||||
const credentials = await this.loadCredentials()
|
||||
|
|
@ -129,7 +146,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[auth] Error handling credentials change:", error)
|
||||
this.log(`[auth:${this.environment.id}] Error handling credentials change:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +162,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
|
||||
this.emit("logged-out", { previousState })
|
||||
|
||||
this.log("[auth] Transitioned to logged-out state")
|
||||
this.log(`[auth:${this.environment.id}] Transitioned to logged-out state`)
|
||||
}
|
||||
|
||||
private transitionToInactiveSession(credentials: AuthCredentials): void {
|
||||
|
|
@ -161,7 +178,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
|
||||
this.timer.start()
|
||||
|
||||
this.log("[auth] Transitioned to inactive-session state")
|
||||
this.log(`[auth:${this.environment.id}] Transitioned to inactive-session state`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,7 +189,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
if (this.state !== "initializing") {
|
||||
this.log("[auth] initialize() called after already initialized")
|
||||
this.log(`[auth:${this.environment.id}] initialize() called after already initialized`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +197,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
|
||||
this.context.subscriptions.push(
|
||||
this.context.secrets.onDidChange((e) => {
|
||||
if (e.key === AUTH_CREDENTIALS_KEY) {
|
||||
if (e.key === this.getCredentialsKey()) {
|
||||
this.handleCredentialsChange()
|
||||
}
|
||||
}),
|
||||
|
|
@ -188,11 +205,11 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
}
|
||||
|
||||
private async storeCredentials(credentials: AuthCredentials): Promise<void> {
|
||||
await this.context.secrets.store(AUTH_CREDENTIALS_KEY, JSON.stringify(credentials))
|
||||
await this.context.secrets.store(this.getCredentialsKey(), JSON.stringify(credentials))
|
||||
}
|
||||
|
||||
private async loadCredentials(): Promise<AuthCredentials | null> {
|
||||
const credentialsJson = await this.context.secrets.get(AUTH_CREDENTIALS_KEY)
|
||||
const credentialsJson = await this.context.secrets.get(this.getCredentialsKey())
|
||||
if (!credentialsJson) return null
|
||||
|
||||
try {
|
||||
|
|
@ -200,16 +217,16 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
return authCredentialsSchema.parse(parsedJson)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
this.log("[auth] Invalid credentials format:", error.errors)
|
||||
this.log(`[auth:${this.environment.id}] Invalid credentials format:`, error.errors)
|
||||
} else {
|
||||
this.log("[auth] Failed to parse stored credentials:", error)
|
||||
this.log(`[auth:${this.environment.id}] Failed to parse stored credentials:`, error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async clearCredentials(): Promise<void> {
|
||||
await this.context.secrets.delete(AUTH_CREDENTIALS_KEY)
|
||||
await this.context.secrets.delete(this.getCredentialsKey())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,19 +239,21 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
try {
|
||||
// Generate a cryptographically random state parameter.
|
||||
const state = crypto.randomBytes(16).toString("hex")
|
||||
await this.context.globalState.update(AUTH_STATE_KEY, state)
|
||||
await this.context.globalState.update(this.getStateKey(), state)
|
||||
const packageJSON = this.context.extension?.packageJSON
|
||||
const publisher = packageJSON?.publisher ?? "RooVeterinaryInc"
|
||||
const name = packageJSON?.name ?? "roo-cline"
|
||||
const params = new URLSearchParams({
|
||||
state,
|
||||
auth_redirect: `${vscode.env.uriScheme}://${publisher}.${name}`,
|
||||
environment: this.environment.id,
|
||||
})
|
||||
const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}`
|
||||
const apiUrl = this.environment.apiUrl || getRooCodeApiUrl()
|
||||
const url = `${apiUrl}/extension/sign-in?${params.toString()}`
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url))
|
||||
} catch (error) {
|
||||
this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
|
||||
throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`)
|
||||
this.log(`[auth:${this.environment.id}] Error initiating Roo Code Cloud auth: ${error}`)
|
||||
throw new Error(`Failed to initiate Roo Code Cloud authentication for ${this.environment.name}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,10 +274,10 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
|
||||
try {
|
||||
// Validate state parameter to prevent CSRF attacks.
|
||||
const storedState = this.context.globalState.get(AUTH_STATE_KEY)
|
||||
const storedState = this.context.globalState.get(this.getStateKey())
|
||||
|
||||
if (state !== storedState) {
|
||||
this.log("[auth] State mismatch in callback")
|
||||
this.log(`[auth:${this.environment.id}] State mismatch in callback`)
|
||||
throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
|
||||
}
|
||||
|
||||
|
|
@ -266,14 +285,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
|
||||
await this.storeCredentials(credentials)
|
||||
|
||||
vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
|
||||
this.log("[auth] Successfully authenticated with Roo Code Cloud")
|
||||
vscode.window.showInformationMessage(`Successfully authenticated with ${this.environment.name}`)
|
||||
this.log(`[auth:${this.environment.id}] Successfully authenticated with ${this.environment.name}`)
|
||||
} catch (error) {
|
||||
this.log(`[auth] Error handling Roo Code Cloud callback: ${error}`)
|
||||
this.log(`[auth:${this.environment.id}] Error handling Roo Code Cloud callback: ${error}`)
|
||||
const previousState = this.state
|
||||
this.state = "logged-out"
|
||||
this.emit("logged-out", { previousState })
|
||||
throw new Error(`Failed to handle Roo Code Cloud callback: ${error}`)
|
||||
throw new Error(`Failed to handle Roo Code Cloud callback for ${this.environment.name}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -288,24 +307,28 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
try {
|
||||
// Clear credentials from storage - onDidChange will handle state transitions
|
||||
await this.clearCredentials()
|
||||
await this.context.globalState.update(AUTH_STATE_KEY, undefined)
|
||||
await this.context.globalState.update(this.getStateKey(), undefined)
|
||||
|
||||
if (oldCredentials) {
|
||||
try {
|
||||
await this.clerkLogout(oldCredentials)
|
||||
} catch (error) {
|
||||
this.log("[auth] Error calling clerkLogout:", error)
|
||||
this.log(`[auth:${this.environment.id}] Error calling clerkLogout:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("Logged out from Roo Code Cloud")
|
||||
this.log("[auth] Logged out from Roo Code Cloud")
|
||||
vscode.window.showInformationMessage(`Logged out from ${this.environment.name}`)
|
||||
this.log(`[auth:${this.environment.id}] Logged out from ${this.environment.name}`)
|
||||
} catch (error) {
|
||||
this.log(`[auth] Error logging out from Roo Code Cloud: ${error}`)
|
||||
throw new Error(`Failed to log out from Roo Code Cloud: ${error}`)
|
||||
this.log(`[auth:${this.environment.id}] Error logging out from Roo Code Cloud: ${error}`)
|
||||
throw new Error(`Failed to log out from ${this.environment.name}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
public getEnvironment(): CloudEnvironment {
|
||||
return this.environment
|
||||
}
|
||||
|
||||
public getState(): AuthState {
|
||||
return this.state
|
||||
}
|
||||
|
|
@ -338,7 +361,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
*/
|
||||
private async refreshSession(): Promise<void> {
|
||||
if (!this.credentials) {
|
||||
this.log("[auth] Cannot refresh session: missing credentials")
|
||||
this.log(`[auth:${this.environment.id}] Cannot refresh session: missing credentials`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -348,16 +371,16 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
this.state = "active-session"
|
||||
|
||||
if (previousState !== "active-session") {
|
||||
this.log("[auth] Transitioned to active-session state")
|
||||
this.log(`[auth:${this.environment.id}] Transitioned to active-session state`)
|
||||
this.emit("active-session", { previousState })
|
||||
this.fetchUserInfo()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidClientTokenError) {
|
||||
this.log("[auth] Invalid/Expired client token: clearing credentials")
|
||||
this.log(`[auth:${this.environment.id}] Invalid/Expired client token: clearing credentials`)
|
||||
this.clearCredentials()
|
||||
}
|
||||
this.log("[auth] Failed to refresh session", error)
|
||||
this.log(`[auth:${this.environment.id}] Failed to refresh session`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
@ -483,7 +506,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[auth] Failed to fetch organization memberships:", error)
|
||||
this.log(`[auth:${this.environment.id}] Failed to fetch organization memberships:`, error)
|
||||
// Don't throw - organization info is optional
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList } from "@roo-
|
|||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { CloudServiceCallbacks } from "./types"
|
||||
import { AuthService } from "./AuthService"
|
||||
import { AuthService, CloudEnvironment } from "./AuthService"
|
||||
import { SettingsService } from "./SettingsService"
|
||||
import { TelemetryClient } from "./TelemetryClient"
|
||||
import { ShareService } from "./ShareService"
|
||||
|
||||
export class CloudService {
|
||||
private static _instance: CloudService | null = null
|
||||
export { CloudEnvironment }
|
||||
|
||||
export class CloudService {
|
||||
private static _instances: { [environmentId: string]: CloudService } = {}
|
||||
private static _activeEnvironmentId: string | null = null
|
||||
|
||||
private environment: CloudEnvironment
|
||||
private context: vscode.ExtensionContext
|
||||
private callbacks: CloudServiceCallbacks
|
||||
private authListener: () => void
|
||||
|
|
@ -22,7 +26,8 @@ export class CloudService {
|
|||
private isInitialized = false
|
||||
private log: (...args: unknown[]) => void
|
||||
|
||||
private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) {
|
||||
private constructor(environment: CloudEnvironment, context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) {
|
||||
this.environment = environment
|
||||
this.context = context
|
||||
this.callbacks = callbacks
|
||||
this.log = callbacks.log || console.log
|
||||
|
|
@ -37,7 +42,7 @@ export class CloudService {
|
|||
}
|
||||
|
||||
try {
|
||||
this.authService = new AuthService(this.context, this.log)
|
||||
this.authService = new AuthService(this.environment, this.context, this.log)
|
||||
await this.authService.initialize()
|
||||
|
||||
this.authService.on("inactive-session", this.authListener)
|
||||
|
|
@ -57,16 +62,30 @@ export class CloudService {
|
|||
try {
|
||||
TelemetryService.instance.register(this.telemetryClient)
|
||||
} catch (error) {
|
||||
this.log("[CloudService] Failed to register TelemetryClient:", error)
|
||||
this.log(`[CloudService:${this.environment.id}] Failed to register TelemetryClient:`, error)
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
} catch (error) {
|
||||
this.log("[CloudService] Failed to initialize:", error)
|
||||
throw new Error(`Failed to initialize CloudService: ${error}`)
|
||||
this.log(`[CloudService:${this.environment.id}] Failed to initialize:`, error)
|
||||
throw new Error(`Failed to initialize CloudService for ${this.environment.name}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Environment Management
|
||||
|
||||
public getEnvironment(): CloudEnvironment {
|
||||
return this.environment
|
||||
}
|
||||
|
||||
public getEnvironmentId(): string {
|
||||
return this.environment.id
|
||||
}
|
||||
|
||||
public getEnvironmentName(): string {
|
||||
return this.environment.name
|
||||
}
|
||||
|
||||
// AuthService
|
||||
|
||||
public async login(): Promise<void> {
|
||||
|
|
@ -165,43 +184,121 @@ export class CloudService {
|
|||
|
||||
private ensureInitialized(): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error("CloudService not initialized.")
|
||||
throw new Error(`CloudService for ${this.environment.name} not initialized.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods for managing multiple instances
|
||||
|
||||
static getActiveEnvironmentId(): string | null {
|
||||
return this._activeEnvironmentId
|
||||
}
|
||||
|
||||
static getAllInstances(): { [environmentId: string]: CloudService } {
|
||||
return { ...this._instances }
|
||||
}
|
||||
|
||||
static get instance(): CloudService {
|
||||
if (!this._instance) {
|
||||
throw new Error("CloudService not initialized")
|
||||
const activeId = this._activeEnvironmentId
|
||||
if (!activeId || !this._instances[activeId]) {
|
||||
throw new Error("No active CloudService instance")
|
||||
}
|
||||
return this._instances[activeId]
|
||||
}
|
||||
|
||||
return this._instance
|
||||
static getInstance(environmentId: string): CloudService | null {
|
||||
return this._instances[environmentId] || null
|
||||
}
|
||||
|
||||
static setActiveEnvironment(environmentId: string): void {
|
||||
if (!this._instances[environmentId]) {
|
||||
throw new Error(`CloudService instance for environment '${environmentId}' not found`)
|
||||
}
|
||||
this._activeEnvironmentId = environmentId
|
||||
}
|
||||
|
||||
static async createInstance(
|
||||
environment: CloudEnvironment,
|
||||
context: vscode.ExtensionContext,
|
||||
callbacks: CloudServiceCallbacks = {},
|
||||
): Promise<CloudService> {
|
||||
if (this._instance) {
|
||||
throw new Error("CloudService instance already created")
|
||||
if (this._instances[environment.id]) {
|
||||
throw new Error(`CloudService instance for environment '${environment.id}' already exists`)
|
||||
}
|
||||
|
||||
this._instance = new CloudService(context, callbacks)
|
||||
await this._instance.initialize()
|
||||
return this._instance
|
||||
const instance = new CloudService(environment, context, callbacks)
|
||||
await instance.initialize()
|
||||
this._instances[environment.id] = instance
|
||||
|
||||
// Set as active if it's the first instance
|
||||
if (!this._activeEnvironmentId) {
|
||||
this._activeEnvironmentId = environment.id
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
static hasInstance(): boolean {
|
||||
return this._instance !== null && this._instance.isInitialized
|
||||
static hasInstance(environmentId?: string): boolean {
|
||||
if (environmentId) {
|
||||
const instance = this._instances[environmentId]
|
||||
return instance !== undefined && instance.isInitialized
|
||||
}
|
||||
return this._activeEnvironmentId !== null && this.hasInstance(this._activeEnvironmentId)
|
||||
}
|
||||
|
||||
static resetInstance(): void {
|
||||
if (this._instance) {
|
||||
this._instance.dispose()
|
||||
this._instance = null
|
||||
static removeInstance(environmentId: string): void {
|
||||
const instance = this._instances[environmentId]
|
||||
if (instance) {
|
||||
instance.dispose()
|
||||
delete this._instances[environmentId]
|
||||
|
||||
// Update active environment if needed
|
||||
if (this._activeEnvironmentId === environmentId) {
|
||||
const remainingIds = Object.keys(this._instances)
|
||||
this._activeEnvironmentId = remainingIds.length > 0 ? remainingIds[0] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static isEnabled(): boolean {
|
||||
return !!this._instance?.isAuthenticated()
|
||||
static resetAllInstances(): void {
|
||||
for (const instance of Object.values(this._instances)) {
|
||||
instance.dispose()
|
||||
}
|
||||
this._instances = {}
|
||||
this._activeEnvironmentId = null
|
||||
}
|
||||
|
||||
static isEnabled(environmentId?: string): boolean {
|
||||
if (environmentId) {
|
||||
const instance = this._instances[environmentId]
|
||||
return !!instance?.isAuthenticated()
|
||||
}
|
||||
return !!this.instance?.isAuthenticated()
|
||||
}
|
||||
|
||||
static getAuthenticatedEnvironments(): string[] {
|
||||
const authenticated: string[] = []
|
||||
for (const [envId, instance] of Object.entries(this._instances)) {
|
||||
if (instance.isAuthenticated()) {
|
||||
authenticated.push(envId)
|
||||
}
|
||||
}
|
||||
return authenticated
|
||||
}
|
||||
|
||||
static async loginToEnvironment(environmentId: string): Promise<void> {
|
||||
const instance = this._instances[environmentId]
|
||||
if (!instance) {
|
||||
throw new Error(`CloudService instance for environment '${environmentId}' not found`)
|
||||
}
|
||||
return instance.login()
|
||||
}
|
||||
|
||||
static async logoutFromEnvironment(environmentId: string): Promise<void> {
|
||||
const instance = this._instances[environmentId]
|
||||
if (!instance) {
|
||||
throw new Error(`CloudService instance for environment '${environmentId}' not found`)
|
||||
}
|
||||
return instance.logout()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
180
packages/cloud/src/EnvironmentManager.ts
Normal file
180
packages/cloud/src/EnvironmentManager.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import * as vscode from "vscode"
|
||||
import { CloudService, CloudEnvironment } from "./CloudService"
|
||||
import { CloudServiceCallbacks } from "./types"
|
||||
|
||||
export interface EnvironmentConfig {
|
||||
environments: CloudEnvironment[]
|
||||
defaultEnvironmentId?: string
|
||||
}
|
||||
|
||||
export class EnvironmentManager {
|
||||
private static _instance: EnvironmentManager | null = null
|
||||
private context: vscode.ExtensionContext
|
||||
private callbacks: CloudServiceCallbacks
|
||||
private log: (...args: unknown[]) => void
|
||||
|
||||
private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks = {}) {
|
||||
this.context = context
|
||||
this.callbacks = callbacks
|
||||
this.log = callbacks.log || console.log
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
// Load environment configuration from VS Code settings
|
||||
const config = this.loadEnvironmentConfig()
|
||||
|
||||
// Initialize CloudService instances for each environment
|
||||
for (const environment of config.environments) {
|
||||
try {
|
||||
await CloudService.createInstance(environment, this.context, this.callbacks)
|
||||
this.log(`[EnvironmentManager] Initialized environment: ${environment.name}`)
|
||||
} catch (error) {
|
||||
// Skip if instance already exists
|
||||
if (!error.message.includes('already exists')) {
|
||||
this.log(`[EnvironmentManager] Failed to initialize environment ${environment.name}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set default active environment
|
||||
if (config.defaultEnvironmentId) {
|
||||
try {
|
||||
CloudService.setActiveEnvironment(config.defaultEnvironmentId)
|
||||
} catch (error) {
|
||||
this.log(`[EnvironmentManager] Failed to set default environment:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadEnvironmentConfig(): EnvironmentConfig {
|
||||
const workspaceConfig = vscode.workspace.getConfiguration('rooCode.cloud')
|
||||
const environments = workspaceConfig.get<CloudEnvironment[]>('environments', [])
|
||||
|
||||
// If no environments are configured, use the default production environment
|
||||
if (environments.length === 0) {
|
||||
environments.push({
|
||||
id: 'production',
|
||||
name: 'Roo Code Cloud (Production)',
|
||||
})
|
||||
}
|
||||
|
||||
const defaultEnvironmentId = workspaceConfig.get<string>('defaultEnvironment') || environments[0]?.id
|
||||
|
||||
return {
|
||||
environments,
|
||||
defaultEnvironmentId
|
||||
}
|
||||
}
|
||||
|
||||
public getEnvironments(): CloudEnvironment[] {
|
||||
const instances = CloudService.getAllInstances()
|
||||
const environments: CloudEnvironment[] = []
|
||||
for (const environmentId in instances) {
|
||||
if (instances.hasOwnProperty(environmentId)) {
|
||||
environments.push(instances[environmentId].getEnvironment())
|
||||
}
|
||||
}
|
||||
return environments
|
||||
}
|
||||
|
||||
public getAuthenticatedEnvironments(): CloudEnvironment[] {
|
||||
const authenticatedIds = CloudService.getAuthenticatedEnvironments()
|
||||
const instances = CloudService.getAllInstances()
|
||||
return authenticatedIds
|
||||
.map(id => instances[id])
|
||||
.filter(instance => instance)
|
||||
.map(instance => instance.getEnvironment())
|
||||
}
|
||||
|
||||
public async loginToEnvironment(environmentId: string): Promise<void> {
|
||||
try {
|
||||
await CloudService.loginToEnvironment(environmentId)
|
||||
this.log(`[EnvironmentManager] Successfully logged into environment: ${environmentId}`)
|
||||
} catch (error) {
|
||||
this.log(`[EnvironmentManager] Failed to login to environment ${environmentId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async logoutFromEnvironment(environmentId: string): Promise<void> {
|
||||
try {
|
||||
await CloudService.logoutFromEnvironment(environmentId)
|
||||
this.log(`[EnvironmentManager] Successfully logged out from environment: ${environmentId}`)
|
||||
} catch (error) {
|
||||
this.log(`[EnvironmentManager] Failed to logout from environment ${environmentId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async logoutFromAllEnvironments(): Promise<void> {
|
||||
const authenticatedIds = CloudService.getAuthenticatedEnvironments()
|
||||
const results = await Promise.allSettled(
|
||||
authenticatedIds.map(id => this.logoutFromEnvironment(id))
|
||||
)
|
||||
|
||||
// Log any failures
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.log(`[EnvironmentManager] Failed to logout from ${authenticatedIds[index]}:`, result.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getActiveEnvironment(): CloudEnvironment | null {
|
||||
const activeId = CloudService.getActiveEnvironmentId()
|
||||
if (!activeId) return null
|
||||
|
||||
const instance = CloudService.getInstance(activeId)
|
||||
return instance?.getEnvironment() || null
|
||||
}
|
||||
|
||||
public setActiveEnvironment(environmentId: string): void {
|
||||
CloudService.setActiveEnvironment(environmentId)
|
||||
this.log(`[EnvironmentManager] Set active environment to: ${environmentId}`)
|
||||
}
|
||||
|
||||
public isAuthenticated(environmentId?: string): boolean {
|
||||
return CloudService.isEnabled(environmentId)
|
||||
}
|
||||
|
||||
public getUserInfo(environmentId?: string): any {
|
||||
if (environmentId) {
|
||||
const instance = CloudService.getInstance(environmentId)
|
||||
return instance?.getUserInfo() || null
|
||||
}
|
||||
return CloudService.instance?.getUserInfo() || null
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
CloudService.resetAllInstances()
|
||||
}
|
||||
|
||||
// Static methods
|
||||
static get instance(): EnvironmentManager {
|
||||
if (!this._instance) {
|
||||
throw new Error("EnvironmentManager not initialized")
|
||||
}
|
||||
return this._instance
|
||||
}
|
||||
|
||||
static async createInstance(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks = {}): Promise<EnvironmentManager> {
|
||||
if (this._instance) {
|
||||
throw new Error("EnvironmentManager instance already exists")
|
||||
}
|
||||
|
||||
this._instance = new EnvironmentManager(context, callbacks)
|
||||
await this._instance.initialize()
|
||||
return this._instance
|
||||
}
|
||||
|
||||
static hasInstance(): boolean {
|
||||
return this._instance !== null
|
||||
}
|
||||
|
||||
static resetInstance(): void {
|
||||
if (this._instance) {
|
||||
this._instance.dispose()
|
||||
this._instance = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1414,6 +1414,101 @@ export const webviewMessageHandler = async (
|
|||
|
||||
break
|
||||
}
|
||||
case "rooCloudSignInToEnvironment": {
|
||||
if (message.environmentId) {
|
||||
try {
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
|
||||
await CloudService.loginToEnvironment(message.environmentId)
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
provider.log(`AuthService#loginToEnvironment failed for ${message.environmentId}: ${error}`)
|
||||
vscode.window.showErrorMessage(`Sign in to ${message.environmentId} failed.`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudSignOutFromEnvironment": {
|
||||
if (message.environmentId) {
|
||||
try {
|
||||
await CloudService.logoutFromEnvironment(message.environmentId)
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
provider.log(`AuthService#logoutFromEnvironment failed for ${message.environmentId}: ${error}`)
|
||||
vscode.window.showErrorMessage(`Sign out from ${message.environmentId} failed.`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudSetActiveEnvironment": {
|
||||
if (message.environmentId) {
|
||||
try {
|
||||
CloudService.setActiveEnvironment(message.environmentId)
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
provider.log(`Failed to set active environment to ${message.environmentId}: ${error}`)
|
||||
vscode.window.showErrorMessage(`Failed to switch to ${message.environmentId}.`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudGetEnvironments": {
|
||||
try {
|
||||
const instances = CloudService.getAllInstances()
|
||||
const environments: any[] = []
|
||||
for (const environmentId in instances) {
|
||||
if (instances.hasOwnProperty(environmentId)) {
|
||||
environments.push(instances[environmentId].getEnvironment())
|
||||
}
|
||||
}
|
||||
provider.postMessageToWebview({
|
||||
type: "cloudEnvironments",
|
||||
environments
|
||||
})
|
||||
} catch (error) {
|
||||
provider.log(`Failed to get environments: ${error}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudGetAuthenticatedEnvironments": {
|
||||
try {
|
||||
const authenticatedIds = CloudService.getAuthenticatedEnvironments()
|
||||
const instances = CloudService.getAllInstances()
|
||||
const authenticatedEnvironments: any[] = []
|
||||
|
||||
for (const id of authenticatedIds) {
|
||||
const instance = instances[id]
|
||||
if (instance) {
|
||||
authenticatedEnvironments.push(instance.getEnvironment())
|
||||
}
|
||||
}
|
||||
|
||||
provider.postMessageToWebview({
|
||||
type: "cloudAuthenticatedEnvironments",
|
||||
authenticatedEnvironments
|
||||
})
|
||||
} catch (error) {
|
||||
provider.log(`Failed to get authenticated environments: ${error}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudLogoutFromAllEnvironments": {
|
||||
try {
|
||||
const authenticatedIds = CloudService.getAuthenticatedEnvironments()
|
||||
const logoutPromises: Promise<void>[] = []
|
||||
|
||||
for (const id of authenticatedIds) {
|
||||
logoutPromises.push(CloudService.logoutFromEnvironment(id))
|
||||
}
|
||||
|
||||
await Promise.allSettled(logoutPromises)
|
||||
await provider.postStateToWebview()
|
||||
provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined })
|
||||
} catch (error) {
|
||||
provider.log(`Failed to logout from all environments: ${error}`)
|
||||
vscode.window.showErrorMessage("Failed to logout from all environments.")
|
||||
}
|
||||
break
|
||||
}
|
||||
case "codebaseIndexConfig": {
|
||||
const codebaseIndexConfig = message.values ?? {
|
||||
codebaseIndexEnabled: false,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ export interface ExtensionMessage {
|
|||
| "indexCleared"
|
||||
| "codebaseIndexConfig"
|
||||
| "marketplaceInstallResult"
|
||||
| "cloudEnvironments"
|
||||
| "cloudAuthenticatedEnvironments"
|
||||
| "cloudActiveEnvironment"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -136,6 +139,24 @@ export interface ExtensionMessage {
|
|||
userInfo?: CloudUserInfo
|
||||
organizationAllowList?: OrganizationAllowList
|
||||
tab?: string
|
||||
environments?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
apiUrl?: string
|
||||
clerkBaseUrl?: string
|
||||
}>
|
||||
authenticatedEnvironments?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
apiUrl?: string
|
||||
clerkBaseUrl?: string
|
||||
}>
|
||||
activeEnvironment?: {
|
||||
id: string
|
||||
name: string
|
||||
apiUrl?: string
|
||||
clerkBaseUrl?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -168,6 +168,12 @@ export interface WebviewMessage {
|
|||
| "removeInstalledMarketplaceItem"
|
||||
| "marketplaceInstallResult"
|
||||
| "switchTab"
|
||||
| "rooCloudSignInToEnvironment"
|
||||
| "rooCloudSignOutFromEnvironment"
|
||||
| "rooCloudSetActiveEnvironment"
|
||||
| "rooCloudGetEnvironments"
|
||||
| "rooCloudGetAuthenticatedEnvironments"
|
||||
| "rooCloudLogoutFromAllEnvironments"
|
||||
text?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
disabled?: boolean
|
||||
|
|
@ -205,6 +211,7 @@ export interface WebviewMessage {
|
|||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
visibility?: "organization" | "public" // For share visibility
|
||||
environmentId?: string
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue