From 01c673a9e3703be6e0095ecbcb4001c8052b0ba0 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:46:11 +0000 Subject: [PATCH 1/2] Initial plan From 80bc87787139c43ec373fa3f74f0100ab048ca66 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:56:07 +0000 Subject: [PATCH 2/2] Add comprehensive Claude skills for CloudBridge development - Created .claude/skills directory with 7 specialized skills - gpui-style-guide: GPUI coding standards and best practices - gpui-test: Testing patterns for GPUI applications - refactor-large-files: Guide for splitting large files - reduce-clones: Memory optimization strategies - add-cloud-provider: Guide for adding new cloud providers - security-audit: Security review checklist - debug-gpui: Common GPUI debugging patterns - Added comprehensive README with project context These skills help maintain code quality and provide guidance for: - Writing consistent GPUI UI code - Testing strategies - Performance optimization - Security best practices - Adding new features - Troubleshooting common issues Co-authored-by: JetSquirrel <20291255+JetSquirrel@users.noreply.github.com> Agent-Logs-Url: https://github.com/JetSquirrel/cloudbridge/sessions/d6338c42-5608-4e91-a21e-f0e23a446794 --- .claude/skills/README.md | 275 +++++++++ .claude/skills/add-cloud-provider/SKILL.md | 577 +++++++++++++++++++ .claude/skills/debug-gpui/SKILL.md | 488 ++++++++++++++++ .claude/skills/gpui-style-guide/SKILL.md | 334 +++++++++++ .claude/skills/gpui-test/SKILL.md | 431 ++++++++++++++ .claude/skills/reduce-clones/SKILL.md | 476 +++++++++++++++ .claude/skills/refactor-large-files/SKILL.md | 407 +++++++++++++ .claude/skills/security-audit/SKILL.md | 493 ++++++++++++++++ 8 files changed, 3481 insertions(+) create mode 100644 .claude/skills/README.md create mode 100644 .claude/skills/add-cloud-provider/SKILL.md create mode 100644 .claude/skills/debug-gpui/SKILL.md create mode 100644 .claude/skills/gpui-style-guide/SKILL.md create mode 100644 .claude/skills/gpui-test/SKILL.md create mode 100644 .claude/skills/reduce-clones/SKILL.md create mode 100644 .claude/skills/refactor-large-files/SKILL.md create mode 100644 .claude/skills/security-audit/SKILL.md diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 0000000..84540b1 --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,275 @@ +# CloudBridge Claude Skills + +This directory contains Claude Code skills to help with CloudBridge development. These skills provide guidance, patterns, and best practices specific to this GPUI Rust project. + +## Available Skills + +### 1. GPUI Style Guide (`gpui-style-guide/`) +Provides GPUI framework coding standards and best practices. + +**Use when:** +- Writing new GPUI UI components +- Refactoring existing UI code +- Reviewing code for GPUI patterns +- Need guidance on GPUI best practices + +**Key Topics:** +- Component structure patterns +- Styling order conventions +- Theme system usage +- State management +- Async operations +- Performance optimization + +### 2. GPUI Testing (`gpui-test/`) +Testing patterns and best practices for GPUI applications. + +**Use when:** +- Writing unit tests for GPUI components +- Testing async operations in UI +- Creating integration tests +- Need test examples + +**Key Topics:** +- GPUI test setup +- State change testing +- Async operation testing +- Cloud provider testing +- Database operation testing +- Test organization + +### 3. Refactor Large Files (`refactor-large-files/`) +Helps identify and refactor large files to improve maintainability. + +**Use when:** +- A file exceeds 500 lines of code +- A component has multiple responsibilities +- Code review suggests splitting files +- Adding new features to already large files + +**Key Topics:** +- Extract subcomponents pattern +- Separate data logic from UI +- State management extraction +- Module structure organization +- Step-by-step refactoring process + +**Target Files:** +- `src/ui/dashboard.rs` (837 lines) +- `src/ui/accounts.rs` (684 lines) +- `src/cloud/aws.rs` (758 lines) +- `src/ui/chart.rs` (545 lines) + +### 4. Reduce Clones (`reduce-clones/`) +Identifies and eliminates unnecessary `.clone()` calls to optimize memory usage and performance. + +**Use when:** +- Performance profiling shows excessive memory allocation +- Code review identifies unnecessary clones +- Refactoring to improve efficiency +- Adding new features and want to avoid clone overhead + +**Key Topics:** +- Common clone patterns +- Reference usage +- Arc for shared ownership +- String optimization +- Closure capture optimization +- Clone alternatives cheat sheet + +### 5. Add Cloud Provider (`add-cloud-provider/`) +Guides you through adding a new cloud provider integration. + +**Use when:** +- Adding support for a new cloud platform (Azure, GCP, etc.) +- Implementing a new cost API integration +- Extending CloudBridge with custom providers + +**Key Topics:** +- CloudService trait implementation +- Authentication patterns (OAuth, Signature, API Key) +- API request/response handling +- UI integration +- Testing with mock APIs +- Provider-specific considerations + +### 6. Security Audit (`security-audit/`) +Helps perform security audits and identify potential vulnerabilities. + +**Use when:** +- Before releases to check for security issues +- Reviewing new code for vulnerabilities +- Implementing security-sensitive features +- Responding to security reports + +**Key Topics:** +- Credential management review +- Encryption security audit +- Input validation checklist +- API security verification +- Dependency security checks +- OWASP Top 10 compliance + +### 7. Debug GPUI Issues (`debug-gpui/`) +Helps debug common GPUI framework issues. + +**Use when:** +- UI not rendering correctly +- State updates not reflecting in UI +- Performance issues or lag +- Layout problems +- Event handlers not firing + +**Key Topics:** +- State update troubleshooting +- Async update debugging +- Thread panic handling +- Entity/view lifecycle issues +- Layout debugging +- Event handler fixes +- Performance optimization +- Memory leak prevention + +## How to Use Skills + +Skills are reference documents that provide: +- **When to Use**: Clear scenarios for applying the skill +- **Patterns**: Code examples and best practices +- **Checklists**: Step-by-step guides +- **Common Issues**: Known problems and solutions +- **Resources**: Links to documentation + +### In Claude Code + +When working with Claude Code, you can reference these skills: +``` +"Follow the patterns in .claude/skills/gpui-style-guide when writing UI code" +"Use the add-cloud-provider skill to help me add Azure support" +"Check the security-audit skill before this release" +``` + +### Manual Reference + +Browse the skill files directly for: +- Learning GPUI patterns +- Understanding project conventions +- Quick reference during development +- Code review guidelines + +## Skill Development + +### Creating New Skills + +To add a new skill: + +1. Create a new directory: `.claude/skills/your-skill-name/` +2. Add `SKILL.md` with the skill content +3. Include examples, patterns, and checklists +4. Update this README + +### Skill Structure + +Each skill should have: + +```markdown +# Skill Name + +Brief description + +## When to Use +- Specific scenarios + +## Key Concepts +- Main topics covered + +## Examples +Code examples with explanations + +## Checklists +Step-by-step guides + +## Resources +Links to documentation +``` + +## Project-Specific Context + +### CloudBridge Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GPUI Application Layer │ +│ (CloudBridgeApp → Dashboard/Accounts/Settings) │ +└────────────┬────────────────────────────────────────┘ + │ +┌────────────▼────────────────────────────────────────┐ +│ UI Component Layer (gpui-component) │ +│ (Input, Button, Chart, Switch, Scroll) │ +└────────────┬────────────────────────────────────────┘ + │ +┌────────────▼────────────────────────────────────────┐ +│ Business Logic Layer │ +│ ├─ Cloud Providers (AWS, Aliyun, DeepSeek) │ +│ ├─ Cost Aggregation & Filtering │ +│ └─ Credential Validation │ +└────────────┬────────────────────────────────────────┘ + │ +┌────────────▼────────────────────────────────────────┐ +│ Data Persistence Layer │ +│ ├─ DuckDB (cost_data, cache tables) │ +│ ├─ Config File (config.json - encryption key) │ +│ └─ OS Keyring (AK/SK secrets) │ +└────────────┬────────────────────────────────────────┘ + │ +┌────────────▼────────────────────────────────────────┐ +│ Security Layer │ +│ ├─ AES-256-GCM Encryption (crypto.rs) │ +│ ├─ AWS Signature V4 / Aliyun HMAC-SHA1 │ +│ └─ OS Credential Management │ +└─────────────────────────────────────────────────────┘ +``` + +### Key Technologies + +- **UI Framework**: GPUI 0.2 (GPU-accelerated) +- **Language**: Rust 2021 Edition +- **Database**: DuckDB (embedded) +- **HTTP**: ureq (synchronous) +- **Async Runtime**: smol +- **Encryption**: AES-256-GCM + +### Code Statistics + +- Total Lines: ~4,868 lines of Rust +- UI Layer: 44% (1,698 lines) +- Cloud Integrations: 40% (1,538 lines) +- Core Infrastructure: 8% (303 lines) +- Supporting: 8% (329 lines) + +### Known Technical Debt + +1. Excessive `.clone()` usage (34+ calls) +2. Large files (dashboard.rs: 837 lines, accounts.rs: 684 lines) +3. Manual thread management with message channels +4. Limited test coverage +5. Encryption key storage with encrypted data + +## Contributing + +When adding or updating skills: +1. Keep examples relevant to CloudBridge +2. Use actual code from the project when possible +3. Include both good and bad examples +4. Add checklists for actionable items +5. Update this README + +## Resources + +- [CloudBridge Repository](https://github.com/JetSquirrel/cloudbridge) +- [GPUI Documentation](https://gpui.rs/) +- [GPUI Component Library](https://longbridge.github.io/gpui-component/) +- [Rust Book](https://doc.rust-lang.org/book/) + +--- + +These skills are based on the patterns observed in CloudBridge and inspired by the [longbridge/gpui-component](https://github.com/longbridge/gpui-component) skills structure. diff --git a/.claude/skills/add-cloud-provider/SKILL.md b/.claude/skills/add-cloud-provider/SKILL.md new file mode 100644 index 0000000..1048862 --- /dev/null +++ b/.claude/skills/add-cloud-provider/SKILL.md @@ -0,0 +1,577 @@ +# Add Cloud Provider Skill + +This skill guides you through adding a new cloud provider to CloudBridge. + +## When to Use + +Use this skill when: +- Adding support for a new cloud platform (Azure, GCP, etc.) +- Implementing a new cost API integration +- Extending CloudBridge with custom providers + +## Overview + +Adding a new cloud provider involves: +1. Implementing the `CloudService` trait +2. Adding authentication/signing logic +3. Creating API request/response handlers +4. Adding UI configuration options +5. Testing the integration + +## Step-by-Step Guide + +### Step 1: Review Existing Providers + +CloudBridge currently supports: +- **AWS** - `src/cloud/aws.rs` (758 lines) + - AWS Signature V4 authentication + - Cost Explorer API + +- **Alibaba Cloud** - `src/cloud/aliyun.rs` (480 lines) + - HMAC-SHA1 signature + - Billing API + +- **DeepSeek** - `src/cloud/deepseek.rs` (147 lines) + - Simple API key auth + - Balance queries + +### Step 2: Understand the CloudService Trait + +```rust +// src/cloud/mod.rs +pub trait CloudService: Send + Sync { + /// Validate if the credentials are correct + fn validate_credentials(&self) -> Result; + + /// Get cost data for a specific date range + fn get_cost_data(&self, start_date: &str, end_date: &str) + -> Result, String>; + + /// Get monthly cost summary with service breakdown + fn get_cost_summary(&self) -> Result; + + /// Get daily cost trend for the last 30 days + fn get_cost_trend(&self, start_date: &str, end_date: &str) + -> Result; +} +``` + +### Step 3: Create Provider File + +```bash +# Create new provider file +touch src/cloud/azure.rs + +# Or for GCP +touch src/cloud/gcp.rs +``` + +### Step 4: Implement the Provider + +#### Template Structure + +```rust +// src/cloud/azure.rs +use crate::cloud::{CloudService, CostData, CostSummary, CostTrend}; +use anyhow::{Result, Context}; +use serde::{Deserialize, Serialize}; + +pub struct AzureClient { + subscription_id: String, + tenant_id: String, + client_id: String, + client_secret: String, +} + +impl AzureClient { + pub fn new(subscription_id: String, tenant_id: String, + client_id: String, client_secret: String) -> Self { + Self { + subscription_id, + tenant_id, + client_id, + client_secret, + } + } + + /// Get OAuth token for Azure API + fn get_access_token(&self) -> Result { + let token_url = format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + self.tenant_id + ); + + let response = ureq::post(&token_url) + .send_form(&[ + ("grant_type", "client_credentials"), + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ("scope", "https://management.azure.com/.default"), + ]) + .context("Failed to get Azure access token")?; + + let body: serde_json::Value = response.into_json()?; + let token = body["access_token"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("No access token in response"))? + .to_string(); + + Ok(token) + } + + /// Make authenticated request to Azure Cost Management API + fn make_cost_request(&self, endpoint: &str, body: &str) -> Result { + let token = self.get_access_token()?; + let url = format!( + "https://management.azure.com/subscriptions/{}/providers/Microsoft.CostManagement/{}", + self.subscription_id, endpoint + ); + + let response = ureq::post(&url) + .set("Authorization", &format!("Bearer {}", token)) + .set("Content-Type", "application/json") + .send_string(body) + .context("Failed to call Azure Cost Management API")?; + + let body = response.into_string()?; + Ok(body) + } +} + +impl CloudService for AzureClient { + fn validate_credentials(&self) -> Result { + // Try to get an access token + match self.get_access_token() { + Ok(_) => Ok(true), + Err(e) => Err(format!("Azure credentials validation failed: {}", e)), + } + } + + fn get_cost_data(&self, start_date: &str, end_date: &str) + -> Result, String> { + // Build Azure Cost Management query + let query = serde_json::json!({ + "type": "Usage", + "timeframe": "Custom", + "timePeriod": { + "from": start_date, + "to": end_date + }, + "dataset": { + "granularity": "Daily", + "aggregation": { + "totalCost": { + "name": "Cost", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "ServiceName" + } + ] + } + }); + + let response = self.make_cost_request( + "query?api-version=2023-11-01", + &query.to_string() + ).map_err(|e| e.to_string())?; + + // Parse response + self.parse_cost_data(&response) + } + + fn get_cost_summary(&self) -> Result { + // Get current month costs + let now = chrono::Utc::now(); + let start = now.format("%Y-%m-01").to_string(); + let end = now.format("%Y-%m-%d").to_string(); + + let cost_data = self.get_cost_data(&start, &end)?; + + // Aggregate by service + let mut services = std::collections::HashMap::new(); + let mut total = 0.0; + + for data in cost_data { + *services.entry(data.service_name.clone()).or_insert(0.0) += data.cost; + total += data.cost; + } + + let service_costs = services.into_iter() + .map(|(name, cost)| crate::cloud::ServiceCost { + service_name: name, + cost + }) + .collect(); + + Ok(CostSummary { + total_cost: total, + currency: "USD".to_string(), + service_costs, + start_date: start, + end_date: end, + }) + } + + fn get_cost_trend(&self, start_date: &str, end_date: &str) + -> Result { + let cost_data = self.get_cost_data(start_date, end_date)?; + + // Group by date + let mut daily_costs = std::collections::HashMap::new(); + for data in cost_data { + *daily_costs.entry(data.date.clone()).or_insert(0.0) += data.cost; + } + + let mut dates: Vec<_> = daily_costs.keys().cloned().collect(); + dates.sort(); + + let costs: Vec = dates.iter() + .map(|date| daily_costs[date]) + .collect(); + + Ok(CostTrend { + dates, + costs, + currency: "USD".to_string(), + }) + } +} + +impl AzureClient { + /// Parse Azure Cost Management API response + fn parse_cost_data(&self, json: &str) -> Result, String> { + #[derive(Deserialize)] + struct AzureResponse { + properties: Properties, + } + + #[derive(Deserialize)] + struct Properties { + rows: Vec>, + columns: Vec, + } + + #[derive(Deserialize)] + struct Column { + name: String, + } + + let response: AzureResponse = serde_json::from_str(json) + .map_err(|e| format!("Failed to parse Azure response: {}", e))?; + + let mut cost_data = Vec::new(); + + for row in response.properties.rows { + let cost = row[0].as_f64().unwrap_or(0.0); + let date = row[1].as_str().unwrap_or("").to_string(); + let service_name = row[2].as_str().unwrap_or("Unknown").to_string(); + + cost_data.push(CostData { + date, + service_name, + cost, + currency: "USD".to_string(), + }); + } + + Ok(cost_data) + } +} +``` + +### Step 5: Register Provider in mod.rs + +```rust +// src/cloud/mod.rs +pub mod aws; +pub mod aliyun; +pub mod deepseek; +pub mod azure; // Add your new provider + +pub use aws::AwsClient; +pub use aliyun::AliyunClient; +pub use deepseek::DeepSeekClient; +pub use azure::AzureClient; // Export new provider +``` + +### Step 6: Add to Provider Enum + +```rust +// src/cloud/mod.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CloudProvider { + AWS, + Aliyun, + DeepSeek, + Azure, // Add new provider + GCP, +} + +impl CloudProvider { + pub fn name(&self) -> &str { + match self { + CloudProvider::AWS => "AWS", + CloudProvider::Aliyun => "Alibaba Cloud", + CloudProvider::DeepSeek => "DeepSeek", + CloudProvider::Azure => "Microsoft Azure", // Add name + CloudProvider::GCP => "Google Cloud", + } + } + + pub fn all() -> Vec { + vec![ + CloudProvider::AWS, + CloudProvider::Aliyun, + CloudProvider::DeepSeek, + CloudProvider::Azure, // Add to list + CloudProvider::GCP, + ] + } +} +``` + +### Step 7: Update Database Schema + +```rust +// src/db.rs - Update CloudAccount struct if needed +pub struct CloudAccount { + pub id: String, + pub name: String, + pub provider: String, // "aws", "aliyun", "azure", etc. + pub access_key: String, // Encrypted + pub secret_key: String, // Encrypted + pub region: Option, + // Add provider-specific fields as JSON + pub extra_config: Option, // JSON for provider-specific config +} +``` + +### Step 8: Update UI (Account Management) + +```rust +// src/ui/accounts.rs + +// Add provider to dropdown +fn render_provider_selector(&self, cx: &ViewContext) -> impl IntoElement { + div() + .children(CloudProvider::all().into_iter().map(|provider| { + Button::new(provider.name()) + .on_click(cx.listener(move |this, _, cx| { + this.selected_provider = provider.clone(); + cx.notify(); + })) + })) +} + +// Add provider-specific form fields +fn render_credential_form(&self, cx: &ViewContext) -> impl IntoElement { + match self.selected_provider { + CloudProvider::Azure => self.render_azure_form(cx), + CloudProvider::AWS => self.render_aws_form(cx), + // ... other providers + } +} + +fn render_azure_form(&self, cx: &ViewContext) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_4() + .child(self.render_input("Subscription ID", &self.subscription_id_input)) + .child(self.render_input("Tenant ID", &self.tenant_id_input)) + .child(self.render_input("Client ID", &self.client_id_input)) + .child(self.render_input("Client Secret", &self.client_secret_input)) +} +``` + +### Step 9: Add Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_azure_client_creation() { + let client = AzureClient::new( + "sub-id".to_string(), + "tenant-id".to_string(), + "client-id".to_string(), + "secret".to_string(), + ); + + assert_eq!(client.subscription_id, "sub-id"); + } + + #[test] + fn test_parse_azure_cost_response() { + let json = r#"{ + "properties": { + "rows": [[100.50, 20240101000000, "Virtual Machines"]], + "columns": [ + {"name": "Cost"}, + {"name": "Date"}, + {"name": "ServiceName"} + ] + } + }"#; + + let client = AzureClient::new( + "sub".to_string(), + "tenant".to_string(), + "client".to_string(), + "secret".to_string(), + ); + + let result = client.parse_cost_data(json).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].cost, 100.50); + assert_eq!(result[0].service_name, "Virtual Machines"); + } +} +``` + +### Step 10: Update Documentation + +```rust +// Update README.md with new provider setup instructions +// Update CHANGELOG.md with new feature +// Add provider-specific docs to docs/ directory +``` + +## Provider Implementation Checklist + +- [ ] Create new file in `src/cloud/` +- [ ] Implement `CloudService` trait +- [ ] Add authentication logic +- [ ] Implement `validate_credentials()` +- [ ] Implement `get_cost_data()` +- [ ] Implement `get_cost_summary()` +- [ ] Implement `get_cost_trend()` +- [ ] Add response parsing logic +- [ ] Register in `src/cloud/mod.rs` +- [ ] Add to `CloudProvider` enum +- [ ] Update UI forms in `src/ui/accounts.rs` +- [ ] Update database schema if needed +- [ ] Add unit tests +- [ ] Add integration tests (with mocked API) +- [ ] Test credential validation +- [ ] Test cost data retrieval +- [ ] Update documentation +- [ ] Update README.md + +## Common Patterns + +### Pattern 1: OAuth Authentication (Azure, GCP) + +```rust +fn get_access_token(&self) -> Result { + let response = ureq::post(&token_url) + .send_form(&[ + ("grant_type", "client_credentials"), + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ("scope", &self.scope), + ])?; + + let body: serde_json::Value = response.into_json()?; + Ok(body["access_token"].as_str().unwrap().to_string()) +} +``` + +### Pattern 2: Signature-based Auth (AWS, Alibaba) + +```rust +fn sign_request(&self, request: &Request) -> String { + let string_to_sign = format!( + "{}\n{}\n{}\n{}", + request.method, + request.path, + request.date, + request.params + ); + + let signature = hmac_sha256(&self.secret_key, &string_to_sign); + base64::encode(signature) +} +``` + +### Pattern 3: API Key Auth (Simple APIs) + +```rust +fn make_request(&self, endpoint: &str) -> Result { + let response = ureq::get(endpoint) + .set("Authorization", &format!("Bearer {}", self.api_key)) + .call()?; + + Ok(response.into_string()?) +} +``` + +## Testing with Mock APIs + +```rust +#[cfg(test)] +mod tests { + use mockito::{Server, Mock}; + + #[test] + fn test_azure_cost_api() { + let mut server = Server::new(); + + let mock = server.mock("POST", "/query") + .with_status(200) + .with_body(r#"{"properties": {"rows": [], "columns": []}}"#) + .create(); + + let client = AzureClient::new_with_endpoint( + &server.url(), + "sub".to_string(), + "tenant".to_string(), + "client".to_string(), + "secret".to_string(), + ); + + let result = client.get_cost_data("2024-01-01", "2024-01-31"); + assert!(result.is_ok()); + mock.assert(); + } +} +``` + +## Provider-Specific Considerations + +### Azure +- OAuth 2.0 token-based authentication +- Cost Management API requires specific permissions +- Subscription ID is the main identifier +- Multi-tenant scenarios need careful handling + +### Google Cloud Platform +- OAuth 2.0 with service accounts +- Cloud Billing API +- Project ID is the main identifier +- IAM roles: `roles/billing.viewer` + +### Oracle Cloud +- Signature-based authentication +- Cost and Usage Reports API +- Tenancy OCID required +- Region-specific endpoints + +### DigitalOcean +- API token authentication +- Billing API endpoint +- Simple REST API +- Monthly invoice data + +## Resources + +- Azure Cost Management API: https://learn.microsoft.com/en-us/rest/api/cost-management/ +- GCP Cloud Billing API: https://cloud.google.com/billing/docs/reference/rest +- AWS Cost Explorer API: https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/ +- CloudBridge Examples: `src/cloud/` directory diff --git a/.claude/skills/debug-gpui/SKILL.md b/.claude/skills/debug-gpui/SKILL.md new file mode 100644 index 0000000..b087d03 --- /dev/null +++ b/.claude/skills/debug-gpui/SKILL.md @@ -0,0 +1,488 @@ +# Debug GPUI Issues Skill + +This skill helps debug common GPUI framework issues in CloudBridge. + +## When to Use + +Use this skill when: +- UI not rendering correctly +- State updates not reflecting in UI +- Performance issues or lag +- Layout problems +- Event handlers not firing + +## Common GPUI Issues + +### Issue 1: State Update Not Rendering + +**Symptom:** Changed state but UI doesn't update + +**Cause:** Forgot to call `cx.notify()` + +```rust +// BAD: State changed but UI not updated +impl MyView { + fn update_count(&mut self, cx: &mut ViewContext) { + self.count += 1; + // Missing cx.notify()! + } +} + +// GOOD: Notify context after state change +impl MyView { + fn update_count(&mut self, cx: &mut ViewContext) { + self.count += 1; + cx.notify(); // ✓ Triggers re-render + } +} +``` + +**Solution:** +- Always call `cx.notify()` after changing state +- Or use `cx.update_model()` which notifies automatically + +### Issue 2: Async Updates Not Working + +**Symptom:** Data loaded but UI shows old data + +**Cause:** Async context lost or not updated properly + +```rust +// BAD: Direct mutation in async +cx.spawn(|this, mut cx| async move { + let data = fetch_data().await; + this.data = data; // Error: can't mutate directly +}).detach(); + +// GOOD: Use cx.update +cx.spawn(|this, mut cx| async move { + let data = fetch_data().await; + this.update(&mut cx, |this, cx| { + this.data = data; + cx.notify(); // Important! + }).ok(); +}).detach(); +``` + +**Solution:** +- Use `this.update(&mut cx, |this, cx| { ... })` in async +- Always call `cx.notify()` in the update closure +- Use `.ok()` to handle potential errors + +### Issue 3: Thread Panics in Background Tasks + +**Symptom:** App crashes or hangs after background operation + +**Cause:** Unhandled panics in spawned threads + +```rust +// BAD: Panic not handled +std::thread::spawn(move || { + let result = risky_operation(); // Might panic + tx.send(result).unwrap(); // Might panic if receiver dropped +}); + +// GOOD: Handle panics and errors +std::thread::spawn(move || { + let result = std::panic::catch_unwind(|| { + risky_operation() + }); + + match result { + Ok(data) => { + let _ = tx.send(Ok(data)); // Ignore send error + } + Err(_) => { + let _ = tx.send(Err("Operation panicked".to_string())); + } + } +}); +``` + +**Solution:** +- Use `catch_unwind` for panic recovery +- Use `let _ = tx.send()` to ignore send errors +- Return `Result` types from background tasks + +### Issue 4: Entity/View Lifecycle Issues + +**Symptom:** "Entity not found" or "View dropped" errors + +**Cause:** Trying to update dropped view or entity + +```rust +// BAD: May reference dropped entity +let entity = self.my_entity.clone(); +cx.spawn(|this, mut cx| async move { + sleep(Duration::from_secs(5)).await; + entity.update(&mut cx, |entity, cx| { + // Entity might be dropped by now! + entity.value = 42; + }); +}).detach(); + +// GOOD: Check if entity still exists +let entity = self.my_entity.clone(); +cx.spawn(|this, mut cx| async move { + sleep(Duration::from_secs(5)).await; + if let Ok(_) = entity.update(&mut cx, |entity, cx| { + entity.value = 42; + cx.notify(); + }) { + // Entity still exists + } +}).detach(); +``` + +**Solution:** +- Check update result with pattern matching +- Use weak references if appropriate +- Clean up async tasks when view is dropped + +### Issue 5: Layout Not Working as Expected + +**Symptom:** Elements overlapping, wrong sizes, or positions + +**Cause:** Incorrect flexbox setup + +```rust +// BAD: Conflicting layout properties +div() + .flex() // Flexbox + .absolute() // Absolute positioning (conflicts!) + .child(...) + +// BAD: Missing flex_1 for child +div() + .flex() + .flex_col() + .child( + div().child("Header") // No flex properties + ) + .child( + div().child("Content") // Should grow to fill + ) + +// GOOD: Proper flex layout +div() + .flex() + .flex_col() + .h_full() // Fill height + .child( + div() + .h(px(60.0)) // Fixed height header + .child("Header") + ) + .child( + div() + .flex_1() // Grow to fill remaining space + .child("Content") + ) +``` + +**Solution:** +- Use consistent layout mode (flex OR absolute) +- Use `flex_1()` for children that should grow +- Set explicit sizes or constraints +- Use `w_full()` and `h_full()` appropriately + +### Issue 6: Event Handlers Not Firing + +**Symptom:** Clicks or other events not working + +**Cause:** Missing `cx.listener` or incorrect event setup + +```rust +// BAD: Wrong closure type +Button::new("click-me") + .on_click(|this, event, cx| { + // This won't compile - wrong signature + this.handle_click(); + }) + +// GOOD: Use cx.listener +Button::new("click-me") + .on_click(cx.listener(|this, event, cx| { + this.handle_click(cx); + })) + +// GOOD: For simple cases +Button::new("click-me") + .on_click(cx.listener(|this, _event, cx| { + this.count += 1; + cx.notify(); + })) +``` + +**Solution:** +- Always wrap event handlers with `cx.listener()` +- Ensure proper closure signature +- Call `cx.notify()` if state changes + +### Issue 7: Performance Issues / Lag + +**Symptom:** UI feels slow or unresponsive + +**Causes & Solutions:** + +```rust +// CAUSE 1: Expensive computation in render +impl Render for MyView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + // BAD: Heavy computation every render + let processed = self.items.iter() + .map(|item| expensive_processing(item)) + .collect::>(); + + div().children(processed.into_iter().map(|p| div().child(p))) + } +} + +// SOLUTION: Cache computed values +pub struct MyView { + items: Vec, + cached_processed: Vec, +} + +impl MyView { + fn update_items(&mut self, items: Vec, cx: &mut ViewContext) { + self.items = items; + // Compute once when data changes + self.cached_processed = self.items.iter() + .map(|item| expensive_processing(item)) + .collect(); + cx.notify(); + } +} + +// CAUSE 2: Too many renders +impl MyView { + fn on_timer(&mut self, cx: &mut ViewContext) { + self.tick_count += 1; + cx.notify(); // Renders entire view every tick! + } +} + +// SOLUTION: Update only what changed +// Use separate views for different update frequencies +pub struct MyView { + static_content: View, + dynamic_ticker: View, +} + +// CAUSE 3: Large lists without virtualization +div() + .children(self.thousands_of_items.iter().map(|item| { + div().child(render_item(item)) + })) + +// SOLUTION: Implement virtual scrolling +// Or limit visible items +div() + .children(self.items.iter() + .take(100) // Show only first 100 + .map(|item| div().child(render_item(item))) + ) +``` + +### Issue 8: Theme Colors Not Working + +**Symptom:** Colors not applying or incorrect + +```rust +// BAD: Accessing theme wrong way +.bg(self.theme.background) // theme might not be accessible + +// GOOD: Use cx.theme() +.bg(cx.theme().background) + +// BAD: Custom color not in theme +.bg(rgb(0x1e1e1e)) // Hardcoded + +// GOOD: Extend theme if needed +.bg(cx.theme().background) +``` + +**Solution:** +- Always use `cx.theme()` to access theme +- Add custom colors to theme system if needed +- Don't hardcode colors + +### Issue 9: Memory Leaks + +**Symptom:** Memory usage grows over time + +**Causes:** +1. Not detaching spawned tasks +2. Circular references with Arc +3. Keeping old views alive + +```rust +// BAD: Task not detached +cx.spawn(|this, mut cx| async move { + // Long-running task +}); // Missing .detach()! + +// GOOD: Detach background tasks +cx.spawn(|this, mut cx| async move { + // Long-running task +}).detach(); + +// BAD: Circular reference +struct Parent { + child: Arc, +} + +struct Child { + parent: Arc, // Circular! +} + +// GOOD: Use weak references +struct Child { + parent: Weak, // Weak reference breaks cycle +} +``` + +### Issue 10: Model/View Synchronization + +**Symptom:** Model updates but view shows stale data + +```rust +// BAD: Cloning model instead of referencing +pub struct MyView { + data: MyData, // Owned copy +} + +// Model updates elsewhere but view has stale copy + +// GOOD: Reference shared model +pub struct MyView { + data: Model, // Shared reference +} + +impl MyView { + fn new(data: Model, cx: &mut ViewContext) -> Self { + // Subscribe to model changes + cx.observe(&data, |this, model, cx| { + // Called when model changes + cx.notify(); + }).detach(); + + Self { data } + } +} +``` + +## Debugging Tools + +### 1. Enable GPUI Logging + +```rust +// In main.rs +env_logger::init(); + +// Set log level +std::env::set_var("RUST_LOG", "debug"); +``` + +### 2. Add Debug Prints + +```rust +impl Render for MyView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + eprintln!("Rendering MyView, count: {}", self.count); + div().child(format!("Count: {}", self.count)) + } +} +``` + +### 3. Check View Bounds + +```rust +div() + .border_1() // Add border to see actual size + .border_color(rgb(0xff0000)) // Red border + .child(content) +``` + +### 4. Inspect Context + +```rust +// Check if view is still alive +if let Ok(_) = this.update(&mut cx, |_, _| {}) { + println!("View is alive"); +} else { + println!("View is dropped!"); +} +``` + +## Common Patterns for Debugging + +### Pattern 1: State Change Debugging + +```rust +impl MyView { + fn set_value(&mut self, value: i32, cx: &mut ViewContext) { + eprintln!("set_value called: {} -> {}", self.value, value); + self.value = value; + eprintln!("Calling cx.notify()"); + cx.notify(); + eprintln!("cx.notify() completed"); + } +} +``` + +### Pattern 2: Async Flow Debugging + +```rust +cx.spawn(|this, mut cx| async move { + eprintln!("Async task started"); + let result = fetch_data().await; + eprintln!("Data fetched: {:?}", result); + + this.update(&mut cx, |this, cx| { + eprintln!("Updating view with result"); + this.data = result; + cx.notify(); + eprintln!("View updated"); + }).ok(); + + eprintln!("Async task completed"); +}).detach(); +``` + +### Pattern 3: Event Flow Debugging + +```rust +Button::new("test") + .on_click(cx.listener(|this, event, cx| { + eprintln!("Button clicked!"); + eprintln!(" Position: {:?}", event.position); + eprintln!(" Current count: {}", this.count); + this.count += 1; + eprintln!(" New count: {}", this.count); + cx.notify(); + eprintln!(" Notified context"); + })) +``` + +## Checklist + +When debugging GPUI issues, check: + +- [ ] Is `cx.notify()` called after state changes? +- [ ] Are async updates using `this.update(&mut cx, ...)`? +- [ ] Are spawned tasks `.detach()`ed? +- [ ] Are event handlers wrapped with `cx.listener()`? +- [ ] Is flex layout setup correctly? +- [ ] Are theme colors accessed via `cx.theme()`? +- [ ] Are expensive operations cached? +- [ ] Are model updates observed? +- [ ] Are circular references avoided? +- [ ] Are errors handled in background tasks? + +## Resources + +- GPUI Documentation: https://gpui.rs/ +- GPUI Examples: https://github.com/zed-industries/zed +- CloudBridge Examples: `src/ui/` directory diff --git a/.claude/skills/gpui-style-guide/SKILL.md b/.claude/skills/gpui-style-guide/SKILL.md new file mode 100644 index 0000000..ae7920c --- /dev/null +++ b/.claude/skills/gpui-style-guide/SKILL.md @@ -0,0 +1,334 @@ +# GPUI Style Guide Skill + +This skill provides GPUI framework coding standards and best practices for the CloudBridge project. + +## When to Use + +Use this skill when: +- Writing new GPUI UI components +- Refactoring existing UI code +- Reviewing code for GPUI patterns +- Need guidance on GPUI best practices + +## GPUI Coding Standards + +### 1. Component Structure + +```rust +use gpui::*; + +pub struct MyView { + // State first + data: Model, + // UI state + focus_handle: FocusHandle, +} + +impl MyView { + pub fn new(cx: &mut WindowContext) -> Self { + Self { + data: cx.new_model(|_| MyData::default()), + focus_handle: cx.focus_handle(), + } + } +} + +impl Render for MyView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + // Layout properties first + .w_full() + .h_full() + // Flexbox properties + .flex() + .flex_col() + // Spacing + .gap_4() + .p_4() + // Visual properties + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + // Children last + .child(self.render_header(cx)) + .child(self.render_content(cx)) + } +} +``` + +### 2. Styling Order + +Always apply styles in this order: +1. Size/dimensions (`w_*`, `h_*`, `size_*`) +2. Layout mode (`flex`, `relative`, `absolute`) +3. Flex properties (`flex_col`, `items_center`, `justify_between`) +4. Spacing (`gap_*`, `p_*`, `m_*`) +5. Border (`border_*`, `rounded_*`) +6. Background (`bg`) +7. Text (`text_*`, `font_*`) +8. Interactive states (`hover`, `active`) +9. Children (`.child()`, `.children()`) + +### 3. Theme System + +Always use theme colors instead of hardcoded values: + +```rust +// Good +.bg(cx.theme().background) +.text_color(cx.theme().foreground) +.border_color(cx.theme().border) + +// Avoid +.bg(rgb(0x1e1e1e)) +.text_color(rgb(0xffffff)) +``` + +### 4. State Management + +```rust +// For simple local state +struct MyView { + counter: usize, +} + +// For shared state across components +struct MyView { + shared_data: Model, +} + +// Always notify on state changes +impl MyView { + fn increment(&mut self, cx: &mut ViewContext) { + self.counter += 1; + cx.notify(); // Trigger re-render + } +} +``` + +### 5. Async Operations + +```rust +// Pattern 1: Background thread with channel (for blocking ops) +let (tx, rx) = std::sync::mpsc::channel(); +std::thread::spawn(move || { + let result = blocking_operation(); + let _ = tx.send(result); +}); + +cx.spawn(|mut cx| async move { + if let Ok(data) = rx.recv() { + cx.update(|cx| { + // Update UI with data + }).ok(); + } +}).detach(); + +// Pattern 2: Async spawn (for async ops) +cx.spawn(|this, mut cx| async move { + let result = async_operation().await; + this.update(&mut cx, |this, cx| { + this.data = result; + cx.notify(); + }).ok(); +}).detach(); +``` + +### 6. Component Composition + +```rust +// Break down large views into smaller methods +impl MyView { + fn render_header(&self, cx: &ViewContext) -> impl IntoElement { + div() + .h(px(60.0)) + .w_full() + .child("Header") + } + + fn render_sidebar(&self, cx: &ViewContext) -> impl IntoElement { + div() + .w(px(220.0)) + .h_full() + .child("Sidebar") + } + + fn render_content(&self, cx: &ViewContext) -> impl IntoElement { + div() + .flex_1() + .child("Content") + } +} +``` + +### 7. Input Handling + +```rust +// Use on_click for interactive elements +Button::new("submit") + .label("Submit") + .on_click(cx.listener(|this, _event, cx| { + this.handle_submit(cx); + })) + +// Use on_mouse_down for custom interactions +div() + .on_mouse_down(MouseButton::Left, cx.listener(|this, event, cx| { + this.handle_click(event.position, cx); + })) +``` + +### 8. Error Handling in UI + +```rust +// Pattern: Use Option/Result in state +struct MyView { + data: Option>, + error: Option, +} + +impl Render for MyView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div().child( + if let Some(error) = &self.error { + div() + .text_color(cx.theme().error) + .child(format!("Error: {}", error)) + .into_any_element() + } else if let Some(data) = &self.data { + self.render_data(data, cx).into_any_element() + } else { + div().child("Loading...").into_any_element() + } + ) + } +} +``` + +### 9. Performance Tips + +```rust +// 1. Avoid unnecessary clones - use references when possible +// Bad +.children(items.clone().into_iter().map(|item| { + div().child(item.name.clone()) +})) + +// Good +.children(items.iter().map(|item| { + div().child(&item.name) +})) + +// 2. Use conditional rendering wisely +// Bad: Creates element even when hidden +.child( + div() + .when(!visible, |this| this.hidden()) + .child(expensive_component()) +) + +// Good: Skips creation when not needed +.when(visible, |this| { + this.child(expensive_component()) +}) + +// 3. Cache computed values +struct MyView { + items: Vec, + filtered_items: Vec, // Cached +} +``` + +### 10. Common Patterns in CloudBridge + +#### Dashboard Cards +```rust +fn render_card(&self, title: &str, value: &str, cx: &ViewContext) -> impl IntoElement { + div() + .w_full() + .p_4() + .rounded_lg() + .border_1() + .border_color(cx.theme().border) + .bg(cx.theme().card_background) + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(title) + ) + .child( + div() + .text_2xl() + .font_weight(FontWeight::BOLD) + .child(value) + ) +} +``` + +#### Loading States +```rust +fn render_loading(&self) -> impl IntoElement { + div() + .w_full() + .h_full() + .flex() + .items_center() + .justify_center() + .child("Loading...") +} +``` + +#### Empty States +```rust +fn render_empty(&self, message: &str) -> impl IntoElement { + div() + .w_full() + .h_full() + .flex() + .flex_col() + .items_center() + .justify_center() + .gap_2() + .child( + div() + .text_lg() + .text_color(cx.theme().muted_foreground) + .child(message) + ) +} +``` + +## Anti-Patterns to Avoid + +1. **Don't use `unwrap()` in UI code** - Always handle errors gracefully +2. **Don't hold locks across await points** - Use channels instead +3. **Don't clone entire structs unnecessarily** - Use `Arc` or references +4. **Don't create deeply nested div hierarchies** - Extract helper methods +5. **Don't forget to call `cx.notify()`** - State changes won't render without it +6. **Don't hardcode pixel values** - Use theme spacing when possible +7. **Don't forget `.detach()` on spawned tasks** - Memory leaks otherwise + +## Testing GPUI Components + +```rust +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + #[gpui::test] + fn test_component_creation(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| MyView::new(cx)); + // Test assertions + } +} +``` + +## Resources + +- GPUI Documentation: https://gpui.rs/ +- GPUI Component Library: https://longbridge.github.io/gpui-component/ +- CloudBridge Examples: See `src/ui/` directory diff --git a/.claude/skills/gpui-test/SKILL.md b/.claude/skills/gpui-test/SKILL.md new file mode 100644 index 0000000..40c6ea9 --- /dev/null +++ b/.claude/skills/gpui-test/SKILL.md @@ -0,0 +1,431 @@ +# GPUI Testing Skill + +This skill provides testing patterns and best practices for GPUI applications in CloudBridge. + +## When to Use + +Use this skill when: +- Writing unit tests for GPUI components +- Testing async operations in UI +- Creating integration tests +- Need test examples for cloud providers + +## GPUI Test Basics + +### Setup + +```rust +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, VisualTestContext}; + + #[gpui::test] + fn test_basic_component(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| MyView::new(cx)); + // Assertions + } +} +``` + +### Testing State Changes + +```rust +#[gpui::test] +fn test_state_update(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| CounterView::new(cx)); + + view.update(cx, |view, cx| { + assert_eq!(view.count, 0); + view.increment(cx); + assert_eq!(view.count, 1); + }); +} +``` + +### Testing Async Operations + +```rust +#[gpui::test] +async fn test_async_data_loading(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| DataView::new(cx)); + + view.update(cx, |view, cx| { + view.load_data(cx); + }); + + // Wait for async operation + cx.run_until_parked(); + + view.update(cx, |view, _| { + assert!(view.data.is_some()); + }); +} +``` + +## Testing CloudBridge Components + +### 1. Testing Crypto Operations + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt() { + let key = generate_key(); + let plaintext = "sensitive-data"; + + let encrypted = encrypt(plaintext, &key).unwrap(); + let decrypted = decrypt(&encrypted, &key).unwrap(); + + assert_eq!(plaintext, decrypted); + } + + #[test] + fn test_encryption_with_different_key_fails() { + let key1 = generate_key(); + let key2 = generate_key(); + let plaintext = "data"; + + let encrypted = encrypt(plaintext, &key1).unwrap(); + let result = decrypt(&encrypted, &key2); + + assert!(result.is_err()); + } +} +``` + +### 2. Testing Cloud Providers + +```rust +#[cfg(test)] +mod tests { + use super::*; + use mockito::{Server, Mock}; + + #[test] + fn test_aws_cost_parsing() { + let json = r#"{ + "ResultsByTime": [{ + "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-31"}, + "Total": { + "UnblendedCost": {"Amount": "100.50", "Unit": "USD"} + } + }] + }"#; + + let result = parse_aws_cost_response(json).unwrap(); + assert_eq!(result.total_cost, 100.50); + } + + #[tokio::test] + async fn test_aws_api_call_with_mock() { + let mut server = Server::new(); + let mock = server.mock("POST", "/") + .with_status(200) + .with_body(r#"{"ResultsByTime": []}"#) + .create(); + + let client = AwsClient::new_with_endpoint(&server.url()); + let result = client.get_cost_data("2024-01-01", "2024-01-31").await; + + assert!(result.is_ok()); + mock.assert(); + } +} +``` + +### 3. Testing Database Operations + +```rust +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn setup_test_db() -> (Connection, TempDir) { + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.duckdb"); + let conn = Connection::open(&db_path).unwrap(); + init_database(&conn).unwrap(); + (conn, temp_dir) + } + + #[test] + fn test_insert_and_retrieve_account() { + let (conn, _temp) = setup_test_db(); + + let account = CloudAccount { + name: "test-aws".to_string(), + provider: "aws".to_string(), + access_key: "encrypted-key".to_string(), + secret_key: "encrypted-secret".to_string(), + }; + + insert_account(&conn, &account).unwrap(); + let retrieved = get_account_by_name(&conn, "test-aws").unwrap(); + + assert_eq!(retrieved.name, account.name); + assert_eq!(retrieved.provider, account.provider); + } + + #[test] + fn test_cache_expiration() { + let (conn, _temp) = setup_test_db(); + + // Insert cache entry + let summary = CostSummary { /* ... */ }; + cache_cost_summary(&conn, "account-1", &summary).unwrap(); + + // Immediately should be valid + assert!(is_cache_valid(&conn, "account-1", 6).unwrap()); + + // Simulate time passing (in real test, use time mocking) + // For now, test with 0 hour TTL + assert!(!is_cache_valid(&conn, "account-1", 0).unwrap()); + } +} +``` + +### 4. Testing UI Components + +```rust +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + #[gpui::test] + fn test_dashboard_view_creation(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| DashboardView::new(cx)); + assert!(view.is_ok()); + } + + #[gpui::test] + async fn test_refresh_button_click(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| DashboardView::new(cx)); + + view.update(cx, |view, cx| { + // Simulate refresh button click + view.refresh(cx); + assert_eq!(view.loading, true); + }); + + cx.run_until_parked(); + + view.update(cx, |view, _| { + // After loading completes + assert_eq!(view.loading, false); + }); + } +} +``` + +## Test Organization + +### File Structure + +``` +src/ +├── crypto.rs +├── crypto_tests.rs # or #[cfg(test)] mod in crypto.rs +├── cloud/ +│ ├── aws.rs +│ ├── aws_tests.rs +│ ├── aliyun.rs +│ └── aliyun_tests.rs +└── ui/ + ├── dashboard.rs + └── dashboard_tests.rs +``` + +### Integration Tests + +``` +tests/ +├── integration_test.rs +├── fixtures/ +│ ├── aws_response.json +│ └── aliyun_response.json +└── helpers/ + └── mod.rs +``` + +## Common Test Patterns + +### 1. Parameterized Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_currencies() { + let test_cases = vec![ + ("100.00", "USD", 100.0), + ("50.50", "EUR", 50.5), + ("1000", "CNY", 1000.0), + ]; + + for (amount, currency, expected) in test_cases { + let result = parse_cost(amount, currency).unwrap(); + assert_eq!(result, expected); + } + } +} +``` + +### 2. Testing Error Conditions + +```rust +#[test] +fn test_invalid_credentials() { + let client = AwsClient::new("invalid-key", "invalid-secret"); + let result = client.validate_credentials(); + + assert!(result.is_err()); + assert!(matches!(result, Err(CloudError::InvalidCredentials))); +} + +#[test] +#[should_panic(expected = "encryption key must be 32 bytes")] +fn test_invalid_key_length_panics() { + let short_key = vec![0u8; 16]; // Too short + encrypt("data", &short_key).unwrap(); +} +``` + +### 3. Using Test Fixtures + +```rust +#[cfg(test)] +mod tests { + use super::*; + + fn load_fixture(name: &str) -> String { + std::fs::read_to_string( + format!("tests/fixtures/{}.json", name) + ).unwrap() + } + + #[test] + fn test_parse_aws_response() { + let json = load_fixture("aws_cost_response"); + let result = parse_aws_response(&json).unwrap(); + + assert_eq!(result.len(), 3); + assert_eq!(result[0].service_name, "EC2"); + } +} +``` + +### 4. Mocking External Dependencies + +```rust +#[cfg(test)] +mod tests { + use super::*; + + // Mock HTTP client + struct MockHttpClient { + responses: Vec, + } + + impl HttpClient for MockHttpClient { + fn post(&self, url: &str, body: &str) -> Result { + Ok(self.responses[0].clone()) + } + } + + #[test] + fn test_with_mock_http() { + let mock_client = MockHttpClient { + responses: vec![r#"{"status": "ok"}"#.to_string()], + }; + + let service = MyService::new(Box::new(mock_client)); + let result = service.fetch_data().unwrap(); + + assert_eq!(result.status, "ok"); + } +} +``` + +## Running Tests + +```bash +# Run all tests +cargo test + +# Run specific test +cargo test test_encrypt_decrypt + +# Run tests with output +cargo test -- --nocapture + +# Run tests in specific module +cargo test crypto::tests + +# Run tests matching pattern +cargo test aws_ + +# Run with coverage (requires tarpaulin) +cargo tarpaulin --out Html +``` + +## Best Practices + +1. **Test one thing per test** - Keep tests focused and simple +2. **Use descriptive test names** - `test_encryption_fails_with_wrong_key` vs `test_1` +3. **Arrange-Act-Assert** - Clear structure in tests +4. **Clean up resources** - Use `Drop` trait or `TempDir` for cleanup +5. **Don't test third-party code** - Test your own logic +6. **Make tests deterministic** - Avoid time-based or random tests +7. **Test edge cases** - Empty inputs, maximum values, invalid data +8. **Use test helpers** - DRY principle applies to tests too + +## Example: Complete Test Module + +```rust +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + // Helper functions + fn create_test_account() -> CloudAccount { + CloudAccount { + name: "test".to_string(), + provider: "aws".to_string(), + access_key: "key".to_string(), + secret_key: "secret".to_string(), + } + } + + // Unit tests + #[test] + fn test_account_creation() { + let account = create_test_account(); + assert_eq!(account.name, "test"); + } + + // Async tests + #[tokio::test] + async fn test_async_operation() { + let result = fetch_data().await; + assert!(result.is_ok()); + } + + // GPUI tests + #[gpui::test] + fn test_ui_component(cx: &mut TestAppContext) { + let view = cx.new_view(|cx| MyView::new(cx)); + view.update(cx, |view, cx| { + assert!(view.initialized); + }); + } +} +``` diff --git a/.claude/skills/reduce-clones/SKILL.md b/.claude/skills/reduce-clones/SKILL.md new file mode 100644 index 0000000..6ce675a --- /dev/null +++ b/.claude/skills/reduce-clones/SKILL.md @@ -0,0 +1,476 @@ +# Reduce Clones Skill + +This skill helps identify and eliminate unnecessary `.clone()` calls to optimize memory usage and performance in CloudBridge. + +## When to Use + +Use this skill when: +- Performance profiling shows excessive memory allocation +- Code review identifies unnecessary clones +- Refactoring to improve efficiency +- Adding new features and want to avoid clone overhead + +## Why Clones Are Problematic + +1. **Memory Overhead** - Each clone allocates new memory +2. **Performance Impact** - Cloning large structures is expensive +3. **Cache Pollution** - More allocations = worse cache performance +4. **Unnecessary Copies** - Often data can be borrowed instead + +## Common Clone Patterns in CloudBridge + +### Pattern 1: Cloning for View Passing + +**Problem:** +```rust +// src/ui/dashboard.rs +impl Render for DashboardView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .children(self.accounts.iter().map(|account| { + // Cloning the entire account for each render + self.render_account_card(account.clone(), cx) + })) + } + + fn render_account_card(&self, account: CloudAccount, cx: &ViewContext) -> impl IntoElement { + div().child(&account.name) + } +} +``` + +**Solution 1: Use References** +```rust +impl Render for DashboardView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .children(self.accounts.iter().map(|account| { + // Pass reference instead of clone + self.render_account_card(account, cx) + })) + } + + fn render_account_card(&self, account: &CloudAccount, cx: &ViewContext) -> impl IntoElement { + div().child(&account.name) + } +} +``` + +**Solution 2: Use Arc for Shared Ownership** +```rust +use std::sync::Arc; + +pub struct DashboardView { + // Store accounts in Arc for cheap cloning + accounts: Vec>, +} + +impl Render for DashboardView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .children(self.accounts.iter().map(|account| { + // Arc::clone is just incrementing reference count + self.render_account_card(Arc::clone(account), cx) + })) + } + + fn render_account_card(&self, account: Arc, cx: &ViewContext) -> impl IntoElement { + div().child(&account.name) + } +} +``` + +### Pattern 2: Cloning Strings + +**Problem:** +```rust +// Cloning strings unnecessarily +fn format_cost(amount: f64, currency: String) -> String { + format!("{:.2} {}", amount, currency) // currency moved here +} + +let currency = "USD".to_string(); +let formatted1 = format_cost(100.0, currency.clone()); // Clone! +let formatted2 = format_cost(200.0, currency.clone()); // Clone! +``` + +**Solution 1: Use String Slices** +```rust +fn format_cost(amount: f64, currency: &str) -> String { + format!("{:.2} {}", amount, currency) +} + +let currency = "USD"; // &str instead of String +let formatted1 = format_cost(100.0, currency); // No clone +let formatted2 = format_cost(200.0, currency); // No clone +``` + +**Solution 2: Use Owned When Necessary** +```rust +fn format_cost(amount: f64, currency: impl AsRef) -> String { + format!("{:.2} {}", amount, currency.as_ref()) +} + +// Works with both &str and String +let formatted1 = format_cost(100.0, "USD"); +let formatted2 = format_cost(200.0, String::from("EUR")); +``` + +### Pattern 3: Cloning in Closures + +**Problem:** +```rust +let account_name = account.name.clone(); +let account_id = account.id.clone(); + +Button::new("delete") + .on_click(cx.listener(move |this, _event, cx| { + // Closure captures clones + this.delete_account(&account_name, &account_id, cx); + })) +``` + +**Solution 1: Capture References (if lifetime allows)** +```rust +// Store account in a way that outlives the closure +let account_ref = &self.accounts[index]; + +Button::new("delete") + .on_click(cx.listener(move |this, _event, cx| { + this.delete_account(&account_ref.name, &account_ref.id, cx); + })) +``` + +**Solution 2: Clone Only What's Needed** +```rust +// Clone only the small pieces +let account_id = account.id; // Copy for simple types + +Button::new("delete") + .on_click(cx.listener(move |this, _event, cx| { + // Only account_id is captured, no string clones + this.delete_account(account_id, cx); + })) +``` + +**Solution 3: Use Arc** +```rust +let account = Arc::clone(&self.accounts[index]); + +Button::new("delete") + .on_click(cx.listener(move |this, _event, cx| { + // Arc clone is cheap (just a reference count increment) + this.delete_account(Arc::clone(&account), cx); + })) +``` + +### Pattern 4: Cloning in Iterations + +**Problem:** +```rust +// Cloning in filter_map +let active_accounts: Vec = self.accounts.clone() + .into_iter() + .filter(|a| a.is_active) + .collect(); +``` + +**Solution:** +```rust +// Use references +let active_accounts: Vec<&CloudAccount> = self.accounts + .iter() + .filter(|a| a.is_active) + .collect(); + +// Or if you need owned values and can't use references: +let active_accounts: Vec = self.accounts + .iter() + .filter(|a| a.is_active) + .cloned() // Clone only filtered items, not all items first + .collect(); +``` + +### Pattern 5: Cloning in Async Contexts + +**Problem:** +```rust +let accounts = self.accounts.clone(); +let config = self.config.clone(); + +cx.spawn(move |this, mut cx| async move { + // Using cloned data in async context + let results = fetch_costs(&accounts, &config).await; + // ... +}).detach(); +``` + +**Solution 1: Use Arc** +```rust +use std::sync::Arc; + +pub struct DashboardView { + accounts: Arc>, + config: Arc, +} + +// In async spawn +let accounts = Arc::clone(&self.accounts); +let config = Arc::clone(&self.config); + +cx.spawn(move |this, mut cx| async move { + let results = fetch_costs(&accounts, &config).await; + // ... +}).detach(); +``` + +**Solution 2: Clone Only IDs** +```rust +// Instead of cloning entire accounts +let account_ids: Vec = self.accounts + .iter() + .map(|a| a.id.clone()) + .collect(); + +cx.spawn(move |this, mut cx| async move { + // Fetch using just IDs + let results = fetch_costs_by_ids(&account_ids).await; + // ... +}).detach(); +``` + +## Refactoring Strategy + +### Step 1: Find Clones + +```bash +# Search for .clone() calls +rg "\.clone\(\)" --type rust + +# Count clones per file +rg "\.clone\(\)" --type rust --count + +# Find expensive clones (structs, vecs) +rg "Vec<.*>.*\.clone\(\)" --type rust +rg "HashMap<.*>.*\.clone\(\)" --type rust +``` + +### Step 2: Analyze Each Clone + +For each `.clone()`, ask: + +1. **Is it necessary?** + - Can we use a reference instead? + - Can we restructure to avoid the need? + +2. **Is it expensive?** + - Simple types (i32, bool) - Copy is fine + - Strings, Vecs - Consider alternatives + - Large structs - Definitely avoid if possible + +3. **What's the alternative?** + - Use `&T` reference + - Use `Arc` for shared ownership + - Use `Cow` for clone-on-write + - Restructure code to avoid need + +### Step 3: Replace Clones + +#### Replacement Pattern 1: Function Signatures + +```rust +// Before +fn process_account(account: CloudAccount) { } +let account = get_account(); +process_account(account.clone()); // Clone needed because function takes ownership + +// After +fn process_account(account: &CloudAccount) { } +let account = get_account(); +process_account(&account); // No clone, just borrow +``` + +#### Replacement Pattern 2: Struct Fields + +```rust +// Before +pub struct View { + data: Vec, // Owned +} + +fn render(&self) -> Element { + // Need to clone to pass to helper + self.render_list(self.data.clone()) +} + +// After +pub struct View { + data: Arc>, // Shared +} + +fn render(&self) -> Element { + // Arc clone is cheap + self.render_list(Arc::clone(&self.data)) +} +``` + +#### Replacement Pattern 3: Return Values + +```rust +// Before +fn get_accounts(&self) -> Vec { + self.accounts.clone() // Clone entire vector +} + +// After - Option 1: Return reference +fn get_accounts(&self) -> &[CloudAccount] { + &self.accounts +} + +// After - Option 2: Return iterator +fn get_accounts(&self) -> impl Iterator { + self.accounts.iter() +} +``` + +## Smart Clone Usage + +Sometimes clones ARE necessary. Use them wisely: + +### When Clone is Acceptable + +1. **Simple Copy Types** +```rust +let count = other.count; // i32 - Copy, not Clone +let flag = other.flag; // bool - Copy, not Clone +``` + +2. **Small Strings** +```rust +// Short error messages - clone is fine +let error = "Invalid input".to_string(); +``` + +3. **Breaking Borrow Checker Deadlocks** +```rust +// Sometimes clone is simplest solution to complex borrows +let temp = complex_data.clone(); +self.process(&temp); // Avoids borrow checker issues +``` + +4. **Async Boundaries** +```rust +// Need owned data in async block +let name = self.name.clone(); +tokio::spawn(async move { + process(&name).await +}); +``` + +### Clone Alternatives Cheat Sheet + +| Scenario | Instead of Clone | Use | +|----------|-----------------|-----| +| Read-only access | `data.clone()` | `&data` | +| Shared ownership | `data.clone()` | `Arc` | +| Maybe modify | `data.clone()` | `Cow` | +| Async context | `data.clone()` | `Arc` | +| Thread send | `data.clone()` | `Arc` | +| Function param | `fn(data: T)` | `fn(data: &T)` | +| Return value | `return data.clone()` | `return &data` or `Arc` | + +## Measuring Impact + +### Before Refactoring +```bash +# Run with memory profiling +cargo build --release +valgrind --tool=massif ./target/release/cloudbridge + +# Or use built-in allocator stats +cargo run --release -- --mem-stats +``` + +### After Refactoring +```bash +# Compare memory usage +# Should see: +# - Fewer allocations +# - Lower peak memory +# - Faster execution +``` + +## CloudBridge Specific Patterns + +### Pattern: Dashboard Account Cards + +**Before (with clones):** +```rust +// In dashboard.rs +self.accounts.iter().map(|account| { + div().child(self.render_account_card(account.clone())) +}) +``` + +**After (without clones):** +```rust +self.accounts.iter().map(|account| { + div().child(self.render_account_card(account)) +}) + +fn render_account_card(&self, account: &CloudAccount) -> impl IntoElement { + // Use reference throughout +} +``` + +### Pattern: Cloud Provider Clients + +**Before:** +```rust +pub struct AwsClient { + access_key: String, + secret_key: String, + region: String, +} + +// Expensive clones for signing +fn sign_request(&self, req: &Request) -> SignedRequest { + let ak = self.access_key.clone(); + let sk = self.secret_key.clone(); + let region = self.region.clone(); + // ... signing logic +} +``` + +**After:** +```rust +pub struct AwsClient { + access_key: Arc, // Use Arc for immutable shared strings + secret_key: Arc, + region: Arc, +} + +fn sign_request(&self, req: &Request) -> SignedRequest { + // Just pass references - no clones + sign_with_v4(&self.access_key, &self.secret_key, &self.region, req) +} +``` + +## Checklist + +- [ ] Search for all `.clone()` calls +- [ ] Categorize by necessity (required vs unnecessary) +- [ ] Identify expensive clones (large structs, collections) +- [ ] Replace with references where possible +- [ ] Use `Arc` for shared ownership +- [ ] Update function signatures to accept references +- [ ] Update struct fields to use `Arc` if needed +- [ ] Test performance before/after +- [ ] Verify memory usage improvement +- [ ] Document any remaining necessary clones + +## Resources + +- [The Rust Book - References and Borrowing](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html) +- [Rust Performance Book - Clone](https://nnethercote.github.io/perf-book/clone.html) +- [Arc Documentation](https://doc.rust-lang.org/std/sync/struct.Arc.html) diff --git a/.claude/skills/refactor-large-files/SKILL.md b/.claude/skills/refactor-large-files/SKILL.md new file mode 100644 index 0000000..496e29b --- /dev/null +++ b/.claude/skills/refactor-large-files/SKILL.md @@ -0,0 +1,407 @@ +# Refactor Large Files Skill + +This skill helps identify and refactor large files in the CloudBridge codebase to improve maintainability. + +## When to Use + +Use this skill when: +- A file exceeds 500 lines of code +- A component has multiple responsibilities +- Code review suggests splitting files +- Adding new features to already large files + +## Large Files in CloudBridge + +Current large files (as of analysis): +- `src/ui/dashboard.rs` - 837 lines +- `src/ui/accounts.rs` - 684 lines +- `src/cloud/aws.rs` - 758 lines +- `src/ui/chart.rs` - 545 lines +- `src/cloud/aliyun.rs` - 480 lines + +## Refactoring Patterns + +### Pattern 1: Extract Subcomponents + +**Before: dashboard.rs (837 lines)** +```rust +// All in one file +impl DashboardView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .child(self.render_overview_cards()) + .child(self.render_account_cards()) + .child(self.render_trend_modal()) + } + + fn render_overview_cards(&self, cx: &ViewContext) -> impl IntoElement { + // 100+ lines of code + } + + fn render_account_cards(&self, cx: &ViewContext) -> impl IntoElement { + // 200+ lines of code + } + + fn render_trend_modal(&self, cx: &ViewContext) -> impl IntoElement { + // 150+ lines of code + } +} +``` + +**After: Split into multiple files** + +``` +src/ui/dashboard/ +├── mod.rs # Main dashboard view (200 lines) +├── overview_cards.rs # Overview statistics (150 lines) +├── account_card.rs # Individual account card (200 lines) +└── trend_modal.rs # Trend chart modal (250 lines) +``` + +**Implementation:** + +```rust +// src/ui/dashboard/mod.rs +mod overview_cards; +mod account_card; +mod trend_modal; + +use overview_cards::OverviewCards; +use account_card::AccountCard; +use trend_modal::TrendModal; + +pub struct DashboardView { + overview: View, + accounts: Vec>, + trend_modal: Option>, +} + +impl Render for DashboardView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .child(self.overview.clone()) + .children(self.accounts.clone()) + .children(self.trend_modal.clone()) + } +} + +// src/ui/dashboard/overview_cards.rs +pub struct OverviewCards { + current_month_cost: f64, + last_month_cost: f64, + account_count: usize, +} + +impl Render for OverviewCards { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .gap_4() + .child(self.render_cost_card("This Month", self.current_month_cost)) + .child(self.render_cost_card("Last Month", self.last_month_cost)) + .child(self.render_count_card("Accounts", self.account_count)) + } +} +``` + +### Pattern 2: Extract Data Logic + +**Before: aws.rs (758 lines with mixed concerns)** + +```rust +// aws.rs - Both HTTP logic and data parsing +impl AwsClient { + fn get_cost_data(&self) -> Result> { + // 50 lines of AWS Signature V4 calculation + // 30 lines of HTTP request building + // 40 lines of response parsing + // 50 lines of error handling + } +} +``` + +**After: Separate concerns** + +``` +src/cloud/aws/ +├── mod.rs # Public API and client struct +├── auth.rs # AWS Signature V4 signing +├── api.rs # HTTP request/response handling +├── parser.rs # JSON response parsing +└── types.rs # AWS-specific types +``` + +```rust +// src/cloud/aws/mod.rs +mod auth; +mod api; +mod parser; +mod types; + +pub use types::*; + +pub struct AwsClient { + credentials: AwsCredentials, +} + +impl CloudService for AwsClient { + fn get_cost_data(&self, start: &str, end: &str) -> Result> { + let request = api::build_cost_explorer_request(start, end)?; + let signed = auth::sign_request(&request, &self.credentials)?; + let response = api::send_request(&signed)?; + parser::parse_cost_data(&response) + } +} + +// src/cloud/aws/auth.rs +pub fn sign_request(request: &Request, credentials: &AwsCredentials) -> Result { + // AWS Signature V4 implementation +} + +// src/cloud/aws/parser.rs +pub fn parse_cost_data(json: &str) -> Result> { + // JSON parsing logic +} +``` + +### Pattern 3: Extract State Management + +**Before: accounts.rs (684 lines)** + +```rust +pub struct AccountsView { + // UI state + selected_provider: String, + name_input: String, + ak_input: String, + sk_input: String, + region_input: String, + validating: bool, + error: Option, + + // Data state + accounts: Vec, + + // UI components + focus_handle: FocusHandle, +} +``` + +**After: Separate state and view** + +```rust +// src/ui/accounts/state.rs +pub struct AccountFormState { + pub selected_provider: String, + pub name_input: String, + pub ak_input: String, + pub sk_input: String, + pub region_input: String, + pub validating: bool, + pub error: Option, +} + +impl AccountFormState { + pub fn validate(&self) -> Result<(), String> { + if self.name_input.is_empty() { + return Err("Name is required".to_string()); + } + // ... more validation + Ok(()) + } + + pub fn reset(&mut self) { + self.name_input.clear(); + self.ak_input.clear(); + self.sk_input.clear(); + self.error = None; + } +} + +// src/ui/accounts/mod.rs +mod state; +mod form; +mod list; + +use state::AccountFormState; + +pub struct AccountsView { + form_state: Model, + accounts: Vec, + focus_handle: FocusHandle, +} +``` + +## Step-by-Step Refactoring Process + +### 1. Analyze the File + +```bash +# Count lines in large files +wc -l src/ui/dashboard.rs +wc -l src/ui/accounts.rs + +# Identify logical sections +# Look for: +# - Multiple impl blocks +# - Groups of related methods +# - Large nested functions +# - Repeated patterns +``` + +### 2. Identify Boundaries + +Look for natural separation points: +- **UI Components**: Different visual sections +- **Data Operations**: CRUD operations, queries +- **Business Logic**: Validation, calculations +- **External I/O**: HTTP requests, file operations + +### 3. Create Module Structure + +```bash +# For dashboard.rs +mkdir -p src/ui/dashboard +mv src/ui/dashboard.rs src/ui/dashboard/mod.rs + +# Create submodule files +touch src/ui/dashboard/overview.rs +touch src/ui/dashboard/accounts.rs +touch src/ui/dashboard/trends.rs +``` + +### 4. Extract and Move Code + +```rust +// Step 1: Copy the section to new file +// Step 2: Update imports in new file +// Step 3: Make struct/functions public +// Step 4: Import in mod.rs +// Step 5: Update references in original file +// Step 6: Test compilation +``` + +### 5. Update Tests + +```rust +// Update test imports +#[cfg(test)] +mod tests { + use super::*; + // May need to update paths + use crate::ui::dashboard::overview::OverviewCards; +} +``` + +## Common Refactoring Scenarios + +### Scenario 1: Large UI Component + +**Signs:** +- Render method > 100 lines +- Multiple helper render methods +- Complex state management + +**Solution:** +```rust +// Extract visual sections into separate View structs +// Use composition in main view +// Share state via Model +``` + +### Scenario 2: God Object (Many Responsibilities) + +**Signs:** +- Struct with 10+ fields +- 20+ methods +- Multiple unrelated operations + +**Solution:** +```rust +// Split into multiple focused structs +// Use traits for common behavior +// Inject dependencies +``` + +### Scenario 3: Mixed Concerns + +**Signs:** +- UI code mixed with business logic +- HTTP code mixed with parsing +- Database code mixed with validation + +**Solution:** +```rust +// Separate layers: +// - Presentation layer (UI) +// - Business logic layer (validation, rules) +// - Data access layer (API, DB) +``` + +## Refactoring Checklist + +- [ ] File exceeds 500 lines +- [ ] Identify logical boundaries +- [ ] Create new module structure +- [ ] Extract first section +- [ ] Update imports +- [ ] Compile and fix errors +- [ ] Run tests +- [ ] Extract next section +- [ ] Repeat until complete +- [ ] Remove old file if fully migrated +- [ ] Update documentation +- [ ] Commit changes + +## Safety Rules + +1. **One refactoring at a time** - Don't mix with feature changes +2. **Keep tests passing** - Compile after each extraction +3. **Maintain public API** - Don't break external users +4. **Preserve behavior** - No logic changes during refactor +5. **Commit frequently** - Small, reversible commits + +## Example: Refactoring dashboard.rs + +```bash +# Current structure +src/ui/dashboard.rs (837 lines) + +# Target structure +src/ui/dashboard/ +├── mod.rs # Main coordinator (150 lines) +├── overview.rs # Overview cards (120 lines) +├── account_card.rs # Account display (180 lines) +├── trend_modal.rs # Chart modal (200 lines) +└── state.rs # Shared state (100 lines) + +# Commands +mkdir -p src/ui/dashboard +git mv src/ui/dashboard.rs src/ui/dashboard/mod.rs + +# Extract each section +# 1. Extract state to state.rs +# 2. Extract overview cards to overview.rs +# 3. Extract account card to account_card.rs +# 4. Extract trend modal to trend_modal.rs +# 5. Update mod.rs to use new modules + +cargo build # Verify after each extraction +cargo test # Run tests after each step +``` + +## Benefits of Refactoring + +1. **Easier to understand** - Smaller, focused files +2. **Easier to test** - Test individual components +3. **Easier to modify** - Clear boundaries +4. **Better reusability** - Extract common code +5. **Team collaboration** - Fewer merge conflicts + +## When NOT to Refactor + +- File is < 300 lines and cohesive +- Code is rarely changed +- No clear separation boundaries +- Under tight deadline (refactor later) +- Extensive changes planned anyway diff --git a/.claude/skills/security-audit/SKILL.md b/.claude/skills/security-audit/SKILL.md new file mode 100644 index 0000000..4d72cb1 --- /dev/null +++ b/.claude/skills/security-audit/SKILL.md @@ -0,0 +1,493 @@ +# Security Audit Skill + +This skill helps perform security audits on CloudBridge code and identify potential vulnerabilities. + +## When to Use + +Use this skill when: +- Before releases to check for security issues +- Reviewing new code for vulnerabilities +- Implementing security-sensitive features +- Responding to security reports + +## Security Checklist + +### 1. Credential Management + +#### Current Implementation Review + +```bash +# Check for hardcoded credentials +rg -i "password|secret|key|token" --type rust -g "!*.md" + +# Check for exposed secrets in git history +git log -p | rg -i "password|secret|key" +``` + +#### Best Practices + +- [ ] Credentials encrypted at rest (AES-256-GCM) ✓ +- [ ] Credentials never logged +- [ ] Credentials cleared from memory when possible +- [ ] No credentials in error messages +- [ ] Encryption keys stored separately from data +- [ ] OS keyring used for sensitive data ✓ + +#### Vulnerabilities to Check + +```rust +// BAD: Logging credentials +println!("Using key: {}", secret_key); + +// BAD: Exposing in error messages +Err(format!("Authentication failed with key: {}", key)) + +// BAD: Storing plaintext +struct Config { + api_key: String, // Should be encrypted +} + +// GOOD: Encrypted storage +let encrypted_key = encrypt(&api_key, &encryption_key)?; +store_credential("api_key", &encrypted_key)?; +``` + +### 2. Encryption Security + +#### Audit Points + +```rust +// Check encryption implementation +// src/crypto.rs + +// ✓ GOOD: Using AES-256-GCM (authenticated encryption) +// ✓ GOOD: Random nonces +// ✗ CONCERN: Key stored with encrypted data (config.json + database) + +// Improvements: +// 1. Derive key from user password + salt +// 2. Store key in OS keyring only +// 3. Use key rotation +``` + +#### Encryption Checklist + +- [ ] Use authenticated encryption (GCM mode) ✓ +- [ ] Generate random nonces/IVs ✓ +- [ ] Use crypto libraries (don't roll your own) ✓ +- [ ] Key length appropriate (256 bits for AES) ✓ +- [ ] Keys not hardcoded ✓ +- [ ] Keys separated from encrypted data ⚠️ (needs improvement) + +### 3. Input Validation + +#### Check All User Inputs + +```rust +// src/ui/accounts.rs + +// Validate account names +fn validate_account_name(name: &str) -> Result<(), String> { + if name.is_empty() { + return Err("Name cannot be empty".to_string()); + } + if name.len() > 100 { + return Err("Name too long".to_string()); + } + // Check for invalid characters + if name.contains(|c: char| !c.is_alphanumeric() && c != '-' && c != '_') { + return Err("Name contains invalid characters".to_string()); + } + Ok(()) +} + +// Validate credentials format +fn validate_aws_credentials(ak: &str, sk: &str) -> Result<(), String> { + if ak.len() < 16 || ak.len() > 128 { + return Err("Invalid access key format".to_string()); + } + if sk.len() < 16 || sk.len() > 128 { + return Err("Invalid secret key format".to_string()); + } + Ok(()) +} +``` + +#### Input Validation Checklist + +- [ ] All user inputs validated +- [ ] Length limits enforced +- [ ] Format validation (regex, patterns) +- [ ] Character whitelisting +- [ ] SQL injection prevention (parameterized queries) ✓ +- [ ] XSS prevention (not applicable for desktop app) +- [ ] Path traversal prevention + +### 4. API Security + +#### HTTP Request Security + +```rust +// Check all HTTP requests in cloud providers + +// ✓ GOOD: Using HTTPS +// ✓ GOOD: Request signing (AWS Signature V4, Aliyun HMAC) +// ⚠️ CHECK: Certificate validation +// ⚠️ CHECK: Timeout handling +// ⚠️ CHECK: Rate limiting + +// Add timeout to all requests +let response = ureq::get(url) + .timeout(std::time::Duration::from_secs(30)) + .call()?; + +// Validate response status +if response.status() != 200 { + return Err(format!("API error: {}", response.status())); +} +``` + +#### API Security Checklist + +- [ ] All API calls use HTTPS ✓ +- [ ] Certificate validation enabled +- [ ] Timeouts configured +- [ ] Rate limiting implemented +- [ ] Retry logic with backoff +- [ ] Error responses sanitized + +### 5. Error Handling + +#### Secure Error Handling + +```rust +// BAD: Exposing sensitive info +fn decrypt_data(data: &str, key: &[u8]) -> Result { + cipher.decrypt(data) + .map_err(|e| format!("Decryption failed with key {:?}: {}", key, e)) + // ^^^ Exposes key! +} + +// GOOD: Generic error messages +fn decrypt_data(data: &str, key: &[u8]) -> Result { + cipher.decrypt(data) + .map_err(|_| "Decryption failed".to_string()) + // Or log detailed error separately for debugging +} +``` + +#### Error Handling Checklist + +- [ ] No sensitive data in error messages +- [ ] Generic errors to users, detailed logs for developers +- [ ] Error messages don't reveal system internals +- [ ] Stack traces not exposed to users +- [ ] Graceful degradation on errors + +### 6. Dependency Security + +#### Check Dependencies + +```bash +# Run cargo audit +cargo audit + +# Check for outdated dependencies +cargo outdated + +# Check for unsafe code in dependencies +cargo geiger +``` + +#### Update Cargo.toml + +```toml +[dependencies] +# Pin major versions to avoid breaking changes +gpui = "0.2" # Not "0.2.*" or "*" + +# Use specific versions for security-critical deps +aes-gcm = "0.10" # Encryption +ring = "0.17" # Cryptography + +# Avoid git dependencies in production +# Some-dep = { git = "..." } # BAD +``` + +#### Dependency Checklist + +- [ ] Run `cargo audit` regularly ✓ (in CI) +- [ ] Review all dependencies +- [ ] Pin versions +- [ ] Avoid git dependencies +- [ ] Check for known vulnerabilities +- [ ] Review dependency licenses + +### 7. Memory Safety + +#### Check for Unsafe Code + +```bash +# Find all unsafe blocks +rg "unsafe" --type rust + +# Should be ZERO unsafe blocks in CloudBridge +# ✓ GOOD: No unsafe code found +``` + +#### Memory Safety Checklist + +- [ ] No `unsafe` blocks ✓ +- [ ] No raw pointers +- [ ] No manual memory management +- [ ] Use Rust's ownership system ✓ +- [ ] Avoid panics in production code +- [ ] Handle all `Result` and `Option` properly + +### 8. Thread Safety + +#### Check Concurrent Access + +```rust +// Check all shared state access + +// BAD: Unsynchronized access +static mut COUNTER: i32 = 0; +unsafe { COUNTER += 1; } + +// GOOD: Using Mutex +use std::sync::Mutex; +static COUNTER: Mutex = Mutex::new(0); +*COUNTER.lock().unwrap() += 1; + +// GOOD: Using Arc for shared ownership +let data = Arc::new(Mutex::new(vec![])); +let data_clone = Arc::clone(&data); +thread::spawn(move || { + let mut d = data_clone.lock().unwrap(); + d.push(1); +}); +``` + +#### Thread Safety Checklist + +- [ ] All shared state protected (Mutex, RwLock) +- [ ] No data races +- [ ] Send/Sync bounds respected +- [ ] No unsafe thread operations + +### 9. Database Security + +#### Check SQL Queries + +```rust +// src/db.rs + +// ✓ GOOD: Using parameterized queries +conn.execute( + "INSERT INTO accounts (name, provider) VALUES (?, ?)", + params![name, provider], +)?; + +// BAD: String concatenation (SQL injection risk) +let query = format!("SELECT * FROM accounts WHERE name = '{}'", name); +conn.execute(&query, [])?; // NEVER DO THIS +``` + +#### Database Security Checklist + +- [ ] All queries parameterized ✓ +- [ ] No SQL injection vulnerabilities ✓ +- [ ] Database file permissions restricted +- [ ] Encrypted credentials in database ✓ +- [ ] Regular backups with encryption +- [ ] Input sanitization before queries + +### 10. File System Security + +#### Check File Operations + +```rust +// Check all file reads/writes + +// Validate paths to prevent directory traversal +fn safe_config_path(filename: &str) -> Result { + // Sanitize filename + if filename.contains("..") || filename.contains("/") || filename.contains("\\") { + return Err("Invalid filename".to_string()); + } + + let config_dir = get_config_dir()?; + let path = config_dir.join(filename); + + // Ensure path is within config directory + if !path.starts_with(&config_dir) { + return Err("Path traversal detected".to_string()); + } + + Ok(path) +} +``` + +#### File System Checklist + +- [ ] Path traversal prevention +- [ ] File permissions properly set +- [ ] Temp files cleaned up +- [ ] No sensitive data in temp files +- [ ] Config files not world-readable + +## Security Testing + +### Manual Testing + +```bash +# Test with invalid inputs +# - Empty strings +# - Very long strings (> 10KB) +# - Special characters +# - SQL injection patterns: ' OR 1=1-- +# - Path traversal: ../../etc/passwd + +# Test with invalid credentials +# - Wrong format +# - Expired credentials +# - Revoked credentials + +# Test error conditions +# - Network failures +# - API errors +# - Database errors +# - File system errors +``` + +### Automated Testing + +```rust +#[cfg(test)] +mod security_tests { + use super::*; + + #[test] + fn test_sql_injection_prevention() { + let malicious_name = "'; DROP TABLE accounts; --"; + let result = validate_account_name(malicious_name); + assert!(result.is_err()); + } + + #[test] + fn test_path_traversal_prevention() { + let malicious_path = "../../etc/passwd"; + let result = safe_config_path(malicious_path); + assert!(result.is_err()); + } + + #[test] + fn test_xss_prevention() { + let malicious_input = ""; + let result = validate_account_name(malicious_input); + assert!(result.is_err()); + } + + #[test] + fn test_encryption_key_not_logged() { + // Ensure encryption key is never in logs + // This test would check log output + } +} +``` + +## Common Vulnerabilities (OWASP Top 10) + +### 1. Broken Access Control +- [ ] Users can only access their own accounts ✓ +- [ ] No privilege escalation possible ✓ +- [ ] API calls authenticated ✓ + +### 2. Cryptographic Failures +- [ ] Strong encryption algorithms ✓ +- [ ] Proper key management ⚠️ (needs improvement) +- [ ] TLS for all network traffic ✓ + +### 3. Injection +- [ ] SQL injection prevented ✓ +- [ ] Command injection prevented ✓ +- [ ] No eval() or similar + +### 4. Insecure Design +- [ ] Security considered in design +- [ ] Threat modeling performed +- [ ] Secure by default + +### 5. Security Misconfiguration +- [ ] Secure defaults ✓ +- [ ] No debug info in production +- [ ] Minimal attack surface + +### 6. Vulnerable Components +- [ ] Dependencies audited ✓ +- [ ] Regular updates ✓ +- [ ] No known vulnerabilities + +### 7. Authentication Failures +- [ ] Strong credential validation ✓ +- [ ] Credentials encrypted ✓ +- [ ] Session management (N/A for desktop) + +### 8. Software & Data Integrity +- [ ] Code signing (planned) +- [ ] Dependency verification +- [ ] Update validation + +### 9. Logging & Monitoring +- [ ] Security events logged +- [ ] No sensitive data in logs +- [ ] Audit trail maintained + +### 10. Server-Side Request Forgery +- [ ] URL validation +- [ ] No user-controlled URLs +- [ ] Whitelist of allowed domains + +## Action Items + +Based on audit, prioritize fixes: + +1. **High Priority** (Security Critical) + - Separate encryption key from data + - Add key derivation from password + - Implement key rotation + +2. **Medium Priority** (Security Enhancement) + - Add certificate pinning + - Implement rate limiting + - Add request timeouts + +3. **Low Priority** (Defense in Depth) + - Add additional input validation + - Enhance error messages + - Add security logging + +## Tools + +```bash +# Install security tools +cargo install cargo-audit +cargo install cargo-outdated +cargo install cargo-geiger + +# Run security checks +cargo audit +cargo outdated +cargo geiger + +# Check for secrets in code +cargo install ripgrep +rg -i "password|secret|key|token" --type rust +``` + +## Resources + +- OWASP Top 10: https://owasp.org/www-project-top-ten/ +- Rust Security: https://anssi-fr.github.io/rust-guide/ +- Cargo Audit: https://github.com/RustSec/rustsec