diff --git a/crates/extension_host/src/copilot_migration.rs b/crates/extension_host/src/copilot_migration.rs deleted file mode 100644 index dd5ede4687fb16723960920f5d14e26e49e09954..0000000000000000000000000000000000000000 --- a/crates/extension_host/src/copilot_migration.rs +++ /dev/null @@ -1,216 +0,0 @@ -use credentials_provider::CredentialsProvider; -use gpui::App; -use std::path::PathBuf; - -const COPILOT_CHAT_EXTENSION_ID: &str = "copilot-chat"; -const COPILOT_CHAT_PROVIDER_ID: &str = "copilot-chat"; - -/// Migrates Copilot OAuth credentials from the GitHub Copilot config files -/// to the new extension-based credential location. -/// -/// This should only be called during auto-install of the extension. -pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) { - if extension_id != COPILOT_CHAT_EXTENSION_ID { - return; - } - - let credential_key = format!( - "extension-llm-{}:{}", - COPILOT_CHAT_EXTENSION_ID, COPILOT_CHAT_PROVIDER_ID - ); - - let credentials_provider = ::global(cx); - - cx.spawn(async move |_cx| { - // Read from copilot config files - let oauth_token = match read_copilot_oauth_token().await { - Some(token) if !token.is_empty() => token, - _ => { - log::debug!("No existing Copilot OAuth token found to migrate"); - return; - } - }; - - log::info!("Migrating existing Copilot OAuth token to Copilot Chat extension"); - - match credentials_provider - .write_credentials(&credential_key, "api_key", oauth_token.as_bytes(), &_cx) - .await - { - Ok(()) => { - log::info!("Successfully migrated Copilot OAuth token to Copilot Chat extension"); - } - Err(err) => { - log::error!("Failed to migrate Copilot OAuth token: {}", err); - } - } - }) - .detach(); -} - -async fn read_copilot_oauth_token() -> Option { - let config_paths = copilot_config_paths(); - - for path in config_paths { - if let Some(token) = read_oauth_token_from_file(&path).await { - return Some(token); - } - } - - None -} - -fn copilot_config_paths() -> Vec { - let config_dir = if cfg!(target_os = "windows") { - dirs::data_local_dir() - } else { - std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .ok() - .or_else(|| dirs::home_dir().map(|h| h.join(".config"))) - }; - - let Some(config_dir) = config_dir else { - return Vec::new(); - }; - - let copilot_dir = config_dir.join("github-copilot"); - - vec![ - copilot_dir.join("hosts.json"), - copilot_dir.join("apps.json"), - ] -} - -async fn read_oauth_token_from_file(path: &PathBuf) -> Option { - let contents = match smol::fs::read_to_string(path).await { - Ok(contents) => contents, - Err(_) => return None, - }; - - extract_oauth_token(&contents, "github.com") -} - -fn extract_oauth_token(contents: &str, domain: &str) -> Option { - let value: serde_json::Value = serde_json::from_str(contents).ok()?; - let obj = value.as_object()?; - - for (key, value) in obj.iter() { - if key.starts_with(domain) { - if let Some(token) = value.get("oauth_token").and_then(|v| v.as_str()) { - return Some(token.to_string()); - } - } - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - - #[test] - fn test_extract_oauth_token_from_hosts_json() { - let contents = r#"{ - "github.com": { - "oauth_token": "ghu_test_token_12345" - } - }"#; - - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, Some("ghu_test_token_12345".to_string())); - } - - #[test] - fn test_extract_oauth_token_with_user_suffix() { - let contents = r#"{ - "github.com:user": { - "oauth_token": "ghu_another_token" - } - }"#; - - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, Some("ghu_another_token".to_string())); - } - - #[test] - fn test_extract_oauth_token_wrong_domain() { - let contents = r#"{ - "gitlab.com": { - "oauth_token": "some_token" - } - }"#; - - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, None); - } - - #[test] - fn test_extract_oauth_token_invalid_json() { - let contents = "not valid json"; - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, None); - } - - #[test] - fn test_extract_oauth_token_missing_oauth_token_field() { - let contents = r#"{ - "github.com": { - "user": "testuser" - } - }"#; - - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, None); - } - - #[test] - fn test_extract_oauth_token_multiple_entries_picks_first_match() { - let contents = r#"{ - "gitlab.com": { - "oauth_token": "gitlab_token" - }, - "github.com": { - "oauth_token": "github_token" - } - }"#; - - let token = extract_oauth_token(contents, "github.com"); - assert_eq!(token, Some("github_token".to_string())); - } - - #[gpui::test] - async fn test_skips_migration_for_other_extensions(cx: &mut TestAppContext) { - cx.update(|cx| { - migrate_copilot_credentials_if_needed("some-other-extension", cx); - }); - - cx.run_until_parked(); - - let credentials = cx.read_credentials("extension-llm-copilot-chat:copilot-chat"); - assert!( - credentials.is_none(), - "Should not create credentials for other extensions" - ); - } - - // Note: Unlike the other migrations, copilot migration reads from the filesystem - // (copilot config files), not from the credentials provider. In tests, these files - // don't exist, so no migration occurs. - #[gpui::test] - async fn test_no_credentials_when_no_copilot_config_exists(cx: &mut TestAppContext) { - cx.update(|cx| { - migrate_copilot_credentials_if_needed(COPILOT_CHAT_EXTENSION_ID, cx); - }); - - cx.run_until_parked(); - - let credentials = cx.read_credentials("extension-llm-copilot-chat:copilot-chat"); - assert!( - credentials.is_none(), - "No credentials should be written when copilot config doesn't exist" - ); - } -}